Skip to main content

embedded_gui/context/
core_impl.rs

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