Skip to main content

slt/
context.rs

1use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
2use crate::layout::{Command, Direction};
3use crate::rect::Rect;
4use crate::style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
5use crate::widgets::{
6    ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
7    ToastLevel, ToastState,
8};
9use unicode_width::UnicodeWidthStr;
10
11/// Result of a container mouse interaction.
12///
13/// Returned by [`Context::col`], [`Context::row`], and [`ContainerBuilder::col`] /
14/// [`ContainerBuilder::row`] so you can react to clicks and hover without a separate
15/// event loop.
16#[derive(Debug, Clone, Copy, Default)]
17pub struct Response {
18    /// Whether the container was clicked this frame.
19    pub clicked: bool,
20    /// Whether the mouse is over the container.
21    pub hovered: bool,
22}
23
24/// The main rendering context passed to your closure each frame.
25///
26/// Provides all methods for building UI: text, containers, widgets, and event
27/// handling. You receive a `&mut Context` on every frame and describe what to
28/// render by calling its methods. SLT collects those calls, lays them out with
29/// flexbox, diffs against the previous frame, and flushes only changed cells.
30///
31/// # Example
32///
33/// ```no_run
34/// slt::run(|ui: &mut slt::Context| {
35///     if ui.key('q') { ui.quit(); }
36///     ui.text("Hello, world!").bold();
37/// });
38/// ```
39pub struct Context {
40    pub(crate) commands: Vec<Command>,
41    pub(crate) events: Vec<Event>,
42    pub(crate) consumed: Vec<bool>,
43    pub(crate) should_quit: bool,
44    pub(crate) area_width: u32,
45    pub(crate) area_height: u32,
46    pub(crate) tick: u64,
47    pub(crate) focus_index: usize,
48    pub(crate) focus_count: usize,
49    prev_focus_count: usize,
50    scroll_count: usize,
51    prev_scroll_infos: Vec<(u32, u32)>,
52    interaction_count: usize,
53    prev_hit_map: Vec<Rect>,
54    mouse_pos: Option<(u32, u32)>,
55    click_pos: Option<(u32, u32)>,
56    last_mouse_pos: Option<(u32, u32)>,
57    last_text_idx: Option<usize>,
58    debug: bool,
59    theme: Theme,
60}
61
62/// Fluent builder for configuring containers before calling `.col()` or `.row()`.
63///
64/// Obtain one via [`Context::container`] or [`Context::bordered`]. Chain the
65/// configuration methods you need, then finalize with `.col(|ui| { ... })` or
66/// `.row(|ui| { ... })`.
67///
68/// # Example
69///
70/// ```no_run
71/// # slt::run(|ui: &mut slt::Context| {
72/// use slt::{Border, Color};
73/// ui.container()
74///     .border(Border::Rounded)
75///     .pad(1)
76///     .grow(1)
77///     .col(|ui| {
78///         ui.text("inside a bordered, padded, growing column");
79///     });
80/// # });
81/// ```
82pub struct ContainerBuilder<'a> {
83    ctx: &'a mut Context,
84    gap: u32,
85    align: Align,
86    border: Option<Border>,
87    border_style: Style,
88    padding: Padding,
89    margin: Margin,
90    constraints: Constraints,
91    title: Option<(String, Style)>,
92    grow: u16,
93    scroll_offset: Option<u32>,
94}
95
96impl<'a> ContainerBuilder<'a> {
97    // ── border ───────────────────────────────────────────────────────
98
99    /// Set the border style.
100    pub fn border(mut self, border: Border) -> Self {
101        self.border = Some(border);
102        self
103    }
104
105    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
106    pub fn rounded(self) -> Self {
107        self.border(Border::Rounded)
108    }
109
110    /// Set the style applied to the border characters.
111    pub fn border_style(mut self, style: Style) -> Self {
112        self.border_style = style;
113        self
114    }
115
116    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
117
118    /// Set uniform padding on all sides. Alias for [`pad`](Self::pad).
119    pub fn p(self, value: u32) -> Self {
120        self.pad(value)
121    }
122
123    /// Set uniform padding on all sides.
124    pub fn pad(mut self, value: u32) -> Self {
125        self.padding = Padding::all(value);
126        self
127    }
128
129    /// Set horizontal padding (left and right).
130    pub fn px(mut self, value: u32) -> Self {
131        self.padding.left = value;
132        self.padding.right = value;
133        self
134    }
135
136    /// Set vertical padding (top and bottom).
137    pub fn py(mut self, value: u32) -> Self {
138        self.padding.top = value;
139        self.padding.bottom = value;
140        self
141    }
142
143    /// Set top padding.
144    pub fn pt(mut self, value: u32) -> Self {
145        self.padding.top = value;
146        self
147    }
148
149    /// Set right padding.
150    pub fn pr(mut self, value: u32) -> Self {
151        self.padding.right = value;
152        self
153    }
154
155    /// Set bottom padding.
156    pub fn pb(mut self, value: u32) -> Self {
157        self.padding.bottom = value;
158        self
159    }
160
161    /// Set left padding.
162    pub fn pl(mut self, value: u32) -> Self {
163        self.padding.left = value;
164        self
165    }
166
167    /// Set per-side padding using a [`Padding`] value.
168    pub fn padding(mut self, padding: Padding) -> Self {
169        self.padding = padding;
170        self
171    }
172
173    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
174
175    /// Set uniform margin on all sides.
176    pub fn m(mut self, value: u32) -> Self {
177        self.margin = Margin::all(value);
178        self
179    }
180
181    /// Set horizontal margin (left and right).
182    pub fn mx(mut self, value: u32) -> Self {
183        self.margin.left = value;
184        self.margin.right = value;
185        self
186    }
187
188    /// Set vertical margin (top and bottom).
189    pub fn my(mut self, value: u32) -> Self {
190        self.margin.top = value;
191        self.margin.bottom = value;
192        self
193    }
194
195    /// Set top margin.
196    pub fn mt(mut self, value: u32) -> Self {
197        self.margin.top = value;
198        self
199    }
200
201    /// Set right margin.
202    pub fn mr(mut self, value: u32) -> Self {
203        self.margin.right = value;
204        self
205    }
206
207    /// Set bottom margin.
208    pub fn mb(mut self, value: u32) -> Self {
209        self.margin.bottom = value;
210        self
211    }
212
213    /// Set left margin.
214    pub fn ml(mut self, value: u32) -> Self {
215        self.margin.left = value;
216        self
217    }
218
219    /// Set per-side margin using a [`Margin`] value.
220    pub fn margin(mut self, margin: Margin) -> Self {
221        self.margin = margin;
222        self
223    }
224
225    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
226
227    /// Set a fixed width (sets both min and max width).
228    pub fn w(mut self, value: u32) -> Self {
229        self.constraints.min_width = Some(value);
230        self.constraints.max_width = Some(value);
231        self
232    }
233
234    /// Set a fixed height (sets both min and max height).
235    pub fn h(mut self, value: u32) -> Self {
236        self.constraints.min_height = Some(value);
237        self.constraints.max_height = Some(value);
238        self
239    }
240
241    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
242    pub fn min_w(mut self, value: u32) -> Self {
243        self.constraints.min_width = Some(value);
244        self
245    }
246
247    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
248    pub fn max_w(mut self, value: u32) -> Self {
249        self.constraints.max_width = Some(value);
250        self
251    }
252
253    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
254    pub fn min_h(mut self, value: u32) -> Self {
255        self.constraints.min_height = Some(value);
256        self
257    }
258
259    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
260    pub fn max_h(mut self, value: u32) -> Self {
261        self.constraints.max_height = Some(value);
262        self
263    }
264
265    /// Set the minimum width constraint in cells.
266    pub fn min_width(mut self, value: u32) -> Self {
267        self.constraints.min_width = Some(value);
268        self
269    }
270
271    /// Set the maximum width constraint in cells.
272    pub fn max_width(mut self, value: u32) -> Self {
273        self.constraints.max_width = Some(value);
274        self
275    }
276
277    /// Set the minimum height constraint in rows.
278    pub fn min_height(mut self, value: u32) -> Self {
279        self.constraints.min_height = Some(value);
280        self
281    }
282
283    /// Set the maximum height constraint in rows.
284    pub fn max_height(mut self, value: u32) -> Self {
285        self.constraints.max_height = Some(value);
286        self
287    }
288
289    /// Set all size constraints at once using a [`Constraints`] value.
290    pub fn constraints(mut self, constraints: Constraints) -> Self {
291        self.constraints = constraints;
292        self
293    }
294
295    // ── flex ─────────────────────────────────────────────────────────
296
297    /// Set the gap (in cells) between child elements.
298    pub fn gap(mut self, gap: u32) -> Self {
299        self.gap = gap;
300        self
301    }
302
303    /// Set the flex-grow factor. `1` means the container expands to fill available space.
304    pub fn grow(mut self, grow: u16) -> Self {
305        self.grow = grow;
306        self
307    }
308
309    // ── alignment ───────────────────────────────────────────────────
310
311    /// Set the cross-axis alignment of child elements.
312    pub fn align(mut self, align: Align) -> Self {
313        self.align = align;
314        self
315    }
316
317    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
318    pub fn center(self) -> Self {
319        self.align(Align::Center)
320    }
321
322    // ── title ────────────────────────────────────────────────────────
323
324    /// Set a plain-text title rendered in the top border.
325    pub fn title(self, title: impl Into<String>) -> Self {
326        self.title_styled(title, Style::new())
327    }
328
329    /// Set a styled title rendered in the top border.
330    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
331        self.title = Some((title.into(), style));
332        self
333    }
334
335    // ── internal ─────────────────────────────────────────────────────
336
337    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
338    pub fn scroll_offset(mut self, offset: u32) -> Self {
339        self.scroll_offset = Some(offset);
340        self
341    }
342
343    /// Finalize the builder as a vertical (column) container.
344    ///
345    /// The closure receives a `&mut Context` for rendering children.
346    /// Returns a [`Response`] with click/hover state for this container.
347    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
348        self.finish(Direction::Column, f)
349    }
350
351    /// Finalize the builder as a horizontal (row) container.
352    ///
353    /// The closure receives a `&mut Context` for rendering children.
354    /// Returns a [`Response`] with click/hover state for this container.
355    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
356        self.finish(Direction::Row, f)
357    }
358
359    fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
360        let interaction_id = self.ctx.interaction_count;
361        self.ctx.interaction_count += 1;
362
363        if let Some(scroll_offset) = self.scroll_offset {
364            self.ctx.commands.push(Command::BeginScrollable {
365                grow: self.grow,
366                border: self.border,
367                border_style: self.border_style,
368                padding: self.padding,
369                margin: self.margin,
370                constraints: self.constraints,
371                title: self.title,
372                scroll_offset,
373            });
374        } else {
375            self.ctx.commands.push(Command::BeginContainer {
376                direction,
377                gap: self.gap,
378                align: self.align,
379                border: self.border,
380                border_style: self.border_style,
381                padding: self.padding,
382                margin: self.margin,
383                constraints: self.constraints,
384                title: self.title,
385                grow: self.grow,
386            });
387        }
388        f(self.ctx);
389        self.ctx.commands.push(Command::EndContainer);
390        self.ctx.last_text_idx = None;
391
392        self.ctx.response_for(interaction_id)
393    }
394}
395
396impl Context {
397    #[allow(clippy::too_many_arguments)]
398    pub(crate) fn new(
399        events: Vec<Event>,
400        width: u32,
401        height: u32,
402        tick: u64,
403        focus_index: usize,
404        prev_focus_count: usize,
405        prev_scroll_infos: Vec<(u32, u32)>,
406        prev_hit_map: Vec<Rect>,
407        debug: bool,
408        theme: Theme,
409        last_mouse_pos: Option<(u32, u32)>,
410    ) -> Self {
411        let consumed = vec![false; events.len()];
412
413        let mut mouse_pos = last_mouse_pos;
414        let mut click_pos = None;
415        for event in &events {
416            if let Event::Mouse(mouse) = event {
417                mouse_pos = Some((mouse.x, mouse.y));
418                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
419                    click_pos = Some((mouse.x, mouse.y));
420                }
421            }
422        }
423
424        Self {
425            commands: Vec::new(),
426            events,
427            consumed,
428            should_quit: false,
429            area_width: width,
430            area_height: height,
431            tick,
432            focus_index,
433            focus_count: 0,
434            prev_focus_count,
435            scroll_count: 0,
436            prev_scroll_infos,
437            interaction_count: 0,
438            prev_hit_map,
439            mouse_pos,
440            click_pos,
441            last_mouse_pos,
442            last_text_idx: None,
443            debug,
444            theme,
445        }
446    }
447
448    pub(crate) fn process_focus_keys(&mut self) {
449        for (i, event) in self.events.iter().enumerate() {
450            if let Event::Key(key) = event {
451                if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
452                    if self.prev_focus_count > 0 {
453                        self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
454                    }
455                    self.consumed[i] = true;
456                } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
457                    || key.code == KeyCode::BackTab
458                {
459                    if self.prev_focus_count > 0 {
460                        self.focus_index = if self.focus_index == 0 {
461                            self.prev_focus_count - 1
462                        } else {
463                            self.focus_index - 1
464                        };
465                    }
466                    self.consumed[i] = true;
467                }
468            }
469        }
470    }
471
472    /// Register a widget as focusable and return whether it currently has focus.
473    ///
474    /// Used internally by built-in widgets. Call this when building custom widgets
475    /// that need keyboard focus. Each call increments the internal focus counter,
476    /// so the order of calls must be stable across frames.
477    pub fn register_focusable(&mut self) -> bool {
478        let id = self.focus_count;
479        self.focus_count += 1;
480        if self.prev_focus_count == 0 {
481            return true;
482        }
483        self.focus_index % self.prev_focus_count == id
484    }
485
486    // ── text ──────────────────────────────────────────────────────────
487
488    /// Render a text element. Returns `&mut Self` for style chaining.
489    ///
490    /// # Example
491    ///
492    /// ```no_run
493    /// # slt::run(|ui: &mut slt::Context| {
494    /// use slt::Color;
495    /// ui.text("hello").bold().fg(Color::Cyan);
496    /// # });
497    /// ```
498    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
499        let content = s.into();
500        self.commands.push(Command::Text {
501            content,
502            style: Style::new(),
503            grow: 0,
504            align: Align::Start,
505            wrap: false,
506            margin: Margin::default(),
507            constraints: Constraints::default(),
508        });
509        self.last_text_idx = Some(self.commands.len() - 1);
510        self
511    }
512
513    /// Render a text element with word-boundary wrapping.
514    ///
515    /// Long lines are broken at word boundaries to fit the container width.
516    /// Style chaining works the same as [`Context::text`].
517    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
518        let content = s.into();
519        self.commands.push(Command::Text {
520            content,
521            style: Style::new(),
522            grow: 0,
523            align: Align::Start,
524            wrap: true,
525            margin: Margin::default(),
526            constraints: Constraints::default(),
527        });
528        self.last_text_idx = Some(self.commands.len() - 1);
529        self
530    }
531
532    // ── style chain (applies to last text) ───────────────────────────
533
534    /// Apply bold to the last rendered text element.
535    pub fn bold(&mut self) -> &mut Self {
536        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
537        self
538    }
539
540    /// Apply dim styling to the last rendered text element.
541    ///
542    /// Also sets the foreground color to the theme's `text_dim` color if no
543    /// explicit foreground has been set.
544    pub fn dim(&mut self) -> &mut Self {
545        let text_dim = self.theme.text_dim;
546        self.modify_last_style(|s| {
547            s.modifiers |= Modifiers::DIM;
548            if s.fg.is_none() {
549                s.fg = Some(text_dim);
550            }
551        });
552        self
553    }
554
555    /// Apply italic to the last rendered text element.
556    pub fn italic(&mut self) -> &mut Self {
557        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
558        self
559    }
560
561    /// Apply underline to the last rendered text element.
562    pub fn underline(&mut self) -> &mut Self {
563        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
564        self
565    }
566
567    /// Apply reverse-video to the last rendered text element.
568    pub fn reversed(&mut self) -> &mut Self {
569        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
570        self
571    }
572
573    /// Apply strikethrough to the last rendered text element.
574    pub fn strikethrough(&mut self) -> &mut Self {
575        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
576        self
577    }
578
579    /// Set the foreground color of the last rendered text element.
580    pub fn fg(&mut self, color: Color) -> &mut Self {
581        self.modify_last_style(|s| s.fg = Some(color));
582        self
583    }
584
585    /// Set the background color of the last rendered text element.
586    pub fn bg(&mut self, color: Color) -> &mut Self {
587        self.modify_last_style(|s| s.bg = Some(color));
588        self
589    }
590
591    /// Render a text element with an explicit [`Style`] applied immediately.
592    ///
593    /// Equivalent to calling `text(s)` followed by style-chain methods, but
594    /// more concise when you already have a `Style` value.
595    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
596        self.commands.push(Command::Text {
597            content: s.into(),
598            style,
599            grow: 0,
600            align: Align::Start,
601            wrap: false,
602            margin: Margin::default(),
603            constraints: Constraints::default(),
604        });
605        self.last_text_idx = Some(self.commands.len() - 1);
606        self
607    }
608
609    /// Enable word-boundary wrapping on the last rendered text element.
610    pub fn wrap(&mut self) -> &mut Self {
611        if let Some(idx) = self.last_text_idx {
612            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
613                *wrap = true;
614            }
615        }
616        self
617    }
618
619    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
620        if let Some(idx) = self.last_text_idx {
621            if let Command::Text { style, .. } = &mut self.commands[idx] {
622                f(style);
623            }
624        }
625    }
626
627    // ── containers ───────────────────────────────────────────────────
628
629    /// Create a vertical (column) container.
630    ///
631    /// Children are stacked top-to-bottom. Returns a [`Response`] with
632    /// click/hover state for the container area.
633    ///
634    /// # Example
635    ///
636    /// ```no_run
637    /// # slt::run(|ui: &mut slt::Context| {
638    /// ui.col(|ui| {
639    ///     ui.text("line one");
640    ///     ui.text("line two");
641    /// });
642    /// # });
643    /// ```
644    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
645        self.push_container(Direction::Column, 0, f)
646    }
647
648    /// Create a vertical (column) container with a gap between children.
649    ///
650    /// `gap` is the number of blank rows inserted between each child.
651    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
652        self.push_container(Direction::Column, gap, f)
653    }
654
655    /// Create a horizontal (row) container.
656    ///
657    /// Children are placed left-to-right. Returns a [`Response`] with
658    /// click/hover state for the container area.
659    ///
660    /// # Example
661    ///
662    /// ```no_run
663    /// # slt::run(|ui: &mut slt::Context| {
664    /// ui.row(|ui| {
665    ///     ui.text("left");
666    ///     ui.spacer();
667    ///     ui.text("right");
668    /// });
669    /// # });
670    /// ```
671    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
672        self.push_container(Direction::Row, 0, f)
673    }
674
675    /// Create a horizontal (row) container with a gap between children.
676    ///
677    /// `gap` is the number of blank columns inserted between each child.
678    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
679        self.push_container(Direction::Row, gap, f)
680    }
681
682    /// Create a container with a fluent builder.
683    ///
684    /// Use this for borders, padding, grow, constraints, and titles. Chain
685    /// configuration methods on the returned [`ContainerBuilder`], then call
686    /// `.col()` or `.row()` to finalize.
687    ///
688    /// # Example
689    ///
690    /// ```no_run
691    /// # slt::run(|ui: &mut slt::Context| {
692    /// use slt::Border;
693    /// ui.container()
694    ///     .border(Border::Rounded)
695    ///     .pad(1)
696    ///     .title("My Panel")
697    ///     .col(|ui| {
698    ///         ui.text("content");
699    ///     });
700    /// # });
701    /// ```
702    pub fn container(&mut self) -> ContainerBuilder<'_> {
703        let border = self.theme.border;
704        ContainerBuilder {
705            ctx: self,
706            gap: 0,
707            align: Align::Start,
708            border: None,
709            border_style: Style::new().fg(border),
710            padding: Padding::default(),
711            margin: Margin::default(),
712            constraints: Constraints::default(),
713            title: None,
714            grow: 0,
715            scroll_offset: None,
716        }
717    }
718
719    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
720    ///
721    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
722    /// is updated in-place with the current scroll offset and bounds.
723    ///
724    /// # Example
725    ///
726    /// ```no_run
727    /// # use slt::widgets::ScrollState;
728    /// # slt::run(|ui: &mut slt::Context| {
729    /// let mut scroll = ScrollState::new();
730    /// ui.scrollable(&mut scroll).col(|ui| {
731    ///     for i in 0..100 {
732    ///         ui.text(format!("Line {i}"));
733    ///     }
734    /// });
735    /// # });
736    /// ```
737    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
738        let index = self.scroll_count;
739        self.scroll_count += 1;
740        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
741            state.set_bounds(ch, vh);
742            let max = ch.saturating_sub(vh) as usize;
743            state.offset = state.offset.min(max);
744        }
745
746        let next_id = self.interaction_count;
747        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
748            self.auto_scroll(&rect, state);
749        }
750
751        self.container().scroll_offset(state.offset as u32)
752    }
753
754    fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
755        let last_y = self.last_mouse_pos.map(|(_, y)| y);
756        let mut to_consume: Vec<usize> = Vec::new();
757
758        for (i, event) in self.events.iter().enumerate() {
759            if self.consumed[i] {
760                continue;
761            }
762            if let Event::Mouse(mouse) = event {
763                let in_bounds = mouse.x >= rect.x
764                    && mouse.x < rect.right()
765                    && mouse.y >= rect.y
766                    && mouse.y < rect.bottom();
767                if !in_bounds {
768                    continue;
769                }
770                match mouse.kind {
771                    MouseKind::ScrollUp => {
772                        state.scroll_up(1);
773                        to_consume.push(i);
774                    }
775                    MouseKind::ScrollDown => {
776                        state.scroll_down(1);
777                        to_consume.push(i);
778                    }
779                    MouseKind::Drag(MouseButton::Left) => {
780                        if let Some(prev_y) = last_y {
781                            let delta = mouse.y as i32 - prev_y as i32;
782                            if delta < 0 {
783                                state.scroll_down((-delta) as usize);
784                            } else if delta > 0 {
785                                state.scroll_up(delta as usize);
786                            }
787                        }
788                        to_consume.push(i);
789                    }
790                    _ => {}
791                }
792            }
793        }
794
795        for i in to_consume {
796            self.consumed[i] = true;
797        }
798    }
799
800    /// Shortcut for `container().border(border)`.
801    ///
802    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
803    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
804        self.container().border(border)
805    }
806
807    fn push_container(
808        &mut self,
809        direction: Direction,
810        gap: u32,
811        f: impl FnOnce(&mut Context),
812    ) -> Response {
813        let interaction_id = self.interaction_count;
814        self.interaction_count += 1;
815        let border = self.theme.border;
816
817        self.commands.push(Command::BeginContainer {
818            direction,
819            gap,
820            align: Align::Start,
821            border: None,
822            border_style: Style::new().fg(border),
823            padding: Padding::default(),
824            margin: Margin::default(),
825            constraints: Constraints::default(),
826            title: None,
827            grow: 0,
828        });
829        f(self);
830        self.commands.push(Command::EndContainer);
831        self.last_text_idx = None;
832
833        self.response_for(interaction_id)
834    }
835
836    fn response_for(&self, interaction_id: usize) -> Response {
837        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
838            let clicked = self
839                .click_pos
840                .map(|(mx, my)| {
841                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
842                })
843                .unwrap_or(false);
844            let hovered = self
845                .mouse_pos
846                .map(|(mx, my)| {
847                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
848                })
849                .unwrap_or(false);
850            Response { clicked, hovered }
851        } else {
852            Response::default()
853        }
854    }
855
856    /// Set the flex-grow factor of the last rendered text element.
857    ///
858    /// A value of `1` causes the element to expand and fill remaining space
859    /// along the main axis.
860    pub fn grow(&mut self, value: u16) -> &mut Self {
861        if let Some(idx) = self.last_text_idx {
862            if let Command::Text { grow, .. } = &mut self.commands[idx] {
863                *grow = value;
864            }
865        }
866        self
867    }
868
869    /// Set the text alignment of the last rendered text element.
870    pub fn align(&mut self, align: Align) -> &mut Self {
871        if let Some(idx) = self.last_text_idx {
872            if let Command::Text {
873                align: text_align, ..
874            } = &mut self.commands[idx]
875            {
876                *text_align = align;
877            }
878        }
879        self
880    }
881
882    /// Render an invisible spacer that expands to fill available space.
883    ///
884    /// Useful for pushing siblings to opposite ends of a row or column.
885    pub fn spacer(&mut self) -> &mut Self {
886        self.commands.push(Command::Spacer { grow: 1 });
887        self.last_text_idx = None;
888        self
889    }
890
891    /// Render a single-line text input. Auto-handles cursor, typing, and backspace.
892    ///
893    /// The widget claims focus via [`Context::register_focusable`]. When focused,
894    /// it consumes character, backspace, arrow, Home, and End key events.
895    ///
896    /// # Example
897    ///
898    /// ```no_run
899    /// # use slt::widgets::TextInputState;
900    /// # slt::run(|ui: &mut slt::Context| {
901    /// let mut input = TextInputState::with_placeholder("Search...");
902    /// ui.text_input(&mut input);
903    /// // input.value holds the current text
904    /// # });
905    /// ```
906    pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
907        let focused = self.register_focusable();
908
909        if focused {
910            let mut consumed_indices = Vec::new();
911            for (i, event) in self.events.iter().enumerate() {
912                if let Event::Key(key) = event {
913                    match key.code {
914                        KeyCode::Char(ch) => {
915                            let index = byte_index_for_char(&state.value, state.cursor);
916                            state.value.insert(index, ch);
917                            state.cursor += 1;
918                            consumed_indices.push(i);
919                        }
920                        KeyCode::Backspace => {
921                            if state.cursor > 0 {
922                                let start = byte_index_for_char(&state.value, state.cursor - 1);
923                                let end = byte_index_for_char(&state.value, state.cursor);
924                                state.value.replace_range(start..end, "");
925                                state.cursor -= 1;
926                            }
927                            consumed_indices.push(i);
928                        }
929                        KeyCode::Left => {
930                            state.cursor = state.cursor.saturating_sub(1);
931                            consumed_indices.push(i);
932                        }
933                        KeyCode::Right => {
934                            state.cursor = (state.cursor + 1).min(state.value.chars().count());
935                            consumed_indices.push(i);
936                        }
937                        KeyCode::Home => {
938                            state.cursor = 0;
939                            consumed_indices.push(i);
940                        }
941                        KeyCode::End => {
942                            state.cursor = state.value.chars().count();
943                            consumed_indices.push(i);
944                        }
945                        _ => {}
946                    }
947                }
948            }
949
950            for index in consumed_indices {
951                self.consumed[index] = true;
952            }
953        }
954
955        if state.value.is_empty() {
956            self.styled(
957                state.placeholder.clone(),
958                Style::new().dim().fg(self.theme.text_dim),
959            )
960        } else {
961            let mut rendered = String::new();
962            for (idx, ch) in state.value.chars().enumerate() {
963                if focused && idx == state.cursor {
964                    rendered.push('▎');
965                }
966                rendered.push(ch);
967            }
968            if focused && state.cursor >= state.value.chars().count() {
969                rendered.push('▎');
970            }
971            self.styled(rendered, Style::new().fg(self.theme.text))
972        }
973    }
974
975    /// Render an animated spinner.
976    ///
977    /// The spinner advances one frame per tick. Use [`SpinnerState::dots`] or
978    /// [`SpinnerState::line`] to create the state, then chain style methods to
979    /// color it.
980    pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
981        self.styled(
982            state.frame(self.tick).to_string(),
983            Style::new().fg(self.theme.primary),
984        )
985    }
986
987    /// Render toast notifications. Calls `state.cleanup(tick)` automatically.
988    ///
989    /// Expired messages are removed before rendering. If there are no active
990    /// messages, nothing is rendered and `self` is returned unchanged.
991    pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
992        state.cleanup(self.tick);
993        if state.messages.is_empty() {
994            return self;
995        }
996
997        self.interaction_count += 1;
998        self.commands.push(Command::BeginContainer {
999            direction: Direction::Column,
1000            gap: 0,
1001            align: Align::Start,
1002            border: None,
1003            border_style: Style::new().fg(self.theme.border),
1004            padding: Padding::default(),
1005            margin: Margin::default(),
1006            constraints: Constraints::default(),
1007            title: None,
1008            grow: 0,
1009        });
1010        for message in state.messages.iter().rev() {
1011            let color = match message.level {
1012                ToastLevel::Info => self.theme.primary,
1013                ToastLevel::Success => self.theme.success,
1014                ToastLevel::Warning => self.theme.warning,
1015                ToastLevel::Error => self.theme.error,
1016            };
1017            self.styled(format!("  ● {}", message.text), Style::new().fg(color));
1018        }
1019        self.commands.push(Command::EndContainer);
1020        self.last_text_idx = None;
1021
1022        self
1023    }
1024
1025    /// Render a multi-line text area with the given number of visible rows.
1026    ///
1027    /// When focused, handles character input, Enter (new line), Backspace,
1028    /// arrow keys, Home, and End. The cursor is rendered as a block character.
1029    pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1030        if state.lines.is_empty() {
1031            state.lines.push(String::new());
1032        }
1033        state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1034        state.cursor_col = state
1035            .cursor_col
1036            .min(state.lines[state.cursor_row].chars().count());
1037
1038        let focused = self.register_focusable();
1039
1040        if focused {
1041            let mut consumed_indices = Vec::new();
1042            for (i, event) in self.events.iter().enumerate() {
1043                if let Event::Key(key) = event {
1044                    match key.code {
1045                        KeyCode::Char(ch) => {
1046                            let index = byte_index_for_char(
1047                                &state.lines[state.cursor_row],
1048                                state.cursor_col,
1049                            );
1050                            state.lines[state.cursor_row].insert(index, ch);
1051                            state.cursor_col += 1;
1052                            consumed_indices.push(i);
1053                        }
1054                        KeyCode::Enter => {
1055                            let split_index = byte_index_for_char(
1056                                &state.lines[state.cursor_row],
1057                                state.cursor_col,
1058                            );
1059                            let remainder = state.lines[state.cursor_row].split_off(split_index);
1060                            state.cursor_row += 1;
1061                            state.lines.insert(state.cursor_row, remainder);
1062                            state.cursor_col = 0;
1063                            consumed_indices.push(i);
1064                        }
1065                        KeyCode::Backspace => {
1066                            if state.cursor_col > 0 {
1067                                let start = byte_index_for_char(
1068                                    &state.lines[state.cursor_row],
1069                                    state.cursor_col - 1,
1070                                );
1071                                let end = byte_index_for_char(
1072                                    &state.lines[state.cursor_row],
1073                                    state.cursor_col,
1074                                );
1075                                state.lines[state.cursor_row].replace_range(start..end, "");
1076                                state.cursor_col -= 1;
1077                            } else if state.cursor_row > 0 {
1078                                let current = state.lines.remove(state.cursor_row);
1079                                state.cursor_row -= 1;
1080                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1081                                state.lines[state.cursor_row].push_str(&current);
1082                            }
1083                            consumed_indices.push(i);
1084                        }
1085                        KeyCode::Left => {
1086                            if state.cursor_col > 0 {
1087                                state.cursor_col -= 1;
1088                            } else if state.cursor_row > 0 {
1089                                state.cursor_row -= 1;
1090                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1091                            }
1092                            consumed_indices.push(i);
1093                        }
1094                        KeyCode::Right => {
1095                            let line_len = state.lines[state.cursor_row].chars().count();
1096                            if state.cursor_col < line_len {
1097                                state.cursor_col += 1;
1098                            } else if state.cursor_row + 1 < state.lines.len() {
1099                                state.cursor_row += 1;
1100                                state.cursor_col = 0;
1101                            }
1102                            consumed_indices.push(i);
1103                        }
1104                        KeyCode::Up => {
1105                            if state.cursor_row > 0 {
1106                                state.cursor_row -= 1;
1107                                state.cursor_col = state
1108                                    .cursor_col
1109                                    .min(state.lines[state.cursor_row].chars().count());
1110                            }
1111                            consumed_indices.push(i);
1112                        }
1113                        KeyCode::Down => {
1114                            if state.cursor_row + 1 < state.lines.len() {
1115                                state.cursor_row += 1;
1116                                state.cursor_col = state
1117                                    .cursor_col
1118                                    .min(state.lines[state.cursor_row].chars().count());
1119                            }
1120                            consumed_indices.push(i);
1121                        }
1122                        KeyCode::Home => {
1123                            state.cursor_col = 0;
1124                            consumed_indices.push(i);
1125                        }
1126                        KeyCode::End => {
1127                            state.cursor_col = state.lines[state.cursor_row].chars().count();
1128                            consumed_indices.push(i);
1129                        }
1130                        _ => {}
1131                    }
1132                }
1133            }
1134
1135            for index in consumed_indices {
1136                self.consumed[index] = true;
1137            }
1138        }
1139
1140        self.interaction_count += 1;
1141        self.commands.push(Command::BeginContainer {
1142            direction: Direction::Column,
1143            gap: 0,
1144            align: Align::Start,
1145            border: None,
1146            border_style: Style::new().fg(self.theme.border),
1147            padding: Padding::default(),
1148            margin: Margin::default(),
1149            constraints: Constraints::default(),
1150            title: None,
1151            grow: 0,
1152        });
1153        for row in 0..visible_rows as usize {
1154            let line = state.lines.get(row).cloned().unwrap_or_default();
1155            let mut rendered = line.clone();
1156            let mut style = if line.is_empty() {
1157                Style::new().fg(self.theme.text_dim)
1158            } else {
1159                Style::new().fg(self.theme.text)
1160            };
1161
1162            if focused && row == state.cursor_row {
1163                rendered.clear();
1164                for (idx, ch) in line.chars().enumerate() {
1165                    if idx == state.cursor_col {
1166                        rendered.push('▎');
1167                    }
1168                    rendered.push(ch);
1169                }
1170                if state.cursor_col >= line.chars().count() {
1171                    rendered.push('▎');
1172                }
1173                style = Style::new().fg(self.theme.text);
1174            }
1175
1176            self.styled(rendered, style);
1177        }
1178        self.commands.push(Command::EndContainer);
1179        self.last_text_idx = None;
1180
1181        self
1182    }
1183
1184    /// Render a progress bar (20 chars wide). `ratio` is clamped to `0.0..=1.0`.
1185    ///
1186    /// Uses block characters (`█` filled, `░` empty). For a custom width use
1187    /// [`Context::progress_bar`].
1188    pub fn progress(&mut self, ratio: f64) -> &mut Self {
1189        self.progress_bar(ratio, 20)
1190    }
1191
1192    /// Render a progress bar with a custom character width.
1193    ///
1194    /// `ratio` is clamped to `0.0..=1.0`. `width` is the total number of
1195    /// characters rendered.
1196    pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1197        let clamped = ratio.clamp(0.0, 1.0);
1198        let filled = (clamped * width as f64).round() as u32;
1199        let empty = width.saturating_sub(filled);
1200        let mut bar = String::new();
1201        for _ in 0..filled {
1202            bar.push('█');
1203        }
1204        for _ in 0..empty {
1205            bar.push('░');
1206        }
1207        self.text(bar)
1208    }
1209
1210    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
1211    ///
1212    /// The selected item is highlighted with the theme's primary color. If the
1213    /// list is empty, nothing is rendered.
1214    pub fn list(&mut self, state: &mut ListState) -> &mut Self {
1215        if state.items.is_empty() {
1216            state.selected = 0;
1217            return self;
1218        }
1219
1220        let focused = self.register_focusable();
1221
1222        if focused {
1223            let mut consumed_indices = Vec::new();
1224            for (i, event) in self.events.iter().enumerate() {
1225                if let Event::Key(key) = event {
1226                    match key.code {
1227                        KeyCode::Up | KeyCode::Char('k') => {
1228                            state.selected = state.selected.saturating_sub(1);
1229                            consumed_indices.push(i);
1230                        }
1231                        KeyCode::Down | KeyCode::Char('j') => {
1232                            state.selected = (state.selected + 1).min(state.items.len() - 1);
1233                            consumed_indices.push(i);
1234                        }
1235                        _ => {}
1236                    }
1237                }
1238            }
1239
1240            for index in consumed_indices {
1241                self.consumed[index] = true;
1242            }
1243        }
1244
1245        for (idx, item) in state.items.iter().enumerate() {
1246            if idx == state.selected {
1247                if focused {
1248                    self.styled(
1249                        format!("▸ {item}"),
1250                        Style::new().bold().fg(self.theme.primary),
1251                    );
1252                } else {
1253                    self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
1254                }
1255            } else {
1256                self.styled(format!("  {item}"), Style::new().fg(self.theme.text));
1257            }
1258        }
1259
1260        self
1261    }
1262
1263    /// Render a data table with column headers. Handles Up/Down selection when focused.
1264    ///
1265    /// Column widths are computed automatically from header and cell content.
1266    /// The selected row is highlighted with the theme's selection colors.
1267    pub fn table(&mut self, state: &mut TableState) -> &mut Self {
1268        if state.is_dirty() {
1269            state.recompute_widths();
1270        }
1271
1272        let focused = self.register_focusable();
1273
1274        if focused && !state.rows.is_empty() {
1275            let mut consumed_indices = Vec::new();
1276            for (i, event) in self.events.iter().enumerate() {
1277                if let Event::Key(key) = event {
1278                    match key.code {
1279                        KeyCode::Up | KeyCode::Char('k') => {
1280                            state.selected = state.selected.saturating_sub(1);
1281                            consumed_indices.push(i);
1282                        }
1283                        KeyCode::Down | KeyCode::Char('j') => {
1284                            state.selected = (state.selected + 1).min(state.rows.len() - 1);
1285                            consumed_indices.push(i);
1286                        }
1287                        _ => {}
1288                    }
1289                }
1290            }
1291            for index in consumed_indices {
1292                self.consumed[index] = true;
1293            }
1294        }
1295
1296        state.selected = state.selected.min(state.rows.len().saturating_sub(1));
1297
1298        let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
1299        self.styled(header_line, Style::new().bold().fg(self.theme.text));
1300
1301        let separator = state
1302            .column_widths()
1303            .iter()
1304            .map(|w| "─".repeat(*w as usize))
1305            .collect::<Vec<_>>()
1306            .join("─┼─");
1307        self.text(separator);
1308
1309        for (idx, row) in state.rows.iter().enumerate() {
1310            let line = format_table_row(row, state.column_widths(), " │ ");
1311            if idx == state.selected {
1312                let mut style = Style::new()
1313                    .bg(self.theme.selected_bg)
1314                    .fg(self.theme.selected_fg);
1315                if focused {
1316                    style = style.bold();
1317                }
1318                self.styled(line, style);
1319            } else {
1320                self.styled(line, Style::new().fg(self.theme.text));
1321            }
1322        }
1323
1324        self
1325    }
1326
1327    /// Render a tab bar. Handles Left/Right navigation when focused.
1328    ///
1329    /// The active tab is rendered in the theme's primary color. If the labels
1330    /// list is empty, nothing is rendered.
1331    pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
1332        if state.labels.is_empty() {
1333            state.selected = 0;
1334            return self;
1335        }
1336
1337        state.selected = state.selected.min(state.labels.len() - 1);
1338        let focused = self.register_focusable();
1339
1340        if focused {
1341            let mut consumed_indices = Vec::new();
1342            for (i, event) in self.events.iter().enumerate() {
1343                if let Event::Key(key) = event {
1344                    match key.code {
1345                        KeyCode::Left => {
1346                            state.selected = if state.selected == 0 {
1347                                state.labels.len() - 1
1348                            } else {
1349                                state.selected - 1
1350                            };
1351                            consumed_indices.push(i);
1352                        }
1353                        KeyCode::Right => {
1354                            state.selected = (state.selected + 1) % state.labels.len();
1355                            consumed_indices.push(i);
1356                        }
1357                        _ => {}
1358                    }
1359                }
1360            }
1361
1362            for index in consumed_indices {
1363                self.consumed[index] = true;
1364            }
1365        }
1366
1367        self.interaction_count += 1;
1368        self.commands.push(Command::BeginContainer {
1369            direction: Direction::Row,
1370            gap: 1,
1371            align: Align::Start,
1372            border: None,
1373            border_style: Style::new().fg(self.theme.border),
1374            padding: Padding::default(),
1375            margin: Margin::default(),
1376            constraints: Constraints::default(),
1377            title: None,
1378            grow: 0,
1379        });
1380        for (idx, label) in state.labels.iter().enumerate() {
1381            let style = if idx == state.selected {
1382                let s = Style::new().fg(self.theme.primary).bold();
1383                if focused {
1384                    s.underline()
1385                } else {
1386                    s
1387                }
1388            } else {
1389                Style::new().fg(self.theme.text_dim)
1390            };
1391            self.styled(format!("[ {label} ]"), style);
1392        }
1393        self.commands.push(Command::EndContainer);
1394        self.last_text_idx = None;
1395
1396        self
1397    }
1398
1399    /// Render a clickable button. Returns `true` when activated via Enter, Space, or mouse click.
1400    ///
1401    /// The button is styled with the theme's primary color when focused and the
1402    /// accent color when hovered.
1403    pub fn button(&mut self, label: impl Into<String>) -> bool {
1404        let focused = self.register_focusable();
1405        let interaction_id = self.interaction_count;
1406        self.interaction_count += 1;
1407        let response = self.response_for(interaction_id);
1408
1409        let mut activated = response.clicked;
1410        if focused {
1411            let mut consumed_indices = Vec::new();
1412            for (i, event) in self.events.iter().enumerate() {
1413                if let Event::Key(key) = event {
1414                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1415                        activated = true;
1416                        consumed_indices.push(i);
1417                    }
1418                }
1419            }
1420
1421            for index in consumed_indices {
1422                self.consumed[index] = true;
1423            }
1424        }
1425
1426        let style = if focused {
1427            Style::new().fg(self.theme.primary).bold()
1428        } else if response.hovered {
1429            Style::new().fg(self.theme.accent)
1430        } else {
1431            Style::new().fg(self.theme.text)
1432        };
1433
1434        self.commands.push(Command::BeginContainer {
1435            direction: Direction::Row,
1436            gap: 0,
1437            align: Align::Start,
1438            border: None,
1439            border_style: Style::new().fg(self.theme.border),
1440            padding: Padding::default(),
1441            margin: Margin::default(),
1442            constraints: Constraints::default(),
1443            title: None,
1444            grow: 0,
1445        });
1446        self.styled(format!("[ {} ]", label.into()), style);
1447        self.commands.push(Command::EndContainer);
1448        self.last_text_idx = None;
1449
1450        activated
1451    }
1452
1453    /// Render a checkbox. Toggles the bool on Enter, Space, or click.
1454    ///
1455    /// The checked state is shown with the theme's success color. When focused,
1456    /// a `▸` prefix is added.
1457    pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
1458        let focused = self.register_focusable();
1459        let interaction_id = self.interaction_count;
1460        self.interaction_count += 1;
1461        let response = self.response_for(interaction_id);
1462        let mut should_toggle = response.clicked;
1463
1464        if focused {
1465            let mut consumed_indices = Vec::new();
1466            for (i, event) in self.events.iter().enumerate() {
1467                if let Event::Key(key) = event {
1468                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1469                        should_toggle = true;
1470                        consumed_indices.push(i);
1471                    }
1472                }
1473            }
1474
1475            for index in consumed_indices {
1476                self.consumed[index] = true;
1477            }
1478        }
1479
1480        if should_toggle {
1481            *checked = !*checked;
1482        }
1483
1484        self.commands.push(Command::BeginContainer {
1485            direction: Direction::Row,
1486            gap: 1,
1487            align: Align::Start,
1488            border: None,
1489            border_style: Style::new().fg(self.theme.border),
1490            padding: Padding::default(),
1491            margin: Margin::default(),
1492            constraints: Constraints::default(),
1493            title: None,
1494            grow: 0,
1495        });
1496        let marker_style = if *checked {
1497            Style::new().fg(self.theme.success)
1498        } else {
1499            Style::new().fg(self.theme.text_dim)
1500        };
1501        let marker = if *checked { "[x]" } else { "[ ]" };
1502        let label_text = label.into();
1503        if focused {
1504            self.styled(format!("▸ {marker}"), marker_style.bold());
1505            self.styled(label_text, Style::new().fg(self.theme.text).bold());
1506        } else {
1507            self.styled(marker, marker_style);
1508            self.styled(label_text, Style::new().fg(self.theme.text));
1509        }
1510        self.commands.push(Command::EndContainer);
1511        self.last_text_idx = None;
1512
1513        self
1514    }
1515
1516    /// Render an on/off toggle switch.
1517    ///
1518    /// Toggles `on` when activated via Enter, Space, or click. The switch
1519    /// renders as `●━━ ON` or `━━● OFF` colored with the theme's success or
1520    /// dim color respectively.
1521    pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
1522        let focused = self.register_focusable();
1523        let interaction_id = self.interaction_count;
1524        self.interaction_count += 1;
1525        let response = self.response_for(interaction_id);
1526        let mut should_toggle = response.clicked;
1527
1528        if focused {
1529            let mut consumed_indices = Vec::new();
1530            for (i, event) in self.events.iter().enumerate() {
1531                if let Event::Key(key) = event {
1532                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1533                        should_toggle = true;
1534                        consumed_indices.push(i);
1535                    }
1536                }
1537            }
1538
1539            for index in consumed_indices {
1540                self.consumed[index] = true;
1541            }
1542        }
1543
1544        if should_toggle {
1545            *on = !*on;
1546        }
1547
1548        self.commands.push(Command::BeginContainer {
1549            direction: Direction::Row,
1550            gap: 2,
1551            align: Align::Start,
1552            border: None,
1553            border_style: Style::new().fg(self.theme.border),
1554            padding: Padding::default(),
1555            margin: Margin::default(),
1556            constraints: Constraints::default(),
1557            title: None,
1558            grow: 0,
1559        });
1560        let label_text = label.into();
1561        let switch = if *on { "●━━ ON" } else { "━━● OFF" };
1562        let switch_style = if *on {
1563            Style::new().fg(self.theme.success)
1564        } else {
1565            Style::new().fg(self.theme.text_dim)
1566        };
1567        if focused {
1568            self.styled(
1569                format!("▸ {label_text}"),
1570                Style::new().fg(self.theme.text).bold(),
1571            );
1572            self.styled(switch, switch_style.bold());
1573        } else {
1574            self.styled(label_text, Style::new().fg(self.theme.text));
1575            self.styled(switch, switch_style);
1576        }
1577        self.commands.push(Command::EndContainer);
1578        self.last_text_idx = None;
1579
1580        self
1581    }
1582
1583    /// Render a horizontal divider line.
1584    ///
1585    /// The line is drawn with the theme's border color and expands to fill the
1586    /// container width.
1587    pub fn separator(&mut self) -> &mut Self {
1588        self.commands.push(Command::Text {
1589            content: "─".repeat(200),
1590            style: Style::new().fg(self.theme.border).dim(),
1591            grow: 0,
1592            align: Align::Start,
1593            wrap: false,
1594            margin: Margin::default(),
1595            constraints: Constraints::default(),
1596        });
1597        self.last_text_idx = Some(self.commands.len() - 1);
1598        self
1599    }
1600
1601    /// Render a help bar showing keybinding hints.
1602    ///
1603    /// `bindings` is a slice of `(key, action)` pairs. Keys are rendered in the
1604    /// theme's primary color; actions in the dim text color. Pairs are separated
1605    /// by a `·` character.
1606    pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
1607        if bindings.is_empty() {
1608            return self;
1609        }
1610
1611        self.interaction_count += 1;
1612        self.commands.push(Command::BeginContainer {
1613            direction: Direction::Row,
1614            gap: 2,
1615            align: Align::Start,
1616            border: None,
1617            border_style: Style::new().fg(self.theme.border),
1618            padding: Padding::default(),
1619            margin: Margin::default(),
1620            constraints: Constraints::default(),
1621            title: None,
1622            grow: 0,
1623        });
1624        for (idx, (key, action)) in bindings.iter().enumerate() {
1625            if idx > 0 {
1626                self.styled("·", Style::new().fg(self.theme.text_dim));
1627            }
1628            self.styled(*key, Style::new().bold().fg(self.theme.primary));
1629            self.styled(*action, Style::new().fg(self.theme.text_dim));
1630        }
1631        self.commands.push(Command::EndContainer);
1632        self.last_text_idx = None;
1633
1634        self
1635    }
1636
1637    // ── events ───────────────────────────────────────────────────────
1638
1639    /// Check if a character key was pressed this frame.
1640    ///
1641    /// Returns `true` if the key event has not been consumed by another widget.
1642    pub fn key(&self, c: char) -> bool {
1643        self.events.iter().enumerate().any(|(i, e)| {
1644            !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
1645        })
1646    }
1647
1648    /// Check if a specific key code was pressed this frame.
1649    ///
1650    /// Returns `true` if the key event has not been consumed by another widget.
1651    pub fn key_code(&self, code: KeyCode) -> bool {
1652        self.events
1653            .iter()
1654            .enumerate()
1655            .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
1656    }
1657
1658    /// Check if a character key with specific modifiers was pressed this frame.
1659    ///
1660    /// Returns `true` if the key event has not been consumed by another widget.
1661    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
1662        self.events.iter().enumerate().any(|(i, e)| {
1663            !self.consumed[i]
1664                && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
1665        })
1666    }
1667
1668    /// Return the position of a left mouse button down event this frame, if any.
1669    ///
1670    /// Returns `None` if no unconsumed mouse-down event occurred.
1671    pub fn mouse_down(&self) -> Option<(u32, u32)> {
1672        self.events.iter().enumerate().find_map(|(i, event)| {
1673            if self.consumed[i] {
1674                return None;
1675            }
1676            if let Event::Mouse(mouse) = event {
1677                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1678                    return Some((mouse.x, mouse.y));
1679                }
1680            }
1681            None
1682        })
1683    }
1684
1685    /// Return the current mouse cursor position, if known.
1686    ///
1687    /// The position is updated on every mouse move or click event. Returns
1688    /// `None` until the first mouse event is received.
1689    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
1690        self.mouse_pos
1691    }
1692
1693    /// Check if an unconsumed scroll-up event occurred this frame.
1694    pub fn scroll_up(&self) -> bool {
1695        self.events.iter().enumerate().any(|(i, event)| {
1696            !self.consumed[i]
1697                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
1698        })
1699    }
1700
1701    /// Check if an unconsumed scroll-down event occurred this frame.
1702    pub fn scroll_down(&self) -> bool {
1703        self.events.iter().enumerate().any(|(i, event)| {
1704            !self.consumed[i]
1705                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
1706        })
1707    }
1708
1709    /// Signal the run loop to exit after this frame.
1710    pub fn quit(&mut self) {
1711        self.should_quit = true;
1712    }
1713
1714    /// Get the current theme.
1715    pub fn theme(&self) -> &Theme {
1716        &self.theme
1717    }
1718
1719    /// Change the theme for subsequent rendering.
1720    ///
1721    /// All widgets rendered after this call will use the new theme's colors.
1722    pub fn set_theme(&mut self, theme: Theme) {
1723        self.theme = theme;
1724    }
1725
1726    // ── info ─────────────────────────────────────────────────────────
1727
1728    /// Get the terminal width in cells.
1729    pub fn width(&self) -> u32 {
1730        self.area_width
1731    }
1732
1733    /// Get the terminal height in cells.
1734    pub fn height(&self) -> u32 {
1735        self.area_height
1736    }
1737
1738    /// Get the current tick count (increments each frame).
1739    ///
1740    /// Useful for animations and time-based logic. The tick starts at 0 and
1741    /// increases by 1 on every rendered frame.
1742    pub fn tick(&self) -> u64 {
1743        self.tick
1744    }
1745
1746    /// Return whether the layout debugger is enabled.
1747    ///
1748    /// The debugger is toggled with F12 at runtime.
1749    pub fn debug_enabled(&self) -> bool {
1750        self.debug
1751    }
1752}
1753
1754fn byte_index_for_char(value: &str, char_index: usize) -> usize {
1755    if char_index == 0 {
1756        return 0;
1757    }
1758    value
1759        .char_indices()
1760        .nth(char_index)
1761        .map_or(value.len(), |(idx, _)| idx)
1762}
1763
1764fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
1765    let mut parts: Vec<String> = Vec::new();
1766    for (i, width) in widths.iter().enumerate() {
1767        let cell = cells.get(i).map(String::as_str).unwrap_or("");
1768        let cell_width = UnicodeWidthStr::width(cell) as u32;
1769        let padding = (*width).saturating_sub(cell_width) as usize;
1770        parts.push(format!("{cell}{}", " ".repeat(padding)));
1771    }
1772    parts.join(separator)
1773}