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