Skip to main content

slt/context/
core.rs

1use super::*;
2
3/// The main rendering context passed to your closure each frame.
4///
5/// Provides all methods for building UI: text, containers, widgets, and event
6/// handling. You receive a `&mut Context` on every frame and describe what to
7/// render by calling its methods. SLT collects those calls, lays them out with
8/// flexbox, diffs against the previous frame, and flushes only changed cells.
9///
10/// # Example
11///
12/// ```no_run
13/// slt::run(|ui: &mut slt::Context| {
14///     if ui.key('q') { ui.quit(); }
15///     ui.text("Hello, world!").bold();
16/// });
17/// ```
18pub struct Context {
19    pub(crate) commands: Vec<Command>,
20    pub(crate) events: Vec<Event>,
21    pub(crate) consumed: Vec<bool>,
22    pub(crate) should_quit: bool,
23    pub(crate) area_width: u32,
24    pub(crate) area_height: u32,
25    pub(crate) tick: u64,
26    pub(crate) focus_index: usize,
27    pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
28    pub(crate) named_states: std::collections::HashMap<&'static str, Box<dyn std::any::Any>>,
29    /// Issue #215: persistent state keyed by a runtime `String`. Mirrors
30    /// `named_states` but accepts dynamic keys (e.g. `format!("item-{i}")`).
31    /// The map is moved into `Context::new` from `FrameState` and moved back
32    /// at frame end, identical to the `named_states` lifetime.
33    pub(crate) keyed_states: std::collections::HashMap<String, Box<dyn std::any::Any>>,
34    pub(crate) context_stack: Vec<Box<dyn std::any::Any>>,
35    pub(crate) prev_focus_count: usize,
36    pub(crate) prev_modal_focus_start: usize,
37    pub(crate) prev_modal_focus_count: usize,
38    pub(crate) prev_scroll_infos: Vec<(u32, u32)>,
39    pub(crate) prev_scroll_rects: Vec<Rect>,
40    pub(crate) prev_hit_map: Vec<Rect>,
41    pub(crate) prev_group_rects: Vec<(std::sync::Arc<str>, Rect)>,
42    pub(crate) prev_focus_groups: Vec<Option<std::sync::Arc<str>>>,
43    pub(crate) mouse_pos: Option<(u32, u32)>,
44    pub(crate) click_pos: Option<(u32, u32)>,
45    /// Issue #208: position of the most recent `MouseButton::Right` `Down`
46    /// event in this frame. Mirrors `click_pos` for the right-button. Used
47    /// by `response_for` to populate `Response::right_clicked`.
48    pub(crate) right_click_pos: Option<(u32, u32)>,
49    pub(crate) prev_modal_active: bool,
50    pub(crate) clipboard_text: Option<String>,
51    pub(crate) debug: bool,
52    /// Issue #201: which layers the F12 debug overlay should outline. Read
53    /// from `state.diagnostics.debug_layer` at frame start and written back
54    /// at frame end so [`Context::set_debug_layer`] persists across frames.
55    pub(crate) debug_layer: crate::DebugLayer,
56    pub(crate) theme: Theme,
57    pub(crate) is_real_terminal: bool,
58    pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
59    pub(crate) rollback: ContextRollbackState,
60    pub(crate) pending_tooltips: Vec<PendingTooltip>,
61    pub(crate) hovered_groups: std::collections::HashSet<std::sync::Arc<str>>,
62    pub(crate) scroll_lines_per_event: u32,
63    pub(crate) screen_hook_map: std::collections::HashMap<String, (usize, usize)>,
64    pub(crate) widget_theme: WidgetTheme,
65    /// Issue #208: which focus index was current at the END of the previous
66    /// frame. `None` on the very first frame. Used to compute
67    /// `Response::gained_focus` / `Response::lost_focus` per widget.
68    pub(crate) prev_focus_index: Option<usize>,
69    /// Issue #217: name → focus-index map built in the previous frame, used
70    /// to resolve `focus_by_name(...)` requests at the start of this frame.
71    /// Empty on the first frame.
72    pub(crate) focus_name_map_prev: std::collections::HashMap<String, usize>,
73    /// Issue #217: name → focus-index map being built this frame as widgets
74    /// call `register_focusable_named(...)`. Swapped into `focus_name_map_prev`
75    /// at frame end.
76    pub(crate) focus_name_map: std::collections::HashMap<String, usize>,
77    /// Issue #217: name requested by `focus_by_name(...)`; consumed at the
78    /// start of the next frame. Outlives a single frame so the resolution
79    /// happens against `focus_name_map_prev`.
80    pub(crate) pending_focus_name: Option<String>,
81}
82
83type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
84
85#[derive(Debug, Clone)]
86pub(crate) struct PendingTooltip {
87    pub anchor_rect: Rect,
88    pub lines: Vec<String>,
89}
90
91#[derive(Clone)]
92pub(crate) struct ContextRollbackState {
93    pub(crate) last_text_idx: Option<usize>,
94    pub(crate) focus_count: usize,
95    /// Issue #208: id assigned by the most recent `register_focusable()` /
96    /// `register_focusable_named(...)` call. `begin_widget_interaction`
97    /// reads this to compute `Response::gained_focus` / `lost_focus`
98    /// without changing the public `register_focusable` signature. Reset
99    /// to `None` at frame start; left alone after read so widgets that
100    /// don't pair `register_focusable` with `begin_widget_interaction`
101    /// still get correct behavior.
102    pub(crate) last_focusable_id: Option<usize>,
103    /// Issue #217 follow-up: slot id reserved by the most-recent
104    /// `register_focusable_named(name)` for the next `register_focusable()`
105    /// to *reuse* instead of allocating a fresh slot.
106    ///
107    /// `register_focusable_named` allocates the slot eagerly (so the name
108    /// is already bound in `focus_name_map` and `focused_name()` works
109    /// even when no widget follows), and stores the slot id here. When a
110    /// SLT widget like `text_input` / `button` / `tabs` calls
111    /// `register_focusable()` immediately after — every such widget does
112    /// — the call drains this reservation and reuses the same slot, so
113    /// the name binds to the slot the widget actually occupies rather
114    /// than to a dummy slot allocated by `register_focusable_named`.
115    ///
116    /// Cleared in three cases:
117    ///   1. drained by the next `register_focusable()` and reused (common
118    ///      path: named widget),
119    ///   2. overwritten by a second `register_focusable_named()` that
120    ///      runs without an intervening widget (last-write-wins on the
121    ///      reservation; the first slot is left orphaned but harmless,
122    ///      its name binding already lives in `focus_name_map`),
123    ///   3. dropped by the modal/overlay suppression branch when the
124    ///      named registration itself is suppressed.
125    pub(crate) pending_focusable_id: Option<usize>,
126    pub(crate) interaction_count: usize,
127    pub(crate) scroll_count: usize,
128    pub(crate) group_count: usize,
129    pub(crate) group_stack: Vec<std::sync::Arc<str>>,
130    pub(crate) overlay_depth: usize,
131    pub(crate) modal_active: bool,
132    pub(crate) modal_focus_start: usize,
133    pub(crate) modal_focus_count: usize,
134    pub(crate) hook_cursor: usize,
135    pub(crate) dark_mode: bool,
136    pub(crate) notification_queue: Vec<(String, ToastLevel, u64)>,
137    pub(crate) text_color_stack: Vec<Option<Color>>,
138}
139
140pub(super) struct ContextCheckpoint {
141    commands_len: usize,
142    hook_states_len: usize,
143    deferred_draws_len: usize,
144    context_stack_len: usize,
145    pending_tooltips_len: usize,
146    rollback: ContextRollbackState,
147}
148
149impl ContextCheckpoint {
150    pub(super) fn capture(ctx: &Context) -> Self {
151        Self {
152            commands_len: ctx.commands.len(),
153            hook_states_len: ctx.hook_states.len(),
154            deferred_draws_len: ctx.deferred_draws.len(),
155            context_stack_len: ctx.context_stack.len(),
156            pending_tooltips_len: ctx.pending_tooltips.len(),
157            rollback: ctx.rollback.clone(),
158        }
159    }
160
161    pub(super) fn restore(&self, ctx: &mut Context) {
162        ctx.commands.truncate(self.commands_len);
163        ctx.hook_states.truncate(self.hook_states_len);
164        ctx.deferred_draws.truncate(self.deferred_draws_len);
165        ctx.context_stack.truncate(self.context_stack_len);
166        ctx.rollback = self.rollback.clone();
167        // Drop tooltips queued by the panicking widget but keep any that were
168        // already pending before the error boundary was entered.
169        ctx.pending_tooltips.truncate(self.pending_tooltips_len);
170    }
171}