Skip to main content

embedded_gui/context/
core_impl.rs

1use heapless::Vec;
2
3#[cfg(not(feature = "std"))]
4use crate::math::F32Ext as _;
5use crate::{
6    geometry::{DirtyTracker, Rect},
7    haptics::{HapticPattern, HapticSequencer},
8    input::UiEvent,
9    present::PresentRegion,
10    render::RenderQuality,
11    style::{Style, Theme, VisualState, WidgetStyle, lerp_style},
12    widget::{MenuContract, StyleClassId, WidgetId},
13    widgets::WidgetNode,
14};
15
16use super::*;
17
18impl<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
19    GuiContext<'a, NODES, EVENTS, DIRTY>
20{
21    pub fn new(viewport: Rect) -> Self {
22        let mut dirty = DirtyTracker::new();
23        let _ = dirty.mark_all(viewport);
24        Self {
25            viewport,
26            widgets: Vec::new(),
27            subscriptions: Vec::new(),
28            dispatch_policies: Vec::new(),
29            class_styles: Vec::new(),
30            events: Vec::new(),
31            dirty,
32            theme: Theme::default(),
33            focus: None,
34            active_focus_group: None,
35            render_quality: RenderQuality::High,
36            long_press_ms: 500,
37            textarea_cursor_blink_ms: 500,
38            textarea_cursor_blink_elapsed_ms: 0,
39            press_repeat_delay_ms: 650,
40            press_repeat_interval_ms: 140,
41            select_double_window_ms: 300,
42            select_elapsed_ms: 0,
43            last_select_id: None,
44            pointer_double_window_ms: 300,
45            pointer_elapsed_ms: 0,
46            last_pointer_id: None,
47            pressed: None,
48            inertia_scroll: None,
49            scroll_physics: ScrollPhysics::default(),
50            state_transition_ms: 0,
51            state_transitions: Vec::new(),
52            widget_press_timings: Vec::new(),
53            widget_key_policies: Vec::new(),
54            widget_key_bindings: Vec::new(),
55            menu_contract: MenuContract::default(),
56            textarea_undo: Vec::new(),
57            textarea_redo: Vec::new(),
58            theme_transition_from: None,
59            theme_transition_to: None,
60            theme_transition_duration_ms: 0,
61            theme_transition_elapsed_ms: 0,
62            haptic_sequencer: HapticSequencer::new(),
63            next_id: 1,
64        }
65    }
66
67    pub const fn viewport(&self) -> Rect {
68        self.viewport
69    }
70
71    pub fn set_viewport(&mut self, viewport: Rect) -> Result<(), GuiError> {
72        self.viewport = viewport;
73        self.dirty.mark_all(viewport)?;
74        Ok(())
75    }
76
77    pub fn clear_widgets(&mut self) -> Result<(), GuiError> {
78        self.widgets.clear();
79        self.subscriptions.clear();
80        self.dispatch_policies.clear();
81        self.class_styles.clear();
82        self.focus = None;
83        self.pressed = None;
84        self.inertia_scroll = None;
85        self.last_select_id = None;
86        self.select_elapsed_ms = 0;
87        self.last_pointer_id = None;
88        self.pointer_elapsed_ms = 0;
89        self.state_transitions.clear();
90        self.widget_press_timings.clear();
91        self.widget_key_policies.clear();
92        self.widget_key_bindings.clear();
93        self.textarea_undo.clear();
94        self.textarea_redo.clear();
95        self.dirty.mark_all(self.viewport)?;
96        Ok(())
97    }
98
99    pub const fn long_press_threshold_ms(&self) -> u32 {
100        self.long_press_ms
101    }
102
103    pub fn set_long_press_threshold_ms(&mut self, threshold_ms: u32) {
104        self.long_press_ms = threshold_ms.max(1);
105    }
106
107    pub fn set_press_repeat_timing(&mut self, delay_ms: u32, interval_ms: u32) {
108        self.press_repeat_delay_ms = delay_ms.max(1);
109        self.press_repeat_interval_ms = interval_ms.max(1);
110    }
111
112    pub fn set_double_select_window_ms(&mut self, window_ms: u32) {
113        self.select_double_window_ms = window_ms.max(1);
114    }
115
116    pub fn set_double_pointer_window_ms(&mut self, window_ms: u32) {
117        self.pointer_double_window_ms = window_ms.max(1);
118    }
119
120    pub fn menu_contract(&self) -> MenuContract {
121        self.menu_contract
122    }
123
124    pub fn set_menu_contract(&mut self, contract: MenuContract) {
125        self.menu_contract = contract;
126    }
127
128    pub fn set_widget_press_timing(
129        &mut self,
130        id: WidgetId,
131        timing: PressTiming,
132    ) -> Result<(), GuiError> {
133        self.node(id).ok_or(GuiError::NotFound)?;
134        let timing = PressTiming {
135            long_press_ms: timing.long_press_ms.max(1),
136            repeat_delay_ms: timing.repeat_delay_ms.max(1),
137            repeat_interval_ms: timing.repeat_interval_ms.max(1),
138        };
139        if let Some((_, current)) = self
140            .widget_press_timings
141            .iter_mut()
142            .find(|(timing_id, _)| *timing_id == id)
143        {
144            *current = timing;
145            return Ok(());
146        }
147        self.widget_press_timings
148            .push((id, timing))
149            .map_err(|_| GuiError::WidgetsFull)
150    }
151
152    pub fn clear_widget_press_timing(&mut self, id: WidgetId) -> Result<(), GuiError> {
153        self.node(id).ok_or(GuiError::NotFound)?;
154        if let Some(pos) = self
155            .widget_press_timings
156            .iter()
157            .position(|(timing_id, _)| *timing_id == id)
158        {
159            self.widget_press_timings.remove(pos);
160        }
161        Ok(())
162    }
163
164    pub fn widget_press_timing(&self, id: WidgetId) -> Result<Option<PressTiming>, GuiError> {
165        self.node(id).ok_or(GuiError::NotFound)?;
166        Ok(self
167            .widget_press_timings
168            .iter()
169            .find(|(timing_id, _)| *timing_id == id)
170            .map(|(_, timing)| *timing))
171    }
172
173    pub fn set_widget_key_input_policy(
174        &mut self,
175        id: WidgetId,
176        policy: WidgetKeyInputPolicy,
177    ) -> Result<(), GuiError> {
178        self.node(id).ok_or(GuiError::NotFound)?;
179        if let Some((_, current)) = self
180            .widget_key_policies
181            .iter_mut()
182            .find(|(policy_id, _)| *policy_id == id)
183        {
184            *current = policy;
185            return Ok(());
186        }
187        self.widget_key_policies
188            .push((id, policy))
189            .map_err(|_| GuiError::WidgetsFull)
190    }
191
192    pub fn clear_widget_key_input_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
193        self.node(id).ok_or(GuiError::NotFound)?;
194        if let Some(pos) = self
195            .widget_key_policies
196            .iter()
197            .position(|(policy_id, _)| *policy_id == id)
198        {
199            self.widget_key_policies.remove(pos);
200        }
201        Ok(())
202    }
203
204    pub fn widget_key_input_policy(
205        &self,
206        id: WidgetId,
207    ) -> Result<Option<WidgetKeyInputPolicy>, GuiError> {
208        self.node(id).ok_or(GuiError::NotFound)?;
209        Ok(self
210            .widget_key_policies
211            .iter()
212            .find(|(policy_id, _)| *policy_id == id)
213            .map(|(_, policy)| *policy))
214    }
215
216    pub fn set_widget_key_bindings(
217        &mut self,
218        id: WidgetId,
219        bindings: WidgetKeyBindings,
220    ) -> Result<(), GuiError> {
221        self.node(id).ok_or(GuiError::NotFound)?;
222        if let Some((_, current)) = self
223            .widget_key_bindings
224            .iter_mut()
225            .find(|(binding_id, _)| *binding_id == id)
226        {
227            *current = bindings;
228            return Ok(());
229        }
230        self.widget_key_bindings
231            .push((id, bindings))
232            .map_err(|_| GuiError::WidgetsFull)
233    }
234
235    pub fn clear_widget_key_bindings(&mut self, id: WidgetId) -> Result<(), GuiError> {
236        self.node(id).ok_or(GuiError::NotFound)?;
237        if let Some(pos) = self
238            .widget_key_bindings
239            .iter()
240            .position(|(binding_id, _)| *binding_id == id)
241        {
242            self.widget_key_bindings.remove(pos);
243        }
244        Ok(())
245    }
246
247    pub fn widget_key_bindings(&self, id: WidgetId) -> Result<Option<WidgetKeyBindings>, GuiError> {
248        self.node(id).ok_or(GuiError::NotFound)?;
249        Ok(self
250            .widget_key_bindings
251            .iter()
252            .find(|(binding_id, _)| *binding_id == id)
253            .map(|(_, bindings)| *bindings))
254    }
255
256    pub fn set_scroll_physics(
257        &mut self,
258        velocity_threshold: f32,
259        velocity_decay: f32,
260        drag_velocity_blend: f32,
261    ) {
262        self.scroll_physics.velocity_threshold = velocity_threshold.max(0.001);
263        self.scroll_physics.velocity_decay = velocity_decay.clamp(0.01, 0.999);
264        self.scroll_physics.drag_velocity_blend = drag_velocity_blend.clamp(0.01, 1.0);
265    }
266
267    pub fn set_state_transition_duration_ms(&mut self, duration_ms: u32) {
268        self.state_transition_ms = duration_ms;
269        if duration_ms == 0 {
270            self.state_transitions.clear();
271        }
272    }
273
274    pub fn active_state_transitions(&self) -> usize {
275        self.state_transitions.len()
276    }
277
278    pub fn set_textarea_cursor_blink_timing(&mut self, period_ms: u32) {
279        self.textarea_cursor_blink_ms = period_ms.max(1);
280    }
281
282    pub fn widgets(&self) -> &[WidgetNode<'a>] {
283        self.widgets.as_slice()
284    }
285
286    pub fn dirty_regions(&self) -> &[Rect] {
287        self.dirty.as_slice()
288    }
289
290    pub fn present_regions(&self) -> impl Iterator<Item = PresentRegion> + '_ {
291        self.dirty
292            .as_slice()
293            .iter()
294            .copied()
295            .map(PresentRegion::from)
296    }
297
298    pub fn bounding_present_region(&self) -> Option<PresentRegion> {
299        self.dirty.bounding_rect().map(PresentRegion::from)
300    }
301
302    pub fn clear_dirty(&mut self) {
303        self.dirty.clear();
304    }
305
306    pub const fn theme(&self) -> Theme {
307        self.theme
308    }
309
310    pub fn set_theme(&mut self, theme: Theme) -> Result<(), GuiError> {
311        self.theme = theme;
312        self.dirty.mark_all(self.viewport)?;
313        Ok(())
314    }
315
316    pub fn start_theme_transition(
317        &mut self,
318        target: Theme,
319        duration_ms: u32,
320    ) -> Result<(), GuiError> {
321        if duration_ms == 0 {
322            self.set_theme(target)?;
323        } else {
324            self.theme_transition_from = Some(self.theme);
325            self.theme_transition_to = Some(target);
326            self.theme_transition_duration_ms = duration_ms;
327            self.theme_transition_elapsed_ms = 0;
328        }
329        Ok(())
330    }
331
332    pub fn play_haptic(&mut self, pattern: HapticPattern) {
333        self.haptic_sequencer.play(pattern);
334    }
335
336    pub fn stop_haptic(&mut self) {
337        self.haptic_sequencer.stop();
338    }
339
340    pub fn haptic_intensity(&self) -> u8 {
341        self.haptic_sequencer.current_intensity()
342    }
343
344    pub fn set_style_class<S>(&mut self, class: StyleClassId, style: S) -> Result<(), GuiError>
345    where
346        S: Into<WidgetStyle>,
347    {
348        if class == StyleClassId::NONE {
349            return Ok(());
350        }
351        if let Some((_, slot)) = self.class_styles.iter_mut().find(|(id, _)| *id == class) {
352            *slot = style.into();
353        } else {
354            self.class_styles
355                .push((class, style.into()))
356                .map_err(|_| GuiError::WidgetsFull)?;
357        }
358        self.dirty.mark_all(self.viewport)?;
359        Ok(())
360    }
361
362    pub fn clear_style_class(&mut self, class: StyleClassId) -> Result<(), GuiError> {
363        if let Some(pos) = self.class_styles.iter().position(|(id, _)| *id == class) {
364            self.class_styles.remove(pos);
365            self.dirty.mark_all(self.viewport)?;
366        }
367        Ok(())
368    }
369
370    pub fn set_style_class_state(
371        &mut self,
372        class: StyleClassId,
373        state: VisualState,
374        style: Style,
375    ) -> Result<(), GuiError> {
376        if class == StyleClassId::NONE {
377            return Ok(());
378        }
379        if let Some((_, slot)) = self.class_styles.iter_mut().find(|(id, _)| *id == class) {
380            *slot = slot.with_state_override(state, style);
381        } else {
382            let base = WidgetStyle::new(Style::new()).with_state_override(state, style);
383            self.class_styles
384                .push((class, base))
385                .map_err(|_| GuiError::WidgetsFull)?;
386        }
387        self.dirty.mark_all(self.viewport)?;
388        Ok(())
389    }
390
391    pub fn set_widget_style_class(
392        &mut self,
393        id: WidgetId,
394        class: Option<StyleClassId>,
395    ) -> Result<(), GuiError> {
396        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
397        node.style_class = class.filter(|c| *c != StyleClassId::NONE);
398        self.mark_subtree_dirty(id)
399    }
400
401    pub fn apply_widget_style_transition(
402        &mut self,
403        id: WidgetId,
404        from: VisualState,
405        to: VisualState,
406        t: f32,
407    ) -> Result<(), GuiError> {
408        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
409        let a = node.style.resolve(from);
410        let b = node.style.resolve(to);
411        let blended = lerp_style(a, b, t);
412        node.style = node.style.with_state_override(VisualState::Normal, blended);
413        self.mark_subtree_dirty(id)
414    }
415
416    pub const fn render_quality(&self) -> RenderQuality {
417        self.render_quality
418    }
419
420    pub fn set_render_quality(&mut self, quality: RenderQuality) -> Result<(), GuiError> {
421        if self.render_quality != quality {
422            self.render_quality = quality;
423            self.dirty.mark_all(self.viewport)?;
424        }
425        Ok(())
426    }
427
428    pub const fn focus(&self) -> Option<WidgetId> {
429        self.focus
430    }
431
432    pub fn set_focus(&mut self, focus: Option<WidgetId>) -> Result<(), GuiError> {
433        if let Some(id) = focus {
434            self.node(id).ok_or(GuiError::NotFound)?;
435            if !self.effective_focusable(id) {
436                return Err(GuiError::NotFound);
437            }
438        }
439
440        let old = self.focus;
441        self.focus = focus;
442        self.textarea_cursor_blink_elapsed_ms = 0;
443        self.set_textarea_cursor_visible(old, true);
444        self.set_textarea_cursor_visible(focus, true);
445        self.start_focus_transitions(old, focus);
446        self.mark_focus_pair(old, focus)?;
447        if let Some(id) = old {
448            self.push_event(UiEvent::Defocused(id))?;
449        }
450        if let Some(id) = focus {
451            self.push_event(UiEvent::Focused(id))?;
452        }
453        self.push_event(UiEvent::FocusChanged { old, new: focus })?;
454        Ok(())
455    }
456}