Skip to main content

slt/context/
state.rs

1use super::*;
2
3/// Internal discriminator for [`State<T>`] handles.
4///
5/// `Indexed` refers to a slot in `Context::hook_states` (positional, used by
6/// [`Context::use_state`] / [`Context::use_memo`]). `Named` refers to a key in
7/// `Context::named_states` (used by [`Context::use_state_named`]). `Keyed`
8/// refers to a runtime-string key in `Context::keyed_states` (used by
9/// [`Context::use_state_keyed`]).
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub(crate) enum StateKey {
12    Indexed(usize),
13    Named(&'static str),
14    Keyed(String),
15}
16
17/// Handle to state created by `use_state()`. Access via `.get(ui)` / `.get_mut(ui)`.
18///
19/// # Note on `Copy`
20///
21/// As of v0.20.0, `State<T>` is no longer `Copy`. The internal key may hold an
22/// owned `String` (for [`Context::use_state_keyed`]), which prevents trivial
23/// duplication. Existing call sites that use the handle locally (`let s =
24/// ui.use_state(...); s.get(ui);`) are unaffected — the handle is moved into
25/// closures or borrowed by reference. If you previously relied on implicit
26/// copy semantics, call `.clone()` explicitly.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct State<T> {
29    key: StateKey,
30    _marker: std::marker::PhantomData<T>,
31}
32
33/// Downcast a stored boxed `Any` to `&T`, panicking with a uniform context
34/// message on mismatch. Internal helper to keep [`State::get`] / [`State::get_mut`]
35/// concise and ensure every panic site formats identically.
36///
37/// `ctx` should be a complete leading clause such as
38/// `"use_state_named type mismatch for id \"foo\""` — the helper appends
39/// `" — expected <type>"` so callers don't repeat that suffix at every site.
40fn downcast_or_panic<'a, T: 'static>(
41    boxed: &'a dyn std::any::Any,
42    ctx: std::fmt::Arguments<'_>,
43) -> &'a T {
44    boxed
45        .downcast_ref::<T>()
46        .unwrap_or_else(|| panic!("{ctx} — expected {}", std::any::type_name::<T>()))
47}
48
49/// Mutable counterpart of [`downcast_or_panic`].
50fn downcast_or_panic_mut<'a, T: 'static>(
51    boxed: &'a mut dyn std::any::Any,
52    ctx: std::fmt::Arguments<'_>,
53) -> &'a mut T {
54    boxed
55        .downcast_mut::<T>()
56        .unwrap_or_else(|| panic!("{ctx} — expected {}", std::any::type_name::<T>()))
57}
58
59impl<T: 'static> State<T> {
60    pub(crate) fn from_idx(idx: usize) -> Self {
61        Self {
62            key: StateKey::Indexed(idx),
63            _marker: std::marker::PhantomData,
64        }
65    }
66
67    pub(crate) fn from_named(id: &'static str) -> Self {
68        Self {
69            key: StateKey::Named(id),
70            _marker: std::marker::PhantomData,
71        }
72    }
73
74    pub(crate) fn from_keyed(id: String) -> Self {
75        Self {
76            key: StateKey::Keyed(id),
77            _marker: std::marker::PhantomData,
78        }
79    }
80
81    /// Read the current value.
82    pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
83        match &self.key {
84            StateKey::Indexed(idx) => downcast_or_panic::<T>(
85                ui.hook_states[*idx].as_ref(),
86                format_args!("use_state type mismatch at hook index {idx}"),
87            ),
88            StateKey::Named(id) => {
89                let boxed = ui.named_states.get(id).unwrap_or_else(|| {
90                    panic!("use_state_named: no entry for id {id:?} — was use_state_named called?")
91                });
92                downcast_or_panic::<T>(
93                    boxed.as_ref(),
94                    format_args!("use_state_named type mismatch for id {id:?}"),
95                )
96            }
97            StateKey::Keyed(id) => {
98                let boxed = ui.keyed_states.get(id).unwrap_or_else(|| {
99                    panic!("use_state_keyed: no entry for id {id:?} — was use_state_keyed called?")
100                });
101                downcast_or_panic::<T>(
102                    boxed.as_ref(),
103                    format_args!("use_state_keyed type mismatch for id {id:?}"),
104                )
105            }
106        }
107    }
108
109    /// Mutably access the current value.
110    pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
111        match &self.key {
112            StateKey::Indexed(idx) => downcast_or_panic_mut::<T>(
113                ui.hook_states[*idx].as_mut(),
114                format_args!("use_state type mismatch at hook index {idx}"),
115            ),
116            StateKey::Named(id) => {
117                let boxed = ui.named_states.get_mut(id).unwrap_or_else(|| {
118                    panic!("use_state_named: no entry for id {id:?} — was use_state_named called?")
119                });
120                downcast_or_panic_mut::<T>(
121                    boxed.as_mut(),
122                    format_args!("use_state_named type mismatch for id {id:?}"),
123                )
124            }
125            StateKey::Keyed(id) => {
126                let boxed = ui.keyed_states.get_mut(id).unwrap_or_else(|| {
127                    panic!("use_state_keyed: no entry for id {id:?} — was use_state_keyed called?")
128                });
129                downcast_or_panic_mut::<T>(
130                    boxed.as_mut(),
131                    format_args!("use_state_keyed type mismatch for id {id:?}"),
132                )
133            }
134        }
135    }
136}
137
138/// Internal storage shape for a value created by [`Context::use_memo`].
139///
140/// The previous-frame dependencies are kept type-erased (`Box<dyn Any>`) so the
141/// read path ([`Memo::get`]) can downcast the slot to `MemoSlot<T>` without
142/// knowing `D`. [`Context::use_memo`] downcasts `deps` back to `&D` when
143/// comparing against the new dependencies to decide whether to recompute.
144///
145/// Kept `pub(crate)` — never part of the public API. The `T` in its type name
146/// appears in the hook-ordering mismatch panic message, mirroring the historic
147/// `(D, T)` message shape.
148pub(crate) struct MemoSlot<T> {
149    pub(crate) deps: Box<dyn std::any::Any>,
150    pub(crate) value: T,
151}
152
153/// Handle to a memoized value created by [`Context::use_memo`].
154///
155/// Like [`State<T>`], this is an *index handle*, not a live borrow — it stores
156/// only the hook slot index and does **not** keep [`Context`] borrowed. That is
157/// the whole point: the handle composes with later `ui.*` calls, where the old
158/// `&T`-returning form (now [`Context::use_memo_ref`]) held an immutable borrow
159/// of `ui` that conflicted with any subsequent mutation.
160///
161/// Read the value with [`get`](Self::get) (`&T`) or [`copied`](Self::copied)
162/// (`T: Copy`).
163///
164/// # Example
165///
166/// ```no_run
167/// # slt::run(|ui: &mut slt::Context| {
168/// let count = ui.use_state(|| 0i32);
169/// let count_val = *count.get(ui);
170/// // Handle releases the `&mut ui` borrow immediately...
171/// let doubled = ui.use_memo(&count_val, |c| c * 2);
172/// // ...so an intervening `ui.*` call composes cleanly.
173/// ui.text("computed:");
174/// ui.text(format!("{}", doubled.copied(ui)));
175/// # });
176/// ```
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct Memo<T> {
179    idx: usize,
180    _marker: std::marker::PhantomData<T>,
181}
182
183impl<T: 'static> Memo<T> {
184    pub(crate) fn from_idx(idx: usize) -> Self {
185        Self {
186            idx,
187            _marker: std::marker::PhantomData,
188        }
189    }
190
191    /// Read the memoized value.
192    ///
193    /// # Panics
194    ///
195    /// Panics with the slot index and expected type name if the hook at this
196    /// index does not hold a `MemoSlot<T>` — i.e. the rules-of-hooks contract
197    /// was broken (hooks called in a different order than the frame that created
198    /// the slot). The message matches [`Context::use_memo`]'s own mismatch
199    /// panic.
200    ///
201    /// # Example
202    ///
203    /// ```no_run
204    /// # slt::run(|ui: &mut slt::Context| {
205    /// let m = ui.use_memo(&3i32, |d| d * 2);
206    /// ui.text(format!("{}", m.get(ui)));
207    /// # });
208    /// ```
209    pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
210        match ui.hook_states[self.idx].downcast_ref::<MemoSlot<T>>() {
211            Some(slot) => &slot.value,
212            None => panic!(
213                "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
214                self.idx,
215                std::any::type_name::<MemoSlot<T>>()
216            ),
217        }
218    }
219
220    /// Read a `Copy` of the memoized value.
221    ///
222    /// Convenience for `*memo.get(ui)`. Panics under the same conditions as
223    /// [`get`](Self::get).
224    ///
225    /// # Example
226    ///
227    /// ```no_run
228    /// # slt::run(|ui: &mut slt::Context| {
229    /// let doubled = ui.use_memo(&21i32, |d| d * 2).copied(ui);
230    /// ui.text(format!("{doubled}"));
231    /// # });
232    /// ```
233    pub fn copied(&self, ui: &Context) -> T
234    where
235        T: Copy,
236    {
237        *self.get(ui)
238    }
239}
240
241/// Interaction response returned by all widgets.
242///
243/// Container methods return a [`Response`]. Check `.clicked`, `.changed`, etc.
244/// to react to user interactions.
245/// `rect` is meaningful after the widget has participated in layout.
246/// Container responses describe the container's own interaction area, not
247/// automatically the focus state of every child widget.
248///
249/// # Examples
250///
251/// ```
252/// # use slt::*;
253/// # TestBackend::new(80, 24).render(|ui| {
254/// let r = ui.row(|ui| {
255///     ui.text("Save");
256/// });
257/// if r.clicked {
258///     // handle save
259/// }
260/// # });
261/// ```
262#[derive(Debug, Clone, Default)]
263#[must_use = "Response contains interaction state — check .clicked, .hovered, or .changed"]
264pub struct Response {
265    /// Whether the widget was left-clicked this frame.
266    pub clicked: bool,
267    /// Whether the widget was right-clicked this frame.
268    ///
269    /// Detected when a `MouseButton::Right` `Down` event lands inside the
270    /// widget's `rect`. Suppressed for non-overlay widgets while a modal is
271    /// active (consistent with the existing modal-suppression behavior of
272    /// `clicked` / `hovered`). Available since v0.20.0.
273    pub right_clicked: bool,
274    /// Whether the mouse is hovering over the widget.
275    pub hovered: bool,
276    /// Whether the widget's value changed this frame.
277    pub changed: bool,
278    /// Whether the widget currently has keyboard focus.
279    pub focused: bool,
280    /// Whether the widget *just* received keyboard focus this frame.
281    ///
282    /// `true` only on the first frame after focus moved to this widget;
283    /// `false` thereafter (until focus moves away and returns). Mutually
284    /// exclusive with [`lost_focus`](Self::lost_focus). Available since
285    /// v0.20.0.
286    pub gained_focus: bool,
287    /// Whether the widget *just* lost keyboard focus this frame.
288    ///
289    /// `true` only on the first frame after focus moved away from this widget;
290    /// `false` on subsequent frames. Mutually exclusive with
291    /// [`gained_focus`](Self::gained_focus). Available since v0.20.0.
292    pub lost_focus: bool,
293    /// Whether the widget was double-clicked this frame.
294    ///
295    /// Detected when two `MouseButton::Left` `Down` events land on the same
296    /// terminal cell within the double-click window (~400ms). When `true`,
297    /// `clicked` is also `true` for the same frame (the second click is still a
298    /// click). This is the standard open/activate gesture for file pickers,
299    /// lists, tables, and trees. Suppressed for non-overlay widgets while a
300    /// modal is active, consistent with `clicked`. Available since v0.21.1.
301    pub double_clicked: bool,
302    /// Whether the widget submitted its value this frame.
303    ///
304    /// Set by widgets that have an explicit submit gesture — e.g. pressing
305    /// `Enter` in a focused single-line [`text_input`](Context::text_input).
306    /// Always `false` for widgets with no submit semantics. Available since
307    /// v0.21.1.
308    pub submitted: bool,
309    /// Net vertical scroll-wheel delta over this widget this frame.
310    ///
311    /// Positive = wheel scrolled up, negative = down, `0` when the wheel did
312    /// not move while the cursor was over the widget's `rect`. Hover-gated, so
313    /// each widget consumes only the wheel motion that occurred above it — a
314    /// chart, canvas, or custom viewport can scroll/zoom locally without a
315    /// frame-global scroll handler. Available since v0.21.1.
316    pub scroll_delta: i32,
317    /// The rectangle the widget occupies after layout.
318    pub rect: Rect,
319}
320
321impl Response {
322    /// Create a Response with all fields false/default.
323    pub fn none() -> Self {
324        Self::default()
325    }
326
327    /// Attach a tooltip to this widget. Renders only when the widget is
328    /// currently hovered.
329    ///
330    /// Equivalent to calling [`Context::tooltip`] immediately after the
331    /// widget, but composes cleanly with the chained `Response` style:
332    ///
333    /// ```ignore
334    /// if ui.button("Save").on_hover(ui, "Saves the file").clicked {
335    ///     save();
336    /// }
337    /// ```
338    ///
339    /// `text` is wrapped at 38 columns and rendered in an overlay panel
340    /// anchored under (or above, if no room below) the widget's rect.
341    /// Empty strings, zero-area rects, and non-hovered responses are
342    /// silently skipped — no allocation in the cold path.
343    ///
344    /// Unlike [`Context::tooltip`], the binding is not order-sensitive:
345    /// the tooltip is attached to *this* response specifically, so
346    /// chaining further widgets afterward does not strip it.
347    #[must_use = "on_hover returns the Response for further chaining"]
348    pub fn on_hover(self, ctx: &mut Context, text: impl Into<String>) -> Self {
349        if !self.hovered || self.rect.width == 0 || self.rect.height == 0 {
350            return self;
351        }
352        let tooltip_text = text.into();
353        if tooltip_text.is_empty() {
354            return self;
355        }
356        let lines = super::widgets_display::wrap_tooltip_text(&tooltip_text, 38);
357        ctx.pending_tooltips.push(PendingTooltip {
358            anchor_rect: self.rect,
359            lines,
360        });
361        self
362    }
363
364    /// Run a closure to render arbitrary tooltip content when the widget is
365    /// hovered.
366    ///
367    /// The closure receives the same `&mut Context` and runs immediately
368    /// (in-place — not deferred). This means the closure can issue any UI
369    /// commands; positioning is the caller's responsibility (use
370    /// [`Context::overlay`] / [`Context::overlay_at`] inside the closure
371    /// for floating panels).
372    ///
373    /// For simple text tooltips, prefer [`Response::on_hover`] which
374    /// auto-positions the tooltip under the widget.
375    ///
376    /// ```ignore
377    /// ui.button("Help").on_hover_ui(ui, |ui| {
378    ///     let _ = ui.overlay(|ui| {
379    ///         ui.text("Custom tooltip body");
380    ///     });
381    /// });
382    /// ```
383    #[must_use = "on_hover_ui returns the Response for further chaining"]
384    pub fn on_hover_ui(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
385        if self.hovered && self.rect.width > 0 && self.rect.height > 0 {
386            f(ctx);
387        }
388        self
389    }
390
391    /// Run `f` if the widget was clicked this frame, then return the Response
392    /// for further chaining.
393    ///
394    /// The closure receives the same `&mut Context` so it can issue UI commands
395    /// (e.g. queue a toast); ignore the argument with `|_|` if you only need to
396    /// mutate application state.
397    ///
398    /// ```ignore
399    /// ui.button("Save").on_click(ui, |_| save());
400    /// ```
401    pub fn on_click(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
402        if self.clicked {
403            f(ctx);
404        }
405        self
406    }
407
408    /// Run `f` if the widget's value changed this frame, then return the
409    /// Response for chaining. See [`on_click`](Self::on_click) for the closure
410    /// argument convention. Available since v0.21.1.
411    pub fn on_changed(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
412        if self.changed {
413            f(ctx);
414        }
415        self
416    }
417
418    /// Run `f` on the frame the widget *gained* keyboard focus, then return the
419    /// Response for chaining. Fires once per focus acquisition (mirrors
420    /// [`gained_focus`](Self::gained_focus)). Available since v0.21.1.
421    pub fn on_focus(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
422        if self.gained_focus {
423            f(ctx);
424        }
425        self
426    }
427
428    /// Run `f` if the widget submitted this frame (e.g. `Enter` in a focused
429    /// single-line text input), then return the Response for chaining. Mirrors
430    /// [`submitted`](Self::submitted). Available since v0.21.1.
431    pub fn on_submit(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
432        if self.submitted {
433            f(ctx);
434        }
435        self
436    }
437
438    /// Run `f` if the widget was double-clicked this frame, then return the
439    /// Response for chaining. Mirrors [`double_clicked`](Self::double_clicked).
440    /// Available since v0.21.1.
441    pub fn on_double_click(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
442        if self.double_clicked {
443            f(ctx);
444        }
445        self
446    }
447}