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