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