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 /// Issue #262: cross-frame partial-chord buffer for [`Context::key_chord`].
35 /// Moved into `Context::new` from `FrameState` and moved back at frame end,
36 /// identical to the `keyed_states` lifetime.
37 pub(crate) chord: crate::widgets::ChordState,
38 pub(crate) context_stack: Vec<Box<dyn std::any::Any>>,
39 pub(crate) prev_focus_count: usize,
40 pub(crate) prev_modal_focus_start: usize,
41 pub(crate) prev_modal_focus_count: usize,
42 /// `(content_extent, viewport_extent, is_horizontal)` per scrollable from
43 /// the previous frame (#247).
44 pub(crate) prev_scroll_infos: Vec<(u32, u32, bool)>,
45 pub(crate) prev_scroll_rects: Vec<Rect>,
46 pub(crate) prev_hit_map: Vec<Rect>,
47 pub(crate) prev_group_rects: Vec<(std::sync::Arc<str>, Rect)>,
48 pub(crate) prev_focus_groups: Vec<Option<std::sync::Arc<str>>>,
49 pub(crate) mouse_pos: Option<(u32, u32)>,
50 pub(crate) click_pos: Option<(u32, u32)>,
51 /// Issue #208: position of the most recent `MouseButton::Right` `Down`
52 /// event in this frame. Mirrors `click_pos` for the right-button. Used
53 /// by `response_for` to populate `Response::right_clicked`.
54 pub(crate) right_click_pos: Option<(u32, u32)>,
55 pub(crate) prev_modal_active: bool,
56 pub(crate) clipboard_text: Option<String>,
57 pub(crate) debug: bool,
58 /// Issue #201: which layers the F12 debug overlay should outline. Read
59 /// from `state.diagnostics.debug_layer` at frame start and written back
60 /// at frame end so [`Context::set_debug_layer`] persists across frames.
61 pub(crate) debug_layer: crate::DebugLayer,
62 /// Issue #268: whether the devtools inspector panel (Ctrl+F12) is active.
63 /// Read from `state.diagnostics.inspector_mode` at frame start and written
64 /// back at frame end so [`Context::set_inspector`] persists across frames.
65 pub(crate) inspector_mode: bool,
66 pub(crate) theme: Theme,
67 pub(crate) is_real_terminal: bool,
68 /// Issue #264: read-only snapshot of negotiated terminal capabilities
69 /// (DA1/DA2/XTGETTCAP), exposed via [`Context::capabilities`]. Populated
70 /// from the process-global probe in `run_frame_kernel`; defaults
71 /// conservatively on headless backends. Diagnostics-only — image rendering
72 /// routes through the automatic blitter ladder, so app code never branches
73 /// on this.
74 #[cfg(feature = "crossterm")]
75 pub(crate) capabilities: crate::terminal::Capabilities,
76 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
77 pub(crate) rollback: ContextRollbackState,
78 pub(crate) pending_tooltips: Vec<PendingTooltip>,
79 pub(crate) hovered_groups: std::collections::HashSet<std::sync::Arc<str>>,
80 /// Issue #273: version keys recorded by [`Context::cached`] regions on the
81 /// PREVIOUS frame, moved in from `FrameState::region_versions`. Indexed by
82 /// the order `cached` regions are declared this frame; consulted by
83 /// `cached` to classify a region as a hit (key unchanged) or miss.
84 pub(crate) region_versions_prev: Vec<u64>,
85 /// Issue #273: version keys recorded by `cached` regions on THIS frame, in
86 /// declaration order. Swapped back into `FrameState::region_versions` at
87 /// frame end to become next frame's `region_versions_prev`.
88 pub(crate) region_versions_cur: Vec<u64>,
89 /// Issue #273: number of `cached` regions this frame whose key matched the
90 /// previous frame (a cache hit). Diagnostics-only — exposed via
91 /// [`Context::region_cache_hits`].
92 pub(crate) region_cache_hits: u32,
93 /// Issue #273: number of `cached` regions this frame whose key changed or
94 /// was new/first-frame (a cache miss). Exposed via
95 /// [`Context::region_cache_misses`].
96 pub(crate) region_cache_misses: u32,
97 pub(crate) scroll_lines_per_event: u32,
98 pub(crate) screen_hook_map: std::collections::HashMap<String, (usize, usize)>,
99 pub(crate) widget_theme: WidgetTheme,
100 /// Issue #208: which focus index was current at the END of the previous
101 /// frame. `None` on the very first frame. Used to compute
102 /// `Response::gained_focus` / `Response::lost_focus` per widget.
103 pub(crate) prev_focus_index: Option<usize>,
104 /// Issue #217: name → focus-index map built in the previous frame, used
105 /// to resolve `focus_by_name(...)` requests at the start of this frame.
106 /// Empty on the first frame.
107 pub(crate) focus_name_map_prev: std::collections::HashMap<String, usize>,
108 /// Issue #217: name → focus-index map being built this frame as widgets
109 /// call `register_focusable_named(...)`. Swapped into `focus_name_map_prev`
110 /// at frame end.
111 pub(crate) focus_name_map: std::collections::HashMap<String, usize>,
112 /// Issue #217: name requested by `focus_by_name(...)`; consumed at the
113 /// start of the next frame. Outlives a single frame so the resolution
114 /// happens against `focus_name_map_prev`.
115 pub(crate) pending_focus_name: Option<String>,
116 /// Issue #248: wall-clock instant sampled once at frame start. All
117 /// frame-clock timer deadlines (`schedule`/`every`/`debounce`) compare
118 /// against this single instant so every timer sampled in the same frame
119 /// sees a consistent "now". Deliberately wall-clock, not the frame tick
120 /// (`run_frame_kernel` never advances `diagnostics.tick`).
121 pub(crate) frame_instant: std::time::Instant,
122 /// Issue #248: persistent timer table. Moved in from `FrameState` at
123 /// frame start and moved back at frame end (where untouched slots are
124 /// GC'd), identical to the `named_states` lifetime.
125 pub(crate) scheduler: SchedulerState,
126 /// Issue #234: in-frame async task registry backing
127 /// [`Context::spawn`](crate::Context::spawn) /
128 /// [`Context::poll`](crate::Context::poll). Round-tripped through
129 /// `FrameState` like `scheduler`. Gated behind `async`; the field does not
130 /// exist (zero overhead) when the feature is off.
131 #[cfg(feature = "async")]
132 pub(crate) async_tasks: AsyncTasks,
133}
134
135type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
136
137#[derive(Debug, Clone)]
138pub(crate) struct PendingTooltip {
139 pub anchor_rect: Rect,
140 pub lines: Vec<String>,
141}
142
143#[derive(Clone)]
144pub(crate) struct ContextRollbackState {
145 pub(crate) last_text_idx: Option<usize>,
146 pub(crate) focus_count: usize,
147 /// Issue #208: id assigned by the most recent `register_focusable()` /
148 /// `register_focusable_named(...)` call. `begin_widget_interaction`
149 /// reads this to compute `Response::gained_focus` / `lost_focus`
150 /// without changing the public `register_focusable` signature. Reset
151 /// to `None` at frame start; left alone after read so widgets that
152 /// don't pair `register_focusable` with `begin_widget_interaction`
153 /// still get correct behavior.
154 pub(crate) last_focusable_id: Option<usize>,
155 /// Issue #217 follow-up: slot id reserved by the most-recent
156 /// `register_focusable_named(name)` for the next `register_focusable()`
157 /// to *reuse* instead of allocating a fresh slot.
158 ///
159 /// `register_focusable_named` allocates the slot eagerly (so the name
160 /// is already bound in `focus_name_map` and `focused_name()` works
161 /// even when no widget follows), and stores the slot id here. When a
162 /// SLT widget like `text_input` / `button` / `tabs` calls
163 /// `register_focusable()` immediately after — every such widget does
164 /// — the call drains this reservation and reuses the same slot, so
165 /// the name binds to the slot the widget actually occupies rather
166 /// than to a dummy slot allocated by `register_focusable_named`.
167 ///
168 /// Cleared in three cases:
169 /// 1. drained by the next `register_focusable()` and reused (common
170 /// path: named widget),
171 /// 2. overwritten by a second `register_focusable_named()` that
172 /// runs without an intervening widget (last-write-wins on the
173 /// reservation; the first slot is left orphaned but harmless,
174 /// its name binding already lives in `focus_name_map`),
175 /// 3. dropped by the modal/overlay suppression branch when the
176 /// named registration itself is suppressed.
177 pub(crate) pending_focusable_id: Option<usize>,
178 pub(crate) interaction_count: usize,
179 pub(crate) scroll_count: usize,
180 pub(crate) group_count: usize,
181 pub(crate) group_stack: Vec<std::sync::Arc<str>>,
182 pub(crate) overlay_depth: usize,
183 pub(crate) modal_active: bool,
184 pub(crate) modal_focus_start: usize,
185 pub(crate) modal_focus_count: usize,
186 pub(crate) hook_cursor: usize,
187 pub(crate) dark_mode: bool,
188 pub(crate) notification_queue: Vec<(String, ToastLevel, u64)>,
189 pub(crate) text_color_stack: Vec<Option<Color>>,
190}
191
192pub(super) struct ContextCheckpoint {
193 commands_len: usize,
194 hook_states_len: usize,
195 deferred_draws_len: usize,
196 context_stack_len: usize,
197 pending_tooltips_len: usize,
198 /// Issue #273: `cached` region keys recorded so far, so a panicking
199 /// `cached` region inside an `error_boundary` rolls back its key entry
200 /// (and any nested ones) — keeping the recorded keys consistent with the
201 /// commands that actually survived the rollback.
202 region_versions_cur_len: usize,
203 rollback: ContextRollbackState,
204}
205
206impl ContextCheckpoint {
207 pub(super) fn capture(ctx: &Context) -> Self {
208 Self {
209 commands_len: ctx.commands.len(),
210 hook_states_len: ctx.hook_states.len(),
211 deferred_draws_len: ctx.deferred_draws.len(),
212 context_stack_len: ctx.context_stack.len(),
213 pending_tooltips_len: ctx.pending_tooltips.len(),
214 region_versions_cur_len: ctx.region_versions_cur.len(),
215 rollback: ctx.rollback.clone(),
216 }
217 }
218
219 pub(super) fn restore(&self, ctx: &mut Context) {
220 ctx.commands.truncate(self.commands_len);
221 ctx.hook_states.truncate(self.hook_states_len);
222 ctx.deferred_draws.truncate(self.deferred_draws_len);
223 ctx.context_stack.truncate(self.context_stack_len);
224 ctx.rollback = self.rollback.clone();
225 // Drop tooltips queued by the panicking widget but keep any that were
226 // already pending before the error boundary was entered.
227 ctx.pending_tooltips.truncate(self.pending_tooltips_len);
228 // Issue #273: drop `cached` keys recorded by the panicking subtree.
229 ctx.region_versions_cur
230 .truncate(self.region_versions_cur_len);
231 }
232}