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