Skip to main content

microui_redux/
widget.rs

1//
2// Copyright 2022-Present (c) Raja Lehtihet & Wael El Oraiby
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are met:
6//
7// 1. Redistributions of source code must retain the above copyright notice,
8// this list of conditions and the following disclaimer.
9//
10// 2. Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13//
14// 3. Neither the name of the copyright holder nor the names of its contributors
15// may be used to endorse or promote products derived from this software without
16// specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28// POSSIBILITY OF SUCH DAMAGE.
29//
30// -----------------------------------------------------------------------------
31// Ported to rust from https://github.com/rxi/microui/ and the original license
32//
33// Copyright (c) 2020 rxi
34//
35// Permission is hereby granted, free of charge, to any person obtaining a copy
36// of this software and associated documentation files (the "Software"), to
37// deal in the Software without restriction, including without limitation the
38// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
39// sell copies of the Software, and to permit persons to whom the Software is
40// furnished to do so, subject to the following conditions:
41//
42// The above copyright notice and this permission notice shall be included in
43// all copies or substantial portions of the Software.
44//
45// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
50// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
51// IN THE SOFTWARE.
52//
53//! Widget runtime contracts and per-frame result tracking.
54
55use std::cmp::max;
56use std::collections::HashMap;
57
58use rs_math3d::Dimensioni;
59
60use crate::atlas::{AtlasHandle, EXPAND_DOWN_ICON};
61use crate::input::{ControlState, ResourceState, WidgetBehaviourOption, WidgetOption};
62use crate::style::Style;
63use crate::widget_ctx::WidgetCtx;
64use crate::widget_tree::WidgetHandle;
65
66/// Trait implemented by persistent widget state structures.
67///
68/// Widgets participate in two retained phases:
69/// 1. `measure`, which reports intrinsic size for the current frame's layout pass.
70/// 2. `run`, which records draw commands, samples interaction, mutates widget-local state,
71///    and produces the current frame result.
72pub trait Widget {
73    /// Returns the widget options for this state.
74    fn widget_opt(&self) -> &WidgetOption;
75    /// Returns the behaviour options for this state.
76    fn behaviour_opt(&self) -> &WidgetBehaviourOption;
77    /// Returns the intrinsic widget size for the current frame's layout pass.
78    ///
79    /// `avail` reports the current container body size visible to the widget.
80    /// Values less than or equal to zero are treated as "use layout defaults" for that axis.
81    fn measure(&self, style: &Style, atlas: &AtlasHandle, avail: Dimensioni) -> Dimensioni;
82    /// Runs the widget for the current frame and returns the current frame result.
83    fn run(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState;
84    /// Returns the effective widget options used by generic dispatch.
85    ///
86    /// Widgets can override this to apply dynamic option adjustments.
87    fn effective_widget_opt(&self) -> WidgetOption {
88        *self.widget_opt()
89    }
90    /// Returns the effective behavior options used by generic dispatch.
91    fn effective_behaviour_opt(&self) -> WidgetBehaviourOption {
92        *self.behaviour_opt()
93    }
94    /// Returns whether this widget needs per-frame input snapshots.
95    fn needs_input_snapshot(&self) -> bool {
96        false
97    }
98}
99
100/// Raw pointer identity used for widget hover/focus tracking.
101pub type WidgetId = *const ();
102
103/// Returns the pointer identity for a widget state object.
104/// Use this when calling APIs such as `Container::set_focus`.
105pub fn widget_id_of<W: Widget + ?Sized>(widget: &W) -> WidgetId {
106    widget as *const W as *const ()
107}
108
109/// Returns the pointer identity for the widget state stored in `handle`.
110pub fn widget_id_of_handle<W: Widget>(handle: &WidgetHandle<W>) -> WidgetId {
111    let widget = handle.borrow();
112    widget_id_of(&*widget)
113}
114
115/// Per-frame widget interaction results keyed by [`WidgetId`].
116///
117/// A single widget state is expected to be dispatched once per frame.
118/// Duplicate dispatches with the same ID panic in all builds.
119///
120/// The storage is split into two generations:
121/// - the committed result set published at the end of the previous frame,
122/// - and the current in-progress result set being written by this frame.
123#[derive(Default)]
124pub(crate) struct FrameResults {
125    committed: HashMap<WidgetId, ResourceState>,
126    current: HashMap<WidgetId, ResourceState>,
127    current_dispatch_sites: HashMap<WidgetId, String>,
128}
129
130/// Read-only view over one frame-result generation.
131#[derive(Copy, Clone)]
132pub struct FrameResultGeneration<'a> {
133    entries: &'a HashMap<WidgetId, ResourceState>,
134}
135
136impl<'a> FrameResultGeneration<'a> {
137    fn new(entries: &'a HashMap<WidgetId, ResourceState>) -> Self {
138        Self { entries }
139    }
140
141    /// Returns the state for `widget_id` in this generation.
142    pub fn state(&self, widget_id: WidgetId) -> ResourceState {
143        self.entries.get(&widget_id).copied().unwrap_or(ResourceState::NONE)
144    }
145
146    /// Returns the state for `widget` in this generation.
147    pub fn state_of<W: Widget + ?Sized>(&self, widget: &W) -> ResourceState {
148        self.state(widget_id_of(widget))
149    }
150
151    /// Returns the state for the widget stored in `handle` in this generation.
152    pub fn state_of_handle<W: Widget>(&self, handle: &WidgetHandle<W>) -> ResourceState {
153        self.state(widget_id_of_handle(handle))
154    }
155}
156
157impl FrameResults {
158    /// Clears the in-progress frame results for a new frame.
159    ///
160    /// Previously committed results remain available through [`FrameResults::committed`].
161    pub(crate) fn begin_frame(&mut self) {
162        self.current.clear();
163        self.current_dispatch_sites.clear();
164    }
165
166    /// Publishes the current frame as the next committed result generation.
167    pub(crate) fn finish_frame(&mut self) {
168        std::mem::swap(&mut self.committed, &mut self.current);
169        self.current.clear();
170        self.current_dispatch_sites.clear();
171    }
172
173    /// Records the current frame state under `widget_id`.
174    #[cfg_attr(not(test), allow(dead_code))]
175    pub(crate) fn record(&mut self, widget_id: WidgetId, state: ResourceState) {
176        self.record_with_context(widget_id, state, "unknown widget dispatch site");
177    }
178
179    /// Records the current frame state under `widget_id` with a human-readable dispatch site.
180    pub(crate) fn record_with_context(&mut self, widget_id: WidgetId, state: ResourceState, dispatch_site: impl Into<String>) {
181        let dispatch_site = dispatch_site.into();
182        if let Some(first_site) = self.current_dispatch_sites.get(&widget_id) {
183            panic!(
184                "duplicate widget dispatch detected for widget {:p}; a WidgetHandle may only be rendered once per frame. first dispatch: {}. duplicate dispatch: {}.",
185                widget_id, first_site, dispatch_site
186            );
187        }
188
189        let prev_state = self.current.insert(widget_id, state);
190        let prev_site = self.current_dispatch_sites.insert(widget_id, dispatch_site);
191        debug_assert_eq!(
192            prev_state.is_some(),
193            prev_site.is_some(),
194            "widget result and dispatch-site tracking diverged for widget {:p}",
195            widget_id
196        );
197    }
198
199    /// Returns the committed result generation published by the previous frame.
200    pub(crate) fn committed(&self) -> FrameResultGeneration<'_> {
201        FrameResultGeneration::new(&self.committed)
202    }
203
204    /// Returns the in-progress result generation for the current frame.
205    #[cfg_attr(not(test), allow(dead_code))]
206    pub(crate) fn current(&self) -> FrameResultGeneration<'_> {
207        FrameResultGeneration::new(&self.current)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn committed_and_current_generation_views_are_explicit() {
217        let committed_widget = 1_u8;
218        let current_widget = 2_u8;
219        let committed_id = (&committed_widget as *const u8).cast::<()>();
220        let current_id = (&current_widget as *const u8).cast::<()>();
221
222        let mut results = FrameResults::default();
223        results.record(committed_id, ResourceState::SUBMIT);
224        results.finish_frame();
225        results.begin_frame();
226        results.record(current_id, ResourceState::CHANGE);
227
228        assert!(results.committed().state(committed_id).is_submitted());
229        assert!(results.current().state(committed_id).is_none());
230        assert!(results.current().state(current_id).is_changed());
231    }
232}
233
234impl Widget for (WidgetOption, WidgetBehaviourOption) {
235    fn widget_opt(&self) -> &WidgetOption {
236        &self.0
237    }
238
239    fn behaviour_opt(&self) -> &WidgetBehaviourOption {
240        &self.1
241    }
242
243    fn measure(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
244        let padding = style.padding.max(0);
245        let vertical_pad = max(1, padding / 2);
246        let font_height = atlas.get_font_height(style.font) as i32;
247        let icon_height = atlas.get_icon_size(EXPAND_DOWN_ICON).height;
248        let content = max(font_height, icon_height);
249        let height = (content + vertical_pad * 2).max(0);
250        let width = (padding * 2 + content).max(0);
251        Dimensioni::new(width, height)
252    }
253
254    fn run(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState {
255        ResourceState::NONE
256    }
257}