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};