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}