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