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}