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    /// 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    /// Issue #279: screen-navigation requests recorded by
93    /// [`Context::push_screen`] / [`Context::pop_screen`] /
94    /// [`Context::reset_screen`] from inside a [`Context::screen`] closure.
95    /// Drained and applied to the active [`crate::ScreenState`] right after the
96    /// closure returns. Deferring the mutation here lets app code navigate from
97    /// within the closure without a double mutable borrow of its `ScreenState`.
98    pub(crate) pending_screen_nav: Vec<ScreenNav>,
99    /// Number of currently active `screen` closures. Each `screen` keeps its
100    /// own pending-navigation start index on the call stack; this depth lets
101    /// navigation helpers reject calls made outside any screen without a
102    /// per-frame heap allocation.
103    pub(crate) screen_nav_depth: usize,
104    /// Original active screen for each `ScreenState` that navigated this frame.
105    /// Later `screen` declarations keep rendering this origin until the next
106    /// frame, preventing source and destination screens from being composed in
107    /// the same terminal buffer. Keys are per-frame `ScreenState` addresses.
108    pub(crate) screen_nav_render_origins: std::collections::HashMap<usize, String>,
109    pub(crate) hovered_groups: std::collections::HashSet<std::sync::Arc<str>>,
110    /// Issue #273: version keys recorded by [`Context::cached`] regions on the
111    /// PREVIOUS frame, moved in from `FrameState::region_versions`. Indexed by
112    /// the order `cached` regions are declared this frame; consulted by
113    /// `cached` to classify a region as a hit (key unchanged) or miss.
114    pub(crate) region_versions_prev: Vec<u64>,
115    /// Issue #273: version keys recorded by `cached` regions on THIS frame, in
116    /// declaration order. Swapped back into `FrameState::region_versions` at
117    /// frame end to become next frame's `region_versions_prev`.
118    pub(crate) region_versions_cur: Vec<u64>,
119    /// Issue #273: number of `cached` regions this frame whose key matched the
120    /// previous frame (a cache hit). Diagnostics-only — exposed via
121    /// [`Context::region_cache_hits`].
122    pub(crate) region_cache_hits: u32,
123    /// Issue #273: number of `cached` regions this frame whose key changed or
124    /// was new/first-frame (a cache miss). Exposed via
125    /// [`Context::region_cache_misses`].
126    pub(crate) region_cache_misses: u32,
127    pub(crate) scroll_lines_per_event: u32,
128    pub(crate) screen_hook_map:
129        std::collections::HashMap<u64, std::collections::HashMap<String, (usize, usize)>>,
130    pub(crate) widget_theme: WidgetTheme,
131    /// Issue #208: which focus index was current at the END of the previous
132    /// frame. `None` on the very first frame. Used to compute
133    /// `Response::gained_focus` / `Response::lost_focus` per widget.
134    pub(crate) prev_focus_index: Option<usize>,
135    /// Issue #217: name → focus-index map built in the previous frame, used
136    /// to resolve `focus_by_name(...)` requests at the start of this frame.
137    /// Empty on the first frame.
138    pub(crate) focus_name_map_prev: std::collections::HashMap<String, usize>,
139    /// Issue #217: name → focus-index map being built this frame as widgets
140    /// call `register_focusable_named(...)`. Swapped into `focus_name_map_prev`
141    /// at frame end.
142    pub(crate) focus_name_map: std::collections::HashMap<String, usize>,
143    /// Issue #217: name requested by `focus_by_name(...)`; consumed at the
144    /// start of the next frame. Outlives a single frame so the resolution
145    /// happens against `focus_name_map_prev`.
146    pub(crate) pending_focus_name: Option<String>,
147    /// Issue #248: wall-clock instant sampled once at frame start. All
148    /// frame-clock timer deadlines (`schedule`/`every`/`debounce`) compare
149    /// against this single instant so every timer sampled in the same frame
150    /// sees a consistent "now". Deliberately wall-clock, not the frame tick
151    /// (`run_frame_kernel` never advances `diagnostics.tick`).
152    pub(crate) frame_instant: std::time::Instant,
153    /// Issue #248: persistent timer table. Moved in from `FrameState` at
154    /// frame start and moved back at frame end (where untouched slots are
155    /// GC'd), identical to the `named_states` lifetime.
156    pub(crate) scheduler: SchedulerState,
157    /// Issue #234: in-frame async task registry backing
158    /// [`Context::spawn`](crate::Context::spawn) /
159    /// [`Context::poll`](crate::Context::poll). Round-tripped through
160    /// `FrameState` like `scheduler`. Gated behind `async`; the field does not
161    /// exist (zero overhead) when the feature is off.
162    #[cfg(feature = "async")]
163    pub(crate) async_tasks: AsyncTasks,
164}
165
166type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
167
168#[derive(Debug, Clone)]
169pub(crate) struct PendingTooltip {
170    pub anchor_rect: Rect,
171    pub lines: Vec<String>,
172}
173
174#[derive(Clone)]
175pub(crate) struct ContextRollbackState {
176    pub(crate) last_text_idx: Option<usize>,
177    pub(crate) focus_count: usize,
178    /// Issue #208: id assigned by the most recent `register_focusable()` /
179    /// `register_focusable_named(...)` call. `begin_widget_interaction`
180    /// reads this to compute `Response::gained_focus` / `lost_focus`
181    /// without changing the public `register_focusable` signature. Reset
182    /// to `None` at frame start; left alone after read so widgets that
183    /// don't pair `register_focusable` with `begin_widget_interaction`
184    /// still get correct behavior.
185    pub(crate) last_focusable_id: Option<usize>,
186    /// Issue #217 follow-up: slot id reserved by the most-recent
187    /// `register_focusable_named(name)` for the next `register_focusable()`
188    /// to *reuse* instead of allocating a fresh slot.
189    ///
190    /// `register_focusable_named` allocates the slot eagerly (so the name
191    /// is already bound in `focus_name_map` and `focused_name()` works
192    /// even when no widget follows), and stores the slot id here. When a
193    /// SLT widget like `text_input` / `button` / `tabs` calls
194    /// `register_focusable()` immediately after — every such widget does
195    /// — the call drains this reservation and reuses the same slot, so
196    /// the name binds to the slot the widget actually occupies rather
197    /// than to a dummy slot allocated by `register_focusable_named`.
198    ///
199    /// Cleared in three cases:
200    ///   1. drained by the next `register_focusable()` and reused (common
201    ///      path: named widget),
202    ///   2. overwritten by a second `register_focusable_named()` that
203    ///      runs without an intervening widget (last-write-wins on the
204    ///      reservation; the first slot is left orphaned but harmless,
205    ///      its name binding already lives in `focus_name_map`),
206    ///   3. dropped by the modal/overlay suppression branch when the
207    ///      named registration itself is suppressed.
208    pub(crate) pending_focusable_id: Option<usize>,
209    pub(crate) interaction_count: usize,
210    pub(crate) scroll_count: usize,
211    pub(crate) group_count: usize,
212    pub(crate) group_stack: Vec<std::sync::Arc<str>>,
213    pub(crate) overlay_depth: usize,
214    pub(crate) modal_active: bool,
215    pub(crate) modal_focus_start: usize,
216    pub(crate) modal_focus_count: usize,
217    pub(crate) hook_cursor: usize,
218    pub(crate) dark_mode: bool,
219    pub(crate) notification_queue: Vec<(String, ToastLevel, u64)>,
220    pub(crate) text_color_stack: Vec<Option<Color>>,
221}
222
223pub(super) struct ContextCheckpoint {
224    commands_len: usize,
225    hook_states_len: usize,
226    deferred_draws_len: usize,
227    context_stack_len: usize,
228    pending_tooltips_len: usize,
229    /// Issue #279: drop deferred screen-navigation requests recorded by a
230    /// panicking subtree inside an `error_boundary`, so a rolled-back screen
231    /// closure does not leave a phantom push/pop queued for its `ScreenState`.
232    pending_screen_nav_len: usize,
233    /// Drop navigation scopes opened by a panicking nested `screen` call.
234    screen_nav_depth: usize,
235    /// Issue #273: `cached` region keys recorded so far, so a panicking
236    /// `cached` region inside an `error_boundary` rolls back its key entry
237    /// (and any nested ones) — keeping the recorded keys consistent with the
238    /// commands that actually survived the rollback.
239    region_versions_cur_len: usize,
240    rollback: ContextRollbackState,
241}
242
243impl ContextCheckpoint {
244    pub(super) fn capture(ctx: &Context) -> Self {
245        Self {
246            commands_len: ctx.commands.len(),
247            hook_states_len: ctx.hook_states.len(),
248            deferred_draws_len: ctx.deferred_draws.len(),
249            context_stack_len: ctx.context_stack.len(),
250            pending_tooltips_len: ctx.pending_tooltips.len(),
251            pending_screen_nav_len: ctx.pending_screen_nav.len(),
252            screen_nav_depth: ctx.screen_nav_depth,
253            region_versions_cur_len: ctx.region_versions_cur.len(),
254            rollback: ctx.rollback.clone(),
255        }
256    }
257
258    pub(super) fn restore(&self, ctx: &mut Context) {
259        ctx.commands.truncate(self.commands_len);
260        ctx.hook_states.truncate(self.hook_states_len);
261        ctx.deferred_draws.truncate(self.deferred_draws_len);
262        ctx.context_stack.truncate(self.context_stack_len);
263        ctx.rollback = self.rollback.clone();
264        // Drop tooltips queued by the panicking widget but keep any that were
265        // already pending before the error boundary was entered.
266        ctx.pending_tooltips.truncate(self.pending_tooltips_len);
267        // Issue #279: drop screen-navigation requests queued by the panicking
268        // subtree but keep any recorded before the error boundary was entered.
269        ctx.pending_screen_nav.truncate(self.pending_screen_nav_len);
270        ctx.screen_nav_depth = self.screen_nav_depth;
271        // Issue #273: drop `cached` keys recorded by the panicking subtree.
272        ctx.region_versions_cur
273            .truncate(self.region_versions_cur_len);
274    }
275}