slt/context/runtime.rs
1use super::*;
2
3impl Context {
4 pub(crate) fn new(
5 events: Vec<Event>,
6 width: u32,
7 height: u32,
8 state: &mut FrameState,
9 theme: Theme,
10 ) -> Self {
11 let hook_states = &mut state.hook_states;
12 let named_states = std::mem::take(&mut state.named_states);
13 // Issue #215: hand off the keyed-state map for this frame. Same
14 // lifetime as `named_states`: moved out at frame start, moved back
15 // at frame end (see `run_frame_kernel`).
16 let keyed_states = std::mem::take(&mut state.keyed_states);
17 // Issue #262: hand off the partial-chord buffer for this frame. Same
18 // lifetime as `keyed_states`: moved out at frame start, moved back at
19 // frame end (see `run_frame_kernel`).
20 let chord = std::mem::take(&mut state.chord_states);
21 // Issue #248: hand off the scheduler timer table for this frame. Same
22 // lifetime as `named_states`: moved out at frame start, moved back at
23 // frame end (where untouched slots are GC'd; see `run_frame_kernel`).
24 let scheduler = std::mem::take(&mut state.scheduler);
25 // Issue #234: hand off the async task registry for this frame. Same
26 // lifetime as `scheduler`: moved out at frame start, moved back at
27 // frame end (see `run_frame_kernel`).
28 #[cfg(feature = "async")]
29 let async_tasks = std::mem::take(&mut state.async_tasks);
30 let screen_hook_map = std::mem::take(&mut state.screen_hook_map);
31 let focus = &mut state.focus;
32 // Issue #217: name→index map from the previous frame, used to resolve
33 // `focus_by_name(name)` at frame start. We move it out so the
34 // `register_focusable_named` calls in this frame can rebuild a fresh
35 // `focus_name_map`. The fresh map is swapped back into
36 // `focus_name_map_prev` at frame end.
37 let focus_name_map_prev = std::mem::take(&mut focus.focus_name_map_prev);
38 let pending_focus_name = focus.pending_focus_name.take();
39 let prev_focus_index = focus.prev_focus_index;
40 let layout_feedback = &mut state.layout_feedback;
41 let diagnostics = &mut state.diagnostics;
42 let consumed = vec![false; events.len()];
43
44 // Single wall-clock sample for this frame, reused for double-click
45 // timing below and for `frame_instant` (the timer/scheduler clock).
46 let frame_now = std::time::Instant::now();
47 let mut mouse_pos = layout_feedback.last_mouse_pos;
48 let mut click_pos = None;
49 let mut right_click_pos = None;
50 let mut double_click_pos = None;
51 let mut scroll_pos = None;
52 let mut scroll_delta_frame: i32 = 0;
53 for event in &events {
54 if let Event::Mouse(mouse) = event {
55 mouse_pos = Some((mouse.x, mouse.y));
56 match mouse.kind {
57 MouseKind::Down(MouseButton::Left) => {
58 click_pos = Some((mouse.x, mouse.y));
59 // v0.21.1: a left click on the same cell as the previous
60 // click, within `DOUBLE_CLICK_WINDOW`, is a double-click.
61 // Clear the tracker after firing so a third click starts
62 // a fresh pair (no triple-counting).
63 let pos = (mouse.x, mouse.y);
64 let is_double = layout_feedback.last_click_pos == Some(pos)
65 && layout_feedback.last_click_at.is_some_and(|t| {
66 frame_now.duration_since(t) <= crate::DOUBLE_CLICK_WINDOW
67 });
68 if is_double {
69 double_click_pos = Some(pos);
70 layout_feedback.last_click_at = None;
71 layout_feedback.last_click_pos = None;
72 } else {
73 layout_feedback.last_click_at = Some(frame_now);
74 layout_feedback.last_click_pos = Some(pos);
75 }
76 }
77 MouseKind::Down(MouseButton::Right) => {
78 // Issue #208: capture last right-click position so
79 // `response_for` can hit-test against per-widget rects.
80 right_click_pos = Some((mouse.x, mouse.y));
81 }
82 // v0.21.1: accumulate net vertical wheel delta + the cursor
83 // position, hover-gated per-widget by `response_for`.
84 MouseKind::ScrollUp => {
85 scroll_pos = Some((mouse.x, mouse.y));
86 scroll_delta_frame = scroll_delta_frame.saturating_add(1);
87 }
88 MouseKind::ScrollDown => {
89 scroll_pos = Some((mouse.x, mouse.y));
90 scroll_delta_frame = scroll_delta_frame.saturating_sub(1);
91 }
92 _ => {}
93 }
94 }
95 }
96
97 let mut focus_index = focus.focus_index;
98 if let Some((mx, my)) = click_pos {
99 let mut best: Option<(usize, u64)> = None;
100 for &(fid, rect) in &layout_feedback.prev_focus_rects {
101 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
102 let area = rect.width as u64 * rect.height as u64;
103 if best.is_none_or(|(_, ba)| area < ba) {
104 best = Some((fid, area));
105 }
106 }
107 }
108 if let Some((fid, _)) = best {
109 focus_index = fid;
110 }
111 }
112
113 // Issue #217: resolve a pending `focus_by_name(...)` request against
114 // the previous frame's `name → index` map. If the name wasn't
115 // registered last frame, we keep the request pending for the next
116 // frame so a widget that registers later can still receive focus.
117 // If the request resolves, we consume it.
118 let mut still_pending: Option<String> = None;
119 if let Some(name) = pending_focus_name {
120 if let Some(&resolved) = focus_name_map_prev.get(&name) {
121 focus_index = resolved;
122 } else {
123 still_pending = Some(name);
124 }
125 }
126
127 // Reuse `commands_buf` capacity from the previous frame (issue #150).
128 // `mem::take` swaps an empty Vec into `state.commands_buf`; we then
129 // clear (no-op when reclaimed from a `build_tree` drain, defensive
130 // when reclaimed from the quit path that ran without `build_tree`)
131 // and reuse the allocation. After `build_tree(&mut ctx.commands)`
132 // drains the Vec in place, the empty (but capacity-bearing) Vec is
133 // moved back into `state.commands_buf` at frame end inside
134 // `run_frame_kernel`.
135 let mut commands = std::mem::take(&mut state.commands_buf);
136 commands.clear();
137
138 // Issue #204: reuse the six per-frame `Vec`/`HashSet` allocations
139 // (`context_stack`, `deferred_draws`, `rollback.group_stack`,
140 // `rollback.text_color_stack`, `pending_tooltips`, `hovered_groups`).
141 // Same `mem::take` pattern as `commands_buf` (#150). Each buffer is
142 // empty at frame end (asserted at `run_frame_kernel`) — `mem::take`
143 // hands a `Default::default()` empty back to the state, the Vec/HashSet
144 // we move into `Context` keeps its capacity from the prior frame, and
145 // `clear()` here is a no-op except as a defensive guard against future
146 // refactors that might leak items past the assertions.
147 let mut context_stack = std::mem::take(&mut state.context_stack_buf);
148 context_stack.clear();
149 let mut deferred_draws = std::mem::take(&mut state.deferred_draws_buf);
150 deferred_draws.clear();
151 let mut group_stack = std::mem::take(&mut state.group_stack_buf);
152 group_stack.clear();
153 let mut text_color_stack = std::mem::take(&mut state.text_color_stack_buf);
154 text_color_stack.clear();
155 let mut pending_tooltips = std::mem::take(&mut state.pending_tooltips_buf);
156 pending_tooltips.clear();
157 let hovered_groups = std::mem::take(&mut state.hovered_groups_buf);
158 // `hovered_groups` is `clear()`-ed inside `build_hovered_groups`
159 // immediately below, so we do not pre-clear here — capacity is
160 // preserved across frames.
161
162 // Issue #273: hand off the previous frame's `cached` region keys and a
163 // recycled (cleared) buffer to record this frame's keys into. Both
164 // round-trip back into `FrameState` at frame end. Empty (zero
165 // overhead) for apps that never call `cached`.
166 let region_versions_prev = std::mem::take(&mut state.region_versions);
167 let mut region_versions_cur = std::mem::take(&mut state.region_versions_buf);
168 region_versions_cur.clear();
169
170 let mut ctx = Self {
171 commands,
172 events,
173 consumed,
174 should_quit: false,
175 area_width: width,
176 area_height: height,
177 tick: diagnostics.tick,
178 focus_index,
179 hook_states: std::mem::take(hook_states),
180 named_states,
181 keyed_states,
182 chord,
183 context_stack,
184 prev_focus_count: focus.prev_focus_count,
185 prev_modal_focus_start: focus.prev_modal_focus_start,
186 prev_modal_focus_count: focus.prev_modal_focus_count,
187 prev_scroll_infos: std::mem::take(&mut layout_feedback.prev_scroll_infos),
188 prev_scroll_rects: std::mem::take(&mut layout_feedback.prev_scroll_rects),
189 prev_hit_map: std::mem::take(&mut layout_feedback.prev_hit_map),
190 prev_group_rects: std::mem::take(&mut layout_feedback.prev_group_rects),
191 prev_focus_groups: std::mem::take(&mut layout_feedback.prev_focus_groups),
192 mouse_pos,
193 click_pos,
194 right_click_pos,
195 double_click_pos,
196 scroll_pos,
197 scroll_delta_frame,
198 prev_modal_active: focus.prev_modal_active,
199 clipboard_text: None,
200 debug: diagnostics.debug_mode,
201 debug_layer: diagnostics.debug_layer,
202 inspector_mode: diagnostics.inspector_mode,
203 theme,
204 is_real_terminal: false,
205 // Issue #264: conservative default; overwritten by the probed
206 // snapshot in `run_frame_kernel` on a real terminal.
207 #[cfg(feature = "crossterm")]
208 capabilities: crate::terminal::Capabilities::default(),
209 deferred_draws,
210 rollback: ContextRollbackState {
211 last_text_idx: None,
212 focus_count: 0,
213 last_focusable_id: None,
214 pending_focusable_id: None,
215 interaction_count: 0,
216 scroll_count: 0,
217 group_count: 0,
218 group_stack,
219 overlay_depth: 0,
220 modal_active: false,
221 modal_focus_start: 0,
222 modal_focus_count: 0,
223 hook_cursor: 0,
224 dark_mode: theme.is_dark,
225 notification_queue: std::mem::take(&mut diagnostics.notification_queue),
226 text_color_stack,
227 },
228 pending_tooltips,
229 pending_screen_nav: Vec::new(),
230 screen_nav_depth: 0,
231 screen_nav_render_origins: std::collections::HashMap::new(),
232 hovered_groups,
233 region_versions_prev,
234 region_versions_cur,
235 region_cache_hits: 0,
236 region_cache_misses: 0,
237 scroll_lines_per_event: 1,
238 screen_hook_map,
239 widget_theme: WidgetTheme::new(),
240 prev_focus_index,
241 focus_name_map_prev,
242 focus_name_map: std::collections::HashMap::new(),
243 pending_focus_name: still_pending,
244 // Issue #248: sample a single wall-clock "now" for every timer
245 // method called this frame. v0.21.1: reuse the `frame_now` sampled
246 // above (also used for double-click timing) so the frame has one
247 // coherent clock reading.
248 frame_instant: frame_now,
249 scheduler,
250 // Issue #234: async task registry round-tripped like `scheduler`.
251 #[cfg(feature = "async")]
252 async_tasks,
253 };
254 ctx.build_hovered_groups();
255 ctx
256 }
257
258 fn build_hovered_groups(&mut self) {
259 self.hovered_groups.clear();
260 if let Some(pos) = self.mouse_pos {
261 for (name, rect) in &self.prev_group_rects {
262 if pos.0 >= rect.x
263 && pos.0 < rect.x + rect.width
264 && pos.1 >= rect.y
265 && pos.1 < rect.y + rect.height
266 {
267 self.hovered_groups.insert(std::sync::Arc::clone(name));
268 }
269 }
270 }
271 }
272
273 /// Set how many lines each scroll event moves. Default is 1.
274 pub fn set_scroll_speed(&mut self, lines: u32) {
275 self.scroll_lines_per_event = lines.max(1);
276 }
277
278 /// Get the current scroll speed (lines per scroll event).
279 pub fn scroll_speed(&self) -> u32 {
280 self.scroll_lines_per_event
281 }
282
283 /// Get the current focus index.
284 ///
285 /// Widget indices are assigned in the order [`register_focusable()`](Self::register_focusable) is called.
286 /// Indices are 0-based and wrap at [`focus_count()`](Self::focus_count).
287 pub fn focus_index(&self) -> usize {
288 self.focus_index
289 }
290
291 /// Set the focus index to a specific focusable widget.
292 ///
293 /// Widget indices are assigned in the order [`register_focusable()`](Self::register_focusable) is called
294 /// (0-based). If `index` exceeds the number of focusable widgets it will
295 /// be clamped by the modulo in [`register_focusable`](Self::register_focusable).
296 ///
297 /// # Example
298 ///
299 /// ```no_run
300 /// # slt::run(|ui: &mut slt::Context| {
301 /// // Focus the second focusable widget (index 1)
302 /// ui.set_focus_index(1);
303 /// # });
304 /// ```
305 pub fn set_focus_index(&mut self, index: usize) {
306 self.focus_index = index;
307 }
308
309 /// Get the number of focusable widgets registered in the previous frame.
310 ///
311 /// Returns 0 on the very first frame. Useful together with
312 /// [`set_focus_index()`](Self::set_focus_index) for programmatic focus control.
313 ///
314 /// Note: this intentionally reads `prev_focus_count` (the settled count
315 /// from the last completed frame) rather than `focus_count` (the
316 /// still-incrementing counter for the current frame).
317 #[allow(clippy::misnamed_getters)]
318 pub fn focus_count(&self) -> usize {
319 self.prev_focus_count
320 }
321
322 /// Advance keyboard focus one step, honoring an active modal's focus trap.
323 /// `forward` selects next vs previous; both wrap. Shared by
324 /// [`focus_next`](Self::focus_next) / [`focus_prev`](Self::focus_prev) and
325 /// the `Tab`/`Shift+Tab` handler in `process_focus_keys` (v0.21.1).
326 pub(crate) fn advance_focus(&mut self, forward: bool) {
327 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
328 let mut modal_local = self.focus_index.saturating_sub(self.prev_modal_focus_start);
329 modal_local %= self.prev_modal_focus_count;
330 let next = if forward {
331 (modal_local + 1) % self.prev_modal_focus_count
332 } else if modal_local == 0 {
333 self.prev_modal_focus_count - 1
334 } else {
335 modal_local - 1
336 };
337 self.focus_index = self.prev_modal_focus_start + next;
338 } else if self.prev_focus_count > 0 {
339 self.focus_index = if forward {
340 (self.focus_index + 1) % self.prev_focus_count
341 } else if self.focus_index == 0 {
342 self.prev_focus_count - 1
343 } else {
344 self.focus_index - 1
345 };
346 }
347 }
348
349 /// Move keyboard focus to the next focusable widget (wrapping), exactly as
350 /// pressing `Tab` would. Honors an active modal's focus trap. Pairs with
351 /// [`set_focus_index`](Self::set_focus_index) / [`focus_count`](Self::focus_count)
352 /// for programmatic focus control (e.g. an app-level shortcut). Available
353 /// since v0.21.1.
354 ///
355 /// # Example
356 ///
357 /// ```no_run
358 /// # slt::run(|ui: &mut slt::Context| {
359 /// // Advance focus on a custom shortcut (e.g. a vim-style 'j').
360 /// if ui.key('j') {
361 /// ui.focus_next();
362 /// }
363 /// # });
364 /// ```
365 pub fn focus_next(&mut self) {
366 self.advance_focus(true);
367 }
368
369 /// Move keyboard focus to the previous focusable widget (wrapping), exactly
370 /// as `Shift+Tab` would. Honors an active modal's focus trap. Available
371 /// since v0.21.1.
372 pub fn focus_prev(&mut self) {
373 self.advance_focus(false);
374 }
375
376 /// Move focus to the next focusable widget belonging to the named focus
377 /// group, wrapping within the group. If focus is currently outside the
378 /// group it jumps to the group's first member. No-op if the group had no
379 /// focusable widgets on the previous frame.
380 ///
381 /// Focus groups are declared with [`group`](Self::group); this is the
382 /// scoped counterpart to [`focus_next`](Self::focus_next) for building a
383 /// focus trap around a panel or sub-form without a modal. Available since
384 /// v0.21.1.
385 pub fn focus_next_in_group(&mut self, group: &str) {
386 self.advance_focus_in_group(group, true);
387 }
388
389 /// Move focus to the previous focusable widget in the named group
390 /// (wrapping). See [`focus_next_in_group`](Self::focus_next_in_group).
391 /// Available since v0.21.1.
392 pub fn focus_prev_in_group(&mut self, group: &str) {
393 self.advance_focus_in_group(group, false);
394 }
395
396 fn advance_focus_in_group(&mut self, group: &str, forward: bool) {
397 // Membership comes from the previous frame's `index -> group` table,
398 // the same source `is_group_focused` consults. Indices are valid
399 // focus indices (0..prev_focus_count).
400 let members: Vec<usize> = self
401 .prev_focus_groups
402 .iter()
403 .enumerate()
404 .filter_map(|(idx, g)| match g.as_deref() {
405 Some(name) if name == group => Some(idx),
406 _ => None,
407 })
408 .collect();
409 if members.is_empty() {
410 return;
411 }
412 let new_pos = match members.iter().position(|&m| m == self.focus_index) {
413 Some(p) => {
414 if forward {
415 (p + 1) % members.len()
416 } else if p == 0 {
417 members.len() - 1
418 } else {
419 p - 1
420 }
421 }
422 // Focus is outside the group: jump to its first member.
423 None => 0,
424 };
425 self.focus_index = members[new_pos];
426 }
427
428 /// Read-only snapshot of the terminal's negotiated capabilities
429 /// (issue #264).
430 ///
431 /// Populated once at session enter via a DA1/DA2/XTGETTCAP probe. This is
432 /// **diagnostics-only**: image rendering already routes through the
433 /// automatic blitter ladder (Kitty > Sixel > sextant > half-block), so app
434 /// code is never required to branch on the returned value. On a headless
435 /// backend (e.g. [`TestBackend`](crate::TestBackend)) or piped stdout, the
436 /// probe is skipped and every field is a conservative default.
437 ///
438 /// Available since `0.21.0`.
439 ///
440 /// # Example
441 ///
442 /// ```no_run
443 /// # slt::run(|ui: &mut slt::Context| {
444 /// let caps = ui.capabilities();
445 /// // e.g. surface a "truecolor: on" line in a diagnostics panel.
446 /// let _ = caps.truecolor;
447 /// # });
448 /// ```
449 #[cfg(feature = "crossterm")]
450 #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
451 pub fn capabilities(&self) -> &crate::terminal::Capabilities {
452 &self.capabilities
453 }
454
455 pub(crate) fn process_focus_keys(&mut self) {
456 // Scan for Tab / Shift+Tab / BackTab, recording the direction of each
457 // and consuming the event. The mutation (`advance_focus`) is applied
458 // after the scan: it borrows `&mut self` wholesale, which cannot run
459 // while `self.events` is iterated by reference. Collecting first
460 // preserves the original "each Tab advances once" semantics.
461 let mut actions: Vec<bool> = Vec::new();
462 for (i, event) in self.events.iter().enumerate() {
463 if self.consumed[i] {
464 continue;
465 }
466 if let Event::Key(key) = event {
467 if key.kind != KeyEventKind::Press {
468 continue;
469 }
470 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
471 actions.push(true);
472 self.consumed[i] = true;
473 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
474 || key.code == KeyCode::BackTab
475 {
476 actions.push(false);
477 self.consumed[i] = true;
478 }
479 }
480 }
481 for forward in actions {
482 self.advance_focus(forward);
483 }
484 }
485
486 /// Render a custom [`Widget`].
487 ///
488 /// Calls [`Widget::ui`] with this context and returns the widget's response.
489 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
490 w.ui(self)
491 }
492
493 /// Wrap child widgets in a panic boundary.
494 ///
495 /// If the closure panics, the panic is caught and an error message is
496 /// rendered in place of the children. The app continues running.
497 ///
498 /// # Example
499 ///
500 /// ```no_run
501 /// # slt::run(|ui: &mut slt::Context| {
502 /// ui.error_boundary(|ui| {
503 /// ui.text("risky widget");
504 /// });
505 /// # });
506 /// ```
507 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
508 self.error_boundary_with(f, |ui, msg| {
509 ui.styled(
510 format!("⚠ Error: {msg}"),
511 Style::new().fg(ui.theme.error).bold(),
512 );
513 });
514 }
515
516 /// Like [`error_boundary`](Self::error_boundary), but renders a custom
517 /// fallback instead of the default error message.
518 ///
519 /// The fallback closure receives the panic message as a [`String`].
520 ///
521 /// # Example
522 ///
523 /// ```no_run
524 /// # slt::run(|ui: &mut slt::Context| {
525 /// ui.error_boundary_with(
526 /// |ui| {
527 /// ui.text("risky widget");
528 /// },
529 /// |ui, msg| {
530 /// ui.text(format!("Recovered from panic: {msg}"));
531 /// },
532 /// );
533 /// # });
534 /// ```
535 pub fn error_boundary_with(
536 &mut self,
537 f: impl FnOnce(&mut Context),
538 fallback: impl FnOnce(&mut Context, String),
539 ) {
540 let snapshot = ContextCheckpoint::capture(self);
541
542 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
543 f(self);
544 }));
545
546 match result {
547 Ok(()) => {}
548 Err(panic_info) => {
549 if self.is_real_terminal {
550 #[cfg(feature = "crossterm")]
551 {
552 let _ = crossterm::terminal::enable_raw_mode();
553 let _ = crossterm::execute!(
554 std::io::stdout(),
555 crossterm::terminal::EnterAlternateScreen
556 );
557 }
558
559 #[cfg(not(feature = "crossterm"))]
560 {}
561 }
562
563 snapshot.restore(self);
564
565 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
566 (*s).to_string()
567 } else if let Some(s) = panic_info.downcast_ref::<String>() {
568 s.clone()
569 } else {
570 "widget panicked".to_string()
571 };
572
573 fallback(self, msg);
574 }
575 }
576 }
577
578 /// Reserve the next interaction slot without emitting a marker command.
579 pub(crate) fn reserve_interaction_slot(&mut self) -> usize {
580 let id = self.rollback.interaction_count;
581 self.rollback.interaction_count += 1;
582 id
583 }
584
585 /// Advance the interaction counter for structural commands that still
586 /// participate in hit-map indexing.
587 pub(crate) fn skip_interaction_slot(&mut self) {
588 self.reserve_interaction_slot();
589 }
590
591 /// Issue #273: record a [`ContainerBuilder::cached`] region's version key
592 /// at its (declaration-ordered) call site and classify it as a hit or
593 /// miss versus the previous frame.
594 ///
595 /// Returns `true` if `version_key` matches the value this call site
596 /// recorded last frame (a hit), `false` on a key change, a brand-new slot,
597 /// the first frame, or after a resize (all misses).
598 ///
599 /// This is purely an *author-declared stability signal*: the caller still
600 /// re-runs its closure every frame, so output stays byte-identical and the
601 /// immediate-mode invariant is preserved exactly. The hit/miss result is
602 /// recorded for diagnostics ([`Context::region_cache_hits`] /
603 /// [`Context::region_cache_misses`]) and to give a future cell-level cache
604 /// a sound, principle-preserving gate. See the type-level docs on
605 /// [`ContainerBuilder::cached`] for the full design rationale.
606 pub(crate) fn record_cached_region(&mut self, version_key: u64) -> bool {
607 let idx = self.region_versions_cur.len();
608 let hit = self
609 .region_versions_prev
610 .get(idx)
611 .is_some_and(|&prev| prev == version_key);
612 self.region_versions_cur.push(version_key);
613 if hit {
614 self.region_cache_hits = self.region_cache_hits.saturating_add(1);
615 } else {
616 self.region_cache_misses = self.region_cache_misses.saturating_add(1);
617 }
618 hit
619 }
620
621 /// Number of [`ContainerBuilder::cached`] regions this frame whose version
622 /// key was unchanged from the previous frame (cache hits).
623 ///
624 /// Diagnostics for the opt-in streaming cache (issue #273). A region is a
625 /// hit when its author-supplied `version_key` matches the value the same
626 /// call site recorded last frame; it misses on a key change, a new call
627 /// site, the first frame, or after a terminal resize.
628 ///
629 /// Since 0.21.0.
630 ///
631 /// # Example
632 /// ```no_run
633 /// # slt::run(|ui: &mut slt::Context| {
634 /// ui.container().cached(42, |ui| {
635 /// ui.text("stable chrome");
636 /// });
637 /// let _hits = ui.region_cache_hits();
638 /// # });
639 /// ```
640 pub fn region_cache_hits(&self) -> u32 {
641 self.region_cache_hits
642 }
643
644 /// Number of [`ContainerBuilder::cached`] regions this frame whose version
645 /// key changed (or was new / first-frame / post-resize) — cache misses.
646 ///
647 /// The counterpart to [`Context::region_cache_hits`]. See issue #273.
648 ///
649 /// Since 0.21.0.
650 ///
651 /// # Example
652 /// ```no_run
653 /// # slt::run(|ui: &mut slt::Context| {
654 /// ui.container().cached(7, |ui| {
655 /// ui.text("chrome");
656 /// });
657 /// let _misses = ui.region_cache_misses();
658 /// # });
659 /// ```
660 pub fn region_cache_misses(&self) -> u32 {
661 self.region_cache_misses
662 }
663
664 /// Reserve the next interaction ID and emit a marker command.
665 pub(crate) fn next_interaction_id(&mut self) -> usize {
666 let id = self.reserve_interaction_slot();
667 self.commands.push(Command::InteractionMarker(id));
668 id
669 }
670
671 /// Allocate a click/hover interaction slot and return the [`Response`].
672 ///
673 /// Use this in custom widgets to detect mouse clicks and hovers without
674 /// wrapping content in a container. Call it immediately before the text,
675 /// rich text, link, or container that should own the interaction rect.
676 /// Each call reserves one slot in the hit-test map, so the call order
677 /// must be stable across frames.
678 pub fn interaction(&mut self) -> Response {
679 if (self.rollback.modal_active || self.prev_modal_active)
680 && self.rollback.overlay_depth == 0
681 {
682 return Response::none();
683 }
684 let id = self.next_interaction_id();
685 self.response_for(id)
686 }
687
688 /// Compute and consume the `(gained_focus, lost_focus)` edge flags for the
689 /// widget most recently registered via [`register_focusable`].
690 ///
691 /// If that focusable lined up with the previously-focused widget index from
692 /// the prior frame, the focus change since maps directly to gained/lost.
693 /// Takes (consumes) the `last_focusable_id` marker so a single
694 /// `register_focusable` powers exactly one transition computation.
695 ///
696 /// Shared by [`begin_widget_interaction`](Self::begin_widget_interaction)
697 /// and the widgets that assemble their `Response` by hand rather than
698 /// through it (`text_input`, `slider`, `number_input`) — issue #208 left
699 /// those three reporting `gained_focus`/`lost_focus` as always-false; this
700 /// closes that gap (v0.21.1).
701 pub(crate) fn focus_transitions(&mut self, focused: bool) -> (bool, bool) {
702 if let Some(this_id) = self.rollback.last_focusable_id.take() {
703 let was_focused = self
704 .prev_focus_index
705 .map(|prev| prev == this_id)
706 .unwrap_or(false);
707 (focused && !was_focused, !focused && was_focused)
708 } else {
709 (false, false)
710 }
711 }
712
713 pub(crate) fn begin_widget_interaction(&mut self, focused: bool) -> (usize, Response) {
714 let interaction_id = self.next_interaction_id();
715 let mut response = self.response_for(interaction_id);
716 response.focused = focused;
717 let (gained, lost) = self.focus_transitions(focused);
718 response.gained_focus = gained;
719 response.lost_focus = lost;
720 (interaction_id, response)
721 }
722
723 pub(crate) fn consume_indices<I>(&mut self, indices: I)
724 where
725 I: IntoIterator<Item = usize>,
726 {
727 for index in indices {
728 self.consumed[index] = true;
729 }
730 }
731
732 pub(crate) fn available_key_presses(
733 &self,
734 ) -> impl Iterator<Item = (usize, &crate::event::KeyEvent)> + '_ {
735 self.events.iter().enumerate().filter_map(|(i, event)| {
736 if self.consumed[i] {
737 return None;
738 }
739 match event {
740 Event::Key(key) if key.kind == KeyEventKind::Press => Some((i, key)),
741 _ => None,
742 }
743 })
744 }
745
746 pub(crate) fn available_pastes(&self) -> impl Iterator<Item = (usize, &str)> + '_ {
747 self.events.iter().enumerate().filter_map(|(i, event)| {
748 if self.consumed[i] {
749 return None;
750 }
751 match event {
752 Event::Paste(text) => Some((i, text.as_str())),
753 _ => None,
754 }
755 })
756 }
757
758 pub(crate) fn left_clicks_in_rect(
759 &self,
760 rect: Rect,
761 ) -> impl Iterator<Item = (usize, &crate::event::MouseEvent)> + '_ {
762 self.mouse_events_in_rect(rect).filter_map(|(i, mouse)| {
763 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
764 Some((i, mouse))
765 } else {
766 None
767 }
768 })
769 }
770
771 pub(crate) fn mouse_events_in_rect(
772 &self,
773 rect: Rect,
774 ) -> impl Iterator<Item = (usize, &crate::event::MouseEvent)> + '_ {
775 self.events
776 .iter()
777 .enumerate()
778 .filter_map(move |(i, event)| {
779 if self.consumed[i] {
780 return None;
781 }
782
783 let Event::Mouse(mouse) = event else {
784 return None;
785 };
786
787 if mouse.x < rect.x
788 || mouse.x >= rect.right()
789 || mouse.y < rect.y
790 || mouse.y >= rect.bottom()
791 {
792 return None;
793 }
794
795 Some((i, mouse))
796 })
797 }
798
799 pub(crate) fn left_clicks_for_interaction(
800 &self,
801 interaction_id: usize,
802 ) -> Option<(Rect, Vec<(usize, &crate::event::MouseEvent)>)> {
803 let rect = self.prev_hit_map.get(interaction_id).copied()?;
804 let clicks = self.left_clicks_in_rect(rect).collect();
805 Some((rect, clicks))
806 }
807
808 pub(crate) fn consume_activation_keys(&mut self, focused: bool) -> bool {
809 if !focused {
810 return false;
811 }
812
813 // Activation keys (Enter / Space) are typically 0–1 per frame and
814 // bounded above by the simultaneous-keypress count from the input
815 // pipeline (well under 8 in practice). A `SmallVec` with an 8-slot
816 // inline capacity eliminates the per-focusable `Vec<usize>` heap
817 // allocation that showed up on every focused widget × every frame.
818 // Spillover beyond 8 falls back to the heap automatically. Closes #135.
819 let consumed: smallvec::SmallVec<[usize; 8]> = self
820 .available_key_presses()
821 .filter_map(|(i, key)| {
822 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
823 Some(i)
824 } else {
825 None
826 }
827 })
828 .collect();
829 let activated = !consumed.is_empty();
830 if activated {
831 // `consume_indices` takes `IntoIterator<Item = usize>` — `SmallVec`
832 // satisfies that bound directly, no signature change needed.
833 self.consume_indices(consumed);
834 }
835 activated
836 }
837
838 /// Register a widget as focusable and return whether it currently has focus.
839 ///
840 /// Call this in custom widgets that need keyboard focus. Each call increments
841 /// the internal focus counter, so the call order must be stable across frames.
842 ///
843 /// # Slot reservation by `register_focusable_named`
844 ///
845 /// If [`register_focusable_named`](Self::register_focusable_named) was
846 /// called immediately before this call, it has already allocated a
847 /// slot and bound a name to it; this call **reuses** that slot
848 /// instead of allocating a fresh one. That keeps the name binding
849 /// pointed at the widget the user sees rather than at a dummy slot.
850 pub fn register_focusable(&mut self) -> bool {
851 if (self.rollback.modal_active || self.prev_modal_active)
852 && self.rollback.overlay_depth == 0
853 {
854 self.rollback.last_focusable_id = None;
855 // Drop any pending reservation: the suppressed widget never
856 // attached, so reusing the reserved id from a later widget in
857 // the same frame would silently rebind the name to the wrong
858 // slot.
859 self.rollback.pending_focusable_id = None;
860 return false;
861 }
862 // Issue #217 follow-up: if `register_focusable_named` reserved a
863 // slot for us, reuse it (and skip the FocusMarker push — it was
864 // already emitted when the reservation was made). Otherwise,
865 // allocate a fresh slot the normal way.
866 let (id, freshly_allocated) =
867 if let Some(reserved) = self.rollback.pending_focusable_id.take() {
868 (reserved, false)
869 } else {
870 let id = self.rollback.focus_count;
871 self.rollback.focus_count += 1;
872 (id, true)
873 };
874 // Issue #208: remember this widget's focus id so the immediately
875 // following `begin_widget_interaction` call can compare against
876 // `prev_focus_index` and emit gained/lost focus signals.
877 self.rollback.last_focusable_id = Some(id);
878 if freshly_allocated {
879 self.commands.push(Command::FocusMarker(id));
880 }
881 if self.prev_modal_active
882 && self.prev_modal_focus_count > 0
883 && self.rollback.modal_active
884 && self.rollback.overlay_depth > 0
885 {
886 let mut modal_local_id = id.saturating_sub(self.rollback.modal_focus_start);
887 modal_local_id %= self.prev_modal_focus_count;
888 let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
889 modal_focus_idx %= self.prev_modal_focus_count;
890 return modal_local_id == modal_focus_idx;
891 }
892 if self.prev_focus_count == 0 {
893 return true;
894 }
895 self.focus_index % self.prev_focus_count == id
896 }
897
898 /// Create persistent state that survives across frames.
899 ///
900 /// Returns a `State<T>` handle. Access with `state.get(ui)` / `state.get_mut(ui)`.
901 ///
902 /// # Rules
903 /// - Must be called in the same order every frame (like React hooks)
904 /// - Do NOT call inside if/else that changes between frames
905 ///
906 /// # Example
907 /// ```ignore
908 /// let count = ui.use_state(|| 0i32);
909 /// let val = count.get(ui);
910 /// ui.text(format!("Count: {val}"));
911 /// if ui.button("+1").clicked {
912 /// *count.get_mut(ui) += 1;
913 /// }
914 /// ```
915 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
916 let idx = self.rollback.hook_cursor;
917 self.rollback.hook_cursor += 1;
918
919 if idx >= self.hook_states.len() {
920 self.hook_states.push(Box::new(init()));
921 }
922
923 State::from_idx(idx)
924 }
925
926 /// Component-local persistent state keyed by a stable id.
927 ///
928 /// Unlike [`use_state`](Self::use_state), this is **not order-dependent** —
929 /// the value is looked up by `id` instead of call position. Safe to call
930 /// inside conditional branches or reusable component functions.
931 ///
932 /// Returns a `State<T>` handle. Access with `state.get(ui)` /
933 /// `state.get_mut(ui)`. Persists across frames.
934 ///
935 /// # Scoping
936 ///
937 /// Keys are `&'static str` and live in a single global namespace per
938 /// `Context` (no automatic per-component scoping). Two calls with the same
939 /// `id` in the same frame share the same value, regardless of where they
940 /// occur in the tree. Pick unique ids — for example, prefix with a
941 /// component name (`"counter::value"`).
942 ///
943 /// # Naming
944 ///
945 /// The no-suffix form takes an `init` closure, matching
946 /// [`use_state`](Self::use_state)`(init)` and
947 /// [`use_state_keyed`](Self::use_state_keyed)`(id, init)`. Use
948 /// [`use_state_named_default`](Self::use_state_named_default) for the
949 /// `T: Default` shorthand.
950 ///
951 /// # Example
952 ///
953 /// ```no_run
954 /// fn counter(ui: &mut slt::Context) {
955 /// let count = ui.use_state_named("counter::value", || 0i32);
956 /// ui.text(format!("Count: {}", count.get(ui)));
957 /// if ui.button("+1").clicked {
958 /// *count.get_mut(ui) += 1;
959 /// }
960 /// }
961 /// ```
962 pub fn use_state_named<T: 'static>(
963 &mut self,
964 id: &'static str,
965 init: impl FnOnce() -> T,
966 ) -> State<T> {
967 self.named_states
968 .entry(id)
969 .or_insert_with(|| Box::new(init()));
970 State::from_named(id)
971 }
972
973 /// Like [`use_state_named`](Self::use_state_named), but uses
974 /// [`Default::default()`] to initialize the value on first call.
975 ///
976 /// Mirrors [`use_state_keyed_default`](Self::use_state_keyed_default): the
977 /// `_default` suffix means "no init closure, `T: Default` required".
978 ///
979 /// # Example
980 ///
981 /// ```no_run
982 /// # slt::run(|ui: &mut slt::Context| {
983 /// let value = ui.use_state_named_default::<i32>("counter::value");
984 /// ui.text(format!("{}", value.get(ui)));
985 /// # });
986 /// ```
987 pub fn use_state_named_default<T: 'static + Default>(&mut self, id: &'static str) -> State<T> {
988 self.use_state_named(id, T::default)
989 }
990
991 /// Deprecated alias for [`use_state_named`](Self::use_state_named).
992 ///
993 /// **Deprecated since 0.21.0**: the `_named` family now follows the
994 /// "no-suffix = init closure" convention so it matches
995 /// [`use_state`](Self::use_state) and
996 /// [`use_state_keyed`](Self::use_state_keyed). The init-closure form is now
997 /// spelled `use_state_named(id, init)`; the `T: Default` shorthand is
998 /// [`use_state_named_default`](Self::use_state_named_default).
999 ///
1000 /// # Example
1001 ///
1002 /// ```no_run
1003 /// # slt::run(|ui: &mut slt::Context| {
1004 /// // Old: ui.use_state_named_with("counter::value", || 0i32)
1005 /// let count = ui.use_state_named("counter::value", || 0i32);
1006 /// ui.text(format!("{}", count.get(ui)));
1007 /// # });
1008 /// ```
1009 #[deprecated(
1010 since = "0.21.0",
1011 note = "Renamed to `use_state_named` — the no-suffix form now takes the init closure, matching `use_state` / `use_state_keyed`."
1012 )]
1013 pub fn use_state_named_with<T: 'static>(
1014 &mut self,
1015 id: &'static str,
1016 init: impl FnOnce() -> T,
1017 ) -> State<T> {
1018 self.use_state_named(id, init)
1019 }
1020
1021 /// Smoothly animate between `0.0` and `1.0` driven by a boolean.
1022 ///
1023 /// Returns the current interpolated value (0.0..=1.0). When `value` is
1024 /// `true` the result tweens toward `1.0`; when `false` it tweens back
1025 /// toward `0.0`. The transition duration defaults to
1026 /// [`DEFAULT_ANIMATE_TICKS`](crate::anim::DEFAULT_ANIMATE_TICKS) (12 ticks
1027 /// ≈ 200 ms at 60 Hz). Use [`Context::animate_value`] for custom duration
1028 /// or non-binary targets.
1029 ///
1030 /// State is stored in the per-context named-state map under `id`. The
1031 /// id is `&'static str` (single global namespace per context), matching
1032 /// [`Context::use_state_named`]. Pick a unique key per call site — two
1033 /// `animate_bool` calls with the same id share state.
1034 ///
1035 /// On the first call, the value snaps to the target with no visible
1036 /// transition (so widgets that mount in their final state don't pop).
1037 ///
1038 /// # Example
1039 /// ```ignore
1040 /// let opacity = ui.animate_bool("sidebar::visible", is_open);
1041 /// // 0.0 ≤ opacity ≤ 1.0; use as alpha or visibility threshold.
1042 /// ```
1043 ///
1044 /// # See also
1045 ///
1046 /// - [`animate_value`](Self::animate_value) — the underlying primitive this
1047 /// delegates to; use it for a custom duration or a non-binary target.
1048 /// - [`Tween`](crate::Tween) — full control over easing and lifecycle.
1049 pub fn animate_bool(&mut self, id: &'static str, value: bool) -> f64 {
1050 let target = if value { 1.0 } else { 0.0 };
1051 self.animate_value(id, target, crate::anim::DEFAULT_ANIMATE_TICKS)
1052 }
1053
1054 /// Smoothly animate a `f64` value toward `target` over `duration_ticks`.
1055 ///
1056 /// Uses a linear-easing [`crate::Tween`] stored implicitly in the
1057 /// per-context named-state map under `id`. Returns the current
1058 /// interpolated value. On the first call the value snaps to `target`
1059 /// with no visible transition; on subsequent calls when `target`
1060 /// changes the tween is rebuilt starting from the current interpolated
1061 /// value, so retargeting mid-flight does not produce a jump.
1062 ///
1063 /// `duration_ticks == 0` snaps immediately to the new target.
1064 ///
1065 /// # Panics
1066 ///
1067 /// Panics if `id` is already bound in the named-state map to a value of a
1068 /// different type (e.g. a [`use_state_named`](Self::use_state_named) call
1069 /// reused the same id), since the stored entry then fails to downcast to
1070 /// the internal animation state:
1071 ///
1072 /// ```text
1073 /// animate_value: id {id} is already used for a different state type
1074 /// ```
1075 ///
1076 /// Pick a unique id per call site to avoid the collision.
1077 ///
1078 /// # Example
1079 /// ```ignore
1080 /// let bar_height = ui.animate_value("loading::bar", target_height, 30);
1081 /// ui.bar(bar_height);
1082 /// ```
1083 ///
1084 /// # Comparison with `Tween`
1085 /// Use this shorthand when you want zero boilerplate and linear easing
1086 /// is acceptable. For custom easing, a non-static key, or
1087 /// non-tick-based control, construct a [`crate::Tween`] explicitly via
1088 /// [`Context::use_state_named`](Self::use_state_named).
1089 ///
1090 /// # See also
1091 ///
1092 /// - [`animate_bool`](Self::animate_bool) — boolean-driven shorthand that
1093 /// tweens between `0.0` and `1.0`.
1094 /// - [`Tween`](crate::Tween) — explicit easing and lifecycle control.
1095 pub fn animate_value(&mut self, id: &'static str, target: f64, duration_ticks: u64) -> f64 {
1096 let tick = self.tick;
1097 let entry = self
1098 .named_states
1099 .entry(id)
1100 .or_insert_with(|| Box::new(crate::anim::AnimState::new(target, tick)));
1101 let state = entry
1102 .downcast_mut::<crate::anim::AnimState>()
1103 .unwrap_or_else(|| {
1104 panic!("animate_value: id {id:?} is already used for a different state type")
1105 });
1106 state.sample(target, duration_ticks, tick)
1107 }
1108
1109 /// One-shot frame-clock timer (issue #248).
1110 ///
1111 /// Returns `true` exactly once — on the first frame at or after `dur` has
1112 /// elapsed since the first `schedule` call for `id` — and `false` on every
1113 /// other frame, both before and after. Re-arm by calling
1114 /// [`cancel`](Self::cancel) and then `schedule` again.
1115 ///
1116 /// Wall-clock based ([`std::time::Instant`] sampled once at frame start),
1117 /// so it works with the default feature set and without the `async`
1118 /// feature. Precision is bounded by the run loop's `tick_rate` (the
1119 /// deadline is observed on the next frame after it elapses), so durations
1120 /// well below the frame cadence are not meaningful.
1121 ///
1122 /// The id lives in the same per-context namespace as
1123 /// [`use_state_named`](Self::use_state_named): pick a unique key per call
1124 /// site.
1125 ///
1126 /// # Example
1127 /// ```no_run
1128 /// use std::time::Duration;
1129 ///
1130 /// slt::run(|ui: &mut slt::Context| {
1131 /// if ui.schedule("splash::dismiss", Duration::from_millis(800)) {
1132 /// // Runs once, ~800ms after the first frame that called this.
1133 /// ui.text("Splash dismissed.");
1134 /// }
1135 /// })?;
1136 /// # Ok::<_, std::io::Error>(())
1137 /// ```
1138 pub fn schedule(&mut self, id: &'static str, dur: std::time::Duration) -> bool {
1139 let now = self.frame_instant;
1140 let slot = self
1141 .scheduler
1142 .named
1143 .entry(id)
1144 .or_insert_with(|| SchedulerSlot {
1145 started: now,
1146 kind: SchedKind::Once { dur, fired: false },
1147 touched_this_frame: false,
1148 });
1149 slot.touched_this_frame = true;
1150 let elapsed = now.saturating_duration_since(slot.started);
1151 match &mut slot.kind {
1152 SchedKind::Once { dur, fired } if !*fired && elapsed >= *dur => {
1153 *fired = true;
1154 true
1155 }
1156 // Not yet due, already fired, or a re-used id bound to a different
1157 // timer kind: do not fire (a typo can't crash the app).
1158 _ => false,
1159 }
1160 }
1161
1162 /// Recurring frame-clock timer (issue #248).
1163 ///
1164 /// Returns the number of whole `dur` intervals that elapsed since the
1165 /// previous frame this `id` was sampled: `0` on most frames, `1` typically,
1166 /// and `> 1` if the frame loop stalled past several intervals — so no ticks
1167 /// are silently dropped. The internal clock advances by exactly the
1168 /// returned number of intervals each frame, so counts never drift.
1169 ///
1170 /// Wall-clock based and `async`-free, like [`schedule`](Self::schedule).
1171 ///
1172 /// # Example
1173 /// ```no_run
1174 /// use std::time::Duration;
1175 ///
1176 /// slt::run(|ui: &mut slt::Context| {
1177 /// let ticks = ui.every("clock::second", Duration::from_secs(1));
1178 /// if ticks > 0 {
1179 /// // Advance a once-per-second animation by `ticks` steps.
1180 /// }
1181 /// })?;
1182 /// # Ok::<_, std::io::Error>(())
1183 /// ```
1184 pub fn every(&mut self, id: &'static str, dur: std::time::Duration) -> u32 {
1185 let now = self.frame_instant;
1186 let interval = dur.max(std::time::Duration::from_nanos(1));
1187 let slot = self
1188 .scheduler
1189 .named
1190 .entry(id)
1191 .or_insert_with(|| SchedulerSlot {
1192 started: now,
1193 kind: SchedKind::Every {
1194 interval,
1195 last: now,
1196 },
1197 touched_this_frame: false,
1198 });
1199 slot.touched_this_frame = true;
1200 match &mut slot.kind {
1201 SchedKind::Every { interval, last } => {
1202 let elapsed = now.saturating_duration_since(*last);
1203 let fired = crate::widgets::intervals_elapsed(elapsed, *interval);
1204 if fired > 0 {
1205 // Advance by exactly the intervals reported so counts never
1206 // drift, even across stalled frames.
1207 let advance = interval.saturating_mul(fired);
1208 *last = last.checked_add(advance).unwrap_or(now);
1209 }
1210 fired
1211 }
1212 _ => 0,
1213 }
1214 }
1215
1216 /// Debounce timer — the typeahead / search-as-you-type primitive (#248).
1217 ///
1218 /// Each frame where `dirty == true` resets the quiet window to `dur`.
1219 /// Returns `true` exactly once on the first frame after `dur` of quiet (no
1220 /// `dirty`), then stays `false` until the next dirty frame re-arms it. This
1221 /// mirrors Textual's `@work(exclusive=True)` debounce: collapse a burst of
1222 /// keystrokes so only the final, settled query runs.
1223 ///
1224 /// Wall-clock based and `async`-free, like [`schedule`](Self::schedule).
1225 ///
1226 /// # Example
1227 /// ```no_run
1228 /// use std::time::Duration;
1229 /// use slt::TextInputState;
1230 ///
1231 /// let mut query = TextInputState::with_placeholder("Search...");
1232 /// slt::run(move |ui: &mut slt::Context| {
1233 /// // `resp.changed` is true on the keystroke frame -> the dirty signal.
1234 /// let resp = ui.text_input(&mut query);
1235 /// // Fire the search only after 250ms of no typing.
1236 /// if ui.debounce("search::run", Duration::from_millis(250), resp.changed) {
1237 /// // run_search(&query.value());
1238 /// }
1239 /// })?;
1240 /// # Ok::<_, std::io::Error>(())
1241 /// ```
1242 pub fn debounce(&mut self, id: &'static str, dur: std::time::Duration, dirty: bool) -> bool {
1243 let now = self.frame_instant;
1244 let slot = self
1245 .scheduler
1246 .named
1247 .entry(id)
1248 .or_insert_with(|| SchedulerSlot {
1249 started: now,
1250 kind: SchedKind::Debounce {
1251 dur,
1252 quiet_started: now,
1253 fired: false,
1254 },
1255 touched_this_frame: false,
1256 });
1257 slot.touched_this_frame = true;
1258 match &mut slot.kind {
1259 SchedKind::Debounce {
1260 dur: slot_dur,
1261 quiet_started,
1262 fired,
1263 } => {
1264 *slot_dur = dur;
1265 if dirty {
1266 // Re-arm the quiet window from this frame.
1267 *quiet_started = now;
1268 *fired = false;
1269 false
1270 } else if !*fired && now.saturating_duration_since(*quiet_started) >= *slot_dur {
1271 *fired = true;
1272 true
1273 } else {
1274 false
1275 }
1276 }
1277 _ => false,
1278 }
1279 }
1280
1281 /// Exclusive-group claim — cancel stale work on supersede (issue #248).
1282 ///
1283 /// Within a `group`, only the most-recently-claimed `id` returns `true`;
1284 /// once a newer `id` claims the group, every prior `id` returns `false`
1285 /// from then on. Use it to cancel an in-flight typeahead query when a newer
1286 /// query supersedes it: pair with [`debounce`](Self::debounce) to fire the
1287 /// settled query, then guard the work with `exclusive` so only the latest
1288 /// claim proceeds.
1289 ///
1290 /// # Example
1291 /// ```no_run
1292 /// use std::time::Duration;
1293 ///
1294 /// slt::run(|ui: &mut slt::Context| {
1295 /// let query_id = "q-42"; // e.g. a per-keystroke sequence id
1296 /// if ui.exclusive("search", query_id) {
1297 /// // Only the latest claimed query runs; older ones are cancelled.
1298 /// }
1299 /// })?;
1300 /// # Ok::<_, std::io::Error>(())
1301 /// ```
1302 pub fn exclusive(&mut self, group: &'static str, id: &str) -> bool {
1303 let entry = self
1304 .scheduler
1305 .exclusive
1306 .entry(group.to_string())
1307 .or_default();
1308 if entry.winner == id {
1309 // The reigning claim re-polls itself: still the winner.
1310 return true;
1311 }
1312 if entry.retired.contains(id) {
1313 // A previously-superseded id can never win again: stale work stays
1314 // cancelled even if re-polled.
1315 return false;
1316 }
1317 // A new id supersedes the group: retire the old winner (if any) and
1318 // become the active claim.
1319 if !entry.winner.is_empty() {
1320 let old = std::mem::take(&mut entry.winner);
1321 entry.retired.insert(old);
1322 }
1323 entry.winner = id.to_string();
1324 true
1325 }
1326
1327 /// Drop the scheduler slot for `id`, re-arming it on the next
1328 /// [`schedule`](Self::schedule) / [`every`](Self::every) /
1329 /// [`debounce`](Self::debounce) call (issue #248).
1330 ///
1331 /// Accepts both `&'static str` and runtime-`String` ids: clears the slot
1332 /// from the named map and the dynamic-id map.
1333 ///
1334 /// # Example
1335 /// ```no_run
1336 /// use std::time::Duration;
1337 ///
1338 /// slt::run(|ui: &mut slt::Context| {
1339 /// if ui.schedule("retry", Duration::from_secs(5)) {
1340 /// // ...
1341 /// }
1342 /// if ui.key('r') {
1343 /// ui.cancel("retry"); // next `schedule("retry", ..)` starts fresh
1344 /// }
1345 /// })?;
1346 /// # Ok::<_, std::io::Error>(())
1347 /// ```
1348 pub fn cancel(&mut self, id: &str) {
1349 self.scheduler.named.remove(id);
1350 self.scheduler.keyed.remove(id);
1351 }
1352
1353 /// Wall-clock time elapsed since `id` was first scheduled, or `None` if no
1354 /// live timer slot exists for `id` (issue #248).
1355 ///
1356 /// Useful for progress UIs ("retrying in 3s…") that want the raw elapsed
1357 /// duration rather than a fire/no-fire signal. Measured against the same
1358 /// frame instant the timer methods use.
1359 ///
1360 /// # Example
1361 /// ```no_run
1362 /// use std::time::Duration;
1363 ///
1364 /// slt::run(|ui: &mut slt::Context| {
1365 /// ui.schedule("upload", Duration::from_secs(30));
1366 /// if let Some(elapsed) = ui.elapsed("upload") {
1367 /// ui.text(format!("Uploading for {}s", elapsed.as_secs()));
1368 /// }
1369 /// })?;
1370 /// # Ok::<_, std::io::Error>(())
1371 /// ```
1372 pub fn elapsed(&self, id: &str) -> Option<std::time::Duration> {
1373 let started = self
1374 .scheduler
1375 .named
1376 .get(id)
1377 .or_else(|| self.scheduler.keyed.get(id))
1378 .map(|slot| slot.started)?;
1379 Some(self.frame_instant.saturating_duration_since(started))
1380 }
1381
1382 /// Remove dynamic keyed state created by
1383 /// [`use_state_keyed`](Self::use_state_keyed).
1384 ///
1385 /// Returns `true` when a slot existed. Any old [`State`] handle for the
1386 /// removed id becomes invalid and will panic if used before the state is
1387 /// recreated by `use_state_keyed`.
1388 pub fn remove_state_keyed(&mut self, id: &str) -> bool {
1389 self.keyed_states.remove(id).is_some()
1390 }
1391
1392 /// Retain only dynamic keyed-state entries accepted by `keep`.
1393 ///
1394 /// Returns the number of removed entries. This is intended for long-lived
1395 /// dynamic lists where ids come from data and removed items should release
1396 /// their per-row state.
1397 pub fn retain_state_keyed(&mut self, mut keep: impl FnMut(&str) -> bool) -> usize {
1398 let before = self.keyed_states.len();
1399 self.keyed_states.retain(|key, _| keep(key));
1400 before - self.keyed_states.len()
1401 }
1402
1403 /// Number of live dynamic keyed-state entries.
1404 ///
1405 /// Diagnostic helper for spotting churn when using runtime ids.
1406 pub fn keyed_state_count(&self) -> usize {
1407 self.keyed_states.len()
1408 }
1409
1410 /// Push a value onto the context stack for the duration of `body`.
1411 ///
1412 /// Inside `body`, child widgets can call
1413 /// [`use_context::<T>()`](Self::use_context) or
1414 /// [`try_use_context::<T>()`](Self::try_use_context) to look up the
1415 /// nearest provided value of type `T`. Provides cascade in LIFO order:
1416 /// nested calls with the same `T` shadow outer ones.
1417 ///
1418 /// The value is automatically popped when `body` returns — including on
1419 /// panic, so the context stack is always restored.
1420 ///
1421 /// # Example
1422 ///
1423 /// ```ignore
1424 /// struct Theme { accent: slt::Color }
1425 /// ui.provide(Theme { accent: slt::Color::Red }, |ui| {
1426 /// // Any widget here can `let theme = ui.use_context::<Theme>();`
1427 /// render_button(ui);
1428 /// });
1429 /// ```
1430 pub fn provide<T: 'static, R>(&mut self, value: T, body: impl FnOnce(&mut Context) -> R) -> R {
1431 self.context_stack
1432 .push(Box::new(value) as Box<dyn std::any::Any>);
1433
1434 // catch_unwind ensures the entry is popped even if `body` panics, so
1435 // the context stack is never left with leaked frames. We re-panic
1436 // afterwards so the panic propagates normally to outer scopes.
1437 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
1438
1439 // Pop in both success and panic paths.
1440 self.context_stack.pop();
1441
1442 match result {
1443 Ok(value) => value,
1444 Err(panic) => std::panic::resume_unwind(panic),
1445 }
1446 }
1447
1448 /// Spawn a fire-and-forget async task from inside the frame closure.
1449 ///
1450 /// Returns a [`TaskHandle<T>`](crate::TaskHandle) you store and pass to
1451 /// [`poll`](Self::poll) on later frames to retrieve the result. This closes
1452 /// the ergonomics gap of the channel pattern (`run_async` + an external
1453 /// `Sender`) for the common case: "click a button, kick off one async call,
1454 /// show its result next frame" — without wiring a channel yourself.
1455 ///
1456 /// **Dropping the returned handle cancels the in-flight task.** Keep it
1457 /// alive (e.g. in `use_state`) for as long as you care about the result.
1458 /// Each handle carries a unique id, so two `TaskHandle<String>` live at the
1459 /// same time never cross their results.
1460 ///
1461 /// Requires the `async` feature and an active Tokio runtime — call it
1462 /// inside [`run_async`](crate::run_async) /
1463 /// [`run_async_with`](crate::run_async_with), which inject the runtime
1464 /// handle.
1465 ///
1466 /// # Panics
1467 ///
1468 /// Panics if no Tokio runtime was injected (e.g. when called from the sync
1469 /// [`run`](crate::run) loop or `TestBackend` without a runtime).
1470 ///
1471 /// # Example
1472 ///
1473 /// ```no_run
1474 /// # #[cfg(feature = "async")]
1475 /// # async fn run() -> std::io::Result<()> {
1476 /// use slt::{Context, RunConfig, TaskHandle};
1477 ///
1478 /// async fn fetch() -> String {
1479 /// // e.g. an HTTP request
1480 /// "result".to_string()
1481 /// }
1482 ///
1483 /// slt::run_async_with(RunConfig::default(), |ui: &mut Context, _: &mut Vec<()>| {
1484 /// // One handle, stored across frames via `use_state`.
1485 /// let handle = ui.use_state(|| None::<TaskHandle<String>>);
1486 ///
1487 /// if ui.button("Fetch").clicked && handle.get(ui).is_none() {
1488 /// *handle.get_mut(ui) = Some(ui.spawn(async { fetch().await }));
1489 /// }
1490 ///
1491 /// // Take the handle out of state to poll it: `ui.poll` needs `&mut ui`,
1492 /// // which cannot coexist with a `&TaskHandle` borrowed from `ui`'s own
1493 /// // state. Put it back if the task is still pending.
1494 /// if let Some(h) = handle.get_mut(ui).take() {
1495 /// match ui.poll(&h) {
1496 /// Some(result) => {
1497 /// ui.text(format!("Got: {result}"));
1498 /// }
1499 /// None => {
1500 /// *handle.get_mut(ui) = Some(h);
1501 /// ui.text("Loading...");
1502 /// }
1503 /// }
1504 /// }
1505 /// })?;
1506 /// # Ok(())
1507 /// # }
1508 /// ```
1509 #[cfg(feature = "async")]
1510 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
1511 pub fn spawn<T: Send + 'static>(
1512 &mut self,
1513 fut: impl std::future::Future<Output = T> + Send + 'static,
1514 ) -> TaskHandle<T> {
1515 self.async_tasks.spawn(fut)
1516 }
1517
1518 /// Poll a [`TaskHandle`](crate::TaskHandle) for its result.
1519 ///
1520 /// Returns `Some(result)` exactly once — on the first frame after the task
1521 /// completes — then `None` on every subsequent call. Returns `None` while
1522 /// the task is still in flight.
1523 ///
1524 /// Pairs with [`spawn`](Self::spawn). Requires the `async` feature.
1525 ///
1526 /// # Example
1527 ///
1528 /// ```no_run
1529 /// # #[cfg(feature = "async")]
1530 /// # fn ex(ui: &mut slt::Context, handle: &slt::TaskHandle<u32>) {
1531 /// if let Some(value) = ui.poll(handle) {
1532 /// ui.text(format!("done: {value}"));
1533 /// }
1534 /// # }
1535 /// ```
1536 #[cfg(feature = "async")]
1537 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
1538 pub fn poll<T: 'static>(&mut self, handle: &TaskHandle<T>) -> Option<T> {
1539 self.async_tasks.poll::<T>(handle.id())
1540 }
1541
1542 /// Look up the nearest provided value of type `T` on the context stack.
1543 ///
1544 /// Searches from the top of the stack (most-recent
1545 /// [`provide`](Self::provide)) downward. Returns the first match.
1546 ///
1547 /// # Panics
1548 ///
1549 /// Panics if no value of type `T` is currently provided. Use
1550 /// [`try_use_context`](Self::try_use_context) for a non-panicking variant.
1551 pub fn use_context<T: 'static>(&self) -> &T {
1552 self.try_use_context::<T>().unwrap_or_else(|| {
1553 panic!(
1554 "no context of type {} was provided; use ui.provide(value, |ui| ...) in a parent scope",
1555 std::any::type_name::<T>()
1556 )
1557 })
1558 }
1559
1560 /// Like [`use_context`](Self::use_context), but returns `None` instead of
1561 /// panicking when no value of type `T` is on the stack.
1562 pub fn try_use_context<T: 'static>(&self) -> Option<&T> {
1563 self.context_stack
1564 .iter()
1565 .rev()
1566 .find_map(|entry| entry.downcast_ref::<T>())
1567 }
1568
1569 /// Memoize a computed value. Recomputes only when `deps` changes.
1570 ///
1571 /// Returns a [`Memo<T>`] *index handle*, mirroring [`use_state`]'s
1572 /// [`State<T>`]. The handle holds **no** borrow of `ui`, so it composes with
1573 /// later `ui.*` calls — read the value on demand with `.get(ui)` /
1574 /// `.copied(ui)`.
1575 ///
1576 /// Before v0.21.0 this returned `&T`, a live borrow of `&mut Context` that
1577 /// could not be held across subsequent `ui.*` mutations. That form is now
1578 /// [`use_memo_ref`](Self::use_memo_ref) (deprecated). Migrate
1579 /// `let x = *ui.use_memo(&d, f);` to `let x = ui.use_memo(&d, f).copied(ui);`.
1580 ///
1581 /// [`use_state`]: Self::use_state
1582 ///
1583 /// # Panics
1584 ///
1585 /// Panics if the hook slot at this call position was previously used for a
1586 /// different hook (a rules-of-hooks / call-order violation), since the
1587 /// type-erased slot then fails to downcast to `MemoSlot<T>`:
1588 ///
1589 /// ```text
1590 /// Hook type mismatch at index {idx}: expected {type}. Hooks must be called in the same order every frame.
1591 /// ```
1592 ///
1593 /// Keep hook calls in the same order every frame — do not call this inside
1594 /// an `if`/`else` whose branch changes between frames.
1595 ///
1596 /// # Example
1597 /// ```no_run
1598 /// # slt::run(|ui: &mut slt::Context| {
1599 /// let count = ui.use_state(|| 0i32);
1600 /// let count_val = *count.get(ui);
1601 /// let doubled = ui.use_memo(&count_val, |c| c * 2);
1602 /// // The handle survives an intervening `ui.*` call (this is the whole point).
1603 /// ui.text("doubled:");
1604 /// ui.text(format!("{}", doubled.copied(ui)));
1605 /// # });
1606 /// ```
1607 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1608 &mut self,
1609 deps: &D,
1610 compute: impl FnOnce(&D) -> T,
1611 ) -> Memo<T> {
1612 let idx = self.rollback.hook_cursor;
1613 self.rollback.hook_cursor += 1;
1614
1615 // First call at this slot: allocate fresh state. Deps are stored
1616 // type-erased so the read path (`Memo::get`) can downcast `MemoSlot<T>`
1617 // without restating `D`.
1618 if idx >= self.hook_states.len() {
1619 self.hook_states.push(Box::new(MemoSlot {
1620 deps: Box::new(deps.clone()),
1621 value: compute(deps),
1622 }));
1623 return Memo::from_idx(idx);
1624 }
1625
1626 // Slot already exists: it must be the same `MemoSlot<T>` shape we used
1627 // last frame, or the caller broke the rules-of-hooks contract.
1628 match self.hook_states[idx].downcast_mut::<MemoSlot<T>>() {
1629 Some(slot) => {
1630 // Compare against the previous (type-erased) deps. A failed
1631 // downcast of the stored deps to `&D` is treated as stale so the
1632 // value is recomputed rather than silently kept.
1633 let stale = slot
1634 .deps
1635 .downcast_ref::<D>()
1636 .map(|prev| *prev != *deps)
1637 .unwrap_or(true);
1638 if stale {
1639 slot.deps = Box::new(deps.clone());
1640 slot.value = compute(deps);
1641 }
1642 }
1643 None => panic!(
1644 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
1645 idx,
1646 std::any::type_name::<MemoSlot<T>>()
1647 ),
1648 }
1649 Memo::from_idx(idx)
1650 }
1651
1652 /// Deprecated `&T`-returning form of [`use_memo`](Self::use_memo).
1653 ///
1654 /// **Deprecated since 0.21.0**: [`use_memo`](Self::use_memo) now returns a
1655 /// [`Memo<T>`] handle that does not borrow `ui`, so it composes with later
1656 /// `ui.*` calls. This alias preserves the original behaviour (returning a
1657 /// `&T` borrow of `ui`) for callers that cannot migrate immediately; the
1658 /// borrow keeps `ui` immutably borrowed until the reference is dropped.
1659 ///
1660 /// Migrate `let x = *ui.use_memo_ref(&d, f);` to
1661 /// `let x = ui.use_memo(&d, f).copied(ui);` (or `.get(ui)` for a reference).
1662 ///
1663 /// # Panics
1664 ///
1665 /// Panics if the hook slot at this call position was previously used for a
1666 /// different hook (a rules-of-hooks / call-order violation), since the
1667 /// type-erased slot then fails to downcast to `(D, T)`:
1668 ///
1669 /// ```text
1670 /// Hook type mismatch at index {idx}: expected {type}. Hooks must be called in the same order every frame.
1671 /// ```
1672 ///
1673 /// # Example
1674 /// ```no_run
1675 /// # slt::run(|ui: &mut slt::Context| {
1676 /// # #[allow(deprecated)]
1677 /// let doubled = *ui.use_memo_ref(&21i32, |c| c * 2);
1678 /// ui.text(format!("{doubled}"));
1679 /// # });
1680 /// ```
1681 #[deprecated(
1682 since = "0.21.0",
1683 note = "use_memo now returns a Memo<T> handle; call `.get(ui)` / `.copied(ui)`"
1684 )]
1685 pub fn use_memo_ref<T: 'static, D: PartialEq + Clone + 'static>(
1686 &mut self,
1687 deps: &D,
1688 compute: impl FnOnce(&D) -> T,
1689 ) -> &T {
1690 let idx = self.rollback.hook_cursor;
1691 self.rollback.hook_cursor += 1;
1692
1693 // First call at this slot: allocate fresh state.
1694 if idx >= self.hook_states.len() {
1695 let value = compute(deps);
1696 self.hook_states.push(Box::new((deps.clone(), value)));
1697 return self.hook_states[idx]
1698 .downcast_ref::<(D, T)>()
1699 .map(|(_, v)| v)
1700 .expect("freshly inserted slot must downcast to its own type");
1701 }
1702
1703 // Slot already exists: it must be the same `(D, T)` shape we used last
1704 // frame, or the caller broke the rules-of-hooks contract.
1705 //
1706 // Single downcast on the cache-hit path (closes #133): use
1707 // `downcast_mut` to update deps/value in place when they change, and
1708 // return `&stored.1` directly — eliminating the redundant second
1709 // `downcast_ref` that ran on every call regardless of cache state.
1710 match self.hook_states[idx].downcast_mut::<(D, T)>() {
1711 Some(stored) => {
1712 if stored.0 != *deps {
1713 stored.0 = deps.clone();
1714 stored.1 = compute(deps);
1715 }
1716 &stored.1
1717 }
1718 None => panic!(
1719 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
1720 idx,
1721 std::any::type_name::<(D, T)>()
1722 ),
1723 }
1724 }
1725
1726 /// Returns `light` color if current theme is light mode, `dark` color if dark mode.
1727 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
1728 if self.theme.is_dark { dark } else { light }
1729 }
1730
1731 /// Show a toast notification without managing ToastState.
1732 ///
1733 /// # Examples
1734 /// ```
1735 /// # use slt::*;
1736 /// # TestBackend::new(80, 24).render(|ui| {
1737 /// ui.notify("File saved!", ToastLevel::Success);
1738 /// # });
1739 /// ```
1740 pub fn notify(&mut self, message: &str, level: ToastLevel) {
1741 let tick = self.tick;
1742 self.rollback
1743 .notification_queue
1744 .push((message.to_string(), level, tick));
1745 }
1746
1747 pub(crate) fn render_notifications(&mut self) {
1748 let tick = self.tick;
1749 self.rollback
1750 .notification_queue
1751 .retain(|(_, _, created)| tick.saturating_sub(*created) < 180);
1752 if self.rollback.notification_queue.is_empty() {
1753 return;
1754 }
1755
1756 // The `overlay` closure captures `self` mutably, so we cannot keep an
1757 // immutable borrow of `self.rollback.notification_queue` alive across
1758 // the call. Move the queue out for the render, then move it back —
1759 // no `String::clone` per notification, no intermediate `Vec` alloc.
1760 // Closes the non-empty path of #138.
1761 let queue = std::mem::take(&mut self.rollback.notification_queue);
1762 let theme = self.theme;
1763
1764 let _ = self.overlay(|ui| {
1765 let _ = ui.row(|ui| {
1766 ui.spacer();
1767 let _ = ui.col(|ui| {
1768 for (message, level, _) in queue.iter().rev() {
1769 let color = match level {
1770 ToastLevel::Info => theme.primary,
1771 ToastLevel::Success => theme.success,
1772 ToastLevel::Warning => theme.warning,
1773 ToastLevel::Error => theme.error,
1774 };
1775 let mut line = String::with_capacity(2 + message.len());
1776 line.push_str("● ");
1777 line.push_str(message);
1778 ui.styled(line, Style::new().fg(color));
1779 }
1780 });
1781 });
1782 });
1783
1784 // Restore the queue so subsequent frames can re-render until each
1785 // entry's TTL expires above.
1786 self.rollback.notification_queue = queue;
1787 }
1788
1789 // ----------------------------------------------------------------
1790 // v0.20.0 hooks: keyed state, effects, named focus, key gating
1791 // ----------------------------------------------------------------
1792
1793 /// Component-local persistent state keyed by a runtime string.
1794 ///
1795 /// Unlike [`use_state_named`](Self::use_state_named), `id` can be a
1796 /// runtime value such as `format!("row-{i}")`. The key is converted to
1797 /// `String` once per call. The hot path (key already present) performs
1798 /// **zero string allocations beyond the [`Into<String>`] conversion at
1799 /// the call site** — first looking up by `&str`, only allocating a
1800 /// fresh map key on first insert. Together: at most **one allocation
1801 /// per call, regardless of cache state**.
1802 ///
1803 /// # When to use
1804 /// - Per-item state in a dynamic list where positional [`use_state`]
1805 /// would break if items are reordered or filtered.
1806 /// - Reusable component functions called with a runtime discriminator.
1807 ///
1808 /// # Namespace
1809 /// Keys live in a single global namespace per `Context`. Prefix them
1810 /// to avoid collisions: `format!("my_component::item-{i}")`.
1811 ///
1812 /// # Stale entries
1813 /// Removed items leak their state until the `Context` is dropped (or
1814 /// the program exits). For long-running sessions with churn, manage
1815 /// state externally via a single `Vec<T>` in [`use_state`].
1816 ///
1817 /// # Example
1818 ///
1819 /// ```ignore
1820 /// for (i, item) in items.iter().enumerate() {
1821 /// let row_state = ui.use_state_keyed(format!("row-{i}"), || ItemState::default());
1822 /// // ...
1823 /// }
1824 /// ```
1825 ///
1826 /// [`use_state`]: Self::use_state
1827 pub fn use_state_keyed<T: 'static>(
1828 &mut self,
1829 id: impl Into<String>,
1830 init: impl FnOnce() -> T,
1831 ) -> State<T> {
1832 let key: String = id.into();
1833 // Lookup by `&str` first to avoid cloning on the hot
1834 // (already-populated) path. Only on first insert do we clone the
1835 // key into the map; otherwise the original `key` String is the
1836 // sole allocation and is moved into `State::from_keyed`.
1837 if !self.keyed_states.contains_key(key.as_str()) {
1838 self.keyed_states.insert(key.clone(), Box::new(init()));
1839 }
1840 State::from_keyed(key)
1841 }
1842
1843 /// Like [`use_state_keyed`](Self::use_state_keyed), but uses
1844 /// [`Default::default()`] to initialize the value on first call.
1845 ///
1846 /// # Example
1847 ///
1848 /// ```ignore
1849 /// let counter = ui.use_state_keyed_default::<i32>(format!("c-{i}"));
1850 /// ```
1851 pub fn use_state_keyed_default<T: Default + 'static>(
1852 &mut self,
1853 id: impl Into<String>,
1854 ) -> State<T> {
1855 self.use_state_keyed(id, T::default)
1856 }
1857
1858 /// Run a side-effecting closure when `deps` changes.
1859 ///
1860 /// On the **first frame** the hook slot is encountered, `f` is called
1861 /// unconditionally. On **subsequent frames**, `f` is only called when
1862 /// `*deps != stored_deps`. The hook is **positional** (same ordering
1863 /// rules as [`use_state`](Self::use_state)).
1864 ///
1865 /// # Fire-and-forget semantics
1866 ///
1867 /// There is no cleanup callback. If setup resources need teardown,
1868 /// store a handle in [`use_state`](Self::use_state) and drop it on
1869 /// a later frame.
1870 ///
1871 /// # Caveat: `error_boundary` re-fire
1872 ///
1873 /// Effects placed inside an [`error_boundary`](Self::error_boundary)
1874 /// scope can re-fire when the boundary catches a panic and rolls back
1875 /// the hook slots. For non-idempotent side effects (network requests,
1876 /// payments) put the effect outside the boundary or guard with an
1877 /// idempotency key.
1878 ///
1879 /// # Panics
1880 ///
1881 /// Panics if the hook slot at this call position was previously used for a
1882 /// different hook (a rules-of-hooks / call-order violation), since the
1883 /// type-erased slot then fails to downcast to the deps type `D`:
1884 ///
1885 /// ```text
1886 /// Hook type mismatch at index {idx}: expected {type}. Hooks must be called in the same order every frame.
1887 /// ```
1888 ///
1889 /// # Common patterns
1890 ///
1891 /// ```ignore
1892 /// // Run once on first frame:
1893 /// ui.use_effect(|_| initialize_logger(), &());
1894 ///
1895 /// // Run when `selected_tab` changes:
1896 /// ui.use_effect(|tab| load_tab_data(*tab), &selected_tab);
1897 /// ```
1898 pub fn use_effect<D: PartialEq + Clone + 'static>(&mut self, f: impl FnOnce(&D), deps: &D) {
1899 let idx = self.rollback.hook_cursor;
1900 self.rollback.hook_cursor += 1;
1901
1902 if idx >= self.hook_states.len() {
1903 // First encounter: run the effect, then store the deps so we
1904 // can detect future changes.
1905 f(deps);
1906 self.hook_states.push(Box::new(deps.clone()));
1907 return;
1908 }
1909
1910 match self.hook_states[idx].downcast_mut::<D>() {
1911 Some(stored) => {
1912 if *stored != *deps {
1913 f(deps);
1914 *stored = deps.clone();
1915 }
1916 }
1917 None => panic!(
1918 "Hook type mismatch at index {idx}: expected {}. \
1919 Hooks must be called in the same order every frame.",
1920 std::any::type_name::<D>()
1921 ),
1922 }
1923 }
1924
1925 /// Register a focusable slot bound to a stable string name.
1926 ///
1927 /// Returns `true` if the registered slot currently has focus, exactly
1928 /// like [`register_focusable`](Self::register_focusable) — but also
1929 /// records the `name → slot` mapping so other code can later call
1930 /// [`focus_by_name`](Self::focus_by_name) and
1931 /// [`focused_name`](Self::focused_name).
1932 ///
1933 /// # How the slot is shared with the widget that follows
1934 ///
1935 /// Every SLT widget that takes focus (`button`, `text_input`,
1936 /// `tabs`, …) internally calls `register_focusable()` to claim its
1937 /// own slot. To keep the name pointed at the **widget the user
1938 /// sees**, this call:
1939 ///
1940 /// 1. allocates a slot eagerly (so the name binding works even when
1941 /// no widget follows — useful for tests and for custom focusable
1942 /// regions),
1943 /// 2. records the `name → slot` mapping into the frame's
1944 /// `focus_name_map` (first-write-wins on duplicate names within
1945 /// a frame),
1946 /// 3. **reserves** the slot id so the next `register_focusable()`
1947 /// on the same frame *reuses* it instead of allocating a fresh
1948 /// slot — that's how `text_input(&mut state)` placed right after
1949 /// inherits the name.
1950 ///
1951 /// Names are re-registered each frame; the previous frame's map is
1952 /// kept under `focus_name_map_prev` so [`focus_by_name`](Context::focus_by_name) can resolve
1953 /// a name that has already been registered.
1954 ///
1955 /// # Two valid usage shapes
1956 ///
1957 /// **Shape A — name a widget that follows immediately** (the common
1958 /// pattern; the widget reuses the reserved slot):
1959 ///
1960 /// ```ignore
1961 /// let _ = ui.register_focusable_named("search");
1962 /// let _ = ui.text_input(&mut search_state);
1963 /// // later: ui.focus_by_name("search") jumps to the text_input
1964 /// ```
1965 ///
1966 /// **Shape B — register a named focusable region with no inner
1967 /// widget** (e.g. a custom render area that handles its own keys
1968 /// when focused):
1969 ///
1970 /// ```ignore
1971 /// let focused = ui.register_focusable_named("canvas");
1972 /// if focused { /* react to keys via key_presses_when */ }
1973 /// ```
1974 pub fn register_focusable_named(&mut self, name: &str) -> bool {
1975 // Modal/overlay suppression: when a modal is active and we're not
1976 // inside it, focusables outside the modal must be invisible to
1977 // tab/click cycling. Drop the registration entirely (no slot
1978 // allocation, no name binding, no reservation leak).
1979 if (self.rollback.modal_active || self.prev_modal_active)
1980 && self.rollback.overlay_depth == 0
1981 {
1982 self.rollback.pending_focusable_id = None;
1983 return false;
1984 }
1985 // Eagerly allocate the slot — symmetric with `register_focusable`,
1986 // so the slot exists even when no widget follows.
1987 let id = self.rollback.focus_count;
1988 self.rollback.focus_count += 1;
1989 self.rollback.last_focusable_id = Some(id);
1990 self.commands.push(Command::FocusMarker(id));
1991 // First-write-wins on duplicate names within a single frame —
1992 // a second `register_focusable_named("dup")` keeps the first
1993 // slot bound to the name and orphans its own slot's name binding.
1994 self.focus_name_map.entry(name.to_string()).or_insert(id);
1995 // Reserve `id` for the very next `register_focusable()` call to
1996 // reuse, so widgets like `text_input` placed immediately after
1997 // share the named slot rather than allocating a fresh one.
1998 // Last-write-wins on the reservation: stacking two
1999 // `register_focusable_named` calls without an intervening widget
2000 // leaves the second slot reserved (the first slot stays bound to
2001 // its name in `focus_name_map`, just without a widget attached).
2002 self.rollback.pending_focusable_id = Some(id);
2003 // Same focus-index prediction as `register_focusable`.
2004 if self.prev_modal_active
2005 && self.prev_modal_focus_count > 0
2006 && self.rollback.modal_active
2007 && self.rollback.overlay_depth > 0
2008 {
2009 let mut modal_local_id = id.saturating_sub(self.rollback.modal_focus_start);
2010 modal_local_id %= self.prev_modal_focus_count;
2011 let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
2012 modal_focus_idx %= self.prev_modal_focus_count;
2013 return modal_local_id == modal_focus_idx;
2014 }
2015 if self.prev_focus_count == 0 {
2016 return true;
2017 }
2018 self.focus_index % self.prev_focus_count == id
2019 }
2020
2021 /// Request focus on the named widget.
2022 ///
2023 /// If the named widget was registered last frame the focus change
2024 /// takes effect at the **start of the next frame** (one-frame delay
2025 /// is the deferred-command pattern used throughout SLT). If the name
2026 /// has never been registered, the request stays pending: the next
2027 /// frame to register that name receives focus.
2028 ///
2029 /// Returns `true` if the call **will** resolve — i.e. the name was
2030 /// either registered earlier in this frame (via
2031 /// [`register_focusable_named`](Self::register_focusable_named)) or in
2032 /// the previous frame. Returns `false` only when the name has not been
2033 /// seen by either frame, in which case the request stays pending until
2034 /// some future frame registers the name.
2035 ///
2036 /// # Example
2037 ///
2038 /// ```ignore
2039 /// if ui.button("Find").clicked {
2040 /// ui.focus_by_name("search");
2041 /// }
2042 /// ```
2043 pub fn focus_by_name(&mut self, name: &str) -> bool {
2044 // Resolve against either the previous frame's settled map or the
2045 // in-progress map being built right now. The latter handles the
2046 // common "register, then focus_by_name in the same frame" pattern
2047 // that callers naturally expect to return `true`.
2048 //
2049 // The actual focus change still lands at the start of the next
2050 // frame via `focus_name_map_prev` lookup in `Context::new`. The
2051 // return value is purely about resolvability: "true" means the name
2052 // is known and the focus shift will land next frame; "false" means
2053 // the request is pending a future registration.
2054 let resolved =
2055 self.focus_name_map_prev.contains_key(name) || self.focus_name_map.contains_key(name);
2056 // Always store the request — even if it resolved this frame, the
2057 // next-frame plumbing (`Context::new`) is what actually applies
2058 // the index. We use take/replace so the caller cannot stack two
2059 // pending names; the most recent wins.
2060 self.pending_focus_name = Some(name.to_string());
2061 resolved
2062 }
2063
2064 /// Return the name of the currently focused widget, if it was
2065 /// registered with
2066 /// [`register_focusable_named`](Self::register_focusable_named) this
2067 /// frame.
2068 ///
2069 /// Returns `None` if the focused widget used the unnamed
2070 /// [`register_focusable`](Self::register_focusable) API or if no widget
2071 /// has focus.
2072 pub fn focused_name(&self) -> Option<&str> {
2073 // Search this frame's map for the entry whose index equals
2074 // `focus_index`. The map is small (one entry per named focusable),
2075 // so a linear scan is fine — typical apps register <50 names.
2076 self.focus_name_map
2077 .iter()
2078 .find_map(|(name, &idx)| (idx == self.focus_index).then_some(name.as_str()))
2079 }
2080
2081 /// Iterate unconsumed key-press events, gated on `active`.
2082 ///
2083 /// When `active` is `false`, returns an empty iterator. When `active`
2084 /// is `true`, behaves identically to the internal
2085 /// `available_key_presses`. The returned indices are valid for
2086 /// [`consume_event`](Self::consume_event).
2087 ///
2088 /// This is the **preferred pattern** for focus-gated keyboard handling
2089 /// in custom widgets. Because the iterator borrows `self.events`
2090 /// immutably, collect the indices first and consume them after the
2091 /// loop:
2092 ///
2093 /// ```ignore
2094 /// let focused = ui.register_focusable();
2095 /// let mut hits: Vec<usize> = Vec::new();
2096 /// for (i, key) in ui.key_presses_when(focused) {
2097 /// if key.code == slt::KeyCode::Enter {
2098 /// hits.push(i);
2099 /// // ... handle Enter ...
2100 /// }
2101 /// }
2102 /// for i in hits { ui.consume_event(i); }
2103 /// ```
2104 pub fn key_presses_when(
2105 &self,
2106 active: bool,
2107 ) -> impl Iterator<Item = (usize, &crate::event::KeyEvent)> + '_ {
2108 // The `!active` short-circuit at the head of the predicate yields
2109 // an empty iterator at zero allocation cost when the widget isn't
2110 // focused. Indices are still drawn from `self.events` so callers
2111 // can pass them straight to `consume_event`.
2112 self.events
2113 .iter()
2114 .enumerate()
2115 .filter_map(move |(i, event)| {
2116 if !active {
2117 return None;
2118 }
2119 if self.consumed.get(i).copied().unwrap_or(true) {
2120 return None;
2121 }
2122 match event {
2123 Event::Key(key) if key.kind == KeyEventKind::Press => Some((i, key)),
2124 _ => None,
2125 }
2126 })
2127 }
2128
2129 /// Mark the event at `index` as consumed.
2130 ///
2131 /// Public counterpart to the crate-internal `consume_indices`. Use
2132 /// this in custom widgets after handling an event yielded by
2133 /// [`key_presses_when`](Self::key_presses_when) so subsequent widgets
2134 /// don't react to the same key. Out-of-range indices are silently
2135 /// ignored (matching the iterator-pair semantics).
2136 pub fn consume_event(&mut self, index: usize) {
2137 if let Some(slot) = self.consumed.get_mut(index) {
2138 *slot = true;
2139 }
2140 }
2141
2142 // ── Issue #233: in-frame static-log append ───────────────────────────
2143 //
2144 // The runtime holds the buffer inside `named_states` under a reserved
2145 // sentinel key. `Context::new` (owned by another agent) does not need to
2146 // initialise this field — `or_insert_with` handles first-call creation,
2147 // and `lib::run_frame_kernel` drains the buffer back into `FrameState`
2148 // for the run-loop to consume.
2149
2150 /// Append a line that will be flushed to terminal scrollback **before**
2151 /// the dynamic frame content (issue #233).
2152 ///
2153 /// Lines accumulated this frame are written via the active runtime — for
2154 /// [`crate::run_static`] / [`crate::run_static_with`], they are printed
2155 /// above the inline dynamic area as committed scrollback. For full-screen
2156 /// runtimes ([`crate::run`], [`crate::run_async`]) and inline mode
2157 /// ([`crate::run_inline`]), the buffer is silently dropped after a debug
2158 /// warning is emitted on the first call per frame, since those modes have
2159 /// no scrollback area to write to.
2160 ///
2161 /// The headless [`crate::TestBackend`] accumulates the lines into the
2162 /// frame state where they can be drained by tests via
2163 /// [`Context::take_static_log`] (or by inspecting the buffer when
2164 /// constructing a custom backend).
2165 ///
2166 /// # Order
2167 ///
2168 /// `static_log` may be called any number of times per frame. Lines are
2169 /// flushed in call order, all before the dynamic frame for the same
2170 /// tick.
2171 ///
2172 /// # Example
2173 ///
2174 /// ```
2175 /// # use slt::*;
2176 /// # TestBackend::new(40, 4).render(|ui| {
2177 /// ui.static_log("event 1");
2178 /// ui.static_log(format!("event {}", 2));
2179 /// ui.text("dynamic content");
2180 /// # });
2181 /// ```
2182 pub fn static_log(&mut self, line: impl Into<String>) {
2183 let entry = self
2184 .named_states
2185 .entry(STATIC_LOG_KEY)
2186 .or_insert_with(|| Box::new(Vec::<String>::new()) as Box<dyn std::any::Any>);
2187 if let Some(buf) = entry.downcast_mut::<Vec<String>>() {
2188 buf.push(line.into());
2189 }
2190 }
2191
2192 /// Drain and return the queued static-log lines for the current frame
2193 /// (issue #233). Used by tests / external backends to inspect what
2194 /// `ui.static_log(...)` emitted during a [`crate::TestBackend::render`]
2195 /// call.
2196 pub fn take_static_log(&mut self) -> Vec<String> {
2197 if let Some(boxed) = self.named_states.get_mut(STATIC_LOG_KEY)
2198 && let Some(buf) = boxed.downcast_mut::<Vec<String>>()
2199 {
2200 return std::mem::take(buf);
2201 }
2202 Vec::new()
2203 }
2204
2205 // ── Issue #236: widget keymap publishing ─────────────────────────────
2206
2207 /// Publish a widget's keymap so the framework can show it in the help
2208 /// overlay (issue #236).
2209 ///
2210 /// Each call registers `(name, bindings)` for the current frame. Widgets
2211 /// implementing [`crate::keymap::WidgetKeyHelp`] typically forward their
2212 /// `key_help()` slice here:
2213 ///
2214 /// ```
2215 /// # use slt::*;
2216 /// # use slt::keymap::WidgetKeyHelp;
2217 /// struct Counter;
2218 /// impl WidgetKeyHelp for Counter {
2219 /// fn key_help(&self) -> &'static [(&'static str, &'static str)] {
2220 /// const HELP: &[(&str, &str)] = &[("↑", "increment"), ("↓", "decrement")];
2221 /// HELP
2222 /// }
2223 /// }
2224 /// # TestBackend::new(40, 4).render(|ui| {
2225 /// let counter = Counter;
2226 /// ui.publish_keymap("counter", counter.key_help());
2227 /// # });
2228 /// ```
2229 ///
2230 /// The registry is reset at the start of every frame (the first call on a
2231 /// new tick clears stale entries). Both calls in the same frame
2232 /// accumulate; calls across frames do not leak.
2233 pub fn publish_keymap(
2234 &mut self,
2235 name: &'static str,
2236 bindings: &'static [(&'static str, &'static str)],
2237 ) {
2238 // The registry is cleared at frame start by `run_frame_kernel`
2239 // (issue #236) — see `clear_keymap_registry` in `lib.rs`. We just
2240 // need to insert/append here.
2241 let entry = self
2242 .named_states
2243 .entry(KEYMAP_REGISTRY_KEY)
2244 .or_insert_with(|| {
2245 Box::new(Vec::<crate::keymap::PublishedKeymap>::new()) as Box<dyn std::any::Any>
2246 });
2247 if let Some(vec) = entry.downcast_mut::<Vec<crate::keymap::PublishedKeymap>>() {
2248 vec.push(crate::keymap::PublishedKeymap::new(name, bindings));
2249 }
2250 }
2251
2252 /// Return all keymaps published this frame (issue #236).
2253 ///
2254 /// Empty if no widget called [`Context::publish_keymap`] yet on the
2255 /// current frame. The registry is reset at the start of every frame.
2256 pub fn published_keymaps(&self) -> &[crate::keymap::PublishedKeymap] {
2257 if let Some(boxed) = self.named_states.get(KEYMAP_REGISTRY_KEY)
2258 && let Some(vec) = boxed.downcast_ref::<Vec<crate::keymap::PublishedKeymap>>()
2259 {
2260 return vec;
2261 }
2262 &[]
2263 }
2264
2265 /// Render an automatic keymap-help overlay listing every widget keymap
2266 /// published this frame (issue #236).
2267 ///
2268 /// Pass `open = true` to render the overlay (typically gated on a
2269 /// `?` / `F1` keypress). When `open` is `false`, this method is a
2270 /// no-op. The overlay groups bindings by widget name and dismisses
2271 /// when the next frame is rendered with `open = false`.
2272 ///
2273 /// # Example
2274 ///
2275 /// ```
2276 /// # use slt::*;
2277 /// # TestBackend::new(40, 12).render(|ui| {
2278 /// const RICHLOG: &[(&str, &str)] = &[("↑/k", "scroll up"), ("↓/j", "scroll down")];
2279 /// ui.publish_keymap("rich_log", RICHLOG);
2280 /// // Show the help overlay when '?' is pressed
2281 /// let show = ui.key('?');
2282 /// ui.keymap_help_overlay(show);
2283 /// # });
2284 /// ```
2285 pub fn keymap_help_overlay(&mut self, open: bool) {
2286 if !open {
2287 return;
2288 }
2289
2290 let entries: Vec<crate::keymap::PublishedKeymap> = self.published_keymaps().to_vec();
2291 if entries.is_empty() {
2292 return;
2293 }
2294
2295 let theme = self.theme;
2296 let _ = self.modal(|ui| {
2297 ui.styled("Keyboard shortcuts", Style::new().bold().fg(theme.primary));
2298 ui.text("");
2299 for entry in &entries {
2300 ui.styled(entry.name, Style::new().bold().fg(theme.text));
2301 for (key, desc) in entry.bindings {
2302 let line = format!(" {key:<14} {desc}");
2303 ui.styled(line, Style::new().fg(theme.text_dim));
2304 }
2305 ui.text("");
2306 }
2307 ui.styled(
2308 "Press Esc / ? to close",
2309 Style::new().fg(theme.text_dim).italic(),
2310 );
2311 });
2312 }
2313}
2314
2315// Sentinel keys reused from `lib.rs` so the two reads/writes can never drift.
2316use crate::{
2317 KEYMAP_REGISTRY_NAMED_STATE_KEY as KEYMAP_REGISTRY_KEY,
2318 STATIC_LOG_NAMED_STATE_KEY as STATIC_LOG_KEY,
2319};