Skip to main content

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    /// - [`Spring`](crate::Spring) — physics-based motion that reacts smoothly
1050    ///   when its target changes.
1051    pub fn animate_bool(&mut self, id: &'static str, value: bool) -> f64 {
1052        let target = if value { 1.0 } else { 0.0 };
1053        self.animate_value(id, target, crate::anim::DEFAULT_ANIMATE_TICKS)
1054    }
1055
1056    /// Smoothly animate a `f64` value toward `target` over `duration_ticks`.
1057    ///
1058    /// Uses a linear-easing [`crate::Tween`] stored implicitly in the
1059    /// per-context named-state map under `id`. Returns the current
1060    /// interpolated value. On the first call the value snaps to `target`
1061    /// with no visible transition; on subsequent calls when `target`
1062    /// changes the tween is rebuilt starting from the current interpolated
1063    /// value, so retargeting mid-flight does not produce a jump.
1064    ///
1065    /// `duration_ticks == 0` snaps immediately to the new target.
1066    ///
1067    /// # Panics
1068    ///
1069    /// Panics if `id` is already bound in the named-state map to a value of a
1070    /// different type (e.g. a [`use_state_named`](Self::use_state_named) call
1071    /// reused the same id), since the stored entry then fails to downcast to
1072    /// the internal animation state:
1073    ///
1074    /// ```text
1075    /// animate_value: id {id} is already used for a different state type
1076    /// ```
1077    ///
1078    /// Pick a unique id per call site to avoid the collision.
1079    ///
1080    /// # Example
1081    /// ```ignore
1082    /// let bar_height = ui.animate_value("loading::bar", target_height, 30);
1083    /// ui.bar(bar_height);
1084    /// ```
1085    ///
1086    /// # Comparison with `Tween`
1087    /// Use this shorthand when you want zero boilerplate and linear easing
1088    /// is acceptable. For custom easing, a non-static key, or
1089    /// non-tick-based control, construct a [`crate::Tween`] explicitly via
1090    /// [`Context::use_state_named`](Self::use_state_named).
1091    ///
1092    /// # See also
1093    ///
1094    /// - [`animate_bool`](Self::animate_bool) — boolean-driven shorthand that
1095    ///   tweens between `0.0` and `1.0`.
1096    /// - [`Tween`](crate::Tween) — explicit easing and lifecycle control.
1097    /// - [`Spring`](crate::Spring) — physics-based motion for frequently
1098    ///   changing targets.
1099    pub fn animate_value(&mut self, id: &'static str, target: f64, duration_ticks: u64) -> f64 {
1100        let tick = self.tick;
1101        let entry = self
1102            .named_states
1103            .entry(id)
1104            .or_insert_with(|| Box::new(crate::anim::AnimState::new(target, tick)));
1105        let state = entry
1106            .downcast_mut::<crate::anim::AnimState>()
1107            .unwrap_or_else(|| {
1108                panic!("animate_value: id {id:?} is already used for a different state type")
1109            });
1110        state.sample(target, duration_ticks, tick)
1111    }
1112
1113    /// One-shot frame-clock timer (issue #248).
1114    ///
1115    /// Returns `true` exactly once — on the first frame at or after `dur` has
1116    /// elapsed since the first `schedule` call for `id` — and `false` on every
1117    /// other frame, both before and after. Re-arm by calling
1118    /// [`cancel`](Self::cancel) and then `schedule` again.
1119    ///
1120    /// Wall-clock based ([`std::time::Instant`] sampled once at frame start),
1121    /// so it works with the default feature set and without the `async`
1122    /// feature. Precision is bounded by the run loop's `tick_rate` (the
1123    /// deadline is observed on the next frame after it elapses), so durations
1124    /// well below the frame cadence are not meaningful.
1125    ///
1126    /// The id lives in the same per-context namespace as
1127    /// [`use_state_named`](Self::use_state_named): pick a unique key per call
1128    /// site.
1129    ///
1130    /// # Example
1131    /// ```no_run
1132    /// use std::time::Duration;
1133    ///
1134    /// slt::run(|ui: &mut slt::Context| {
1135    ///     if ui.schedule("splash::dismiss", Duration::from_millis(800)) {
1136    ///         // Runs once, ~800ms after the first frame that called this.
1137    ///         ui.text("Splash dismissed.");
1138    ///     }
1139    /// })?;
1140    /// # Ok::<_, std::io::Error>(())
1141    /// ```
1142    pub fn schedule(&mut self, id: &'static str, dur: std::time::Duration) -> bool {
1143        let now = self.frame_instant;
1144        let slot = self
1145            .scheduler
1146            .named
1147            .entry(id)
1148            .or_insert_with(|| SchedulerSlot {
1149                started: now,
1150                kind: SchedKind::Once { dur, fired: false },
1151                touched_this_frame: false,
1152            });
1153        slot.touched_this_frame = true;
1154        let elapsed = now.saturating_duration_since(slot.started);
1155        match &mut slot.kind {
1156            SchedKind::Once { dur, fired } if !*fired && elapsed >= *dur => {
1157                *fired = true;
1158                true
1159            }
1160            // Not yet due, already fired, or a re-used id bound to a different
1161            // timer kind: do not fire (a typo can't crash the app).
1162            _ => false,
1163        }
1164    }
1165
1166    /// Recurring frame-clock timer (issue #248).
1167    ///
1168    /// Returns the number of whole `dur` intervals that elapsed since the
1169    /// previous frame this `id` was sampled: `0` on most frames, `1` typically,
1170    /// and `> 1` if the frame loop stalled past several intervals — so no ticks
1171    /// are silently dropped. The internal clock advances by exactly the
1172    /// returned number of intervals each frame, so counts never drift.
1173    ///
1174    /// Wall-clock based and `async`-free, like [`schedule`](Self::schedule).
1175    ///
1176    /// # Example
1177    /// ```no_run
1178    /// use std::time::Duration;
1179    ///
1180    /// slt::run(|ui: &mut slt::Context| {
1181    ///     let ticks = ui.every("clock::second", Duration::from_secs(1));
1182    ///     if ticks > 0 {
1183    ///         // Advance a once-per-second animation by `ticks` steps.
1184    ///     }
1185    /// })?;
1186    /// # Ok::<_, std::io::Error>(())
1187    /// ```
1188    pub fn every(&mut self, id: &'static str, dur: std::time::Duration) -> u32 {
1189        let now = self.frame_instant;
1190        let interval = dur.max(std::time::Duration::from_nanos(1));
1191        let slot = self
1192            .scheduler
1193            .named
1194            .entry(id)
1195            .or_insert_with(|| SchedulerSlot {
1196                started: now,
1197                kind: SchedKind::Every {
1198                    interval,
1199                    last: now,
1200                },
1201                touched_this_frame: false,
1202            });
1203        slot.touched_this_frame = true;
1204        match &mut slot.kind {
1205            SchedKind::Every { interval, last } => {
1206                let elapsed = now.saturating_duration_since(*last);
1207                let fired = crate::widgets::intervals_elapsed(elapsed, *interval);
1208                if fired > 0 {
1209                    // Advance by exactly the intervals reported so counts never
1210                    // drift, even across stalled frames.
1211                    let advance = interval.saturating_mul(fired);
1212                    *last = last.checked_add(advance).unwrap_or(now);
1213                }
1214                fired
1215            }
1216            _ => 0,
1217        }
1218    }
1219
1220    /// Debounce timer — the typeahead / search-as-you-type primitive (#248).
1221    ///
1222    /// Each frame where `dirty == true` resets the quiet window to `dur`.
1223    /// Returns `true` exactly once on the first frame after `dur` of quiet (no
1224    /// `dirty`), then stays `false` until the next dirty frame re-arms it. This
1225    /// mirrors Textual's `@work(exclusive=True)` debounce: collapse a burst of
1226    /// keystrokes so only the final, settled query runs.
1227    ///
1228    /// Wall-clock based and `async`-free, like [`schedule`](Self::schedule).
1229    ///
1230    /// # Example
1231    /// ```no_run
1232    /// use std::time::Duration;
1233    /// use slt::TextInputState;
1234    ///
1235    /// let mut query = TextInputState::with_placeholder("Search...");
1236    /// slt::run(move |ui: &mut slt::Context| {
1237    ///     // `resp.changed` is true on the keystroke frame -> the dirty signal.
1238    ///     let resp = ui.text_input(&mut query);
1239    ///     // Fire the search only after 250ms of no typing.
1240    ///     if ui.debounce("search::run", Duration::from_millis(250), resp.changed) {
1241    ///         // run_search(&query.value());
1242    ///     }
1243    /// })?;
1244    /// # Ok::<_, std::io::Error>(())
1245    /// ```
1246    pub fn debounce(&mut self, id: &'static str, dur: std::time::Duration, dirty: bool) -> bool {
1247        let now = self.frame_instant;
1248        let slot = self
1249            .scheduler
1250            .named
1251            .entry(id)
1252            .or_insert_with(|| SchedulerSlot {
1253                started: now,
1254                kind: SchedKind::Debounce {
1255                    dur,
1256                    quiet_started: now,
1257                    fired: false,
1258                },
1259                touched_this_frame: false,
1260            });
1261        slot.touched_this_frame = true;
1262        match &mut slot.kind {
1263            SchedKind::Debounce {
1264                dur: slot_dur,
1265                quiet_started,
1266                fired,
1267            } => {
1268                *slot_dur = dur;
1269                if dirty {
1270                    // Re-arm the quiet window from this frame.
1271                    *quiet_started = now;
1272                    *fired = false;
1273                    false
1274                } else if !*fired && now.saturating_duration_since(*quiet_started) >= *slot_dur {
1275                    *fired = true;
1276                    true
1277                } else {
1278                    false
1279                }
1280            }
1281            _ => false,
1282        }
1283    }
1284
1285    /// Exclusive-group claim — cancel stale work on supersede (issue #248).
1286    ///
1287    /// Within a `group`, only the most-recently-claimed `id` returns `true`.
1288    /// Recently superseded ids remain stale when re-polled; the history is
1289    /// bounded so a long-lived group cannot retain every completed claim.
1290    /// Use it to cancel an in-flight typeahead query when a newer query
1291    /// supersedes it: pair with [`debounce`](Self::debounce) to fire the settled
1292    /// query, then guard the work with `exclusive` so only the latest claim
1293    /// proceeds.
1294    ///
1295    /// # Example
1296    /// ```no_run
1297    /// use std::time::Duration;
1298    ///
1299    /// slt::run(|ui: &mut slt::Context| {
1300    ///     let query_id = "q-42"; // e.g. a per-keystroke sequence id
1301    ///     if ui.exclusive("search", query_id) {
1302    ///         // Only the latest claimed query runs; older ones are cancelled.
1303    ///     }
1304    /// })?;
1305    /// # Ok::<_, std::io::Error>(())
1306    /// ```
1307    pub fn exclusive(&mut self, group: &'static str, id: &str) -> bool {
1308        let entry = self
1309            .scheduler
1310            .exclusive
1311            .entry(group.to_string())
1312            .or_default();
1313        entry.claim(id)
1314    }
1315
1316    /// Drop the scheduler slot for `id`, re-arming it on the next
1317    /// [`schedule`](Self::schedule) / [`every`](Self::every) /
1318    /// [`debounce`](Self::debounce) call (issue #248).
1319    ///
1320    /// Accepts both `&'static str` and runtime-`String` ids: clears the slot
1321    /// from the named map and the dynamic-id map.
1322    ///
1323    /// # Example
1324    /// ```no_run
1325    /// use std::time::Duration;
1326    ///
1327    /// slt::run(|ui: &mut slt::Context| {
1328    ///     if ui.schedule("retry", Duration::from_secs(5)) {
1329    ///         // ...
1330    ///     }
1331    ///     if ui.key('r') {
1332    ///         ui.cancel("retry"); // next `schedule("retry", ..)` starts fresh
1333    ///     }
1334    /// })?;
1335    /// # Ok::<_, std::io::Error>(())
1336    /// ```
1337    pub fn cancel(&mut self, id: &str) {
1338        self.scheduler.named.remove(id);
1339        self.scheduler.keyed.remove(id);
1340    }
1341
1342    /// Wall-clock time elapsed since `id` was first scheduled, or `None` if no
1343    /// live timer slot exists for `id` (issue #248).
1344    ///
1345    /// Useful for progress UIs ("retrying in 3s…") that want the raw elapsed
1346    /// duration rather than a fire/no-fire signal. Measured against the same
1347    /// frame instant the timer methods use.
1348    ///
1349    /// # Example
1350    /// ```no_run
1351    /// use std::time::Duration;
1352    ///
1353    /// slt::run(|ui: &mut slt::Context| {
1354    ///     ui.schedule("upload", Duration::from_secs(30));
1355    ///     if let Some(elapsed) = ui.elapsed("upload") {
1356    ///         ui.text(format!("Uploading for {}s", elapsed.as_secs()));
1357    ///     }
1358    /// })?;
1359    /// # Ok::<_, std::io::Error>(())
1360    /// ```
1361    pub fn elapsed(&self, id: &str) -> Option<std::time::Duration> {
1362        let started = self
1363            .scheduler
1364            .named
1365            .get(id)
1366            .or_else(|| self.scheduler.keyed.get(id))
1367            .map(|slot| slot.started)?;
1368        Some(self.frame_instant.saturating_duration_since(started))
1369    }
1370
1371    /// Remove dynamic keyed state created by
1372    /// [`use_state_keyed`](Self::use_state_keyed).
1373    ///
1374    /// Returns `true` when a slot existed. Any old [`State`] handle for the
1375    /// removed id becomes invalid and will panic if used before the state is
1376    /// recreated by `use_state_keyed`.
1377    pub fn remove_state_keyed(&mut self, id: &str) -> bool {
1378        self.keyed_states.remove(id).is_some()
1379    }
1380
1381    /// Retain only dynamic keyed-state entries accepted by `keep`.
1382    ///
1383    /// Returns the number of removed entries. This is intended for long-lived
1384    /// dynamic lists where ids come from data and removed items should release
1385    /// their per-row state.
1386    pub fn retain_state_keyed(&mut self, mut keep: impl FnMut(&str) -> bool) -> usize {
1387        let before = self.keyed_states.len();
1388        self.keyed_states.retain(|key, _| keep(key));
1389        before - self.keyed_states.len()
1390    }
1391
1392    /// Number of live dynamic keyed-state entries.
1393    ///
1394    /// Diagnostic helper for spotting churn when using runtime ids.
1395    pub fn keyed_state_count(&self) -> usize {
1396        self.keyed_states.len()
1397    }
1398
1399    /// Push a value onto the context stack for the duration of `body`.
1400    ///
1401    /// Inside `body`, child widgets can call
1402    /// [`use_context::<T>()`](Self::use_context) or
1403    /// [`try_use_context::<T>()`](Self::try_use_context) to look up the
1404    /// nearest provided value of type `T`. Provides cascade in LIFO order:
1405    /// nested calls with the same `T` shadow outer ones.
1406    ///
1407    /// The value is automatically popped when `body` returns — including on
1408    /// panic, so the context stack is always restored.
1409    ///
1410    /// # Example
1411    ///
1412    /// ```ignore
1413    /// struct Theme { accent: slt::Color }
1414    /// ui.provide(Theme { accent: slt::Color::Red }, |ui| {
1415    ///     // Any widget here can `let theme = ui.use_context::<Theme>();`
1416    ///     render_button(ui);
1417    /// });
1418    /// ```
1419    pub fn provide<T: 'static, R>(&mut self, value: T, body: impl FnOnce(&mut Context) -> R) -> R {
1420        self.context_stack
1421            .push(Box::new(value) as Box<dyn std::any::Any>);
1422
1423        // catch_unwind ensures the entry is popped even if `body` panics, so
1424        // the context stack is never left with leaked frames. We re-panic
1425        // afterwards so the panic propagates normally to outer scopes.
1426        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
1427
1428        // Pop in both success and panic paths.
1429        self.context_stack.pop();
1430
1431        match result {
1432            Ok(value) => value,
1433            Err(panic) => std::panic::resume_unwind(panic),
1434        }
1435    }
1436
1437    /// Spawn a fire-and-forget async task from inside the frame closure.
1438    ///
1439    /// Returns a [`TaskHandle<T>`](crate::TaskHandle) you store and pass to
1440    /// [`poll`](Self::poll) on later frames to retrieve the result. This closes
1441    /// the ergonomics gap of the channel pattern (`run_async` + an external
1442    /// `Sender`) for the common case: "click a button, kick off one async call,
1443    /// show its result next frame" — without wiring a channel yourself.
1444    ///
1445    /// **Dropping the returned handle cancels the in-flight task.** Keep it
1446    /// alive (e.g. in `use_state`) for as long as you care about the result.
1447    /// Each handle carries a unique id, so two `TaskHandle<String>` live at the
1448    /// same time never cross their results.
1449    ///
1450    /// Requires the `async` feature and an active Tokio runtime — call it
1451    /// inside [`run_async`](crate::run_async) /
1452    /// [`run_async_with`](crate::run_async_with), which inject the runtime
1453    /// handle.
1454    ///
1455    /// # Panics
1456    ///
1457    /// Panics if no Tokio runtime was injected (e.g. when called from the sync
1458    /// [`run`](crate::run) loop or `TestBackend` without a runtime).
1459    ///
1460    /// # Example
1461    ///
1462    /// ```no_run
1463    /// # #[cfg(feature = "async")]
1464    /// # async fn run() -> std::io::Result<()> {
1465    /// use slt::{Context, RunConfig, TaskHandle};
1466    ///
1467    /// async fn fetch() -> String {
1468    ///     // e.g. an HTTP request
1469    ///     "result".to_string()
1470    /// }
1471    ///
1472    /// slt::run_async_with(RunConfig::default(), |ui: &mut Context, _: &mut Vec<()>| {
1473    ///     // One handle, stored across frames via `use_state`.
1474    ///     let handle = ui.use_state(|| None::<TaskHandle<String>>);
1475    ///
1476    ///     if ui.button("Fetch").clicked && handle.get(ui).is_none() {
1477    ///         *handle.get_mut(ui) = Some(ui.spawn(async { fetch().await }));
1478    ///     }
1479    ///
1480    ///     // Take the handle out of state to poll it: `ui.poll` needs `&mut ui`,
1481    ///     // which cannot coexist with a `&TaskHandle` borrowed from `ui`'s own
1482    ///     // state. Put it back if the task is still pending.
1483    ///     if let Some(h) = handle.get_mut(ui).take() {
1484    ///         match ui.poll(&h) {
1485    ///             Some(result) => {
1486    ///                 ui.text(format!("Got: {result}"));
1487    ///             }
1488    ///             None => {
1489    ///                 *handle.get_mut(ui) = Some(h);
1490    ///                 ui.text("Loading...");
1491    ///             }
1492    ///         }
1493    ///     }
1494    /// })?;
1495    /// # Ok(())
1496    /// # }
1497    /// ```
1498    #[cfg(feature = "async")]
1499    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
1500    pub fn spawn<T: Send + 'static>(
1501        &mut self,
1502        fut: impl std::future::Future<Output = T> + Send + 'static,
1503    ) -> TaskHandle<T> {
1504        self.async_tasks.spawn(fut)
1505    }
1506
1507    /// Poll a [`TaskHandle`](crate::TaskHandle) for its result.
1508    ///
1509    /// Returns `Some(result)` exactly once — on the first frame after the task
1510    /// completes — then `None` on every subsequent call. Returns `None` while
1511    /// the task is still in flight.
1512    ///
1513    /// Pairs with [`spawn`](Self::spawn). Requires the `async` feature.
1514    ///
1515    /// # Example
1516    ///
1517    /// ```no_run
1518    /// # #[cfg(feature = "async")]
1519    /// # fn ex(ui: &mut slt::Context, handle: &slt::TaskHandle<u32>) {
1520    /// if let Some(value) = ui.poll(handle) {
1521    ///     ui.text(format!("done: {value}"));
1522    /// }
1523    /// # }
1524    /// ```
1525    #[cfg(feature = "async")]
1526    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
1527    pub fn poll<T: 'static>(&mut self, handle: &TaskHandle<T>) -> Option<T> {
1528        self.async_tasks.poll::<T>(handle.id())
1529    }
1530
1531    /// Poll a task without collapsing cancellation or panic into `None`.
1532    ///
1533    #[cfg(feature = "async")]
1534    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
1535    pub fn poll_outcome<T: 'static>(
1536        &mut self,
1537        handle: &TaskHandle<T>,
1538    ) -> Option<super::async_tasks::TaskOutcome<T>> {
1539        self.async_tasks.poll_outcome::<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    ///
1552    /// # Example
1553    ///
1554    /// Take an owned snapshot before calling methods that need `&mut Context`:
1555    ///
1556    /// ```no_run
1557    /// #[derive(Clone)]
1558    /// struct AppContext {
1559    ///     username: String,
1560    ///     show_debug: bool,
1561    /// }
1562    ///
1563    /// fn render_home(ui: &mut slt::Context) {
1564    ///     let app = ui.use_context::<AppContext>().clone();
1565    ///     ui.text(format!("Hello, {}", app.username));
1566    ///     if app.show_debug {
1567    ///         ui.text("debug mode on").dim();
1568    ///     }
1569    /// }
1570    /// ```
1571    pub fn use_context<T: 'static>(&self) -> &T {
1572        self.try_use_context::<T>().unwrap_or_else(|| {
1573            panic!(
1574                "no context of type {} was provided; use ui.provide(value, |ui| ...) in a parent scope",
1575                std::any::type_name::<T>()
1576            )
1577        })
1578    }
1579
1580    /// Like [`use_context`](Self::use_context), but returns `None` instead of
1581    /// panicking when no value of type `T` is on the stack.
1582    pub fn try_use_context<T: 'static>(&self) -> Option<&T> {
1583        self.context_stack
1584            .iter()
1585            .rev()
1586            .find_map(|entry| entry.downcast_ref::<T>())
1587    }
1588
1589    /// Memoize a computed value. Recomputes only when `deps` changes.
1590    ///
1591    /// Returns a [`Memo<T>`] *index handle*, mirroring [`use_state`]'s
1592    /// [`State<T>`]. The handle holds **no** borrow of `ui`, so it composes with
1593    /// later `ui.*` calls — read the value on demand with `.get(ui)` /
1594    /// `.copied(ui)`.
1595    ///
1596    /// Before v0.21.0 this returned `&T`, a live borrow of `&mut Context` that
1597    /// could not be held across subsequent `ui.*` mutations. That form is now
1598    /// [`use_memo_ref`](Self::use_memo_ref) (deprecated). Migrate
1599    /// `let x = *ui.use_memo(&d, f);` to `let x = ui.use_memo(&d, f).copied(ui);`.
1600    ///
1601    /// [`use_state`]: Self::use_state
1602    ///
1603    /// # Panics
1604    ///
1605    /// Panics if the hook slot at this call position was previously used for a
1606    /// different hook (a rules-of-hooks / call-order violation), since the
1607    /// type-erased slot then fails to downcast to `MemoSlot<T>`:
1608    ///
1609    /// ```text
1610    /// Hook type mismatch at index {idx}: expected {type}. Hooks must be called in the same order every frame.
1611    /// ```
1612    ///
1613    /// Keep hook calls in the same order every frame — do not call this inside
1614    /// an `if`/`else` whose branch changes between frames.
1615    ///
1616    /// # Example
1617    /// ```no_run
1618    /// # slt::run(|ui: &mut slt::Context| {
1619    /// let count = ui.use_state(|| 0i32);
1620    /// let count_val = *count.get(ui);
1621    /// let doubled = ui.use_memo(&count_val, |c| c * 2);
1622    /// // The handle survives an intervening `ui.*` call (this is the whole point).
1623    /// ui.text("doubled:");
1624    /// ui.text(format!("{}", doubled.copied(ui)));
1625    /// # });
1626    /// ```
1627    pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1628        &mut self,
1629        deps: &D,
1630        compute: impl FnOnce(&D) -> T,
1631    ) -> Memo<T> {
1632        let idx = self.rollback.hook_cursor;
1633        self.rollback.hook_cursor += 1;
1634
1635        // First call at this slot: allocate fresh state. Deps are stored
1636        // type-erased so the read path (`Memo::get`) can downcast `MemoSlot<T>`
1637        // without restating `D`.
1638        if idx >= self.hook_states.len() {
1639            self.hook_states.push(Box::new(MemoSlot {
1640                deps: Box::new(deps.clone()),
1641                value: compute(deps),
1642            }));
1643            return Memo::from_idx(idx);
1644        }
1645
1646        // Slot already exists: it must be the same `MemoSlot<T>` shape we used
1647        // last frame, or the caller broke the rules-of-hooks contract.
1648        match self.hook_states[idx].downcast_mut::<MemoSlot<T>>() {
1649            Some(slot) => {
1650                // Compare against the previous (type-erased) deps. A failed
1651                // downcast of the stored deps to `&D` is treated as stale so the
1652                // value is recomputed rather than silently kept.
1653                let stale = slot
1654                    .deps
1655                    .downcast_ref::<D>()
1656                    .map(|prev| *prev != *deps)
1657                    .unwrap_or(true);
1658                if stale {
1659                    // Build both halves before mutating the slot. If either
1660                    // dependency cloning or value computation panics inside an
1661                    // error boundary, the previous dependency/value pair stays
1662                    // intact and the same dependency retries next frame.
1663                    let next_deps = deps.clone();
1664                    let next_value = compute(deps);
1665                    slot.deps = Box::new(next_deps);
1666                    slot.value = next_value;
1667                }
1668            }
1669            None => panic!(
1670                "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
1671                idx,
1672                std::any::type_name::<MemoSlot<T>>()
1673            ),
1674        }
1675        Memo::from_idx(idx)
1676    }
1677
1678    /// Deprecated `&T`-returning form of [`use_memo`](Self::use_memo).
1679    ///
1680    /// **Deprecated since 0.21.0**: [`use_memo`](Self::use_memo) now returns a
1681    /// [`Memo<T>`] handle that does not borrow `ui`, so it composes with later
1682    /// `ui.*` calls. This alias preserves the original behaviour (returning a
1683    /// `&T` borrow of `ui`) for callers that cannot migrate immediately; the
1684    /// borrow keeps `ui` immutably borrowed until the reference is dropped.
1685    ///
1686    /// Migrate `let x = *ui.use_memo_ref(&d, f);` to
1687    /// `let x = ui.use_memo(&d, f).copied(ui);` (or `.get(ui)` for a reference).
1688    ///
1689    /// # Panics
1690    ///
1691    /// Panics if the hook slot at this call position was previously used for a
1692    /// different hook (a rules-of-hooks / call-order violation), since the
1693    /// type-erased slot then fails to downcast to `(D, T)`:
1694    ///
1695    /// ```text
1696    /// Hook type mismatch at index {idx}: expected {type}. Hooks must be called in the same order every frame.
1697    /// ```
1698    ///
1699    /// # Example
1700    /// ```no_run
1701    /// # slt::run(|ui: &mut slt::Context| {
1702    /// # #[allow(deprecated)]
1703    /// let doubled = *ui.use_memo_ref(&21i32, |c| c * 2);
1704    /// ui.text(format!("{doubled}"));
1705    /// # });
1706    /// ```
1707    #[deprecated(
1708        since = "0.21.0",
1709        note = "use_memo now returns a Memo<T> handle; call `.get(ui)` / `.copied(ui)`"
1710    )]
1711    pub fn use_memo_ref<T: 'static, D: PartialEq + Clone + 'static>(
1712        &mut self,
1713        deps: &D,
1714        compute: impl FnOnce(&D) -> T,
1715    ) -> &T {
1716        let idx = self.rollback.hook_cursor;
1717        self.rollback.hook_cursor += 1;
1718
1719        // First call at this slot: allocate fresh state.
1720        if idx >= self.hook_states.len() {
1721            let value = compute(deps);
1722            self.hook_states.push(Box::new((deps.clone(), value)));
1723            return self.hook_states[idx]
1724                .downcast_ref::<(D, T)>()
1725                .map(|(_, v)| v)
1726                .expect("freshly inserted slot must downcast to its own type");
1727        }
1728
1729        // Slot already exists: it must be the same `(D, T)` shape we used last
1730        // frame, or the caller broke the rules-of-hooks contract.
1731        //
1732        // Single downcast on the cache-hit path (closes #133): use
1733        // `downcast_mut` to update deps/value in place when they change, and
1734        // return `&stored.1` directly — eliminating the redundant second
1735        // `downcast_ref` that ran on every call regardless of cache state.
1736        match self.hook_states[idx].downcast_mut::<(D, T)>() {
1737            Some(stored) => {
1738                if stored.0 != *deps {
1739                    // Keep the cache pair panic-atomic. Committing deps before
1740                    // compute would make a caught panic look like a cache hit
1741                    // on the retry while retaining the old value.
1742                    let next_deps = deps.clone();
1743                    let next_value = compute(deps);
1744                    stored.0 = next_deps;
1745                    stored.1 = next_value;
1746                }
1747                &stored.1
1748            }
1749            None => panic!(
1750                "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
1751                idx,
1752                std::any::type_name::<(D, T)>()
1753            ),
1754        }
1755    }
1756
1757    /// Returns `light` color if current theme is light mode, `dark` color if dark mode.
1758    pub fn light_dark(&self, light: Color, dark: Color) -> Color {
1759        if self.theme.is_dark { dark } else { light }
1760    }
1761
1762    /// Show a toast notification without managing ToastState.
1763    ///
1764    /// # Examples
1765    /// ```
1766    /// # use slt::*;
1767    /// # TestBackend::new(80, 24).render(|ui| {
1768    /// ui.notify("File saved!", ToastLevel::Success);
1769    /// # });
1770    /// ```
1771    pub fn notify(&mut self, message: &str, level: ToastLevel) {
1772        let tick = self.tick;
1773        self.rollback
1774            .notification_queue
1775            .push((message.to_string(), level, tick));
1776    }
1777
1778    pub(crate) fn render_notifications(&mut self) {
1779        let tick = self.tick;
1780        self.rollback
1781            .notification_queue
1782            .retain(|(_, _, created)| tick.saturating_sub(*created) < 180);
1783        if self.rollback.notification_queue.is_empty() {
1784            return;
1785        }
1786
1787        // The `overlay` closure captures `self` mutably, so we cannot keep an
1788        // immutable borrow of `self.rollback.notification_queue` alive across
1789        // the call. Move the queue out for the render, then move it back —
1790        // no `String::clone` per notification, no intermediate `Vec` alloc.
1791        // Closes the non-empty path of #138.
1792        let queue = std::mem::take(&mut self.rollback.notification_queue);
1793        let theme = self.theme;
1794
1795        let _ = self.overlay(|ui| {
1796            let _ = ui.row(|ui| {
1797                ui.spacer();
1798                let _ = ui.col(|ui| {
1799                    for (message, level, _) in queue.iter().rev() {
1800                        let color = match level {
1801                            ToastLevel::Info => theme.primary,
1802                            ToastLevel::Success => theme.success,
1803                            ToastLevel::Warning => theme.warning,
1804                            ToastLevel::Error => theme.error,
1805                        };
1806                        let mut line = String::with_capacity(2 + message.len());
1807                        line.push_str("● ");
1808                        line.push_str(message);
1809                        ui.styled(line, Style::new().fg(color));
1810                    }
1811                });
1812            });
1813        });
1814
1815        // Restore the queue so subsequent frames can re-render until each
1816        // entry's TTL expires above.
1817        self.rollback.notification_queue = queue;
1818    }
1819
1820    // ----------------------------------------------------------------
1821    // v0.20.0 hooks: keyed state, effects, named focus, key gating
1822    // ----------------------------------------------------------------
1823
1824    /// Component-local persistent state keyed by a runtime string.
1825    ///
1826    /// Unlike [`use_state_named`](Self::use_state_named), `id` can be a
1827    /// runtime value such as `format!("row-{i}")`. The key is converted to
1828    /// `String` once per call. The hot path (key already present) performs
1829    /// **zero string allocations beyond the [`Into<String>`] conversion at
1830    /// the call site** — first looking up by `&str`, only allocating a
1831    /// fresh map key on first insert. Together: at most **one allocation
1832    /// per call, regardless of cache state**.
1833    ///
1834    /// # When to use
1835    /// - Per-item state in a dynamic list where positional [`use_state`]
1836    ///   would break if items are reordered or filtered.
1837    /// - Reusable component functions called with a runtime discriminator.
1838    ///
1839    /// # Namespace
1840    /// Keys live in a single global namespace per `Context`. Prefix them
1841    /// to avoid collisions: `format!("my_component::item-{i}")`.
1842    ///
1843    /// # Stale entries
1844    /// Removed items leak their state until the `Context` is dropped (or
1845    /// the program exits). For long-running sessions with churn, manage
1846    /// state externally via a single `Vec<T>` in [`use_state`].
1847    ///
1848    /// # Example
1849    ///
1850    /// ```ignore
1851    /// for (i, item) in items.iter().enumerate() {
1852    ///     let row_state = ui.use_state_keyed(format!("row-{i}"), || ItemState::default());
1853    ///     // ...
1854    /// }
1855    /// ```
1856    ///
1857    /// [`use_state`]: Self::use_state
1858    pub fn use_state_keyed<T: 'static>(
1859        &mut self,
1860        id: impl Into<String>,
1861        init: impl FnOnce() -> T,
1862    ) -> State<T> {
1863        let key: String = id.into();
1864        // Lookup by `&str` first to avoid cloning on the hot
1865        // (already-populated) path. Only on first insert do we clone the
1866        // key into the map; otherwise the original `key` String is the
1867        // sole allocation and is moved into `State::from_keyed`.
1868        if !self.keyed_states.contains_key(key.as_str()) {
1869            self.keyed_states.insert(key.clone(), Box::new(init()));
1870        }
1871        State::from_keyed(key)
1872    }
1873
1874    /// Like [`use_state_keyed`](Self::use_state_keyed), but uses
1875    /// [`Default::default()`] to initialize the value on first call.
1876    ///
1877    /// # Example
1878    ///
1879    /// ```ignore
1880    /// let counter = ui.use_state_keyed_default::<i32>(format!("c-{i}"));
1881    /// ```
1882    pub fn use_state_keyed_default<T: Default + 'static>(
1883        &mut self,
1884        id: impl Into<String>,
1885    ) -> State<T> {
1886        self.use_state_keyed(id, T::default)
1887    }
1888
1889    /// Run a side-effecting closure when `deps` changes.
1890    ///
1891    /// On the **first frame** the hook slot is encountered, `f` is called
1892    /// unconditionally. On **subsequent frames**, `f` is only called when
1893    /// `*deps != stored_deps`. The hook is **positional** (same ordering
1894    /// rules as [`use_state`](Self::use_state)).
1895    ///
1896    /// # Fire-and-forget semantics
1897    ///
1898    /// There is no cleanup callback. If setup resources need teardown,
1899    /// store a handle in [`use_state`](Self::use_state) and drop it on
1900    /// a later frame.
1901    ///
1902    /// # Caveat: `error_boundary` re-fire
1903    ///
1904    /// Effects placed inside an [`error_boundary`](Self::error_boundary)
1905    /// scope can re-fire when the boundary catches a panic and rolls back
1906    /// the hook slots. For non-idempotent side effects (network requests,
1907    /// payments) put the effect outside the boundary or guard with an
1908    /// idempotency key.
1909    ///
1910    /// # Panics
1911    ///
1912    /// Panics if the hook slot at this call position was previously used for a
1913    /// different hook (a rules-of-hooks / call-order violation), since the
1914    /// type-erased slot then fails to downcast to the deps type `D`:
1915    ///
1916    /// ```text
1917    /// Hook type mismatch at index {idx}: expected {type}. Hooks must be called in the same order every frame.
1918    /// ```
1919    ///
1920    /// # Common patterns
1921    ///
1922    /// ```ignore
1923    /// // Run once on first frame:
1924    /// ui.use_effect(|_| initialize_logger(), &());
1925    ///
1926    /// // Run when `selected_tab` changes:
1927    /// ui.use_effect(|tab| load_tab_data(*tab), &selected_tab);
1928    /// ```
1929    pub fn use_effect<D: PartialEq + Clone + 'static>(&mut self, f: impl FnOnce(&D), deps: &D) {
1930        let idx = self.rollback.hook_cursor;
1931        self.rollback.hook_cursor += 1;
1932
1933        if idx >= self.hook_states.len() {
1934            // First encounter: run the effect, then store the deps so we
1935            // can detect future changes.
1936            f(deps);
1937            self.hook_states.push(Box::new(deps.clone()));
1938            return;
1939        }
1940
1941        match self.hook_states[idx].downcast_mut::<D>() {
1942            Some(stored) => {
1943                if *stored != *deps {
1944                    f(deps);
1945                    *stored = deps.clone();
1946                }
1947            }
1948            None => panic!(
1949                "Hook type mismatch at index {idx}: expected {}. \
1950                 Hooks must be called in the same order every frame.",
1951                std::any::type_name::<D>()
1952            ),
1953        }
1954    }
1955
1956    /// Register a focusable slot bound to a stable string name.
1957    ///
1958    /// Returns `true` if the registered slot currently has focus, exactly
1959    /// like [`register_focusable`](Self::register_focusable) — but also
1960    /// records the `name → slot` mapping so other code can later call
1961    /// [`focus_by_name`](Self::focus_by_name) and
1962    /// [`focused_name`](Self::focused_name).
1963    ///
1964    /// # How the slot is shared with the widget that follows
1965    ///
1966    /// Every SLT widget that takes focus (`button`, `text_input`,
1967    /// `tabs`, …) internally calls `register_focusable()` to claim its
1968    /// own slot. To keep the name pointed at the **widget the user
1969    /// sees**, this call:
1970    ///
1971    /// 1. allocates a slot eagerly (so the name binding works even when
1972    ///    no widget follows — useful for tests and for custom focusable
1973    ///    regions),
1974    /// 2. records the `name → slot` mapping into the frame's
1975    ///    `focus_name_map` (first-write-wins on duplicate names within
1976    ///    a frame),
1977    /// 3. **reserves** the slot id so the next `register_focusable()`
1978    ///    on the same frame *reuses* it instead of allocating a fresh
1979    ///    slot — that's how `text_input(&mut state)` placed right after
1980    ///    inherits the name.
1981    ///
1982    /// Names are re-registered each frame; the previous frame's map is
1983    /// kept under `focus_name_map_prev` so [`focus_by_name`](Context::focus_by_name) can resolve
1984    /// a name that has already been registered.
1985    ///
1986    /// # Two valid usage shapes
1987    ///
1988    /// **Shape A — name a widget that follows immediately** (the common
1989    /// pattern; the widget reuses the reserved slot):
1990    ///
1991    /// ```ignore
1992    /// let _ = ui.register_focusable_named("search");
1993    /// let _ = ui.text_input(&mut search_state);
1994    /// // later: ui.focus_by_name("search") jumps to the text_input
1995    /// ```
1996    ///
1997    /// **Shape B — register a named focusable region with no inner
1998    /// widget** (e.g. a custom render area that handles its own keys
1999    /// when focused):
2000    ///
2001    /// ```ignore
2002    /// let focused = ui.register_focusable_named("canvas");
2003    /// if focused { /* react to keys via key_presses_when */ }
2004    /// ```
2005    pub fn register_focusable_named(&mut self, name: &str) -> bool {
2006        // Modal/overlay suppression: when a modal is active and we're not
2007        // inside it, focusables outside the modal must be invisible to
2008        // tab/click cycling. Drop the registration entirely (no slot
2009        // allocation, no name binding, no reservation leak).
2010        if (self.rollback.modal_active || self.prev_modal_active)
2011            && self.rollback.overlay_depth == 0
2012        {
2013            self.rollback.pending_focusable_id = None;
2014            return false;
2015        }
2016        // Eagerly allocate the slot — symmetric with `register_focusable`,
2017        // so the slot exists even when no widget follows.
2018        let id = self.rollback.focus_count;
2019        self.rollback.focus_count += 1;
2020        self.rollback.last_focusable_id = Some(id);
2021        self.commands.push(Command::FocusMarker(id));
2022        // First-write-wins on duplicate names within a single frame —
2023        // a second `register_focusable_named("dup")` keeps the first
2024        // slot bound to the name and orphans its own slot's name binding.
2025        self.focus_name_map.entry(name.to_string()).or_insert(id);
2026        // Reserve `id` for the very next `register_focusable()` call to
2027        // reuse, so widgets like `text_input` placed immediately after
2028        // share the named slot rather than allocating a fresh one.
2029        // Last-write-wins on the reservation: stacking two
2030        // `register_focusable_named` calls without an intervening widget
2031        // leaves the second slot reserved (the first slot stays bound to
2032        // its name in `focus_name_map`, just without a widget attached).
2033        self.rollback.pending_focusable_id = Some(id);
2034        // Same focus-index prediction as `register_focusable`.
2035        if self.prev_modal_active
2036            && self.prev_modal_focus_count > 0
2037            && self.rollback.modal_active
2038            && self.rollback.overlay_depth > 0
2039        {
2040            let mut modal_local_id = id.saturating_sub(self.rollback.modal_focus_start);
2041            modal_local_id %= self.prev_modal_focus_count;
2042            let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
2043            modal_focus_idx %= self.prev_modal_focus_count;
2044            return modal_local_id == modal_focus_idx;
2045        }
2046        if self.prev_focus_count == 0 {
2047            return true;
2048        }
2049        self.focus_index % self.prev_focus_count == id
2050    }
2051
2052    /// Request focus on the named widget.
2053    ///
2054    /// If the named widget was registered last frame the focus change
2055    /// takes effect at the **start of the next frame** (one-frame delay
2056    /// is the deferred-command pattern used throughout SLT). If the name
2057    /// has never been registered, the request stays pending: the next
2058    /// frame to register that name receives focus.
2059    ///
2060    /// Returns `true` if the call **will** resolve — i.e. the name was
2061    /// either registered earlier in this frame (via
2062    /// [`register_focusable_named`](Self::register_focusable_named)) or in
2063    /// the previous frame. Returns `false` only when the name has not been
2064    /// seen by either frame, in which case the request stays pending until
2065    /// some future frame registers the name.
2066    ///
2067    /// # Example
2068    ///
2069    /// ```ignore
2070    /// if ui.button("Find").clicked {
2071    ///     ui.focus_by_name("search");
2072    /// }
2073    /// ```
2074    pub fn focus_by_name(&mut self, name: &str) -> bool {
2075        // Resolve against either the previous frame's settled map or the
2076        // in-progress map being built right now. The latter handles the
2077        // common "register, then focus_by_name in the same frame" pattern
2078        // that callers naturally expect to return `true`.
2079        //
2080        // The actual focus change still lands at the start of the next
2081        // frame via `focus_name_map_prev` lookup in `Context::new`. The
2082        // return value is purely about resolvability: "true" means the name
2083        // is known and the focus shift will land next frame; "false" means
2084        // the request is pending a future registration.
2085        let resolved =
2086            self.focus_name_map_prev.contains_key(name) || self.focus_name_map.contains_key(name);
2087        // Always store the request — even if it resolved this frame, the
2088        // next-frame plumbing (`Context::new`) is what actually applies
2089        // the index. We use take/replace so the caller cannot stack two
2090        // pending names; the most recent wins.
2091        self.pending_focus_name = Some(name.to_string());
2092        resolved
2093    }
2094
2095    /// Return the name of the currently focused widget, if it was
2096    /// registered with
2097    /// [`register_focusable_named`](Self::register_focusable_named) this
2098    /// frame.
2099    ///
2100    /// Returns `None` if the focused widget used the unnamed
2101    /// [`register_focusable`](Self::register_focusable) API or if no widget
2102    /// has focus.
2103    pub fn focused_name(&self) -> Option<&str> {
2104        // Search this frame's map for the entry whose index equals
2105        // `focus_index`. The map is small (one entry per named focusable),
2106        // so a linear scan is fine — typical apps register <50 names.
2107        self.focus_name_map
2108            .iter()
2109            .find_map(|(name, &idx)| (idx == self.focus_index).then_some(name.as_str()))
2110    }
2111
2112    /// Iterate unconsumed key-press events, gated on `active`.
2113    ///
2114    /// When `active` is `false`, returns an empty iterator. When `active`
2115    /// is `true`, behaves identically to the internal
2116    /// `available_key_presses`. The returned indices are valid for
2117    /// [`consume_event`](Self::consume_event).
2118    ///
2119    /// This is the **preferred pattern** for focus-gated keyboard handling
2120    /// in custom widgets. Because the iterator borrows `self.events`
2121    /// immutably, collect the indices first and consume them after the
2122    /// loop:
2123    ///
2124    /// ```ignore
2125    /// let focused = ui.register_focusable();
2126    /// let mut hits: Vec<usize> = Vec::new();
2127    /// for (i, key) in ui.key_presses_when(focused) {
2128    ///     if key.code == slt::KeyCode::Enter {
2129    ///         hits.push(i);
2130    ///         // ... handle Enter ...
2131    ///     }
2132    /// }
2133    /// for i in hits { ui.consume_event(i); }
2134    /// ```
2135    pub fn key_presses_when(
2136        &self,
2137        active: bool,
2138    ) -> impl Iterator<Item = (usize, &crate::event::KeyEvent)> + '_ {
2139        // The `!active` short-circuit at the head of the predicate yields
2140        // an empty iterator at zero allocation cost when the widget isn't
2141        // focused. Indices are still drawn from `self.events` so callers
2142        // can pass them straight to `consume_event`.
2143        self.events
2144            .iter()
2145            .enumerate()
2146            .filter_map(move |(i, event)| {
2147                if !active {
2148                    return None;
2149                }
2150                if self.consumed.get(i).copied().unwrap_or(true) {
2151                    return None;
2152                }
2153                match event {
2154                    Event::Key(key) if key.kind == KeyEventKind::Press => Some((i, key)),
2155                    _ => None,
2156                }
2157            })
2158    }
2159
2160    /// Mark the event at `index` as consumed.
2161    ///
2162    /// Public counterpart to the crate-internal `consume_indices`. Use
2163    /// this in custom widgets after handling an event yielded by
2164    /// [`key_presses_when`](Self::key_presses_when) so subsequent widgets
2165    /// don't react to the same key. Out-of-range indices are silently
2166    /// ignored (matching the iterator-pair semantics).
2167    pub fn consume_event(&mut self, index: usize) {
2168        if let Some(slot) = self.consumed.get_mut(index) {
2169            *slot = true;
2170        }
2171    }
2172
2173    // ── Issue #233: in-frame static-log append ───────────────────────────
2174    //
2175    // The runtime holds the buffer inside `named_states` under a reserved
2176    // sentinel key. `Context::new` (owned by another agent) does not need to
2177    // initialise this field — `or_insert_with` handles first-call creation,
2178    // and `lib::run_frame_kernel` drains the buffer back into `FrameState`
2179    // for the run-loop to consume.
2180
2181    /// Append a line that will be flushed to terminal scrollback **before**
2182    /// the dynamic frame content (issue #233).
2183    ///
2184    /// Lines accumulated this frame are written via the active runtime — for
2185    /// [`crate::run_static`] / [`crate::run_static_with`], they are printed
2186    /// above the inline dynamic area as committed scrollback. For full-screen
2187    /// runtimes ([`crate::run`], [`crate::run_async`]) and inline mode
2188    /// ([`crate::run_inline`]), the buffer is silently dropped after a debug
2189    /// warning is emitted on the first call per frame, since those modes have
2190    /// no scrollback area to write to.
2191    ///
2192    /// The headless [`crate::TestBackend`] accumulates the lines into the
2193    /// frame state where they can be drained by tests via
2194    /// [`Context::take_static_log`] (or by inspecting the buffer when
2195    /// constructing a custom backend).
2196    ///
2197    /// # Order
2198    ///
2199    /// `static_log` may be called any number of times per frame. Lines are
2200    /// flushed in call order, all before the dynamic frame for the same
2201    /// tick.
2202    ///
2203    /// # Example
2204    ///
2205    /// ```
2206    /// # use slt::*;
2207    /// # TestBackend::new(40, 4).render(|ui| {
2208    /// ui.static_log("event 1");
2209    /// ui.static_log(format!("event {}", 2));
2210    /// ui.text("dynamic content");
2211    /// # });
2212    /// ```
2213    pub fn static_log(&mut self, line: impl Into<String>) {
2214        let entry = self
2215            .named_states
2216            .entry(STATIC_LOG_KEY)
2217            .or_insert_with(|| Box::new(Vec::<String>::new()) as Box<dyn std::any::Any>);
2218        if let Some(buf) = entry.downcast_mut::<Vec<String>>() {
2219            buf.push(line.into());
2220        }
2221    }
2222
2223    /// Drain and return the queued static-log lines for the current frame
2224    /// (issue #233). Used by tests / external backends to inspect what
2225    /// `ui.static_log(...)` emitted during a [`crate::TestBackend::render`]
2226    /// call.
2227    pub fn take_static_log(&mut self) -> Vec<String> {
2228        if let Some(boxed) = self.named_states.get_mut(STATIC_LOG_KEY)
2229            && let Some(buf) = boxed.downcast_mut::<Vec<String>>()
2230        {
2231            return std::mem::take(buf);
2232        }
2233        Vec::new()
2234    }
2235
2236    // ── Issue #236: widget keymap publishing ─────────────────────────────
2237
2238    /// Publish a widget's keymap so the framework can show it in the help
2239    /// overlay (issue #236).
2240    ///
2241    /// Each call registers `(name, bindings)` for the current frame. Widgets
2242    /// implementing [`crate::keymap::WidgetKeyHelp`] typically forward their
2243    /// `key_help()` slice here:
2244    ///
2245    /// ```
2246    /// # use slt::*;
2247    /// # use slt::keymap::WidgetKeyHelp;
2248    /// struct Counter;
2249    /// impl WidgetKeyHelp for Counter {
2250    ///     fn key_help(&self) -> &'static [(&'static str, &'static str)] {
2251    ///         const HELP: &[(&str, &str)] = &[("↑", "increment"), ("↓", "decrement")];
2252    ///         HELP
2253    ///     }
2254    /// }
2255    /// # TestBackend::new(40, 4).render(|ui| {
2256    /// let counter = Counter;
2257    /// ui.publish_keymap("counter", counter.key_help());
2258    /// # });
2259    /// ```
2260    ///
2261    /// The registry is reset at the start of every frame (the first call on a
2262    /// new tick clears stale entries). Both calls in the same frame
2263    /// accumulate; calls across frames do not leak.
2264    pub fn publish_keymap(
2265        &mut self,
2266        name: &'static str,
2267        bindings: &'static [(&'static str, &'static str)],
2268    ) {
2269        // The registry is cleared at frame start by `run_frame_kernel`
2270        // (issue #236) — see `clear_keymap_registry` in `lib.rs`. We just
2271        // need to insert/append here.
2272        let entry = self
2273            .named_states
2274            .entry(KEYMAP_REGISTRY_KEY)
2275            .or_insert_with(|| {
2276                Box::new(Vec::<crate::keymap::PublishedKeymap>::new()) as Box<dyn std::any::Any>
2277            });
2278        if let Some(vec) = entry.downcast_mut::<Vec<crate::keymap::PublishedKeymap>>() {
2279            vec.push(crate::keymap::PublishedKeymap::new(name, bindings));
2280        }
2281    }
2282
2283    /// Return all keymaps published this frame (issue #236).
2284    ///
2285    /// Empty if no widget called [`Context::publish_keymap`] yet on the
2286    /// current frame. The registry is reset at the start of every frame.
2287    pub fn published_keymaps(&self) -> &[crate::keymap::PublishedKeymap] {
2288        if let Some(boxed) = self.named_states.get(KEYMAP_REGISTRY_KEY)
2289            && let Some(vec) = boxed.downcast_ref::<Vec<crate::keymap::PublishedKeymap>>()
2290        {
2291            return vec;
2292        }
2293        &[]
2294    }
2295
2296    /// Render an automatic keymap-help overlay listing every widget keymap
2297    /// published this frame (issue #236).
2298    ///
2299    /// Pass `open = true` to render the overlay (typically gated on a
2300    /// `?` / `F1` keypress). When `open` is `false`, this method is a
2301    /// no-op. The overlay groups bindings by widget name and dismisses
2302    /// when the next frame is rendered with `open = false`.
2303    ///
2304    /// # Example
2305    ///
2306    /// ```
2307    /// # use slt::*;
2308    /// # TestBackend::new(40, 12).render(|ui| {
2309    /// const RICHLOG: &[(&str, &str)] = &[("↑/k", "scroll up"), ("↓/j", "scroll down")];
2310    /// ui.publish_keymap("rich_log", RICHLOG);
2311    /// // Show the help overlay when '?' is pressed
2312    /// let show = ui.key('?');
2313    /// ui.keymap_help_overlay(show);
2314    /// # });
2315    /// ```
2316    pub fn keymap_help_overlay(&mut self, open: bool) {
2317        if !open {
2318            return;
2319        }
2320
2321        let entries: Vec<crate::keymap::PublishedKeymap> = self.published_keymaps().to_vec();
2322        if entries.is_empty() {
2323            return;
2324        }
2325
2326        let theme = self.theme;
2327        let _ = self.modal(|ui| {
2328            ui.styled("Keyboard shortcuts", Style::new().bold().fg(theme.primary));
2329            ui.text("");
2330            for entry in &entries {
2331                ui.styled(entry.name, Style::new().bold().fg(theme.text));
2332                for (key, desc) in entry.bindings {
2333                    let line = format!("  {key:<14}  {desc}");
2334                    ui.styled(line, Style::new().fg(theme.text_dim));
2335                }
2336                ui.text("");
2337            }
2338            ui.styled(
2339                "Press Esc / ? to close",
2340                Style::new().fg(theme.text_dim).italic(),
2341            );
2342        });
2343    }
2344}
2345
2346// Sentinel keys reused from `lib.rs` so the two reads/writes can never drift.
2347use crate::{
2348    KEYMAP_REGISTRY_NAMED_STATE_KEY as KEYMAP_REGISTRY_KEY,
2349    STATIC_LOG_NAMED_STATE_KEY as STATIC_LOG_KEY,
2350};