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 = (¤t_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}