superlighttui 0.21.1

Super Light TUI - A lightweight, ergonomic terminal UI library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
use super::*;

/// Internal discriminator for [`State<T>`] handles.
///
/// `Indexed` refers to a slot in `Context::hook_states` (positional, used by
/// [`Context::use_state`] / [`Context::use_memo`]). `Named` refers to a key in
/// `Context::named_states` (used by [`Context::use_state_named`]). `Keyed`
/// refers to a runtime-string key in `Context::keyed_states` (used by
/// [`Context::use_state_keyed`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StateKey {
    Indexed(usize),
    Named(&'static str),
    Keyed(String),
}

/// Handle to state created by `use_state()`. Access via `.get(ui)` / `.get_mut(ui)`.
///
/// # Note on `Copy`
///
/// As of v0.20.0, `State<T>` is no longer `Copy`. The internal key may hold an
/// owned `String` (for [`Context::use_state_keyed`]), which prevents trivial
/// duplication. Existing call sites that use the handle locally (`let s =
/// ui.use_state(...); s.get(ui);`) are unaffected — the handle is moved into
/// closures or borrowed by reference. If you previously relied on implicit
/// copy semantics, call `.clone()` explicitly.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct State<T> {
    key: StateKey,
    _marker: std::marker::PhantomData<T>,
}

/// Downcast a stored boxed `Any` to `&T`, panicking with a uniform context
/// message on mismatch. Internal helper to keep [`State::get`] / [`State::get_mut`]
/// concise and ensure every panic site formats identically.
///
/// `ctx` should be a complete leading clause such as
/// `"use_state_named type mismatch for id \"foo\""` — the helper appends
/// `" — expected <type>"` so callers don't repeat that suffix at every site.
fn downcast_or_panic<'a, T: 'static>(
    boxed: &'a dyn std::any::Any,
    ctx: std::fmt::Arguments<'_>,
) -> &'a T {
    boxed
        .downcast_ref::<T>()
        .unwrap_or_else(|| panic!("{ctx} — expected {}", std::any::type_name::<T>()))
}

/// Mutable counterpart of [`downcast_or_panic`].
fn downcast_or_panic_mut<'a, T: 'static>(
    boxed: &'a mut dyn std::any::Any,
    ctx: std::fmt::Arguments<'_>,
) -> &'a mut T {
    boxed
        .downcast_mut::<T>()
        .unwrap_or_else(|| panic!("{ctx} — expected {}", std::any::type_name::<T>()))
}

impl<T: 'static> State<T> {
    pub(crate) fn from_idx(idx: usize) -> Self {
        Self {
            key: StateKey::Indexed(idx),
            _marker: std::marker::PhantomData,
        }
    }

    pub(crate) fn from_named(id: &'static str) -> Self {
        Self {
            key: StateKey::Named(id),
            _marker: std::marker::PhantomData,
        }
    }

    pub(crate) fn from_keyed(id: String) -> Self {
        Self {
            key: StateKey::Keyed(id),
            _marker: std::marker::PhantomData,
        }
    }

    /// Read the current value.
    pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
        match &self.key {
            StateKey::Indexed(idx) => downcast_or_panic::<T>(
                ui.hook_states[*idx].as_ref(),
                format_args!("use_state type mismatch at hook index {idx}"),
            ),
            StateKey::Named(id) => {
                let boxed = ui.named_states.get(id).unwrap_or_else(|| {
                    panic!("use_state_named: no entry for id {id:?} — was use_state_named called?")
                });
                downcast_or_panic::<T>(
                    boxed.as_ref(),
                    format_args!("use_state_named type mismatch for id {id:?}"),
                )
            }
            StateKey::Keyed(id) => {
                let boxed = ui.keyed_states.get(id).unwrap_or_else(|| {
                    panic!("use_state_keyed: no entry for id {id:?} — was use_state_keyed called?")
                });
                downcast_or_panic::<T>(
                    boxed.as_ref(),
                    format_args!("use_state_keyed type mismatch for id {id:?}"),
                )
            }
        }
    }

    /// Mutably access the current value.
    pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
        match &self.key {
            StateKey::Indexed(idx) => downcast_or_panic_mut::<T>(
                ui.hook_states[*idx].as_mut(),
                format_args!("use_state type mismatch at hook index {idx}"),
            ),
            StateKey::Named(id) => {
                let boxed = ui.named_states.get_mut(id).unwrap_or_else(|| {
                    panic!("use_state_named: no entry for id {id:?} — was use_state_named called?")
                });
                downcast_or_panic_mut::<T>(
                    boxed.as_mut(),
                    format_args!("use_state_named type mismatch for id {id:?}"),
                )
            }
            StateKey::Keyed(id) => {
                let boxed = ui.keyed_states.get_mut(id).unwrap_or_else(|| {
                    panic!("use_state_keyed: no entry for id {id:?} — was use_state_keyed called?")
                });
                downcast_or_panic_mut::<T>(
                    boxed.as_mut(),
                    format_args!("use_state_keyed type mismatch for id {id:?}"),
                )
            }
        }
    }
}

/// Internal storage shape for a value created by [`Context::use_memo`].
///
/// The previous-frame dependencies are kept type-erased (`Box<dyn Any>`) so the
/// read path ([`Memo::get`]) can downcast the slot to `MemoSlot<T>` without
/// knowing `D`. [`Context::use_memo`] downcasts `deps` back to `&D` when
/// comparing against the new dependencies to decide whether to recompute.
///
/// Kept `pub(crate)` — never part of the public API. The `T` in its type name
/// appears in the hook-ordering mismatch panic message, mirroring the historic
/// `(D, T)` message shape.
pub(crate) struct MemoSlot<T> {
    pub(crate) deps: Box<dyn std::any::Any>,
    pub(crate) value: T,
}

/// Handle to a memoized value created by [`Context::use_memo`].
///
/// Like [`State<T>`], this is an *index handle*, not a live borrow — it stores
/// only the hook slot index and does **not** keep [`Context`] borrowed. That is
/// the whole point: the handle composes with later `ui.*` calls, where the old
/// `&T`-returning form (now [`Context::use_memo_ref`]) held an immutable borrow
/// of `ui` that conflicted with any subsequent mutation.
///
/// Read the value with [`get`](Self::get) (`&T`) or [`copied`](Self::copied)
/// (`T: Copy`).
///
/// # Example
///
/// ```no_run
/// # slt::run(|ui: &mut slt::Context| {
/// let count = ui.use_state(|| 0i32);
/// let count_val = *count.get(ui);
/// // Handle releases the `&mut ui` borrow immediately...
/// let doubled = ui.use_memo(&count_val, |c| c * 2);
/// // ...so an intervening `ui.*` call composes cleanly.
/// ui.text("computed:");
/// ui.text(format!("{}", doubled.copied(ui)));
/// # });
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Memo<T> {
    idx: usize,
    _marker: std::marker::PhantomData<T>,
}

impl<T: 'static> Memo<T> {
    pub(crate) fn from_idx(idx: usize) -> Self {
        Self {
            idx,
            _marker: std::marker::PhantomData,
        }
    }

    /// Read the memoized value.
    ///
    /// # Panics
    ///
    /// Panics with the slot index and expected type name if the hook at this
    /// index does not hold a `MemoSlot<T>` — i.e. the rules-of-hooks contract
    /// was broken (hooks called in a different order than the frame that created
    /// the slot). The message matches [`Context::use_memo`]'s own mismatch
    /// panic.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # slt::run(|ui: &mut slt::Context| {
    /// let m = ui.use_memo(&3i32, |d| d * 2);
    /// ui.text(format!("{}", m.get(ui)));
    /// # });
    /// ```
    pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
        match ui.hook_states[self.idx].downcast_ref::<MemoSlot<T>>() {
            Some(slot) => &slot.value,
            None => panic!(
                "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
                self.idx,
                std::any::type_name::<MemoSlot<T>>()
            ),
        }
    }

    /// Read a `Copy` of the memoized value.
    ///
    /// Convenience for `*memo.get(ui)`. Panics under the same conditions as
    /// [`get`](Self::get).
    ///
    /// # Example
    ///
    /// ```no_run
    /// # slt::run(|ui: &mut slt::Context| {
    /// let doubled = ui.use_memo(&21i32, |d| d * 2).copied(ui);
    /// ui.text(format!("{doubled}"));
    /// # });
    /// ```
    pub fn copied(&self, ui: &Context) -> T
    where
        T: Copy,
    {
        *self.get(ui)
    }
}

/// Interaction response returned by all widgets.
///
/// Container methods return a [`Response`]. Check `.clicked`, `.changed`, etc.
/// to react to user interactions.
/// `rect` is meaningful after the widget has participated in layout.
/// Container responses describe the container's own interaction area, not
/// automatically the focus state of every child widget.
///
/// # Examples
///
/// ```
/// # use slt::*;
/// # TestBackend::new(80, 24).render(|ui| {
/// let r = ui.row(|ui| {
///     ui.text("Save");
/// });
/// if r.clicked {
///     // handle save
/// }
/// # });
/// ```
#[derive(Debug, Clone, Default)]
#[must_use = "Response contains interaction state — check .clicked, .hovered, or .changed"]
pub struct Response {
    /// Whether the widget was left-clicked this frame.
    pub clicked: bool,
    /// Whether the widget was right-clicked this frame.
    ///
    /// Detected when a `MouseButton::Right` `Down` event lands inside the
    /// widget's `rect`. Suppressed for non-overlay widgets while a modal is
    /// active (consistent with the existing modal-suppression behavior of
    /// `clicked` / `hovered`). Available since v0.20.0.
    pub right_clicked: bool,
    /// Whether the mouse is hovering over the widget.
    pub hovered: bool,
    /// Whether the widget's value changed this frame.
    pub changed: bool,
    /// Whether the widget currently has keyboard focus.
    pub focused: bool,
    /// Whether the widget *just* received keyboard focus this frame.
    ///
    /// `true` only on the first frame after focus moved to this widget;
    /// `false` thereafter (until focus moves away and returns). Mutually
    /// exclusive with [`lost_focus`](Self::lost_focus). Available since
    /// v0.20.0.
    pub gained_focus: bool,
    /// Whether the widget *just* lost keyboard focus this frame.
    ///
    /// `true` only on the first frame after focus moved away from this widget;
    /// `false` on subsequent frames. Mutually exclusive with
    /// [`gained_focus`](Self::gained_focus). Available since v0.20.0.
    pub lost_focus: bool,
    /// Whether the widget was double-clicked this frame.
    ///
    /// Detected when two `MouseButton::Left` `Down` events land on the same
    /// terminal cell within the double-click window (~400ms). When `true`,
    /// `clicked` is also `true` for the same frame (the second click is still a
    /// click). This is the standard open/activate gesture for file pickers,
    /// lists, tables, and trees. Suppressed for non-overlay widgets while a
    /// modal is active, consistent with `clicked`. Available since v0.21.1.
    pub double_clicked: bool,
    /// Whether the widget submitted its value this frame.
    ///
    /// Set by widgets that have an explicit submit gesture — e.g. pressing
    /// `Enter` in a focused single-line [`text_input`](Context::text_input).
    /// Always `false` for widgets with no submit semantics. Available since
    /// v0.21.1.
    pub submitted: bool,
    /// Net vertical scroll-wheel delta over this widget this frame.
    ///
    /// Positive = wheel scrolled up, negative = down, `0` when the wheel did
    /// not move while the cursor was over the widget's `rect`. Hover-gated, so
    /// each widget consumes only the wheel motion that occurred above it — a
    /// chart, canvas, or custom viewport can scroll/zoom locally without a
    /// frame-global scroll handler. Available since v0.21.1.
    pub scroll_delta: i32,
    /// The rectangle the widget occupies after layout.
    pub rect: Rect,
}

impl Response {
    /// Create a Response with all fields false/default.
    pub fn none() -> Self {
        Self::default()
    }

    /// Attach a tooltip to this widget. Renders only when the widget is
    /// currently hovered.
    ///
    /// Equivalent to calling [`Context::tooltip`] immediately after the
    /// widget, but composes cleanly with the chained `Response` style:
    ///
    /// ```ignore
    /// if ui.button("Save").on_hover(ui, "Saves the file").clicked {
    ///     save();
    /// }
    /// ```
    ///
    /// `text` is wrapped at 38 columns and rendered in an overlay panel
    /// anchored under (or above, if no room below) the widget's rect.
    /// Empty strings, zero-area rects, and non-hovered responses are
    /// silently skipped — no allocation in the cold path.
    ///
    /// Unlike [`Context::tooltip`], the binding is not order-sensitive:
    /// the tooltip is attached to *this* response specifically, so
    /// chaining further widgets afterward does not strip it.
    #[must_use = "on_hover returns the Response for further chaining"]
    pub fn on_hover(self, ctx: &mut Context, text: impl Into<String>) -> Self {
        if !self.hovered || self.rect.width == 0 || self.rect.height == 0 {
            return self;
        }
        let tooltip_text = text.into();
        if tooltip_text.is_empty() {
            return self;
        }
        let lines = super::widgets_display::wrap_tooltip_text(&tooltip_text, 38);
        ctx.pending_tooltips.push(PendingTooltip {
            anchor_rect: self.rect,
            lines,
        });
        self
    }

    /// Run a closure to render arbitrary tooltip content when the widget is
    /// hovered.
    ///
    /// The closure receives the same `&mut Context` and runs immediately
    /// (in-place — not deferred). This means the closure can issue any UI
    /// commands; positioning is the caller's responsibility (use
    /// [`Context::overlay`] / [`Context::overlay_at`] inside the closure
    /// for floating panels).
    ///
    /// For simple text tooltips, prefer [`Response::on_hover`] which
    /// auto-positions the tooltip under the widget.
    ///
    /// ```ignore
    /// ui.button("Help").on_hover_ui(ui, |ui| {
    ///     let _ = ui.overlay(|ui| {
    ///         ui.text("Custom tooltip body");
    ///     });
    /// });
    /// ```
    #[must_use = "on_hover_ui returns the Response for further chaining"]
    pub fn on_hover_ui(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
        if self.hovered && self.rect.width > 0 && self.rect.height > 0 {
            f(ctx);
        }
        self
    }

    /// Run `f` if the widget was clicked this frame, then return the Response
    /// for further chaining.
    ///
    /// The closure receives the same `&mut Context` so it can issue UI commands
    /// (e.g. queue a toast); ignore the argument with `|_|` if you only need to
    /// mutate application state.
    ///
    /// ```ignore
    /// ui.button("Save").on_click(ui, |_| save());
    /// ```
    pub fn on_click(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
        if self.clicked {
            f(ctx);
        }
        self
    }

    /// Run `f` if the widget's value changed this frame, then return the
    /// Response for chaining. See [`on_click`](Self::on_click) for the closure
    /// argument convention. Available since v0.21.1.
    pub fn on_changed(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
        if self.changed {
            f(ctx);
        }
        self
    }

    /// Run `f` on the frame the widget *gained* keyboard focus, then return the
    /// Response for chaining. Fires once per focus acquisition (mirrors
    /// [`gained_focus`](Self::gained_focus)). Available since v0.21.1.
    pub fn on_focus(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
        if self.gained_focus {
            f(ctx);
        }
        self
    }

    /// Run `f` if the widget submitted this frame (e.g. `Enter` in a focused
    /// single-line text input), then return the Response for chaining. Mirrors
    /// [`submitted`](Self::submitted). Available since v0.21.1.
    pub fn on_submit(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
        if self.submitted {
            f(ctx);
        }
        self
    }

    /// Run `f` if the widget was double-clicked this frame, then return the
    /// Response for chaining. Mirrors [`double_clicked`](Self::double_clicked).
    /// Available since v0.21.1.
    pub fn on_double_click(self, ctx: &mut Context, f: impl FnOnce(&mut Context)) -> Self {
        if self.double_clicked {
            f(ctx);
        }
        self
    }
}