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/// ```
156#[must_use = "configure and finalize with .col() or .row()"]
157pub struct ContainerBuilder<'a> {
158    ctx: &'a mut Context,
159    gap: u32,
160    align: Align,
161    border: Option<Border>,
162    border_style: Style,
163    padding: Padding,
164    margin: Margin,
165    constraints: Constraints,
166    title: Option<(String, Style)>,
167    grow: u16,
168    scroll_offset: Option<u32>,
169}
170
171/// Drawing context for the [`Context::canvas`] widget.
172///
173/// Provides pixel-level drawing on a braille character grid. Each terminal
174/// cell maps to a 2x4 dot matrix, so a canvas of `width` columns x `height`
175/// rows gives `width*2` x `height*4` pixel resolution.
176pub struct CanvasContext {
177    grid: Vec<Vec<u32>>,
178    px_w: usize,
179    px_h: usize,
180}
181
182impl CanvasContext {
183    fn new(cols: usize, rows: usize) -> Self {
184        Self {
185            grid: vec![vec![0u32; cols]; rows],
186            px_w: cols * 2,
187            px_h: rows * 4,
188        }
189    }
190
191    /// Get the pixel width of the canvas.
192    pub fn width(&self) -> usize {
193        self.px_w
194    }
195
196    /// Get the pixel height of the canvas.
197    pub fn height(&self) -> usize {
198        self.px_h
199    }
200
201    /// Set a single pixel at `(x, y)`.
202    pub fn dot(&mut self, x: usize, y: usize) {
203        if x >= self.px_w || y >= self.px_h {
204            return;
205        }
206
207        let char_col = x / 2;
208        let char_row = y / 4;
209        let sub_col = x % 2;
210        let sub_row = y % 4;
211        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
212        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
213
214        self.grid[char_row][char_col] |= if sub_col == 0 {
215            LEFT_BITS[sub_row]
216        } else {
217            RIGHT_BITS[sub_row]
218        };
219    }
220
221    /// Draw a line from `(x0, y0)` to `(x1, y1)` using Bresenham's algorithm.
222    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
223        let (mut x, mut y) = (x0 as isize, y0 as isize);
224        let (x1, y1) = (x1 as isize, y1 as isize);
225        let dx = (x1 - x).abs();
226        let dy = -(y1 - y).abs();
227        let sx = if x < x1 { 1 } else { -1 };
228        let sy = if y < y1 { 1 } else { -1 };
229        let mut err = dx + dy;
230
231        loop {
232            if x >= 0 && y >= 0 {
233                self.dot(x as usize, y as usize);
234            }
235            if x == x1 && y == y1 {
236                break;
237            }
238            let e2 = 2 * err;
239            if e2 >= dy {
240                err += dy;
241                x += sx;
242            }
243            if e2 <= dx {
244                err += dx;
245                y += sy;
246            }
247        }
248    }
249
250    /// Draw a rectangle outline from `(x, y)` with `w` width and `h` height.
251    pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
252        if w == 0 || h == 0 {
253            return;
254        }
255
256        self.line(x, y, x + w.saturating_sub(1), y);
257        self.line(
258            x + w.saturating_sub(1),
259            y,
260            x + w.saturating_sub(1),
261            y + h.saturating_sub(1),
262        );
263        self.line(
264            x + w.saturating_sub(1),
265            y + h.saturating_sub(1),
266            x,
267            y + h.saturating_sub(1),
268        );
269        self.line(x, y + h.saturating_sub(1), x, y);
270    }
271
272    /// Draw a circle outline centered at `(cx, cy)` with radius `r`.
273    pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
274        let mut x = r as isize;
275        let mut y: isize = 0;
276        let mut err: isize = 1 - x;
277        let (cx, cy) = (cx as isize, cy as isize);
278
279        while x >= y {
280            for &(dx, dy) in &[
281                (x, y),
282                (y, x),
283                (-x, y),
284                (-y, x),
285                (x, -y),
286                (y, -x),
287                (-x, -y),
288                (-y, -x),
289            ] {
290                let px = cx + dx;
291                let py = cy + dy;
292                if px >= 0 && py >= 0 {
293                    self.dot(px as usize, py as usize);
294                }
295            }
296
297            y += 1;
298            if err < 0 {
299                err += 2 * y + 1;
300            } else {
301                x -= 1;
302                err += 2 * (y - x) + 1;
303            }
304        }
305    }
306
307    fn render(&self) -> Vec<String> {
308        self.grid
309            .iter()
310            .map(|row| {
311                row.iter()
312                    .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
313                    .collect()
314            })
315            .collect()
316    }
317}
318
319impl<'a> ContainerBuilder<'a> {
320    // ── border ───────────────────────────────────────────────────────
321
322    /// Set the border style.
323    pub fn border(mut self, border: Border) -> Self {
324        self.border = Some(border);
325        self
326    }
327
328    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
329    pub fn rounded(self) -> Self {
330        self.border(Border::Rounded)
331    }
332
333    /// Set the style applied to the border characters.
334    pub fn border_style(mut self, style: Style) -> Self {
335        self.border_style = style;
336        self
337    }
338
339    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
340
341    /// Set uniform padding on all sides. Alias for [`pad`](Self::pad).
342    pub fn p(self, value: u32) -> Self {
343        self.pad(value)
344    }
345
346    /// Set uniform padding on all sides.
347    pub fn pad(mut self, value: u32) -> Self {
348        self.padding = Padding::all(value);
349        self
350    }
351
352    /// Set horizontal padding (left and right).
353    pub fn px(mut self, value: u32) -> Self {
354        self.padding.left = value;
355        self.padding.right = value;
356        self
357    }
358
359    /// Set vertical padding (top and bottom).
360    pub fn py(mut self, value: u32) -> Self {
361        self.padding.top = value;
362        self.padding.bottom = value;
363        self
364    }
365
366    /// Set top padding.
367    pub fn pt(mut self, value: u32) -> Self {
368        self.padding.top = value;
369        self
370    }
371
372    /// Set right padding.
373    pub fn pr(mut self, value: u32) -> Self {
374        self.padding.right = value;
375        self
376    }
377
378    /// Set bottom padding.
379    pub fn pb(mut self, value: u32) -> Self {
380        self.padding.bottom = value;
381        self
382    }
383
384    /// Set left padding.
385    pub fn pl(mut self, value: u32) -> Self {
386        self.padding.left = value;
387        self
388    }
389
390    /// Set per-side padding using a [`Padding`] value.
391    pub fn padding(mut self, padding: Padding) -> Self {
392        self.padding = padding;
393        self
394    }
395
396    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
397
398    /// Set uniform margin on all sides.
399    pub fn m(mut self, value: u32) -> Self {
400        self.margin = Margin::all(value);
401        self
402    }
403
404    /// Set horizontal margin (left and right).
405    pub fn mx(mut self, value: u32) -> Self {
406        self.margin.left = value;
407        self.margin.right = value;
408        self
409    }
410
411    /// Set vertical margin (top and bottom).
412    pub fn my(mut self, value: u32) -> Self {
413        self.margin.top = value;
414        self.margin.bottom = value;
415        self
416    }
417
418    /// Set top margin.
419    pub fn mt(mut self, value: u32) -> Self {
420        self.margin.top = value;
421        self
422    }
423
424    /// Set right margin.
425    pub fn mr(mut self, value: u32) -> Self {
426        self.margin.right = value;
427        self
428    }
429
430    /// Set bottom margin.
431    pub fn mb(mut self, value: u32) -> Self {
432        self.margin.bottom = value;
433        self
434    }
435
436    /// Set left margin.
437    pub fn ml(mut self, value: u32) -> Self {
438        self.margin.left = value;
439        self
440    }
441
442    /// Set per-side margin using a [`Margin`] value.
443    pub fn margin(mut self, margin: Margin) -> Self {
444        self.margin = margin;
445        self
446    }
447
448    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
449
450    /// Set a fixed width (sets both min and max width).
451    pub fn w(mut self, value: u32) -> Self {
452        self.constraints.min_width = Some(value);
453        self.constraints.max_width = Some(value);
454        self
455    }
456
457    /// Set a fixed height (sets both min and max height).
458    pub fn h(mut self, value: u32) -> Self {
459        self.constraints.min_height = Some(value);
460        self.constraints.max_height = Some(value);
461        self
462    }
463
464    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
465    pub fn min_w(mut self, value: u32) -> Self {
466        self.constraints.min_width = Some(value);
467        self
468    }
469
470    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
471    pub fn max_w(mut self, value: u32) -> Self {
472        self.constraints.max_width = Some(value);
473        self
474    }
475
476    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
477    pub fn min_h(mut self, value: u32) -> Self {
478        self.constraints.min_height = Some(value);
479        self
480    }
481
482    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
483    pub fn max_h(mut self, value: u32) -> Self {
484        self.constraints.max_height = Some(value);
485        self
486    }
487
488    /// Set the minimum width constraint in cells.
489    pub fn min_width(mut self, value: u32) -> Self {
490        self.constraints.min_width = Some(value);
491        self
492    }
493
494    /// Set the maximum width constraint in cells.
495    pub fn max_width(mut self, value: u32) -> Self {
496        self.constraints.max_width = Some(value);
497        self
498    }
499
500    /// Set the minimum height constraint in rows.
501    pub fn min_height(mut self, value: u32) -> Self {
502        self.constraints.min_height = Some(value);
503        self
504    }
505
506    /// Set the maximum height constraint in rows.
507    pub fn max_height(mut self, value: u32) -> Self {
508        self.constraints.max_height = Some(value);
509        self
510    }
511
512    /// Set all size constraints at once using a [`Constraints`] value.
513    pub fn constraints(mut self, constraints: Constraints) -> Self {
514        self.constraints = constraints;
515        self
516    }
517
518    // ── flex ─────────────────────────────────────────────────────────
519
520    /// Set the gap (in cells) between child elements.
521    pub fn gap(mut self, gap: u32) -> Self {
522        self.gap = gap;
523        self
524    }
525
526    /// Set the flex-grow factor. `1` means the container expands to fill available space.
527    pub fn grow(mut self, grow: u16) -> Self {
528        self.grow = grow;
529        self
530    }
531
532    // ── alignment ───────────────────────────────────────────────────
533
534    /// Set the cross-axis alignment of child elements.
535    pub fn align(mut self, align: Align) -> Self {
536        self.align = align;
537        self
538    }
539
540    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
541    pub fn center(self) -> Self {
542        self.align(Align::Center)
543    }
544
545    // ── title ────────────────────────────────────────────────────────
546
547    /// Set a plain-text title rendered in the top border.
548    pub fn title(self, title: impl Into<String>) -> Self {
549        self.title_styled(title, Style::new())
550    }
551
552    /// Set a styled title rendered in the top border.
553    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
554        self.title = Some((title.into(), style));
555        self
556    }
557
558    // ── internal ─────────────────────────────────────────────────────
559
560    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
561    pub fn scroll_offset(mut self, offset: u32) -> Self {
562        self.scroll_offset = Some(offset);
563        self
564    }
565
566    /// Finalize the builder as a vertical (column) container.
567    ///
568    /// The closure receives a `&mut Context` for rendering children.
569    /// Returns a [`Response`] with click/hover state for this container.
570    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
571        self.finish(Direction::Column, f)
572    }
573
574    /// Finalize the builder as a horizontal (row) container.
575    ///
576    /// The closure receives a `&mut Context` for rendering children.
577    /// Returns a [`Response`] with click/hover state for this container.
578    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
579        self.finish(Direction::Row, f)
580    }
581
582    fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
583        let interaction_id = self.ctx.interaction_count;
584        self.ctx.interaction_count += 1;
585
586        if let Some(scroll_offset) = self.scroll_offset {
587            self.ctx.commands.push(Command::BeginScrollable {
588                grow: self.grow,
589                border: self.border,
590                border_style: self.border_style,
591                padding: self.padding,
592                margin: self.margin,
593                constraints: self.constraints,
594                title: self.title,
595                scroll_offset,
596            });
597        } else {
598            self.ctx.commands.push(Command::BeginContainer {
599                direction,
600                gap: self.gap,
601                align: self.align,
602                border: self.border,
603                border_style: self.border_style,
604                padding: self.padding,
605                margin: self.margin,
606                constraints: self.constraints,
607                title: self.title,
608                grow: self.grow,
609            });
610        }
611        f(self.ctx);
612        self.ctx.commands.push(Command::EndContainer);
613        self.ctx.last_text_idx = None;
614
615        self.ctx.response_for(interaction_id)
616    }
617}
618
619impl Context {
620    #[allow(clippy::too_many_arguments)]
621    pub(crate) fn new(
622        events: Vec<Event>,
623        width: u32,
624        height: u32,
625        tick: u64,
626        focus_index: usize,
627        prev_focus_count: usize,
628        prev_scroll_infos: Vec<(u32, u32)>,
629        prev_hit_map: Vec<Rect>,
630        debug: bool,
631        theme: Theme,
632        last_mouse_pos: Option<(u32, u32)>,
633    ) -> Self {
634        let consumed = vec![false; events.len()];
635
636        let mut mouse_pos = last_mouse_pos;
637        let mut click_pos = None;
638        for event in &events {
639            if let Event::Mouse(mouse) = event {
640                mouse_pos = Some((mouse.x, mouse.y));
641                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
642                    click_pos = Some((mouse.x, mouse.y));
643                }
644            }
645        }
646
647        Self {
648            commands: Vec::new(),
649            events,
650            consumed,
651            should_quit: false,
652            area_width: width,
653            area_height: height,
654            tick,
655            focus_index,
656            focus_count: 0,
657            prev_focus_count,
658            scroll_count: 0,
659            prev_scroll_infos,
660            interaction_count: 0,
661            prev_hit_map,
662            mouse_pos,
663            click_pos,
664            last_mouse_pos,
665            last_text_idx: None,
666            debug,
667            theme,
668        }
669    }
670
671    pub(crate) fn process_focus_keys(&mut self) {
672        for (i, event) in self.events.iter().enumerate() {
673            if let Event::Key(key) = event {
674                if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
675                    if self.prev_focus_count > 0 {
676                        self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
677                    }
678                    self.consumed[i] = true;
679                } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
680                    || key.code == KeyCode::BackTab
681                {
682                    if self.prev_focus_count > 0 {
683                        self.focus_index = if self.focus_index == 0 {
684                            self.prev_focus_count - 1
685                        } else {
686                            self.focus_index - 1
687                        };
688                    }
689                    self.consumed[i] = true;
690                }
691            }
692        }
693    }
694
695    /// Render a custom [`Widget`].
696    ///
697    /// Calls [`Widget::ui`] with this context and returns the widget's response.
698    pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
699        w.ui(self)
700    }
701
702    /// Wrap child widgets in a panic boundary.
703    ///
704    /// If the closure panics, the panic is caught and an error message is
705    /// rendered in place of the children. The app continues running.
706    ///
707    /// # Example
708    ///
709    /// ```no_run
710    /// # slt::run(|ui: &mut slt::Context| {
711    /// ui.error_boundary(|ui| {
712    ///     ui.text("risky widget");
713    /// });
714    /// # });
715    /// ```
716    pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
717        self.error_boundary_with(f, |ui, msg| {
718            ui.styled(
719                format!("⚠ Error: {msg}"),
720                Style::new().fg(ui.theme.error).bold(),
721            );
722        });
723    }
724
725    /// Like [`error_boundary`](Self::error_boundary), but renders a custom
726    /// fallback instead of the default error message.
727    ///
728    /// The fallback closure receives the panic message as a [`String`].
729    ///
730    /// # Example
731    ///
732    /// ```no_run
733    /// # slt::run(|ui: &mut slt::Context| {
734    /// ui.error_boundary_with(
735    ///     |ui| {
736    ///         ui.text("risky widget");
737    ///     },
738    ///     |ui, msg| {
739    ///         ui.text(format!("Recovered from panic: {msg}"));
740    ///     },
741    /// );
742    /// # });
743    /// ```
744    pub fn error_boundary_with(
745        &mut self,
746        f: impl FnOnce(&mut Context),
747        fallback: impl FnOnce(&mut Context, String),
748    ) {
749        let cmd_count = self.commands.len();
750        let last_text_idx = self.last_text_idx;
751
752        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
753            f(self);
754        }));
755
756        match result {
757            Ok(()) => {}
758            Err(panic_info) => {
759                self.commands.truncate(cmd_count);
760                self.last_text_idx = last_text_idx;
761
762                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
763                    (*s).to_string()
764                } else if let Some(s) = panic_info.downcast_ref::<String>() {
765                    s.clone()
766                } else {
767                    "widget panicked".to_string()
768                };
769
770                fallback(self, msg);
771            }
772        }
773    }
774
775    /// Allocate a click/hover interaction slot and return the [`Response`].
776    ///
777    /// Use this in custom widgets to detect mouse clicks and hovers without
778    /// wrapping content in a container. Each call reserves one slot in the
779    /// hit-test map, so the call order must be stable across frames.
780    pub fn interaction(&mut self) -> Response {
781        let id = self.interaction_count;
782        self.interaction_count += 1;
783        self.response_for(id)
784    }
785
786    /// Register a widget as focusable and return whether it currently has focus.
787    ///
788    /// Call this in custom widgets that need keyboard focus. Each call increments
789    /// the internal focus counter, so the call order must be stable across frames.
790    pub fn register_focusable(&mut self) -> bool {
791        let id = self.focus_count;
792        self.focus_count += 1;
793        if self.prev_focus_count == 0 {
794            return true;
795        }
796        self.focus_index % self.prev_focus_count == id
797    }
798
799    // ── text ──────────────────────────────────────────────────────────
800
801    /// Render a text element. Returns `&mut Self` for style chaining.
802    ///
803    /// # Example
804    ///
805    /// ```no_run
806    /// # slt::run(|ui: &mut slt::Context| {
807    /// use slt::Color;
808    /// ui.text("hello").bold().fg(Color::Cyan);
809    /// # });
810    /// ```
811    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
812        let content = s.into();
813        self.commands.push(Command::Text {
814            content,
815            style: Style::new(),
816            grow: 0,
817            align: Align::Start,
818            wrap: false,
819            margin: Margin::default(),
820            constraints: Constraints::default(),
821        });
822        self.last_text_idx = Some(self.commands.len() - 1);
823        self
824    }
825
826    /// Render a text element with word-boundary wrapping.
827    ///
828    /// Long lines are broken at word boundaries to fit the container width.
829    /// Style chaining works the same as [`Context::text`].
830    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
831        let content = s.into();
832        self.commands.push(Command::Text {
833            content,
834            style: Style::new(),
835            grow: 0,
836            align: Align::Start,
837            wrap: true,
838            margin: Margin::default(),
839            constraints: Constraints::default(),
840        });
841        self.last_text_idx = Some(self.commands.len() - 1);
842        self
843    }
844
845    // ── style chain (applies to last text) ───────────────────────────
846
847    /// Apply bold to the last rendered text element.
848    pub fn bold(&mut self) -> &mut Self {
849        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
850        self
851    }
852
853    /// Apply dim styling to the last rendered text element.
854    ///
855    /// Also sets the foreground color to the theme's `text_dim` color if no
856    /// explicit foreground has been set.
857    pub fn dim(&mut self) -> &mut Self {
858        let text_dim = self.theme.text_dim;
859        self.modify_last_style(|s| {
860            s.modifiers |= Modifiers::DIM;
861            if s.fg.is_none() {
862                s.fg = Some(text_dim);
863            }
864        });
865        self
866    }
867
868    /// Apply italic to the last rendered text element.
869    pub fn italic(&mut self) -> &mut Self {
870        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
871        self
872    }
873
874    /// Apply underline to the last rendered text element.
875    pub fn underline(&mut self) -> &mut Self {
876        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
877        self
878    }
879
880    /// Apply reverse-video to the last rendered text element.
881    pub fn reversed(&mut self) -> &mut Self {
882        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
883        self
884    }
885
886    /// Apply strikethrough to the last rendered text element.
887    pub fn strikethrough(&mut self) -> &mut Self {
888        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
889        self
890    }
891
892    /// Set the foreground color of the last rendered text element.
893    pub fn fg(&mut self, color: Color) -> &mut Self {
894        self.modify_last_style(|s| s.fg = Some(color));
895        self
896    }
897
898    /// Set the background color of the last rendered text element.
899    pub fn bg(&mut self, color: Color) -> &mut Self {
900        self.modify_last_style(|s| s.bg = Some(color));
901        self
902    }
903
904    /// Render a text element with an explicit [`Style`] applied immediately.
905    ///
906    /// Equivalent to calling `text(s)` followed by style-chain methods, but
907    /// more concise when you already have a `Style` value.
908    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
909        self.commands.push(Command::Text {
910            content: s.into(),
911            style,
912            grow: 0,
913            align: Align::Start,
914            wrap: false,
915            margin: Margin::default(),
916            constraints: Constraints::default(),
917        });
918        self.last_text_idx = Some(self.commands.len() - 1);
919        self
920    }
921
922    /// Enable word-boundary wrapping on the last rendered text element.
923    pub fn wrap(&mut self) -> &mut Self {
924        if let Some(idx) = self.last_text_idx {
925            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
926                *wrap = true;
927            }
928        }
929        self
930    }
931
932    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
933        if let Some(idx) = self.last_text_idx {
934            if let Command::Text { style, .. } = &mut self.commands[idx] {
935                f(style);
936            }
937        }
938    }
939
940    // ── containers ───────────────────────────────────────────────────
941
942    /// Create a vertical (column) container.
943    ///
944    /// Children are stacked top-to-bottom. Returns a [`Response`] with
945    /// click/hover state for the container area.
946    ///
947    /// # Example
948    ///
949    /// ```no_run
950    /// # slt::run(|ui: &mut slt::Context| {
951    /// ui.col(|ui| {
952    ///     ui.text("line one");
953    ///     ui.text("line two");
954    /// });
955    /// # });
956    /// ```
957    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
958        self.push_container(Direction::Column, 0, f)
959    }
960
961    /// Create a vertical (column) container with a gap between children.
962    ///
963    /// `gap` is the number of blank rows inserted between each child.
964    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
965        self.push_container(Direction::Column, gap, f)
966    }
967
968    /// Create a horizontal (row) container.
969    ///
970    /// Children are placed left-to-right. Returns a [`Response`] with
971    /// click/hover state for the container area.
972    ///
973    /// # Example
974    ///
975    /// ```no_run
976    /// # slt::run(|ui: &mut slt::Context| {
977    /// ui.row(|ui| {
978    ///     ui.text("left");
979    ///     ui.spacer();
980    ///     ui.text("right");
981    /// });
982    /// # });
983    /// ```
984    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
985        self.push_container(Direction::Row, 0, f)
986    }
987
988    /// Create a horizontal (row) container with a gap between children.
989    ///
990    /// `gap` is the number of blank columns inserted between each child.
991    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
992        self.push_container(Direction::Row, gap, f)
993    }
994
995    /// Create a container with a fluent builder.
996    ///
997    /// Use this for borders, padding, grow, constraints, and titles. Chain
998    /// configuration methods on the returned [`ContainerBuilder`], then call
999    /// `.col()` or `.row()` to finalize.
1000    ///
1001    /// # Example
1002    ///
1003    /// ```no_run
1004    /// # slt::run(|ui: &mut slt::Context| {
1005    /// use slt::Border;
1006    /// ui.container()
1007    ///     .border(Border::Rounded)
1008    ///     .pad(1)
1009    ///     .title("My Panel")
1010    ///     .col(|ui| {
1011    ///         ui.text("content");
1012    ///     });
1013    /// # });
1014    /// ```
1015    pub fn container(&mut self) -> ContainerBuilder<'_> {
1016        let border = self.theme.border;
1017        ContainerBuilder {
1018            ctx: self,
1019            gap: 0,
1020            align: Align::Start,
1021            border: None,
1022            border_style: Style::new().fg(border),
1023            padding: Padding::default(),
1024            margin: Margin::default(),
1025            constraints: Constraints::default(),
1026            title: None,
1027            grow: 0,
1028            scroll_offset: None,
1029        }
1030    }
1031
1032    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
1033    ///
1034    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
1035    /// is updated in-place with the current scroll offset and bounds.
1036    ///
1037    /// # Example
1038    ///
1039    /// ```no_run
1040    /// # use slt::widgets::ScrollState;
1041    /// # slt::run(|ui: &mut slt::Context| {
1042    /// let mut scroll = ScrollState::new();
1043    /// ui.scrollable(&mut scroll).col(|ui| {
1044    ///     for i in 0..100 {
1045    ///         ui.text(format!("Line {i}"));
1046    ///     }
1047    /// });
1048    /// # });
1049    /// ```
1050    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1051        let index = self.scroll_count;
1052        self.scroll_count += 1;
1053        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1054            state.set_bounds(ch, vh);
1055            let max = ch.saturating_sub(vh) as usize;
1056            state.offset = state.offset.min(max);
1057        }
1058
1059        let next_id = self.interaction_count;
1060        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1061            self.auto_scroll(&rect, state);
1062        }
1063
1064        self.container().scroll_offset(state.offset as u32)
1065    }
1066
1067    fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1068        let last_y = self.last_mouse_pos.map(|(_, y)| y);
1069        let mut to_consume: Vec<usize> = Vec::new();
1070
1071        for (i, event) in self.events.iter().enumerate() {
1072            if self.consumed[i] {
1073                continue;
1074            }
1075            if let Event::Mouse(mouse) = event {
1076                let in_bounds = mouse.x >= rect.x
1077                    && mouse.x < rect.right()
1078                    && mouse.y >= rect.y
1079                    && mouse.y < rect.bottom();
1080                if !in_bounds {
1081                    continue;
1082                }
1083                match mouse.kind {
1084                    MouseKind::ScrollUp => {
1085                        state.scroll_up(1);
1086                        to_consume.push(i);
1087                    }
1088                    MouseKind::ScrollDown => {
1089                        state.scroll_down(1);
1090                        to_consume.push(i);
1091                    }
1092                    MouseKind::Drag(MouseButton::Left) => {
1093                        if let Some(prev_y) = last_y {
1094                            let delta = mouse.y as i32 - prev_y as i32;
1095                            if delta < 0 {
1096                                state.scroll_down((-delta) as usize);
1097                            } else if delta > 0 {
1098                                state.scroll_up(delta as usize);
1099                            }
1100                        }
1101                        to_consume.push(i);
1102                    }
1103                    _ => {}
1104                }
1105            }
1106        }
1107
1108        for i in to_consume {
1109            self.consumed[i] = true;
1110        }
1111    }
1112
1113    /// Shortcut for `container().border(border)`.
1114    ///
1115    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
1116    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1117        self.container().border(border)
1118    }
1119
1120    fn push_container(
1121        &mut self,
1122        direction: Direction,
1123        gap: u32,
1124        f: impl FnOnce(&mut Context),
1125    ) -> Response {
1126        let interaction_id = self.interaction_count;
1127        self.interaction_count += 1;
1128        let border = self.theme.border;
1129
1130        self.commands.push(Command::BeginContainer {
1131            direction,
1132            gap,
1133            align: Align::Start,
1134            border: None,
1135            border_style: Style::new().fg(border),
1136            padding: Padding::default(),
1137            margin: Margin::default(),
1138            constraints: Constraints::default(),
1139            title: None,
1140            grow: 0,
1141        });
1142        f(self);
1143        self.commands.push(Command::EndContainer);
1144        self.last_text_idx = None;
1145
1146        self.response_for(interaction_id)
1147    }
1148
1149    fn response_for(&self, interaction_id: usize) -> Response {
1150        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1151            let clicked = self
1152                .click_pos
1153                .map(|(mx, my)| {
1154                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1155                })
1156                .unwrap_or(false);
1157            let hovered = self
1158                .mouse_pos
1159                .map(|(mx, my)| {
1160                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1161                })
1162                .unwrap_or(false);
1163            Response { clicked, hovered }
1164        } else {
1165            Response::default()
1166        }
1167    }
1168
1169    /// Set the flex-grow factor of the last rendered text element.
1170    ///
1171    /// A value of `1` causes the element to expand and fill remaining space
1172    /// along the main axis.
1173    pub fn grow(&mut self, value: u16) -> &mut Self {
1174        if let Some(idx) = self.last_text_idx {
1175            if let Command::Text { grow, .. } = &mut self.commands[idx] {
1176                *grow = value;
1177            }
1178        }
1179        self
1180    }
1181
1182    /// Set the text alignment of the last rendered text element.
1183    pub fn align(&mut self, align: Align) -> &mut Self {
1184        if let Some(idx) = self.last_text_idx {
1185            if let Command::Text {
1186                align: text_align, ..
1187            } = &mut self.commands[idx]
1188            {
1189                *text_align = align;
1190            }
1191        }
1192        self
1193    }
1194
1195    /// Render an invisible spacer that expands to fill available space.
1196    ///
1197    /// Useful for pushing siblings to opposite ends of a row or column.
1198    pub fn spacer(&mut self) -> &mut Self {
1199        self.commands.push(Command::Spacer { grow: 1 });
1200        self.last_text_idx = None;
1201        self
1202    }
1203
1204    /// Render a single-line text input. Auto-handles cursor, typing, and backspace.
1205    ///
1206    /// The widget claims focus via [`Context::register_focusable`]. When focused,
1207    /// it consumes character, backspace, arrow, Home, and End key events.
1208    ///
1209    /// # Example
1210    ///
1211    /// ```no_run
1212    /// # use slt::widgets::TextInputState;
1213    /// # slt::run(|ui: &mut slt::Context| {
1214    /// let mut input = TextInputState::with_placeholder("Search...");
1215    /// ui.text_input(&mut input);
1216    /// // input.value holds the current text
1217    /// # });
1218    /// ```
1219    pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1220        let focused = self.register_focusable();
1221        state.cursor = state.cursor.min(state.value.chars().count());
1222
1223        if focused {
1224            let mut consumed_indices = Vec::new();
1225            for (i, event) in self.events.iter().enumerate() {
1226                if let Event::Key(key) = event {
1227                    match key.code {
1228                        KeyCode::Char(ch) => {
1229                            if let Some(max) = state.max_length {
1230                                if state.value.chars().count() >= max {
1231                                    continue;
1232                                }
1233                            }
1234                            let index = byte_index_for_char(&state.value, state.cursor);
1235                            state.value.insert(index, ch);
1236                            state.cursor += 1;
1237                            consumed_indices.push(i);
1238                        }
1239                        KeyCode::Backspace => {
1240                            if state.cursor > 0 {
1241                                let start = byte_index_for_char(&state.value, state.cursor - 1);
1242                                let end = byte_index_for_char(&state.value, state.cursor);
1243                                state.value.replace_range(start..end, "");
1244                                state.cursor -= 1;
1245                            }
1246                            consumed_indices.push(i);
1247                        }
1248                        KeyCode::Left => {
1249                            state.cursor = state.cursor.saturating_sub(1);
1250                            consumed_indices.push(i);
1251                        }
1252                        KeyCode::Right => {
1253                            state.cursor = (state.cursor + 1).min(state.value.chars().count());
1254                            consumed_indices.push(i);
1255                        }
1256                        KeyCode::Home => {
1257                            state.cursor = 0;
1258                            consumed_indices.push(i);
1259                        }
1260                        KeyCode::End => {
1261                            state.cursor = state.value.chars().count();
1262                            consumed_indices.push(i);
1263                        }
1264                        _ => {}
1265                    }
1266                }
1267            }
1268
1269            for index in consumed_indices {
1270                self.consumed[index] = true;
1271            }
1272        }
1273
1274        if state.value.is_empty() {
1275            self.styled(
1276                state.placeholder.clone(),
1277                Style::new().dim().fg(self.theme.text_dim),
1278            )
1279        } else {
1280            let mut rendered = String::new();
1281            for (idx, ch) in state.value.chars().enumerate() {
1282                if focused && idx == state.cursor {
1283                    rendered.push('▎');
1284                }
1285                rendered.push(ch);
1286            }
1287            if focused && state.cursor >= state.value.chars().count() {
1288                rendered.push('▎');
1289            }
1290            self.styled(rendered, Style::new().fg(self.theme.text))
1291        }
1292    }
1293
1294    /// Render an animated spinner.
1295    ///
1296    /// The spinner advances one frame per tick. Use [`SpinnerState::dots`] or
1297    /// [`SpinnerState::line`] to create the state, then chain style methods to
1298    /// color it.
1299    pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1300        self.styled(
1301            state.frame(self.tick).to_string(),
1302            Style::new().fg(self.theme.primary),
1303        )
1304    }
1305
1306    /// Render toast notifications. Calls `state.cleanup(tick)` automatically.
1307    ///
1308    /// Expired messages are removed before rendering. If there are no active
1309    /// messages, nothing is rendered and `self` is returned unchanged.
1310    pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1311        state.cleanup(self.tick);
1312        if state.messages.is_empty() {
1313            return self;
1314        }
1315
1316        self.interaction_count += 1;
1317        self.commands.push(Command::BeginContainer {
1318            direction: Direction::Column,
1319            gap: 0,
1320            align: Align::Start,
1321            border: None,
1322            border_style: Style::new().fg(self.theme.border),
1323            padding: Padding::default(),
1324            margin: Margin::default(),
1325            constraints: Constraints::default(),
1326            title: None,
1327            grow: 0,
1328        });
1329        for message in state.messages.iter().rev() {
1330            let color = match message.level {
1331                ToastLevel::Info => self.theme.primary,
1332                ToastLevel::Success => self.theme.success,
1333                ToastLevel::Warning => self.theme.warning,
1334                ToastLevel::Error => self.theme.error,
1335            };
1336            self.styled(format!("  ● {}", message.text), Style::new().fg(color));
1337        }
1338        self.commands.push(Command::EndContainer);
1339        self.last_text_idx = None;
1340
1341        self
1342    }
1343
1344    /// Render a multi-line text area with the given number of visible rows.
1345    ///
1346    /// When focused, handles character input, Enter (new line), Backspace,
1347    /// arrow keys, Home, and End. The cursor is rendered as a block character.
1348    pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1349        if state.lines.is_empty() {
1350            state.lines.push(String::new());
1351        }
1352        state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1353        state.cursor_col = state
1354            .cursor_col
1355            .min(state.lines[state.cursor_row].chars().count());
1356
1357        let focused = self.register_focusable();
1358
1359        if focused {
1360            let mut consumed_indices = Vec::new();
1361            for (i, event) in self.events.iter().enumerate() {
1362                if let Event::Key(key) = event {
1363                    match key.code {
1364                        KeyCode::Char(ch) => {
1365                            if let Some(max) = state.max_length {
1366                                let total: usize =
1367                                    state.lines.iter().map(|line| line.chars().count()).sum();
1368                                if total >= max {
1369                                    continue;
1370                                }
1371                            }
1372                            let index = byte_index_for_char(
1373                                &state.lines[state.cursor_row],
1374                                state.cursor_col,
1375                            );
1376                            state.lines[state.cursor_row].insert(index, ch);
1377                            state.cursor_col += 1;
1378                            consumed_indices.push(i);
1379                        }
1380                        KeyCode::Enter => {
1381                            let split_index = byte_index_for_char(
1382                                &state.lines[state.cursor_row],
1383                                state.cursor_col,
1384                            );
1385                            let remainder = state.lines[state.cursor_row].split_off(split_index);
1386                            state.cursor_row += 1;
1387                            state.lines.insert(state.cursor_row, remainder);
1388                            state.cursor_col = 0;
1389                            consumed_indices.push(i);
1390                        }
1391                        KeyCode::Backspace => {
1392                            if state.cursor_col > 0 {
1393                                let start = byte_index_for_char(
1394                                    &state.lines[state.cursor_row],
1395                                    state.cursor_col - 1,
1396                                );
1397                                let end = byte_index_for_char(
1398                                    &state.lines[state.cursor_row],
1399                                    state.cursor_col,
1400                                );
1401                                state.lines[state.cursor_row].replace_range(start..end, "");
1402                                state.cursor_col -= 1;
1403                            } else if state.cursor_row > 0 {
1404                                let current = state.lines.remove(state.cursor_row);
1405                                state.cursor_row -= 1;
1406                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1407                                state.lines[state.cursor_row].push_str(&current);
1408                            }
1409                            consumed_indices.push(i);
1410                        }
1411                        KeyCode::Left => {
1412                            if state.cursor_col > 0 {
1413                                state.cursor_col -= 1;
1414                            } else if state.cursor_row > 0 {
1415                                state.cursor_row -= 1;
1416                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1417                            }
1418                            consumed_indices.push(i);
1419                        }
1420                        KeyCode::Right => {
1421                            let line_len = state.lines[state.cursor_row].chars().count();
1422                            if state.cursor_col < line_len {
1423                                state.cursor_col += 1;
1424                            } else if state.cursor_row + 1 < state.lines.len() {
1425                                state.cursor_row += 1;
1426                                state.cursor_col = 0;
1427                            }
1428                            consumed_indices.push(i);
1429                        }
1430                        KeyCode::Up => {
1431                            if state.cursor_row > 0 {
1432                                state.cursor_row -= 1;
1433                                state.cursor_col = state
1434                                    .cursor_col
1435                                    .min(state.lines[state.cursor_row].chars().count());
1436                            }
1437                            consumed_indices.push(i);
1438                        }
1439                        KeyCode::Down => {
1440                            if state.cursor_row + 1 < state.lines.len() {
1441                                state.cursor_row += 1;
1442                                state.cursor_col = state
1443                                    .cursor_col
1444                                    .min(state.lines[state.cursor_row].chars().count());
1445                            }
1446                            consumed_indices.push(i);
1447                        }
1448                        KeyCode::Home => {
1449                            state.cursor_col = 0;
1450                            consumed_indices.push(i);
1451                        }
1452                        KeyCode::End => {
1453                            state.cursor_col = state.lines[state.cursor_row].chars().count();
1454                            consumed_indices.push(i);
1455                        }
1456                        _ => {}
1457                    }
1458                }
1459            }
1460
1461            for index in consumed_indices {
1462                self.consumed[index] = true;
1463            }
1464        }
1465
1466        self.interaction_count += 1;
1467        self.commands.push(Command::BeginContainer {
1468            direction: Direction::Column,
1469            gap: 0,
1470            align: Align::Start,
1471            border: None,
1472            border_style: Style::new().fg(self.theme.border),
1473            padding: Padding::default(),
1474            margin: Margin::default(),
1475            constraints: Constraints::default(),
1476            title: None,
1477            grow: 0,
1478        });
1479        for row in 0..visible_rows as usize {
1480            let line = state.lines.get(row).cloned().unwrap_or_default();
1481            let mut rendered = line.clone();
1482            let mut style = if line.is_empty() {
1483                Style::new().fg(self.theme.text_dim)
1484            } else {
1485                Style::new().fg(self.theme.text)
1486            };
1487
1488            if focused && row == state.cursor_row {
1489                rendered.clear();
1490                for (idx, ch) in line.chars().enumerate() {
1491                    if idx == state.cursor_col {
1492                        rendered.push('▎');
1493                    }
1494                    rendered.push(ch);
1495                }
1496                if state.cursor_col >= line.chars().count() {
1497                    rendered.push('▎');
1498                }
1499                style = Style::new().fg(self.theme.text);
1500            }
1501
1502            self.styled(rendered, style);
1503        }
1504        self.commands.push(Command::EndContainer);
1505        self.last_text_idx = None;
1506
1507        self
1508    }
1509
1510    /// Render a progress bar (20 chars wide). `ratio` is clamped to `0.0..=1.0`.
1511    ///
1512    /// Uses block characters (`█` filled, `░` empty). For a custom width use
1513    /// [`Context::progress_bar`].
1514    pub fn progress(&mut self, ratio: f64) -> &mut Self {
1515        self.progress_bar(ratio, 20)
1516    }
1517
1518    /// Render a progress bar with a custom character width.
1519    ///
1520    /// `ratio` is clamped to `0.0..=1.0`. `width` is the total number of
1521    /// characters rendered.
1522    pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1523        let clamped = ratio.clamp(0.0, 1.0);
1524        let filled = (clamped * width as f64).round() as u32;
1525        let empty = width.saturating_sub(filled);
1526        let mut bar = String::new();
1527        for _ in 0..filled {
1528            bar.push('█');
1529        }
1530        for _ in 0..empty {
1531            bar.push('░');
1532        }
1533        self.text(bar)
1534    }
1535
1536    /// Render a horizontal bar chart from `(label, value)` pairs.
1537    ///
1538    /// Bars are normalized against the largest value and rendered with `█` up to
1539    /// `max_width` characters.
1540    ///
1541    /// # Example
1542    ///
1543    /// ```ignore
1544    /// # slt::run(|ui: &mut slt::Context| {
1545    /// let data = [
1546    ///     ("Sales", 160.0),
1547    ///     ("Revenue", 120.0),
1548    ///     ("Users", 220.0),
1549    ///     ("Costs", 60.0),
1550    /// ];
1551    /// ui.bar_chart(&data, 24);
1552    /// # });
1553    /// ```
1554    pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
1555        if data.is_empty() {
1556            return self;
1557        }
1558
1559        let max_label_width = data
1560            .iter()
1561            .map(|(label, _)| UnicodeWidthStr::width(*label))
1562            .max()
1563            .unwrap_or(0);
1564        let max_value = data
1565            .iter()
1566            .map(|(_, value)| *value)
1567            .fold(f64::NEG_INFINITY, f64::max);
1568        let denom = if max_value > 0.0 { max_value } else { 1.0 };
1569
1570        self.interaction_count += 1;
1571        self.commands.push(Command::BeginContainer {
1572            direction: Direction::Column,
1573            gap: 0,
1574            align: Align::Start,
1575            border: None,
1576            border_style: Style::new().fg(self.theme.border),
1577            padding: Padding::default(),
1578            margin: Margin::default(),
1579            constraints: Constraints::default(),
1580            title: None,
1581            grow: 0,
1582        });
1583
1584        for (label, value) in data {
1585            let label_width = UnicodeWidthStr::width(*label);
1586            let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
1587            let normalized = (*value / denom).clamp(0.0, 1.0);
1588            let bar_len = (normalized * max_width as f64).round() as usize;
1589            let bar = "█".repeat(bar_len);
1590
1591            self.interaction_count += 1;
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            self.styled(
1605                format!("{label}{label_padding}"),
1606                Style::new().fg(self.theme.text),
1607            );
1608            self.styled(bar, Style::new().fg(self.theme.primary));
1609            self.styled(
1610                format_compact_number(*value),
1611                Style::new().fg(self.theme.text_dim),
1612            );
1613            self.commands.push(Command::EndContainer);
1614            self.last_text_idx = None;
1615        }
1616
1617        self.commands.push(Command::EndContainer);
1618        self.last_text_idx = None;
1619
1620        self
1621    }
1622
1623    /// Render a single-line sparkline from numeric data.
1624    ///
1625    /// Uses the last `width` points (or fewer if the data is shorter) and maps
1626    /// each point to one of `▁▂▃▄▅▆▇█`.
1627    ///
1628    /// # Example
1629    ///
1630    /// ```ignore
1631    /// # slt::run(|ui: &mut slt::Context| {
1632    /// let samples = [12.0, 9.0, 14.0, 18.0, 16.0, 21.0, 20.0, 24.0];
1633    /// ui.sparkline(&samples, 16);
1634    /// # });
1635    /// ```
1636    pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
1637        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
1638
1639        let w = width as usize;
1640        let window = if data.len() > w {
1641            &data[data.len() - w..]
1642        } else {
1643            data
1644        };
1645
1646        if window.is_empty() {
1647            return self;
1648        }
1649
1650        let min = window.iter().copied().fold(f64::INFINITY, f64::min);
1651        let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1652        let range = max - min;
1653
1654        let line: String = window
1655            .iter()
1656            .map(|&value| {
1657                let normalized = if range == 0.0 {
1658                    0.5
1659                } else {
1660                    (value - min) / range
1661                };
1662                let idx = (normalized * 7.0).round() as usize;
1663                BLOCKS[idx.min(7)]
1664            })
1665            .collect();
1666
1667        self.styled(line, Style::new().fg(self.theme.primary))
1668    }
1669
1670    /// Render a multi-row line chart using braille characters.
1671    ///
1672    /// `width` and `height` are terminal cell dimensions. Internally this uses
1673    /// braille dot resolution (`width*2` x `height*4`) for smoother plotting.
1674    ///
1675    /// # Example
1676    ///
1677    /// ```ignore
1678    /// # slt::run(|ui: &mut slt::Context| {
1679    /// let data = [1.0, 3.0, 2.0, 5.0, 4.0, 6.0, 3.0, 7.0];
1680    /// ui.line_chart(&data, 40, 8);
1681    /// # });
1682    /// ```
1683    pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
1684        if data.is_empty() || width == 0 || height == 0 {
1685            return self;
1686        }
1687
1688        let cols = width as usize;
1689        let rows = height as usize;
1690        let px_w = cols * 2;
1691        let px_h = rows * 4;
1692
1693        let min = data.iter().copied().fold(f64::INFINITY, f64::min);
1694        let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1695        let range = if (max - min).abs() < f64::EPSILON {
1696            1.0
1697        } else {
1698            max - min
1699        };
1700
1701        let points: Vec<usize> = (0..px_w)
1702            .map(|px| {
1703                let data_idx = if px_w <= 1 {
1704                    0.0
1705                } else {
1706                    px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
1707                };
1708                let idx = data_idx.floor() as usize;
1709                let frac = data_idx - idx as f64;
1710                let value = if idx + 1 < data.len() {
1711                    data[idx] * (1.0 - frac) + data[idx + 1] * frac
1712                } else {
1713                    data[idx.min(data.len() - 1)]
1714                };
1715
1716                let normalized = (value - min) / range;
1717                let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
1718                py.min(px_h - 1)
1719            })
1720            .collect();
1721
1722        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
1723        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
1724
1725        let mut grid = vec![vec![0u32; cols]; rows];
1726
1727        for i in 0..points.len() {
1728            let px = i;
1729            let py = points[i];
1730            let char_col = px / 2;
1731            let char_row = py / 4;
1732            let sub_col = px % 2;
1733            let sub_row = py % 4;
1734
1735            if char_col < cols && char_row < rows {
1736                grid[char_row][char_col] |= if sub_col == 0 {
1737                    LEFT_BITS[sub_row]
1738                } else {
1739                    RIGHT_BITS[sub_row]
1740                };
1741            }
1742
1743            if i + 1 < points.len() {
1744                let py_next = points[i + 1];
1745                let (y_start, y_end) = if py <= py_next {
1746                    (py, py_next)
1747                } else {
1748                    (py_next, py)
1749                };
1750                for y in y_start..=y_end {
1751                    let cell_row = y / 4;
1752                    let sub_y = y % 4;
1753                    if char_col < cols && cell_row < rows {
1754                        grid[cell_row][char_col] |= if sub_col == 0 {
1755                            LEFT_BITS[sub_y]
1756                        } else {
1757                            RIGHT_BITS[sub_y]
1758                        };
1759                    }
1760                }
1761            }
1762        }
1763
1764        let style = Style::new().fg(self.theme.primary);
1765        for row in grid {
1766            let line: String = row
1767                .iter()
1768                .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
1769                .collect();
1770            self.styled(line, style);
1771        }
1772
1773        self
1774    }
1775
1776    /// Render a braille drawing canvas.
1777    ///
1778    /// The closure receives a [`CanvasContext`] for pixel-level drawing. Each
1779    /// terminal cell maps to a 2x4 braille dot matrix, giving `width*2` x
1780    /// `height*4` pixel resolution.
1781    ///
1782    /// # Example
1783    ///
1784    /// ```ignore
1785    /// # slt::run(|ui: &mut slt::Context| {
1786    /// ui.canvas(40, 10, |cv| {
1787    ///     cv.line(0, 0, cv.width() - 1, cv.height() - 1);
1788    ///     cv.circle(40, 20, 15);
1789    /// });
1790    /// # });
1791    /// ```
1792    pub fn canvas(
1793        &mut self,
1794        width: u32,
1795        height: u32,
1796        draw: impl FnOnce(&mut CanvasContext),
1797    ) -> &mut Self {
1798        if width == 0 || height == 0 {
1799            return self;
1800        }
1801
1802        let mut canvas = CanvasContext::new(width as usize, height as usize);
1803        draw(&mut canvas);
1804
1805        let style = Style::new().fg(self.theme.primary);
1806        for line in canvas.render() {
1807            self.styled(line, style);
1808        }
1809
1810        self
1811    }
1812
1813    /// Render children in a fixed grid with the given number of columns.
1814    ///
1815    /// Children are placed left-to-right, top-to-bottom. Each cell has equal
1816    /// width (`area_width / cols`). Rows wrap automatically.
1817    ///
1818    /// # Example
1819    ///
1820    /// ```no_run
1821    /// # slt::run(|ui: &mut slt::Context| {
1822    /// ui.grid(3, |ui| {
1823    ///     for i in 0..9 {
1824    ///         ui.text(format!("Cell {i}"));
1825    ///     }
1826    /// });
1827    /// # });
1828    /// ```
1829    pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
1830        let interaction_id = self.interaction_count;
1831        self.interaction_count += 1;
1832        let border = self.theme.border;
1833
1834        self.commands.push(Command::BeginContainer {
1835            direction: Direction::Column,
1836            gap: 0,
1837            align: Align::Start,
1838            border: None,
1839            border_style: Style::new().fg(border),
1840            padding: Padding::default(),
1841            margin: Margin::default(),
1842            constraints: Constraints::default(),
1843            title: None,
1844            grow: 0,
1845        });
1846
1847        let children_start = self.commands.len();
1848        f(self);
1849        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
1850
1851        let mut elements: Vec<Vec<Command>> = Vec::new();
1852        let mut iter = child_commands.into_iter().peekable();
1853        while let Some(cmd) = iter.next() {
1854            match cmd {
1855                Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
1856                    let mut depth = 1_u32;
1857                    let mut element = vec![cmd];
1858                    for next in iter.by_ref() {
1859                        match next {
1860                            Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
1861                                depth += 1;
1862                            }
1863                            Command::EndContainer => {
1864                                depth = depth.saturating_sub(1);
1865                            }
1866                            _ => {}
1867                        }
1868                        let at_end = matches!(next, Command::EndContainer) && depth == 0;
1869                        element.push(next);
1870                        if at_end {
1871                            break;
1872                        }
1873                    }
1874                    elements.push(element);
1875                }
1876                Command::EndContainer => {}
1877                _ => elements.push(vec![cmd]),
1878            }
1879        }
1880
1881        let cols = cols.max(1) as usize;
1882        for row in elements.chunks(cols) {
1883            self.interaction_count += 1;
1884            self.commands.push(Command::BeginContainer {
1885                direction: Direction::Row,
1886                gap: 0,
1887                align: Align::Start,
1888                border: None,
1889                border_style: Style::new().fg(border),
1890                padding: Padding::default(),
1891                margin: Margin::default(),
1892                constraints: Constraints::default(),
1893                title: None,
1894                grow: 0,
1895            });
1896
1897            for element in row {
1898                self.interaction_count += 1;
1899                self.commands.push(Command::BeginContainer {
1900                    direction: Direction::Column,
1901                    gap: 0,
1902                    align: Align::Start,
1903                    border: None,
1904                    border_style: Style::new().fg(border),
1905                    padding: Padding::default(),
1906                    margin: Margin::default(),
1907                    constraints: Constraints::default(),
1908                    title: None,
1909                    grow: 1,
1910                });
1911                self.commands.extend(element.iter().cloned());
1912                self.commands.push(Command::EndContainer);
1913            }
1914
1915            self.commands.push(Command::EndContainer);
1916        }
1917
1918        self.commands.push(Command::EndContainer);
1919        self.last_text_idx = None;
1920
1921        self.response_for(interaction_id)
1922    }
1923
1924    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
1925    ///
1926    /// The selected item is highlighted with the theme's primary color. If the
1927    /// list is empty, nothing is rendered.
1928    pub fn list(&mut self, state: &mut ListState) -> &mut Self {
1929        if state.items.is_empty() {
1930            state.selected = 0;
1931            return self;
1932        }
1933
1934        state.selected = state.selected.min(state.items.len().saturating_sub(1));
1935
1936        let focused = self.register_focusable();
1937
1938        if focused {
1939            let mut consumed_indices = Vec::new();
1940            for (i, event) in self.events.iter().enumerate() {
1941                if let Event::Key(key) = event {
1942                    match key.code {
1943                        KeyCode::Up | KeyCode::Char('k') => {
1944                            state.selected = state.selected.saturating_sub(1);
1945                            consumed_indices.push(i);
1946                        }
1947                        KeyCode::Down | KeyCode::Char('j') => {
1948                            state.selected =
1949                                (state.selected + 1).min(state.items.len().saturating_sub(1));
1950                            consumed_indices.push(i);
1951                        }
1952                        _ => {}
1953                    }
1954                }
1955            }
1956
1957            for index in consumed_indices {
1958                self.consumed[index] = true;
1959            }
1960        }
1961
1962        for (idx, item) in state.items.iter().enumerate() {
1963            if idx == state.selected {
1964                if focused {
1965                    self.styled(
1966                        format!("▸ {item}"),
1967                        Style::new().bold().fg(self.theme.primary),
1968                    );
1969                } else {
1970                    self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
1971                }
1972            } else {
1973                self.styled(format!("  {item}"), Style::new().fg(self.theme.text));
1974            }
1975        }
1976
1977        self
1978    }
1979
1980    /// Render a data table with column headers. Handles Up/Down selection when focused.
1981    ///
1982    /// Column widths are computed automatically from header and cell content.
1983    /// The selected row is highlighted with the theme's selection colors.
1984    pub fn table(&mut self, state: &mut TableState) -> &mut Self {
1985        if state.is_dirty() {
1986            state.recompute_widths();
1987        }
1988
1989        let focused = self.register_focusable();
1990
1991        if focused && !state.rows.is_empty() {
1992            let mut consumed_indices = Vec::new();
1993            for (i, event) in self.events.iter().enumerate() {
1994                if let Event::Key(key) = event {
1995                    match key.code {
1996                        KeyCode::Up | KeyCode::Char('k') => {
1997                            state.selected = state.selected.saturating_sub(1);
1998                            consumed_indices.push(i);
1999                        }
2000                        KeyCode::Down | KeyCode::Char('j') => {
2001                            state.selected =
2002                                (state.selected + 1).min(state.rows.len().saturating_sub(1));
2003                            consumed_indices.push(i);
2004                        }
2005                        _ => {}
2006                    }
2007                }
2008            }
2009            for index in consumed_indices {
2010                self.consumed[index] = true;
2011            }
2012        }
2013
2014        state.selected = state.selected.min(state.rows.len().saturating_sub(1));
2015
2016        let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
2017        self.styled(header_line, Style::new().bold().fg(self.theme.text));
2018
2019        let separator = state
2020            .column_widths()
2021            .iter()
2022            .map(|w| "─".repeat(*w as usize))
2023            .collect::<Vec<_>>()
2024            .join("─┼─");
2025        self.text(separator);
2026
2027        for (idx, row) in state.rows.iter().enumerate() {
2028            let line = format_table_row(row, state.column_widths(), " │ ");
2029            if idx == state.selected {
2030                let mut style = Style::new()
2031                    .bg(self.theme.selected_bg)
2032                    .fg(self.theme.selected_fg);
2033                if focused {
2034                    style = style.bold();
2035                }
2036                self.styled(line, style);
2037            } else {
2038                self.styled(line, Style::new().fg(self.theme.text));
2039            }
2040        }
2041
2042        self
2043    }
2044
2045    /// Render a tab bar. Handles Left/Right navigation when focused.
2046    ///
2047    /// The active tab is rendered in the theme's primary color. If the labels
2048    /// list is empty, nothing is rendered.
2049    pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
2050        if state.labels.is_empty() {
2051            state.selected = 0;
2052            return self;
2053        }
2054
2055        state.selected = state.selected.min(state.labels.len().saturating_sub(1));
2056        let focused = self.register_focusable();
2057
2058        if focused {
2059            let mut consumed_indices = Vec::new();
2060            for (i, event) in self.events.iter().enumerate() {
2061                if let Event::Key(key) = event {
2062                    match key.code {
2063                        KeyCode::Left => {
2064                            state.selected = if state.selected == 0 {
2065                                state.labels.len().saturating_sub(1)
2066                            } else {
2067                                state.selected - 1
2068                            };
2069                            consumed_indices.push(i);
2070                        }
2071                        KeyCode::Right => {
2072                            state.selected = (state.selected + 1) % state.labels.len();
2073                            consumed_indices.push(i);
2074                        }
2075                        _ => {}
2076                    }
2077                }
2078            }
2079
2080            for index in consumed_indices {
2081                self.consumed[index] = true;
2082            }
2083        }
2084
2085        self.interaction_count += 1;
2086        self.commands.push(Command::BeginContainer {
2087            direction: Direction::Row,
2088            gap: 1,
2089            align: Align::Start,
2090            border: None,
2091            border_style: Style::new().fg(self.theme.border),
2092            padding: Padding::default(),
2093            margin: Margin::default(),
2094            constraints: Constraints::default(),
2095            title: None,
2096            grow: 0,
2097        });
2098        for (idx, label) in state.labels.iter().enumerate() {
2099            let style = if idx == state.selected {
2100                let s = Style::new().fg(self.theme.primary).bold();
2101                if focused {
2102                    s.underline()
2103                } else {
2104                    s
2105                }
2106            } else {
2107                Style::new().fg(self.theme.text_dim)
2108            };
2109            self.styled(format!("[ {label} ]"), style);
2110        }
2111        self.commands.push(Command::EndContainer);
2112        self.last_text_idx = None;
2113
2114        self
2115    }
2116
2117    /// Render a clickable button. Returns `true` when activated via Enter, Space, or mouse click.
2118    ///
2119    /// The button is styled with the theme's primary color when focused and the
2120    /// accent color when hovered.
2121    pub fn button(&mut self, label: impl Into<String>) -> bool {
2122        let focused = self.register_focusable();
2123        let interaction_id = self.interaction_count;
2124        self.interaction_count += 1;
2125        let response = self.response_for(interaction_id);
2126
2127        let mut activated = response.clicked;
2128        if focused {
2129            let mut consumed_indices = Vec::new();
2130            for (i, event) in self.events.iter().enumerate() {
2131                if let Event::Key(key) = event {
2132                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
2133                        activated = true;
2134                        consumed_indices.push(i);
2135                    }
2136                }
2137            }
2138
2139            for index in consumed_indices {
2140                self.consumed[index] = true;
2141            }
2142        }
2143
2144        let style = if focused {
2145            Style::new().fg(self.theme.primary).bold()
2146        } else if response.hovered {
2147            Style::new().fg(self.theme.accent)
2148        } else {
2149            Style::new().fg(self.theme.text)
2150        };
2151
2152        self.commands.push(Command::BeginContainer {
2153            direction: Direction::Row,
2154            gap: 0,
2155            align: Align::Start,
2156            border: None,
2157            border_style: Style::new().fg(self.theme.border),
2158            padding: Padding::default(),
2159            margin: Margin::default(),
2160            constraints: Constraints::default(),
2161            title: None,
2162            grow: 0,
2163        });
2164        self.styled(format!("[ {} ]", label.into()), style);
2165        self.commands.push(Command::EndContainer);
2166        self.last_text_idx = None;
2167
2168        activated
2169    }
2170
2171    /// Render a checkbox. Toggles the bool on Enter, Space, or click.
2172    ///
2173    /// The checked state is shown with the theme's success color. When focused,
2174    /// a `▸` prefix is added.
2175    pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
2176        let focused = self.register_focusable();
2177        let interaction_id = self.interaction_count;
2178        self.interaction_count += 1;
2179        let response = self.response_for(interaction_id);
2180        let mut should_toggle = response.clicked;
2181
2182        if focused {
2183            let mut consumed_indices = Vec::new();
2184            for (i, event) in self.events.iter().enumerate() {
2185                if let Event::Key(key) = event {
2186                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
2187                        should_toggle = true;
2188                        consumed_indices.push(i);
2189                    }
2190                }
2191            }
2192
2193            for index in consumed_indices {
2194                self.consumed[index] = true;
2195            }
2196        }
2197
2198        if should_toggle {
2199            *checked = !*checked;
2200        }
2201
2202        self.commands.push(Command::BeginContainer {
2203            direction: Direction::Row,
2204            gap: 1,
2205            align: Align::Start,
2206            border: None,
2207            border_style: Style::new().fg(self.theme.border),
2208            padding: Padding::default(),
2209            margin: Margin::default(),
2210            constraints: Constraints::default(),
2211            title: None,
2212            grow: 0,
2213        });
2214        let marker_style = if *checked {
2215            Style::new().fg(self.theme.success)
2216        } else {
2217            Style::new().fg(self.theme.text_dim)
2218        };
2219        let marker = if *checked { "[x]" } else { "[ ]" };
2220        let label_text = label.into();
2221        if focused {
2222            self.styled(format!("▸ {marker}"), marker_style.bold());
2223            self.styled(label_text, Style::new().fg(self.theme.text).bold());
2224        } else {
2225            self.styled(marker, marker_style);
2226            self.styled(label_text, Style::new().fg(self.theme.text));
2227        }
2228        self.commands.push(Command::EndContainer);
2229        self.last_text_idx = None;
2230
2231        self
2232    }
2233
2234    /// Render an on/off toggle switch.
2235    ///
2236    /// Toggles `on` when activated via Enter, Space, or click. The switch
2237    /// renders as `●━━ ON` or `━━● OFF` colored with the theme's success or
2238    /// dim color respectively.
2239    pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
2240        let focused = self.register_focusable();
2241        let interaction_id = self.interaction_count;
2242        self.interaction_count += 1;
2243        let response = self.response_for(interaction_id);
2244        let mut should_toggle = response.clicked;
2245
2246        if focused {
2247            let mut consumed_indices = Vec::new();
2248            for (i, event) in self.events.iter().enumerate() {
2249                if let Event::Key(key) = event {
2250                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
2251                        should_toggle = true;
2252                        consumed_indices.push(i);
2253                    }
2254                }
2255            }
2256
2257            for index in consumed_indices {
2258                self.consumed[index] = true;
2259            }
2260        }
2261
2262        if should_toggle {
2263            *on = !*on;
2264        }
2265
2266        self.commands.push(Command::BeginContainer {
2267            direction: Direction::Row,
2268            gap: 2,
2269            align: Align::Start,
2270            border: None,
2271            border_style: Style::new().fg(self.theme.border),
2272            padding: Padding::default(),
2273            margin: Margin::default(),
2274            constraints: Constraints::default(),
2275            title: None,
2276            grow: 0,
2277        });
2278        let label_text = label.into();
2279        let switch = if *on { "●━━ ON" } else { "━━● OFF" };
2280        let switch_style = if *on {
2281            Style::new().fg(self.theme.success)
2282        } else {
2283            Style::new().fg(self.theme.text_dim)
2284        };
2285        if focused {
2286            self.styled(
2287                format!("▸ {label_text}"),
2288                Style::new().fg(self.theme.text).bold(),
2289            );
2290            self.styled(switch, switch_style.bold());
2291        } else {
2292            self.styled(label_text, Style::new().fg(self.theme.text));
2293            self.styled(switch, switch_style);
2294        }
2295        self.commands.push(Command::EndContainer);
2296        self.last_text_idx = None;
2297
2298        self
2299    }
2300
2301    /// Render a horizontal divider line.
2302    ///
2303    /// The line is drawn with the theme's border color and expands to fill the
2304    /// container width.
2305    pub fn separator(&mut self) -> &mut Self {
2306        self.commands.push(Command::Text {
2307            content: "─".repeat(200),
2308            style: Style::new().fg(self.theme.border).dim(),
2309            grow: 0,
2310            align: Align::Start,
2311            wrap: false,
2312            margin: Margin::default(),
2313            constraints: Constraints::default(),
2314        });
2315        self.last_text_idx = Some(self.commands.len() - 1);
2316        self
2317    }
2318
2319    /// Render a help bar showing keybinding hints.
2320    ///
2321    /// `bindings` is a slice of `(key, action)` pairs. Keys are rendered in the
2322    /// theme's primary color; actions in the dim text color. Pairs are separated
2323    /// by a `·` character.
2324    pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
2325        if bindings.is_empty() {
2326            return self;
2327        }
2328
2329        self.interaction_count += 1;
2330        self.commands.push(Command::BeginContainer {
2331            direction: Direction::Row,
2332            gap: 2,
2333            align: Align::Start,
2334            border: None,
2335            border_style: Style::new().fg(self.theme.border),
2336            padding: Padding::default(),
2337            margin: Margin::default(),
2338            constraints: Constraints::default(),
2339            title: None,
2340            grow: 0,
2341        });
2342        for (idx, (key, action)) in bindings.iter().enumerate() {
2343            if idx > 0 {
2344                self.styled("·", Style::new().fg(self.theme.text_dim));
2345            }
2346            self.styled(*key, Style::new().bold().fg(self.theme.primary));
2347            self.styled(*action, Style::new().fg(self.theme.text_dim));
2348        }
2349        self.commands.push(Command::EndContainer);
2350        self.last_text_idx = None;
2351
2352        self
2353    }
2354
2355    // ── events ───────────────────────────────────────────────────────
2356
2357    /// Check if a character key was pressed this frame.
2358    ///
2359    /// Returns `true` if the key event has not been consumed by another widget.
2360    pub fn key(&self, c: char) -> bool {
2361        self.events.iter().enumerate().any(|(i, e)| {
2362            !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
2363        })
2364    }
2365
2366    /// Check if a specific key code was pressed this frame.
2367    ///
2368    /// Returns `true` if the key event has not been consumed by another widget.
2369    pub fn key_code(&self, code: KeyCode) -> bool {
2370        self.events
2371            .iter()
2372            .enumerate()
2373            .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
2374    }
2375
2376    /// Check if a character key with specific modifiers was pressed this frame.
2377    ///
2378    /// Returns `true` if the key event has not been consumed by another widget.
2379    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
2380        self.events.iter().enumerate().any(|(i, e)| {
2381            !self.consumed[i]
2382                && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
2383        })
2384    }
2385
2386    /// Return the position of a left mouse button down event this frame, if any.
2387    ///
2388    /// Returns `None` if no unconsumed mouse-down event occurred.
2389    pub fn mouse_down(&self) -> Option<(u32, u32)> {
2390        self.events.iter().enumerate().find_map(|(i, event)| {
2391            if self.consumed[i] {
2392                return None;
2393            }
2394            if let Event::Mouse(mouse) = event {
2395                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
2396                    return Some((mouse.x, mouse.y));
2397                }
2398            }
2399            None
2400        })
2401    }
2402
2403    /// Return the current mouse cursor position, if known.
2404    ///
2405    /// The position is updated on every mouse move or click event. Returns
2406    /// `None` until the first mouse event is received.
2407    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
2408        self.mouse_pos
2409    }
2410
2411    /// Check if an unconsumed scroll-up event occurred this frame.
2412    pub fn scroll_up(&self) -> bool {
2413        self.events.iter().enumerate().any(|(i, event)| {
2414            !self.consumed[i]
2415                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
2416        })
2417    }
2418
2419    /// Check if an unconsumed scroll-down event occurred this frame.
2420    pub fn scroll_down(&self) -> bool {
2421        self.events.iter().enumerate().any(|(i, event)| {
2422            !self.consumed[i]
2423                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
2424        })
2425    }
2426
2427    /// Signal the run loop to exit after this frame.
2428    pub fn quit(&mut self) {
2429        self.should_quit = true;
2430    }
2431
2432    /// Get the current theme.
2433    pub fn theme(&self) -> &Theme {
2434        &self.theme
2435    }
2436
2437    /// Change the theme for subsequent rendering.
2438    ///
2439    /// All widgets rendered after this call will use the new theme's colors.
2440    pub fn set_theme(&mut self, theme: Theme) {
2441        self.theme = theme;
2442    }
2443
2444    // ── info ─────────────────────────────────────────────────────────
2445
2446    /// Get the terminal width in cells.
2447    pub fn width(&self) -> u32 {
2448        self.area_width
2449    }
2450
2451    /// Get the terminal height in cells.
2452    pub fn height(&self) -> u32 {
2453        self.area_height
2454    }
2455
2456    /// Get the current tick count (increments each frame).
2457    ///
2458    /// Useful for animations and time-based logic. The tick starts at 0 and
2459    /// increases by 1 on every rendered frame.
2460    pub fn tick(&self) -> u64 {
2461        self.tick
2462    }
2463
2464    /// Return whether the layout debugger is enabled.
2465    ///
2466    /// The debugger is toggled with F12 at runtime.
2467    pub fn debug_enabled(&self) -> bool {
2468        self.debug
2469    }
2470}
2471
2472#[inline]
2473fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2474    if char_index == 0 {
2475        return 0;
2476    }
2477    value
2478        .char_indices()
2479        .nth(char_index)
2480        .map_or(value.len(), |(idx, _)| idx)
2481}
2482
2483fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2484    let mut parts: Vec<String> = Vec::new();
2485    for (i, width) in widths.iter().enumerate() {
2486        let cell = cells.get(i).map(String::as_str).unwrap_or("");
2487        let cell_width = UnicodeWidthStr::width(cell) as u32;
2488        let padding = (*width).saturating_sub(cell_width) as usize;
2489        parts.push(format!("{cell}{}", " ".repeat(padding)));
2490    }
2491    parts.join(separator)
2492}
2493
2494fn format_compact_number(value: f64) -> String {
2495    if value.fract().abs() < f64::EPSILON {
2496        return format!("{value:.0}");
2497    }
2498
2499    let mut s = format!("{value:.2}");
2500    while s.contains('.') && s.ends_with('0') {
2501        s.pop();
2502    }
2503    if s.ends_with('.') {
2504        s.pop();
2505    }
2506    s
2507}