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/// Interaction response returned by all widgets.
139///
140/// Container methods return a [`Response`]. Check `.clicked`, `.changed`, etc.
141/// to react to user interactions.
142/// `rect` is meaningful after the widget has participated in layout.
143/// Container responses describe the container's own interaction area, not
144/// automatically the focus state of every child widget.
145///
146/// # Examples
147///
148/// ```
149/// # use slt::*;
150/// # TestBackend::new(80, 24).render(|ui| {
151/// let r = ui.row(|ui| {
152/// ui.text("Save");
153/// });
154/// if r.clicked {
155/// // handle save
156/// }
157/// # });
158/// ```
159#[derive(Debug, Clone, Default)]
160#[must_use = "Response contains interaction state — check .clicked, .hovered, or .changed"]
161pub struct Response {
162 /// Whether the widget was left-clicked this frame.
163 pub clicked: bool,
164 /// Whether the widget was right-clicked this frame.
165 ///
166 /// Detected when a `MouseButton::Right` `Down` event lands inside the
167 /// widget's `rect`. Suppressed for non-overlay widgets while a modal is
168 /// active (consistent with the existing modal-suppression behavior of
169 /// `clicked` / `hovered`). Available since v0.20.0.
170 pub right_clicked: bool,
171 /// Whether the mouse is hovering over the widget.
172 pub hovered: bool,
173 /// Whether the widget's value changed this frame.
174 pub changed: bool,
175 /// Whether the widget currently has keyboard focus.
176 pub focused: bool,
177 /// Whether the widget *just* received keyboard focus this frame.
178 ///
179 /// `true` only on the first frame after focus moved to this widget;
180 /// `false` thereafter (until focus moves away and returns). Mutually
181 /// exclusive with [`lost_focus`](Self::lost_focus). Available since
182 /// v0.20.0.
183 pub gained_focus: bool,
184 /// Whether the widget *just* lost keyboard focus this frame.
185 ///
186 /// `true` only on the first frame after focus moved away from this widget;
187 /// `false` on subsequent frames. Mutually exclusive with
188 /// [`gained_focus`](Self::gained_focus). Available since v0.20.0.
189 pub lost_focus: bool,
190 /// The rectangle the widget occupies after layout.
191 pub rect: Rect,
192}
193
194impl Response {
195 /// Create a Response with all fields false/default.
196 pub fn none() -> Self {
197 Self::default()
198 }
199
200 /// Attach a tooltip to this widget. Renders only when the widget is
201 /// currently hovered.
202 ///
203 /// Equivalent to calling [`Context::tooltip`] immediately after the
204 /// widget, but composes cleanly with the chained `Response` style:
205 ///
206 /// ```ignore
207 /// if ui.button("Save").on_hover(ui, "Saves the file").clicked {
208 /// save();
209 /// }
210 /// ```
211 ///
212 /// `text` is wrapped at 38 columns and rendered in an overlay panel
213 /// anchored under (or above, if no room below) the widget's rect.
214 /// Empty strings, zero-area rects, and non-hovered responses are
215 /// silently skipped — no allocation in the cold path.
216 ///
217 /// Unlike [`Context::tooltip`], the binding is not order-sensitive:
218 /// the tooltip is attached to *this* response specifically, so
219 /// chaining further widgets afterward does not strip it.
220 #[must_use = "on_hover returns the Response for further chaining"]
221 pub fn on_hover(self, ctx: &mut Context, text: impl Into<String>) -> Self {
222 if !self.hovered || self.rect.width == 0 || self.rect.height == 0 {
223 return self;
224 }
225 let tooltip_text = text.into();
226 if tooltip_text.is_empty() {
227 return self;
228 }
229 let lines = super::widgets_display::wrap_tooltip_text(&tooltip_text, 38);
230 ctx.pending_tooltips.push(PendingTooltip {
231 anchor_rect: self.rect,
232 lines,
233 });
234 self
235 }
236
237 /// Run a closure to render arbitrary tooltip content when the widget is
238 /// hovered.
239 ///
240 /// The closure receives the same `&mut Context` and runs immediately
241 /// (in-place — not deferred). This means the closure can issue any UI
242 /// commands; positioning is the caller's responsibility (use
243 /// [`Context::overlay`] / [`Context::overlay_at`] inside the closure
244 /// for floating panels).
245 ///
246 /// For simple text tooltips, prefer [`Response::on_hover`] which
247 /// auto-positions the tooltip under the widget.
248 ///
249 /// ```ignore
250 /// ui.button("Help").on_hover_ui(ui, |ui| {
251 /// let _ = ui.overlay(|ui| {
252 /// ui.text("Custom tooltip body");
253 /// });
254 /// });
255 /// ```
256 #[must_use = "on_hover_ui returns the Response for further chaining"]
257 pub fn on_hover_ui(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
258 if self.hovered && self.rect.width > 0 && self.rect.height > 0 {
259 f(ctx);
260 }
261 self
262 }
263}