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 let screen_hook_map = std::mem::take(&mut state.screen_hook_map);
18 let focus = &mut state.focus;
19 // Issue #217: name→index map from the previous frame, used to resolve
20 // `focus_by_name(name)` at frame start. We move it out so the
21 // `register_focusable_named` calls in this frame can rebuild a fresh
22 // `focus_name_map`. The fresh map is swapped back into
23 // `focus_name_map_prev` at frame end.
24 let focus_name_map_prev = std::mem::take(&mut focus.focus_name_map_prev);
25 let pending_focus_name = focus.pending_focus_name.take();
26 let prev_focus_index = focus.prev_focus_index;
27 let layout_feedback = &mut state.layout_feedback;
28 let diagnostics = &mut state.diagnostics;
29 let consumed = vec![false; events.len()];
30
31 let mut mouse_pos = layout_feedback.last_mouse_pos;
32 let mut click_pos = None;
33 let mut right_click_pos = None;
34 for event in &events {
35 if let Event::Mouse(mouse) = event {
36 mouse_pos = Some((mouse.x, mouse.y));
37 match mouse.kind {
38 MouseKind::Down(MouseButton::Left) => {
39 click_pos = Some((mouse.x, mouse.y));
40 }
41 MouseKind::Down(MouseButton::Right) => {
42 // Issue #208: capture last right-click position so
43 // `response_for` can hit-test against per-widget rects.
44 right_click_pos = Some((mouse.x, mouse.y));
45 }
46 _ => {}
47 }
48 }
49 }
50
51 let mut focus_index = focus.focus_index;
52 if let Some((mx, my)) = click_pos {
53 let mut best: Option<(usize, u64)> = None;
54 for &(fid, rect) in &layout_feedback.prev_focus_rects {
55 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
56 let area = rect.width as u64 * rect.height as u64;
57 if best.map_or(true, |(_, ba)| area < ba) {
58 best = Some((fid, area));
59 }
60 }
61 }
62 if let Some((fid, _)) = best {
63 focus_index = fid;
64 }
65 }
66
67 // Issue #217: resolve a pending `focus_by_name(...)` request against
68 // the previous frame's `name → index` map. If the name wasn't
69 // registered last frame, we keep the request pending for the next
70 // frame so a widget that registers later can still receive focus.
71 // If the request resolves, we consume it.
72 let mut still_pending: Option<String> = None;
73 if let Some(name) = pending_focus_name {
74 if let Some(&resolved) = focus_name_map_prev.get(&name) {
75 focus_index = resolved;
76 } else {
77 still_pending = Some(name);
78 }
79 }
80
81 // Reuse `commands_buf` capacity from the previous frame (issue #150).
82 // `mem::take` swaps an empty Vec into `state.commands_buf`; we then
83 // clear (no-op when reclaimed from a `build_tree` drain, defensive
84 // when reclaimed from the quit path that ran without `build_tree`)
85 // and reuse the allocation. After `build_tree(&mut ctx.commands)`
86 // drains the Vec in place, the empty (but capacity-bearing) Vec is
87 // moved back into `state.commands_buf` at frame end inside
88 // `run_frame_kernel`.
89 let mut commands = std::mem::take(&mut state.commands_buf);
90 commands.clear();
91
92 // Issue #204: reuse the six per-frame `Vec`/`HashSet` allocations
93 // (`context_stack`, `deferred_draws`, `rollback.group_stack`,
94 // `rollback.text_color_stack`, `pending_tooltips`, `hovered_groups`).
95 // Same `mem::take` pattern as `commands_buf` (#150). Each buffer is
96 // empty at frame end (asserted at `run_frame_kernel`) — `mem::take`
97 // hands a `Default::default()` empty back to the state, the Vec/HashSet
98 // we move into `Context` keeps its capacity from the prior frame, and
99 // `clear()` here is a no-op except as a defensive guard against future
100 // refactors that might leak items past the assertions.
101 let mut context_stack = std::mem::take(&mut state.context_stack_buf);
102 context_stack.clear();
103 let mut deferred_draws = std::mem::take(&mut state.deferred_draws_buf);
104 deferred_draws.clear();
105 let mut group_stack = std::mem::take(&mut state.group_stack_buf);
106 group_stack.clear();
107 let mut text_color_stack = std::mem::take(&mut state.text_color_stack_buf);
108 text_color_stack.clear();
109 let mut pending_tooltips = std::mem::take(&mut state.pending_tooltips_buf);
110 pending_tooltips.clear();
111 let hovered_groups = std::mem::take(&mut state.hovered_groups_buf);
112 // `hovered_groups` is `clear()`-ed inside `build_hovered_groups`
113 // immediately below, so we do not pre-clear here — capacity is
114 // preserved across frames.
115
116 let mut ctx = Self {
117 commands,
118 events,
119 consumed,
120 should_quit: false,
121 area_width: width,
122 area_height: height,
123 tick: diagnostics.tick,
124 focus_index,
125 hook_states: std::mem::take(hook_states),
126 named_states,
127 keyed_states,
128 context_stack,
129 prev_focus_count: focus.prev_focus_count,
130 prev_modal_focus_start: focus.prev_modal_focus_start,
131 prev_modal_focus_count: focus.prev_modal_focus_count,
132 prev_scroll_infos: std::mem::take(&mut layout_feedback.prev_scroll_infos),
133 prev_scroll_rects: std::mem::take(&mut layout_feedback.prev_scroll_rects),
134 prev_hit_map: std::mem::take(&mut layout_feedback.prev_hit_map),
135 prev_group_rects: std::mem::take(&mut layout_feedback.prev_group_rects),
136 prev_focus_groups: std::mem::take(&mut layout_feedback.prev_focus_groups),
137 mouse_pos,
138 click_pos,
139 right_click_pos,
140 prev_modal_active: focus.prev_modal_active,
141 clipboard_text: None,
142 debug: diagnostics.debug_mode,
143 debug_layer: diagnostics.debug_layer,
144 theme,
145 is_real_terminal: false,
146 deferred_draws,
147 rollback: ContextRollbackState {
148 last_text_idx: None,
149 focus_count: 0,
150 last_focusable_id: None,
151 pending_focusable_id: None,
152 interaction_count: 0,
153 scroll_count: 0,
154 group_count: 0,
155 group_stack,
156 overlay_depth: 0,
157 modal_active: false,
158 modal_focus_start: 0,
159 modal_focus_count: 0,
160 hook_cursor: 0,
161 dark_mode: theme.is_dark,
162 notification_queue: std::mem::take(&mut diagnostics.notification_queue),
163 text_color_stack,
164 },
165 pending_tooltips,
166 hovered_groups,
167 scroll_lines_per_event: 1,
168 screen_hook_map,
169 widget_theme: WidgetTheme::new(),
170 prev_focus_index,
171 focus_name_map_prev,
172 focus_name_map: std::collections::HashMap::new(),
173 pending_focus_name: still_pending,
174 };
175 ctx.build_hovered_groups();
176 ctx
177 }
178
179 fn build_hovered_groups(&mut self) {
180 self.hovered_groups.clear();
181 if let Some(pos) = self.mouse_pos {
182 for (name, rect) in &self.prev_group_rects {
183 if pos.0 >= rect.x
184 && pos.0 < rect.x + rect.width
185 && pos.1 >= rect.y
186 && pos.1 < rect.y + rect.height
187 {
188 self.hovered_groups.insert(std::sync::Arc::clone(name));
189 }
190 }
191 }
192 }
193
194 /// Set how many lines each scroll event moves. Default is 1.
195 pub fn set_scroll_speed(&mut self, lines: u32) {
196 self.scroll_lines_per_event = lines.max(1);
197 }
198
199 /// Get the current scroll speed (lines per scroll event).
200 pub fn scroll_speed(&self) -> u32 {
201 self.scroll_lines_per_event
202 }
203
204 /// Get the current focus index.
205 ///
206 /// Widget indices are assigned in the order [`register_focusable()`](Self::register_focusable) is called.
207 /// Indices are 0-based and wrap at [`focus_count()`](Self::focus_count).
208 pub fn focus_index(&self) -> usize {
209 self.focus_index
210 }
211
212 /// Set the focus index to a specific focusable widget.
213 ///
214 /// Widget indices are assigned in the order [`register_focusable()`](Self::register_focusable) is called
215 /// (0-based). If `index` exceeds the number of focusable widgets it will
216 /// be clamped by the modulo in [`register_focusable`](Self::register_focusable).
217 ///
218 /// # Example
219 ///
220 /// ```no_run
221 /// # slt::run(|ui: &mut slt::Context| {
222 /// // Focus the second focusable widget (index 1)
223 /// ui.set_focus_index(1);
224 /// # });
225 /// ```
226 pub fn set_focus_index(&mut self, index: usize) {
227 self.focus_index = index;
228 }
229
230 /// Get the number of focusable widgets registered in the previous frame.
231 ///
232 /// Returns 0 on the very first frame. Useful together with
233 /// [`set_focus_index()`](Self::set_focus_index) for programmatic focus control.
234 ///
235 /// Note: this intentionally reads `prev_focus_count` (the settled count
236 /// from the last completed frame) rather than `focus_count` (the
237 /// still-incrementing counter for the current frame).
238 #[allow(clippy::misnamed_getters)]
239 pub fn focus_count(&self) -> usize {
240 self.prev_focus_count
241 }
242
243 pub(crate) fn process_focus_keys(&mut self) {
244 for (i, event) in self.events.iter().enumerate() {
245 if self.consumed[i] {
246 continue;
247 }
248 if let Event::Key(key) = event {
249 if key.kind != KeyEventKind::Press {
250 continue;
251 }
252 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
253 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
254 let mut modal_local =
255 self.focus_index.saturating_sub(self.prev_modal_focus_start);
256 modal_local %= self.prev_modal_focus_count;
257 let next = (modal_local + 1) % self.prev_modal_focus_count;
258 self.focus_index = self.prev_modal_focus_start + next;
259 } else if self.prev_focus_count > 0 {
260 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
261 }
262 self.consumed[i] = true;
263 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
264 || key.code == KeyCode::BackTab
265 {
266 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
267 let mut modal_local =
268 self.focus_index.saturating_sub(self.prev_modal_focus_start);
269 modal_local %= self.prev_modal_focus_count;
270 let prev = if modal_local == 0 {
271 self.prev_modal_focus_count - 1
272 } else {
273 modal_local - 1
274 };
275 self.focus_index = self.prev_modal_focus_start + prev;
276 } else if self.prev_focus_count > 0 {
277 self.focus_index = if self.focus_index == 0 {
278 self.prev_focus_count - 1
279 } else {
280 self.focus_index - 1
281 };
282 }
283 self.consumed[i] = true;
284 }
285 }
286 }
287 }
288
289 /// Render a custom [`Widget`].
290 ///
291 /// Calls [`Widget::ui`] with this context and returns the widget's response.
292 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
293 w.ui(self)
294 }
295
296 /// Wrap child widgets in a panic boundary.
297 ///
298 /// If the closure panics, the panic is caught and an error message is
299 /// rendered in place of the children. The app continues running.
300 ///
301 /// # Example
302 ///
303 /// ```no_run
304 /// # slt::run(|ui: &mut slt::Context| {
305 /// ui.error_boundary(|ui| {
306 /// ui.text("risky widget");
307 /// });
308 /// # });
309 /// ```
310 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
311 self.error_boundary_with(f, |ui, msg| {
312 ui.styled(
313 format!("⚠ Error: {msg}"),
314 Style::new().fg(ui.theme.error).bold(),
315 );
316 });
317 }
318
319 /// Like [`error_boundary`](Self::error_boundary), but renders a custom
320 /// fallback instead of the default error message.
321 ///
322 /// The fallback closure receives the panic message as a [`String`].
323 ///
324 /// # Example
325 ///
326 /// ```no_run
327 /// # slt::run(|ui: &mut slt::Context| {
328 /// ui.error_boundary_with(
329 /// |ui| {
330 /// ui.text("risky widget");
331 /// },
332 /// |ui, msg| {
333 /// ui.text(format!("Recovered from panic: {msg}"));
334 /// },
335 /// );
336 /// # });
337 /// ```
338 pub fn error_boundary_with(
339 &mut self,
340 f: impl FnOnce(&mut Context),
341 fallback: impl FnOnce(&mut Context, String),
342 ) {
343 let snapshot = ContextCheckpoint::capture(self);
344
345 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
346 f(self);
347 }));
348
349 match result {
350 Ok(()) => {}
351 Err(panic_info) => {
352 if self.is_real_terminal {
353 #[cfg(feature = "crossterm")]
354 {
355 let _ = crossterm::terminal::enable_raw_mode();
356 let _ = crossterm::execute!(
357 std::io::stdout(),
358 crossterm::terminal::EnterAlternateScreen
359 );
360 }
361
362 #[cfg(not(feature = "crossterm"))]
363 {}
364 }
365
366 snapshot.restore(self);
367
368 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
369 (*s).to_string()
370 } else if let Some(s) = panic_info.downcast_ref::<String>() {
371 s.clone()
372 } else {
373 "widget panicked".to_string()
374 };
375
376 fallback(self, msg);
377 }
378 }
379 }
380
381 /// Reserve the next interaction slot without emitting a marker command.
382 pub(crate) fn reserve_interaction_slot(&mut self) -> usize {
383 let id = self.rollback.interaction_count;
384 self.rollback.interaction_count += 1;
385 id
386 }
387
388 /// Advance the interaction counter for structural commands that still
389 /// participate in hit-map indexing.
390 pub(crate) fn skip_interaction_slot(&mut self) {
391 self.reserve_interaction_slot();
392 }
393
394 /// Reserve the next interaction ID and emit a marker command.
395 pub(crate) fn next_interaction_id(&mut self) -> usize {
396 let id = self.reserve_interaction_slot();
397 self.commands.push(Command::InteractionMarker(id));
398 id
399 }
400
401 /// Allocate a click/hover interaction slot and return the [`Response`].
402 ///
403 /// Use this in custom widgets to detect mouse clicks and hovers without
404 /// wrapping content in a container. Call it immediately before the text,
405 /// rich text, link, or container that should own the interaction rect.
406 /// Each call reserves one slot in the hit-test map, so the call order
407 /// must be stable across frames.
408 pub fn interaction(&mut self) -> Response {
409 if (self.rollback.modal_active || self.prev_modal_active)
410 && self.rollback.overlay_depth == 0
411 {
412 return Response::none();
413 }
414 let id = self.next_interaction_id();
415 self.response_for(id)
416 }
417
418 pub(crate) fn begin_widget_interaction(&mut self, focused: bool) -> (usize, Response) {
419 let interaction_id = self.next_interaction_id();
420 let mut response = self.response_for(interaction_id);
421 response.focused = focused;
422 // Issue #208: compute focus transitions from the most recent
423 // `register_focusable` call. If that focusable lined up with the
424 // previously-focused widget index from the prior frame, focus
425 // changes since map directly to gained/lost.
426 if let Some(this_id) = self.rollback.last_focusable_id {
427 let was_focused = self
428 .prev_focus_index
429 .map(|prev| prev == this_id)
430 .unwrap_or(false);
431 response.gained_focus = focused && !was_focused;
432 response.lost_focus = !focused && was_focused;
433 // Consume the marker so a single `register_focusable` powers
434 // exactly one `begin_widget_interaction` call.
435 self.rollback.last_focusable_id = None;
436 }
437 (interaction_id, response)
438 }
439
440 pub(crate) fn consume_indices<I>(&mut self, indices: I)
441 where
442 I: IntoIterator<Item = usize>,
443 {
444 for index in indices {
445 self.consumed[index] = true;
446 }
447 }
448
449 pub(crate) fn available_key_presses(
450 &self,
451 ) -> impl Iterator<Item = (usize, &crate::event::KeyEvent)> + '_ {
452 self.events.iter().enumerate().filter_map(|(i, event)| {
453 if self.consumed[i] {
454 return None;
455 }
456 match event {
457 Event::Key(key) if key.kind == KeyEventKind::Press => Some((i, key)),
458 _ => None,
459 }
460 })
461 }
462
463 pub(crate) fn available_pastes(&self) -> impl Iterator<Item = (usize, &str)> + '_ {
464 self.events.iter().enumerate().filter_map(|(i, event)| {
465 if self.consumed[i] {
466 return None;
467 }
468 match event {
469 Event::Paste(text) => Some((i, text.as_str())),
470 _ => None,
471 }
472 })
473 }
474
475 pub(crate) fn left_clicks_in_rect(
476 &self,
477 rect: Rect,
478 ) -> impl Iterator<Item = (usize, &crate::event::MouseEvent)> + '_ {
479 self.mouse_events_in_rect(rect).filter_map(|(i, mouse)| {
480 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
481 Some((i, mouse))
482 } else {
483 None
484 }
485 })
486 }
487
488 pub(crate) fn mouse_events_in_rect(
489 &self,
490 rect: Rect,
491 ) -> impl Iterator<Item = (usize, &crate::event::MouseEvent)> + '_ {
492 self.events
493 .iter()
494 .enumerate()
495 .filter_map(move |(i, event)| {
496 if self.consumed[i] {
497 return None;
498 }
499
500 let Event::Mouse(mouse) = event else {
501 return None;
502 };
503
504 if mouse.x < rect.x
505 || mouse.x >= rect.right()
506 || mouse.y < rect.y
507 || mouse.y >= rect.bottom()
508 {
509 return None;
510 }
511
512 Some((i, mouse))
513 })
514 }
515
516 pub(crate) fn left_clicks_for_interaction(
517 &self,
518 interaction_id: usize,
519 ) -> Option<(Rect, Vec<(usize, &crate::event::MouseEvent)>)> {
520 let rect = self.prev_hit_map.get(interaction_id).copied()?;
521 let clicks = self.left_clicks_in_rect(rect).collect();
522 Some((rect, clicks))
523 }
524
525 pub(crate) fn consume_activation_keys(&mut self, focused: bool) -> bool {
526 if !focused {
527 return false;
528 }
529
530 // Activation keys (Enter / Space) are typically 0–1 per frame and
531 // bounded above by the simultaneous-keypress count from the input
532 // pipeline (well under 8 in practice). A `SmallVec` with an 8-slot
533 // inline capacity eliminates the per-focusable `Vec<usize>` heap
534 // allocation that showed up on every focused widget × every frame.
535 // Spillover beyond 8 falls back to the heap automatically. Closes #135.
536 let consumed: smallvec::SmallVec<[usize; 8]> = self
537 .available_key_presses()
538 .filter_map(|(i, key)| {
539 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
540 Some(i)
541 } else {
542 None
543 }
544 })
545 .collect();
546 let activated = !consumed.is_empty();
547 if activated {
548 // `consume_indices` takes `IntoIterator<Item = usize>` — `SmallVec`
549 // satisfies that bound directly, no signature change needed.
550 self.consume_indices(consumed);
551 }
552 activated
553 }
554
555 /// Register a widget as focusable and return whether it currently has focus.
556 ///
557 /// Call this in custom widgets that need keyboard focus. Each call increments
558 /// the internal focus counter, so the call order must be stable across frames.
559 ///
560 /// # Slot reservation by `register_focusable_named`
561 ///
562 /// If [`register_focusable_named`](Self::register_focusable_named) was
563 /// called immediately before this call, it has already allocated a
564 /// slot and bound a name to it; this call **reuses** that slot
565 /// instead of allocating a fresh one. That keeps the name binding
566 /// pointed at the widget the user sees rather than at a dummy slot.
567 pub fn register_focusable(&mut self) -> bool {
568 if (self.rollback.modal_active || self.prev_modal_active)
569 && self.rollback.overlay_depth == 0
570 {
571 self.rollback.last_focusable_id = None;
572 // Drop any pending reservation: the suppressed widget never
573 // attached, so reusing the reserved id from a later widget in
574 // the same frame would silently rebind the name to the wrong
575 // slot.
576 self.rollback.pending_focusable_id = None;
577 return false;
578 }
579 // Issue #217 follow-up: if `register_focusable_named` reserved a
580 // slot for us, reuse it (and skip the FocusMarker push — it was
581 // already emitted when the reservation was made). Otherwise,
582 // allocate a fresh slot the normal way.
583 let (id, freshly_allocated) =
584 if let Some(reserved) = self.rollback.pending_focusable_id.take() {
585 (reserved, false)
586 } else {
587 let id = self.rollback.focus_count;
588 self.rollback.focus_count += 1;
589 (id, true)
590 };
591 // Issue #208: remember this widget's focus id so the immediately
592 // following `begin_widget_interaction` call can compare against
593 // `prev_focus_index` and emit gained/lost focus signals.
594 self.rollback.last_focusable_id = Some(id);
595 if freshly_allocated {
596 self.commands.push(Command::FocusMarker(id));
597 }
598 if self.prev_modal_active
599 && self.prev_modal_focus_count > 0
600 && self.rollback.modal_active
601 && self.rollback.overlay_depth > 0
602 {
603 let mut modal_local_id = id.saturating_sub(self.rollback.modal_focus_start);
604 modal_local_id %= self.prev_modal_focus_count;
605 let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
606 modal_focus_idx %= self.prev_modal_focus_count;
607 return modal_local_id == modal_focus_idx;
608 }
609 if self.prev_focus_count == 0 {
610 return true;
611 }
612 self.focus_index % self.prev_focus_count == id
613 }
614
615 /// Create persistent state that survives across frames.
616 ///
617 /// Returns a `State<T>` handle. Access with `state.get(ui)` / `state.get_mut(ui)`.
618 ///
619 /// # Rules
620 /// - Must be called in the same order every frame (like React hooks)
621 /// - Do NOT call inside if/else that changes between frames
622 ///
623 /// # Example
624 /// ```ignore
625 /// let count = ui.use_state(|| 0i32);
626 /// let val = count.get(ui);
627 /// ui.text(format!("Count: {val}"));
628 /// if ui.button("+1").clicked {
629 /// *count.get_mut(ui) += 1;
630 /// }
631 /// ```
632 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
633 let idx = self.rollback.hook_cursor;
634 self.rollback.hook_cursor += 1;
635
636 if idx >= self.hook_states.len() {
637 self.hook_states.push(Box::new(init()));
638 }
639
640 State::from_idx(idx)
641 }
642
643 /// Component-local persistent state keyed by a stable id.
644 ///
645 /// Unlike [`use_state`](Self::use_state), this is **not order-dependent** —
646 /// the value is looked up by `id` instead of call position. Safe to call
647 /// inside conditional branches or reusable component functions.
648 ///
649 /// Returns a `State<T>` handle. Access with `state.get(ui)` /
650 /// `state.get_mut(ui)`. Persists across frames.
651 ///
652 /// # Scoping
653 ///
654 /// Keys are `&'static str` and live in a single global namespace per
655 /// `Context` (no automatic per-component scoping). Two calls with the same
656 /// `id` in the same frame share the same value, regardless of where they
657 /// occur in the tree. Pick unique ids — for example, prefix with a
658 /// component name (`"counter::value"`).
659 ///
660 /// # Example
661 ///
662 /// ```ignore
663 /// fn counter(ui: &mut slt::Context) {
664 /// let count = ui.use_state_named_with("counter::value", || 0i32);
665 /// ui.text(format!("Count: {}", count.get(ui)));
666 /// if ui.button("+1").clicked {
667 /// *count.get_mut(ui) += 1;
668 /// }
669 /// }
670 /// ```
671 pub fn use_state_named_with<T: 'static>(
672 &mut self,
673 id: &'static str,
674 init: impl FnOnce() -> T,
675 ) -> State<T> {
676 self.named_states
677 .entry(id)
678 .or_insert_with(|| Box::new(init()));
679 State::from_named(id)
680 }
681
682 /// Like [`use_state_named_with`](Self::use_state_named_with), but uses
683 /// [`Default::default()`] to initialize the value on first call.
684 ///
685 /// # Example
686 ///
687 /// ```ignore
688 /// let value = ui.use_state_named::<i32>("counter::value");
689 /// ```
690 pub fn use_state_named<T: 'static + Default>(&mut self, id: &'static str) -> State<T> {
691 self.use_state_named_with(id, T::default)
692 }
693
694 /// Smoothly animate between `0.0` and `1.0` driven by a boolean.
695 ///
696 /// Returns the current interpolated value (0.0..=1.0). When `value` is
697 /// `true` the result tweens toward `1.0`; when `false` it tweens back
698 /// toward `0.0`. The transition duration defaults to
699 /// [`DEFAULT_ANIMATE_TICKS`](crate::anim::DEFAULT_ANIMATE_TICKS) (12 ticks
700 /// ≈ 200 ms at 60 Hz). Use [`Context::animate_value`] for custom duration
701 /// or non-binary targets.
702 ///
703 /// State is stored in the per-context named-state map under `id`. The
704 /// id is `&'static str` (single global namespace per context), matching
705 /// [`Context::use_state_named`]. Pick a unique key per call site — two
706 /// `animate_bool` calls with the same id share state.
707 ///
708 /// On the first call, the value snaps to the target with no visible
709 /// transition (so widgets that mount in their final state don't pop).
710 ///
711 /// # Example
712 /// ```ignore
713 /// let opacity = ui.animate_bool("sidebar::visible", is_open);
714 /// // 0.0 ≤ opacity ≤ 1.0; use as alpha or visibility threshold.
715 /// ```
716 pub fn animate_bool(&mut self, id: &'static str, value: bool) -> f64 {
717 let target = if value { 1.0 } else { 0.0 };
718 self.animate_value(id, target, crate::anim::DEFAULT_ANIMATE_TICKS)
719 }
720
721 /// Smoothly animate a `f64` value toward `target` over `duration_ticks`.
722 ///
723 /// Uses a linear-easing [`crate::Tween`] stored implicitly in the
724 /// per-context named-state map under `id`. Returns the current
725 /// interpolated value. On the first call the value snaps to `target`
726 /// with no visible transition; on subsequent calls when `target`
727 /// changes the tween is rebuilt starting from the current interpolated
728 /// value, so retargeting mid-flight does not produce a jump.
729 ///
730 /// `duration_ticks == 0` snaps immediately to the new target.
731 ///
732 /// # Example
733 /// ```ignore
734 /// let bar_height = ui.animate_value("loading::bar", target_height, 30);
735 /// ui.bar(bar_height);
736 /// ```
737 ///
738 /// # Comparison with `Tween`
739 /// Use this shorthand when you want zero boilerplate and linear easing
740 /// is acceptable. For custom easing, a non-static key, or
741 /// non-tick-based control, construct a [`crate::Tween`] explicitly via
742 /// [`Context::use_state_named_with`](Self::use_state_named_with).
743 pub fn animate_value(&mut self, id: &'static str, target: f64, duration_ticks: u64) -> f64 {
744 let tick = self.tick;
745 let entry = self
746 .named_states
747 .entry(id)
748 .or_insert_with(|| Box::new(crate::anim::AnimState::new(target, tick)));
749 let state = entry
750 .downcast_mut::<crate::anim::AnimState>()
751 .unwrap_or_else(|| {
752 panic!(
753 "animate_value: id {:?} is already used for a different state type",
754 id
755 )
756 });
757 state.sample(target, duration_ticks, tick)
758 }
759
760 /// Push a value onto the context stack for the duration of `body`.
761 ///
762 /// Inside `body`, child widgets can call
763 /// [`use_context::<T>()`](Self::use_context) or
764 /// [`try_use_context::<T>()`](Self::try_use_context) to look up the
765 /// nearest provided value of type `T`. Provides cascade in LIFO order:
766 /// nested calls with the same `T` shadow outer ones.
767 ///
768 /// The value is automatically popped when `body` returns — including on
769 /// panic, so the context stack is always restored.
770 ///
771 /// # Example
772 ///
773 /// ```ignore
774 /// struct Theme { accent: slt::Color }
775 /// ui.provide(Theme { accent: slt::Color::Red }, |ui| {
776 /// // Any widget here can `let theme = ui.use_context::<Theme>();`
777 /// render_button(ui);
778 /// });
779 /// ```
780 pub fn provide<T: 'static, R>(&mut self, value: T, body: impl FnOnce(&mut Context) -> R) -> R {
781 self.context_stack
782 .push(Box::new(value) as Box<dyn std::any::Any>);
783
784 // catch_unwind ensures the entry is popped even if `body` panics, so
785 // the context stack is never left with leaked frames. We re-panic
786 // afterwards so the panic propagates normally to outer scopes.
787 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
788
789 // Pop in both success and panic paths.
790 self.context_stack.pop();
791
792 match result {
793 Ok(value) => value,
794 Err(panic) => std::panic::resume_unwind(panic),
795 }
796 }
797
798 /// Look up the nearest provided value of type `T` on the context stack.
799 ///
800 /// Searches from the top of the stack (most-recent
801 /// [`provide`](Self::provide)) downward. Returns the first match.
802 ///
803 /// # Panics
804 ///
805 /// Panics if no value of type `T` is currently provided. Use
806 /// [`try_use_context`](Self::try_use_context) for a non-panicking variant.
807 pub fn use_context<T: 'static>(&self) -> &T {
808 self.try_use_context::<T>().unwrap_or_else(|| {
809 panic!(
810 "no context of type {} was provided; use ui.provide(value, |ui| ...) in a parent scope",
811 std::any::type_name::<T>()
812 )
813 })
814 }
815
816 /// Like [`use_context`](Self::use_context), but returns `None` instead of
817 /// panicking when no value of type `T` is on the stack.
818 pub fn try_use_context<T: 'static>(&self) -> Option<&T> {
819 self.context_stack
820 .iter()
821 .rev()
822 .find_map(|entry| entry.downcast_ref::<T>())
823 }
824
825 /// Memoize a computed value. Recomputes only when `deps` changes.
826 ///
827 /// # Example
828 /// ```ignore
829 /// let doubled = ui.use_memo(&count, |c| c * 2);
830 /// ui.text(format!("Doubled: {doubled}"));
831 /// ```
832 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
833 &mut self,
834 deps: &D,
835 compute: impl FnOnce(&D) -> T,
836 ) -> &T {
837 let idx = self.rollback.hook_cursor;
838 self.rollback.hook_cursor += 1;
839
840 // First call at this slot: allocate fresh state.
841 if idx >= self.hook_states.len() {
842 let value = compute(deps);
843 self.hook_states.push(Box::new((deps.clone(), value)));
844 return self.hook_states[idx]
845 .downcast_ref::<(D, T)>()
846 .map(|(_, v)| v)
847 .expect("freshly inserted slot must downcast to its own type");
848 }
849
850 // Slot already exists: it must be the same `(D, T)` shape we used last
851 // frame, or the caller broke the rules-of-hooks contract.
852 //
853 // Single downcast on the cache-hit path (closes #133): use
854 // `downcast_mut` to update deps/value in place when they change, and
855 // return `&stored.1` directly — eliminating the redundant second
856 // `downcast_ref` that ran on every call regardless of cache state.
857 match self.hook_states[idx].downcast_mut::<(D, T)>() {
858 Some(stored) => {
859 if stored.0 != *deps {
860 stored.0 = deps.clone();
861 stored.1 = compute(deps);
862 }
863 &stored.1
864 }
865 None => panic!(
866 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
867 idx,
868 std::any::type_name::<(D, T)>()
869 ),
870 }
871 }
872
873 /// Returns `light` color if current theme is light mode, `dark` color if dark mode.
874 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
875 if self.theme.is_dark {
876 dark
877 } else {
878 light
879 }
880 }
881
882 /// Show a toast notification without managing ToastState.
883 ///
884 /// # Examples
885 /// ```
886 /// # use slt::*;
887 /// # TestBackend::new(80, 24).render(|ui| {
888 /// ui.notify("File saved!", ToastLevel::Success);
889 /// # });
890 /// ```
891 pub fn notify(&mut self, message: &str, level: ToastLevel) {
892 let tick = self.tick;
893 self.rollback
894 .notification_queue
895 .push((message.to_string(), level, tick));
896 }
897
898 pub(crate) fn render_notifications(&mut self) {
899 let tick = self.tick;
900 self.rollback
901 .notification_queue
902 .retain(|(_, _, created)| tick.saturating_sub(*created) < 180);
903 if self.rollback.notification_queue.is_empty() {
904 return;
905 }
906
907 // The `overlay` closure captures `self` mutably, so we cannot keep an
908 // immutable borrow of `self.rollback.notification_queue` alive across
909 // the call. Move the queue out for the render, then move it back —
910 // no `String::clone` per notification, no intermediate `Vec` alloc.
911 // Closes the non-empty path of #138.
912 let queue = std::mem::take(&mut self.rollback.notification_queue);
913 let theme = self.theme;
914
915 let _ = self.overlay(|ui| {
916 let _ = ui.row(|ui| {
917 ui.spacer();
918 let _ = ui.col(|ui| {
919 for (message, level, _) in queue.iter().rev() {
920 let color = match level {
921 ToastLevel::Info => theme.primary,
922 ToastLevel::Success => theme.success,
923 ToastLevel::Warning => theme.warning,
924 ToastLevel::Error => theme.error,
925 };
926 let mut line = String::with_capacity(2 + message.len());
927 line.push_str("● ");
928 line.push_str(message);
929 ui.styled(line, Style::new().fg(color));
930 }
931 });
932 });
933 });
934
935 // Restore the queue so subsequent frames can re-render until each
936 // entry's TTL expires above.
937 self.rollback.notification_queue = queue;
938 }
939
940 // ----------------------------------------------------------------
941 // v0.20.0 hooks: keyed state, effects, named focus, key gating
942 // ----------------------------------------------------------------
943
944 /// Component-local persistent state keyed by a runtime string.
945 ///
946 /// Unlike [`use_state_named`](Self::use_state_named), `id` can be a
947 /// runtime value such as `format!("row-{i}")`. The key is converted to
948 /// `String` once per call. The hot path (key already present) performs
949 /// **zero string allocations beyond the [`Into<String>`] conversion at
950 /// the call site** — first looking up by `&str`, only allocating a
951 /// fresh map key on first insert. Together: at most **one allocation
952 /// per call, regardless of cache state**.
953 ///
954 /// # When to use
955 /// - Per-item state in a dynamic list where positional [`use_state`]
956 /// would break if items are reordered or filtered.
957 /// - Reusable component functions called with a runtime discriminator.
958 ///
959 /// # Namespace
960 /// Keys live in a single global namespace per `Context`. Prefix them
961 /// to avoid collisions: `format!("my_component::item-{i}")`.
962 ///
963 /// # Stale entries
964 /// Removed items leak their state until the `Context` is dropped (or
965 /// the program exits). For long-running sessions with churn, manage
966 /// state externally via a single `Vec<T>` in [`use_state`].
967 ///
968 /// # Example
969 ///
970 /// ```ignore
971 /// for (i, item) in items.iter().enumerate() {
972 /// let row_state = ui.use_state_keyed(format!("row-{i}"), || ItemState::default());
973 /// // ...
974 /// }
975 /// ```
976 ///
977 /// [`use_state`]: Self::use_state
978 pub fn use_state_keyed<T: 'static>(
979 &mut self,
980 id: impl Into<String>,
981 init: impl FnOnce() -> T,
982 ) -> State<T> {
983 let key: String = id.into();
984 // Lookup by `&str` first to avoid cloning on the hot
985 // (already-populated) path. Only on first insert do we clone the
986 // key into the map; otherwise the original `key` String is the
987 // sole allocation and is moved into `State::from_keyed`.
988 if !self.keyed_states.contains_key(key.as_str()) {
989 self.keyed_states.insert(key.clone(), Box::new(init()));
990 }
991 State::from_keyed(key)
992 }
993
994 /// Like [`use_state_keyed`](Self::use_state_keyed), but uses
995 /// [`Default::default()`] to initialize the value on first call.
996 ///
997 /// # Example
998 ///
999 /// ```ignore
1000 /// let counter = ui.use_state_keyed_default::<i32>(format!("c-{i}"));
1001 /// ```
1002 pub fn use_state_keyed_default<T: Default + 'static>(
1003 &mut self,
1004 id: impl Into<String>,
1005 ) -> State<T> {
1006 self.use_state_keyed(id, T::default)
1007 }
1008
1009 /// Run a side-effecting closure when `deps` changes.
1010 ///
1011 /// On the **first frame** the hook slot is encountered, `f` is called
1012 /// unconditionally. On **subsequent frames**, `f` is only called when
1013 /// `*deps != stored_deps`. The hook is **positional** (same ordering
1014 /// rules as [`use_state`](Self::use_state)).
1015 ///
1016 /// # Fire-and-forget semantics
1017 ///
1018 /// There is no cleanup callback. If setup resources need teardown,
1019 /// store a handle in [`use_state`](Self::use_state) and drop it on
1020 /// a later frame.
1021 ///
1022 /// # Caveat: `error_boundary` re-fire
1023 ///
1024 /// Effects placed inside an [`error_boundary`](Self::error_boundary)
1025 /// scope can re-fire when the boundary catches a panic and rolls back
1026 /// the hook slots. For non-idempotent side effects (network requests,
1027 /// payments) put the effect outside the boundary or guard with an
1028 /// idempotency key.
1029 ///
1030 /// # Common patterns
1031 ///
1032 /// ```ignore
1033 /// // Run once on first frame:
1034 /// ui.use_effect(|_| initialize_logger(), &());
1035 ///
1036 /// // Run when `selected_tab` changes:
1037 /// ui.use_effect(|tab| load_tab_data(*tab), &selected_tab);
1038 /// ```
1039 pub fn use_effect<D: PartialEq + Clone + 'static>(&mut self, f: impl FnOnce(&D), deps: &D) {
1040 let idx = self.rollback.hook_cursor;
1041 self.rollback.hook_cursor += 1;
1042
1043 if idx >= self.hook_states.len() {
1044 // First encounter: run the effect, then store the deps so we
1045 // can detect future changes.
1046 f(deps);
1047 self.hook_states.push(Box::new(deps.clone()));
1048 return;
1049 }
1050
1051 match self.hook_states[idx].downcast_mut::<D>() {
1052 Some(stored) => {
1053 if *stored != *deps {
1054 f(deps);
1055 *stored = deps.clone();
1056 }
1057 }
1058 None => panic!(
1059 "Hook type mismatch at index {idx}: expected {}. \
1060 Hooks must be called in the same order every frame.",
1061 std::any::type_name::<D>()
1062 ),
1063 }
1064 }
1065
1066 /// Register a focusable slot bound to a stable string name.
1067 ///
1068 /// Returns `true` if the registered slot currently has focus, exactly
1069 /// like [`register_focusable`](Self::register_focusable) — but also
1070 /// records the `name → slot` mapping so other code can later call
1071 /// [`focus_by_name`](Self::focus_by_name) and
1072 /// [`focused_name`](Self::focused_name).
1073 ///
1074 /// # How the slot is shared with the widget that follows
1075 ///
1076 /// Every SLT widget that takes focus (`button`, `text_input`,
1077 /// `tabs`, …) internally calls `register_focusable()` to claim its
1078 /// own slot. To keep the name pointed at the **widget the user
1079 /// sees**, this call:
1080 ///
1081 /// 1. allocates a slot eagerly (so the name binding works even when
1082 /// no widget follows — useful for tests and for custom focusable
1083 /// regions),
1084 /// 2. records the `name → slot` mapping into the frame's
1085 /// `focus_name_map` (first-write-wins on duplicate names within
1086 /// a frame),
1087 /// 3. **reserves** the slot id so the next `register_focusable()`
1088 /// on the same frame *reuses* it instead of allocating a fresh
1089 /// slot — that's how `text_input(&mut state)` placed right after
1090 /// inherits the name.
1091 ///
1092 /// Names are re-registered each frame; the previous frame's map is
1093 /// kept under `focus_name_map_prev` so [`focus_by_name`] can resolve
1094 /// a name that has already been registered.
1095 ///
1096 /// # Two valid usage shapes
1097 ///
1098 /// **Shape A — name a widget that follows immediately** (the common
1099 /// pattern; the widget reuses the reserved slot):
1100 ///
1101 /// ```ignore
1102 /// let _ = ui.register_focusable_named("search");
1103 /// let _ = ui.text_input(&mut search_state);
1104 /// // later: ui.focus_by_name("search") jumps to the text_input
1105 /// ```
1106 ///
1107 /// **Shape B — register a named focusable region with no inner
1108 /// widget** (e.g. a custom render area that handles its own keys
1109 /// when focused):
1110 ///
1111 /// ```ignore
1112 /// let focused = ui.register_focusable_named("canvas");
1113 /// if focused { /* react to keys via key_presses_when */ }
1114 /// ```
1115 pub fn register_focusable_named(&mut self, name: &str) -> bool {
1116 // Modal/overlay suppression: when a modal is active and we're not
1117 // inside it, focusables outside the modal must be invisible to
1118 // tab/click cycling. Drop the registration entirely (no slot
1119 // allocation, no name binding, no reservation leak).
1120 if (self.rollback.modal_active || self.prev_modal_active)
1121 && self.rollback.overlay_depth == 0
1122 {
1123 self.rollback.pending_focusable_id = None;
1124 return false;
1125 }
1126 // Eagerly allocate the slot — symmetric with `register_focusable`,
1127 // so the slot exists even when no widget follows.
1128 let id = self.rollback.focus_count;
1129 self.rollback.focus_count += 1;
1130 self.rollback.last_focusable_id = Some(id);
1131 self.commands.push(Command::FocusMarker(id));
1132 // First-write-wins on duplicate names within a single frame —
1133 // a second `register_focusable_named("dup")` keeps the first
1134 // slot bound to the name and orphans its own slot's name binding.
1135 self.focus_name_map.entry(name.to_string()).or_insert(id);
1136 // Reserve `id` for the very next `register_focusable()` call to
1137 // reuse, so widgets like `text_input` placed immediately after
1138 // share the named slot rather than allocating a fresh one.
1139 // Last-write-wins on the reservation: stacking two
1140 // `register_focusable_named` calls without an intervening widget
1141 // leaves the second slot reserved (the first slot stays bound to
1142 // its name in `focus_name_map`, just without a widget attached).
1143 self.rollback.pending_focusable_id = Some(id);
1144 // Same focus-index prediction as `register_focusable`.
1145 if self.prev_modal_active
1146 && self.prev_modal_focus_count > 0
1147 && self.rollback.modal_active
1148 && self.rollback.overlay_depth > 0
1149 {
1150 let mut modal_local_id = id.saturating_sub(self.rollback.modal_focus_start);
1151 modal_local_id %= self.prev_modal_focus_count;
1152 let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
1153 modal_focus_idx %= self.prev_modal_focus_count;
1154 return modal_local_id == modal_focus_idx;
1155 }
1156 if self.prev_focus_count == 0 {
1157 return true;
1158 }
1159 self.focus_index % self.prev_focus_count == id
1160 }
1161
1162 /// Request focus on the named widget.
1163 ///
1164 /// If the named widget was registered last frame the focus change
1165 /// takes effect at the **start of the next frame** (one-frame delay
1166 /// is the deferred-command pattern used throughout SLT). If the name
1167 /// has never been registered, the request stays pending: the next
1168 /// frame to register that name receives focus.
1169 ///
1170 /// Returns `true` if the call **will** resolve — i.e. the name was
1171 /// either registered earlier in this frame (via
1172 /// [`register_focusable_named`](Self::register_focusable_named)) or in
1173 /// the previous frame. Returns `false` only when the name has not been
1174 /// seen by either frame, in which case the request stays pending until
1175 /// some future frame registers the name.
1176 ///
1177 /// # Example
1178 ///
1179 /// ```ignore
1180 /// if ui.button("Find").clicked {
1181 /// ui.focus_by_name("search");
1182 /// }
1183 /// ```
1184 pub fn focus_by_name(&mut self, name: &str) -> bool {
1185 // Resolve against either the previous frame's settled map or the
1186 // in-progress map being built right now. The latter handles the
1187 // common "register, then focus_by_name in the same frame" pattern
1188 // that callers naturally expect to return `true`.
1189 //
1190 // The actual focus change still lands at the start of the next
1191 // frame via `focus_name_map_prev` lookup in `Context::new`. The
1192 // return value is purely about resolvability: "true" means the name
1193 // is known and the focus shift will land next frame; "false" means
1194 // the request is pending a future registration.
1195 let resolved =
1196 self.focus_name_map_prev.contains_key(name) || self.focus_name_map.contains_key(name);
1197 // Always store the request — even if it resolved this frame, the
1198 // next-frame plumbing (`Context::new`) is what actually applies
1199 // the index. We use take/replace so the caller cannot stack two
1200 // pending names; the most recent wins.
1201 self.pending_focus_name = Some(name.to_string());
1202 resolved
1203 }
1204
1205 /// Return the name of the currently focused widget, if it was
1206 /// registered with
1207 /// [`register_focusable_named`](Self::register_focusable_named) this
1208 /// frame.
1209 ///
1210 /// Returns `None` if the focused widget used the unnamed
1211 /// [`register_focusable`](Self::register_focusable) API or if no widget
1212 /// has focus.
1213 pub fn focused_name(&self) -> Option<&str> {
1214 // Search this frame's map for the entry whose index equals
1215 // `focus_index`. The map is small (one entry per named focusable),
1216 // so a linear scan is fine — typical apps register <50 names.
1217 self.focus_name_map
1218 .iter()
1219 .find_map(|(name, &idx)| (idx == self.focus_index).then_some(name.as_str()))
1220 }
1221
1222 /// Iterate unconsumed key-press events, gated on `active`.
1223 ///
1224 /// When `active` is `false`, returns an empty iterator. When `active`
1225 /// is `true`, behaves identically to the internal
1226 /// `available_key_presses`. The returned indices are valid for
1227 /// [`consume_event`](Self::consume_event).
1228 ///
1229 /// This is the **preferred pattern** for focus-gated keyboard handling
1230 /// in custom widgets. Because the iterator borrows `self.events`
1231 /// immutably, collect the indices first and consume them after the
1232 /// loop:
1233 ///
1234 /// ```ignore
1235 /// let focused = ui.register_focusable();
1236 /// let mut hits: Vec<usize> = Vec::new();
1237 /// for (i, key) in ui.key_presses_when(focused) {
1238 /// if key.code == slt::KeyCode::Enter {
1239 /// hits.push(i);
1240 /// // ... handle Enter ...
1241 /// }
1242 /// }
1243 /// for i in hits { ui.consume_event(i); }
1244 /// ```
1245 pub fn key_presses_when(
1246 &self,
1247 active: bool,
1248 ) -> impl Iterator<Item = (usize, &crate::event::KeyEvent)> + '_ {
1249 // The `!active` short-circuit at the head of the predicate yields
1250 // an empty iterator at zero allocation cost when the widget isn't
1251 // focused. Indices are still drawn from `self.events` so callers
1252 // can pass them straight to `consume_event`.
1253 self.events
1254 .iter()
1255 .enumerate()
1256 .filter_map(move |(i, event)| {
1257 if !active {
1258 return None;
1259 }
1260 if self.consumed.get(i).copied().unwrap_or(true) {
1261 return None;
1262 }
1263 match event {
1264 Event::Key(key) if key.kind == KeyEventKind::Press => Some((i, key)),
1265 _ => None,
1266 }
1267 })
1268 }
1269
1270 /// Mark the event at `index` as consumed.
1271 ///
1272 /// Public counterpart to the crate-internal `consume_indices`. Use
1273 /// this in custom widgets after handling an event yielded by
1274 /// [`key_presses_when`](Self::key_presses_when) so subsequent widgets
1275 /// don't react to the same key. Out-of-range indices are silently
1276 /// ignored (matching the iterator-pair semantics).
1277 pub fn consume_event(&mut self, index: usize) {
1278 if let Some(slot) = self.consumed.get_mut(index) {
1279 *slot = true;
1280 }
1281 }
1282
1283 // ── Issue #233: in-frame static-log append ───────────────────────────
1284 //
1285 // The runtime holds the buffer inside `named_states` under a reserved
1286 // sentinel key. `Context::new` (owned by another agent) does not need to
1287 // initialise this field — `or_insert_with` handles first-call creation,
1288 // and `lib::run_frame_kernel` drains the buffer back into `FrameState`
1289 // for the run-loop to consume.
1290
1291 /// Append a line that will be flushed to terminal scrollback **before**
1292 /// the dynamic frame content (issue #233).
1293 ///
1294 /// Lines accumulated this frame are written via the active runtime — for
1295 /// [`crate::run_static`] / [`crate::run_static_with`], they are printed
1296 /// above the inline dynamic area as committed scrollback. For full-screen
1297 /// runtimes ([`crate::run`], [`crate::run_async`]) and inline mode
1298 /// ([`crate::run_inline`]), the buffer is silently dropped after a debug
1299 /// warning is emitted on the first call per frame, since those modes have
1300 /// no scrollback area to write to.
1301 ///
1302 /// The headless [`crate::TestBackend`] accumulates the lines into the
1303 /// frame state where they can be drained by tests via
1304 /// [`Context::take_static_log`] (or by inspecting the buffer when
1305 /// constructing a custom backend).
1306 ///
1307 /// # Order
1308 ///
1309 /// `static_log` may be called any number of times per frame. Lines are
1310 /// flushed in call order, all before the dynamic frame for the same
1311 /// tick.
1312 ///
1313 /// # Example
1314 ///
1315 /// ```
1316 /// # use slt::*;
1317 /// # TestBackend::new(40, 4).render(|ui| {
1318 /// ui.static_log("event 1");
1319 /// ui.static_log(format!("event {}", 2));
1320 /// ui.text("dynamic content");
1321 /// # });
1322 /// ```
1323 pub fn static_log(&mut self, line: impl Into<String>) {
1324 let entry = self
1325 .named_states
1326 .entry(STATIC_LOG_KEY)
1327 .or_insert_with(|| Box::new(Vec::<String>::new()) as Box<dyn std::any::Any>);
1328 if let Some(buf) = entry.downcast_mut::<Vec<String>>() {
1329 buf.push(line.into());
1330 }
1331 }
1332
1333 /// Drain and return the queued static-log lines for the current frame
1334 /// (issue #233). Used by tests / external backends to inspect what
1335 /// `ui.static_log(...)` emitted during a [`crate::TestBackend::render`]
1336 /// call.
1337 pub fn take_static_log(&mut self) -> Vec<String> {
1338 if let Some(boxed) = self.named_states.get_mut(STATIC_LOG_KEY) {
1339 if let Some(buf) = boxed.downcast_mut::<Vec<String>>() {
1340 return std::mem::take(buf);
1341 }
1342 }
1343 Vec::new()
1344 }
1345
1346 // ── Issue #236: widget keymap publishing ─────────────────────────────
1347
1348 /// Publish a widget's keymap so the framework can show it in the help
1349 /// overlay (issue #236).
1350 ///
1351 /// Each call registers `(name, bindings)` for the current frame. Widgets
1352 /// implementing [`crate::keymap::WidgetKeyHelp`] typically forward their
1353 /// `key_help()` slice here:
1354 ///
1355 /// ```
1356 /// # use slt::*;
1357 /// # use slt::keymap::WidgetKeyHelp;
1358 /// struct Counter;
1359 /// impl WidgetKeyHelp for Counter {
1360 /// fn key_help(&self) -> &'static [(&'static str, &'static str)] {
1361 /// const HELP: &[(&str, &str)] = &[("↑", "increment"), ("↓", "decrement")];
1362 /// HELP
1363 /// }
1364 /// }
1365 /// # TestBackend::new(40, 4).render(|ui| {
1366 /// let counter = Counter;
1367 /// ui.publish_keymap("counter", counter.key_help());
1368 /// # });
1369 /// ```
1370 ///
1371 /// The registry is reset at the start of every frame (the first call on a
1372 /// new tick clears stale entries). Both calls in the same frame
1373 /// accumulate; calls across frames do not leak.
1374 pub fn publish_keymap(
1375 &mut self,
1376 name: &'static str,
1377 bindings: &'static [(&'static str, &'static str)],
1378 ) {
1379 // The registry is cleared at frame start by `run_frame_kernel`
1380 // (issue #236) — see `clear_keymap_registry` in `lib.rs`. We just
1381 // need to insert/append here.
1382 let entry = self
1383 .named_states
1384 .entry(KEYMAP_REGISTRY_KEY)
1385 .or_insert_with(|| {
1386 Box::new(Vec::<crate::keymap::PublishedKeymap>::new()) as Box<dyn std::any::Any>
1387 });
1388 if let Some(vec) = entry.downcast_mut::<Vec<crate::keymap::PublishedKeymap>>() {
1389 vec.push(crate::keymap::PublishedKeymap::new(name, bindings));
1390 }
1391 }
1392
1393 /// Return all keymaps published this frame (issue #236).
1394 ///
1395 /// Empty if no widget called [`Context::publish_keymap`] yet on the
1396 /// current frame. The registry is reset at the start of every frame.
1397 pub fn published_keymaps(&self) -> &[crate::keymap::PublishedKeymap] {
1398 if let Some(boxed) = self.named_states.get(KEYMAP_REGISTRY_KEY) {
1399 if let Some(vec) = boxed.downcast_ref::<Vec<crate::keymap::PublishedKeymap>>() {
1400 return vec;
1401 }
1402 }
1403 &[]
1404 }
1405
1406 /// Render an automatic keymap-help overlay listing every widget keymap
1407 /// published this frame (issue #236).
1408 ///
1409 /// Pass `open = true` to render the overlay (typically gated on a
1410 /// `?` / `F1` keypress). When `open` is `false`, this method is a
1411 /// no-op. The overlay groups bindings by widget name and dismisses
1412 /// when the next frame is rendered with `open = false`.
1413 ///
1414 /// # Example
1415 ///
1416 /// ```
1417 /// # use slt::*;
1418 /// # TestBackend::new(40, 12).render(|ui| {
1419 /// const RICHLOG: &[(&str, &str)] = &[("↑/k", "scroll up"), ("↓/j", "scroll down")];
1420 /// ui.publish_keymap("rich_log", RICHLOG);
1421 /// // Show the help overlay when '?' is pressed
1422 /// let show = ui.key('?');
1423 /// ui.keymap_help_overlay(show);
1424 /// # });
1425 /// ```
1426 pub fn keymap_help_overlay(&mut self, open: bool) {
1427 if !open {
1428 return;
1429 }
1430
1431 let entries: Vec<crate::keymap::PublishedKeymap> = self.published_keymaps().to_vec();
1432 if entries.is_empty() {
1433 return;
1434 }
1435
1436 let theme = self.theme;
1437 let _ = self.modal(|ui| {
1438 ui.styled("Keyboard shortcuts", Style::new().bold().fg(theme.primary));
1439 ui.text("");
1440 for entry in &entries {
1441 ui.styled(entry.name, Style::new().bold().fg(theme.text));
1442 for (key, desc) in entry.bindings {
1443 let line = format!(" {key:<14} {desc}");
1444 ui.styled(line, Style::new().fg(theme.text_dim));
1445 }
1446 ui.text("");
1447 }
1448 ui.styled(
1449 "Press Esc / ? to close",
1450 Style::new().fg(theme.text_dim).italic(),
1451 );
1452 });
1453 }
1454}
1455
1456// Sentinel keys reused from `lib.rs` so the two reads/writes can never drift.
1457use crate::{
1458 KEYMAP_REGISTRY_NAMED_STATE_KEY as KEYMAP_REGISTRY_KEY,
1459 STATIC_LOG_NAMED_STATE_KEY as STATIC_LOG_KEY,
1460};