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