Skip to main content

slt/
context.rs

1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
3use crate::layout::{Command, Direction};
4use crate::rect::Rect;
5use crate::style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
6use crate::widgets::{
7    ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
8    ToastLevel, ToastState,
9};
10use unicode_width::UnicodeWidthStr;
11
12/// Result of a container mouse interaction.
13///
14/// Returned by [`Context::col`], [`Context::row`], and [`ContainerBuilder::col`] /
15/// [`ContainerBuilder::row`] so you can react to clicks and hover without a separate
16/// event loop.
17#[derive(Debug, Clone, Copy, Default)]
18pub struct Response {
19    /// Whether the container was clicked this frame.
20    pub clicked: bool,
21    /// Whether the mouse is over the container.
22    pub hovered: bool,
23}
24
25/// Direction for bar chart rendering.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BarDirection {
28    /// Bars grow horizontally (default, current behavior).
29    Horizontal,
30    /// Bars grow vertically from bottom to top.
31    Vertical,
32}
33
34/// A single bar in a styled bar chart.
35#[derive(Debug, Clone)]
36pub struct Bar {
37    /// Display label for this bar.
38    pub label: String,
39    /// Numeric value.
40    pub value: f64,
41    /// Bar color. If None, uses theme.primary.
42    pub color: Option<Color>,
43}
44
45impl Bar {
46    /// Create a new bar with a label and value.
47    pub fn new(label: impl Into<String>, value: f64) -> Self {
48        Self {
49            label: label.into(),
50            value,
51            color: None,
52        }
53    }
54
55    /// Set the bar color.
56    pub fn color(mut self, color: Color) -> Self {
57        self.color = Some(color);
58        self
59    }
60}
61
62/// A group of bars rendered together (for grouped bar charts).
63#[derive(Debug, Clone)]
64pub struct BarGroup {
65    /// Group label displayed below the bars.
66    pub label: String,
67    /// Bars in this group.
68    pub bars: Vec<Bar>,
69}
70
71impl BarGroup {
72    /// Create a new bar group with a label and bars.
73    pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
74        Self {
75            label: label.into(),
76            bars,
77        }
78    }
79}
80
81/// Trait for creating custom widgets.
82///
83/// Implement this trait to build reusable, composable widgets with full access
84/// to the [`Context`] API — focus, events, theming, layout, and mouse interaction.
85///
86/// # Examples
87///
88/// A simple rating widget:
89///
90/// ```no_run
91/// use slt::{Context, Widget, Color};
92///
93/// struct Rating {
94///     value: u8,
95///     max: u8,
96/// }
97///
98/// impl Rating {
99///     fn new(value: u8, max: u8) -> Self {
100///         Self { value, max }
101///     }
102/// }
103///
104/// impl Widget for Rating {
105///     type Response = bool;
106///
107///     fn ui(&mut self, ui: &mut Context) -> bool {
108///         let focused = ui.register_focusable();
109///         let mut changed = false;
110///
111///         if focused {
112///             if ui.key('+') && self.value < self.max {
113///                 self.value += 1;
114///                 changed = true;
115///             }
116///             if ui.key('-') && self.value > 0 {
117///                 self.value -= 1;
118///                 changed = true;
119///             }
120///         }
121///
122///         let stars: String = (0..self.max).map(|i| {
123///             if i < self.value { '★' } else { '☆' }
124///         }).collect();
125///
126///         let color = if focused { Color::Yellow } else { Color::White };
127///         ui.styled(stars, slt::Style::new().fg(color));
128///
129///         changed
130///     }
131/// }
132///
133/// fn main() -> std::io::Result<()> {
134///     let mut rating = Rating::new(3, 5);
135///     slt::run(|ui| {
136///         if ui.key('q') { ui.quit(); }
137///         ui.text("Rate this:");
138///         ui.widget(&mut rating);
139///     })
140/// }
141/// ```
142pub trait Widget {
143    /// The value returned after rendering. Use `()` for widgets with no return,
144    /// `bool` for widgets that report changes, or [`Response`] for click/hover.
145    type Response;
146
147    /// Render the widget into the given context.
148    ///
149    /// Use [`Context::register_focusable`] to participate in Tab focus cycling,
150    /// [`Context::key`] / [`Context::key_code`] to handle keyboard input,
151    /// and [`Context::interaction`] to detect clicks and hovers.
152    fn ui(&mut self, ctx: &mut Context) -> Self::Response;
153}
154
155/// The main rendering context passed to your closure each frame.
156///
157/// Provides all methods for building UI: text, containers, widgets, and event
158/// handling. You receive a `&mut Context` on every frame and describe what to
159/// render by calling its methods. SLT collects those calls, lays them out with
160/// flexbox, diffs against the previous frame, and flushes only changed cells.
161///
162/// # Example
163///
164/// ```no_run
165/// slt::run(|ui: &mut slt::Context| {
166///     if ui.key('q') { ui.quit(); }
167///     ui.text("Hello, world!").bold();
168/// });
169/// ```
170pub struct Context {
171    pub(crate) commands: Vec<Command>,
172    pub(crate) events: Vec<Event>,
173    pub(crate) consumed: Vec<bool>,
174    pub(crate) should_quit: bool,
175    pub(crate) area_width: u32,
176    pub(crate) area_height: u32,
177    pub(crate) tick: u64,
178    pub(crate) focus_index: usize,
179    pub(crate) focus_count: usize,
180    prev_focus_count: usize,
181    scroll_count: usize,
182    prev_scroll_infos: Vec<(u32, u32)>,
183    interaction_count: usize,
184    pub(crate) prev_hit_map: Vec<Rect>,
185    _prev_focus_rects: Vec<(usize, Rect)>,
186    mouse_pos: Option<(u32, u32)>,
187    click_pos: Option<(u32, u32)>,
188    last_text_idx: Option<usize>,
189    debug: bool,
190    theme: Theme,
191}
192
193/// Fluent builder for configuring containers before calling `.col()` or `.row()`.
194///
195/// Obtain one via [`Context::container`] or [`Context::bordered`]. Chain the
196/// configuration methods you need, then finalize with `.col(|ui| { ... })` or
197/// `.row(|ui| { ... })`.
198///
199/// # Example
200///
201/// ```no_run
202/// # slt::run(|ui: &mut slt::Context| {
203/// use slt::{Border, Color};
204/// ui.container()
205///     .border(Border::Rounded)
206///     .pad(1)
207///     .grow(1)
208///     .col(|ui| {
209///         ui.text("inside a bordered, padded, growing column");
210///     });
211/// # });
212/// ```
213#[must_use = "configure and finalize with .col() or .row()"]
214pub struct ContainerBuilder<'a> {
215    ctx: &'a mut Context,
216    gap: u32,
217    align: Align,
218    border: Option<Border>,
219    border_style: Style,
220    padding: Padding,
221    margin: Margin,
222    constraints: Constraints,
223    title: Option<(String, Style)>,
224    grow: u16,
225    scroll_offset: Option<u32>,
226}
227
228/// Drawing context for the [`Context::canvas`] widget.
229///
230/// Provides pixel-level drawing on a braille character grid. Each terminal
231/// cell maps to a 2x4 dot matrix, so a canvas of `width` columns x `height`
232/// rows gives `width*2` x `height*4` pixel resolution.
233/// A colored pixel in the canvas grid.
234#[derive(Debug, Clone, Copy)]
235struct CanvasPixel {
236    bits: u32,
237    color: Color,
238}
239
240/// Text label placed on the canvas.
241#[derive(Debug, Clone)]
242struct CanvasLabel {
243    x: usize,
244    y: usize,
245    text: String,
246    color: Color,
247}
248
249/// A layer in the canvas, supporting z-ordering.
250#[derive(Debug, Clone)]
251struct CanvasLayer {
252    grid: Vec<Vec<CanvasPixel>>,
253    labels: Vec<CanvasLabel>,
254}
255
256pub struct CanvasContext {
257    layers: Vec<CanvasLayer>,
258    cols: usize,
259    rows: usize,
260    px_w: usize,
261    px_h: usize,
262    current_color: Color,
263}
264
265impl CanvasContext {
266    fn new(cols: usize, rows: usize) -> Self {
267        Self {
268            layers: vec![Self::new_layer(cols, rows)],
269            cols,
270            rows,
271            px_w: cols * 2,
272            px_h: rows * 4,
273            current_color: Color::Reset,
274        }
275    }
276
277    fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
278        CanvasLayer {
279            grid: vec![
280                vec![
281                    CanvasPixel {
282                        bits: 0,
283                        color: Color::Reset,
284                    };
285                    cols
286                ];
287                rows
288            ],
289            labels: Vec::new(),
290        }
291    }
292
293    fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
294        self.layers.last_mut()
295    }
296
297    fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
298        if x >= self.px_w || y >= self.px_h {
299            return;
300        }
301
302        let char_col = x / 2;
303        let char_row = y / 4;
304        let sub_col = x % 2;
305        let sub_row = y % 4;
306        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
307        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
308
309        let bit = if sub_col == 0 {
310            LEFT_BITS[sub_row]
311        } else {
312            RIGHT_BITS[sub_row]
313        };
314
315        if let Some(layer) = self.current_layer_mut() {
316            let cell = &mut layer.grid[char_row][char_col];
317            let new_bits = cell.bits | bit;
318            if new_bits != cell.bits {
319                cell.bits = new_bits;
320                cell.color = color;
321            }
322        }
323    }
324
325    fn dot_isize(&mut self, x: isize, y: isize) {
326        if x >= 0 && y >= 0 {
327            self.dot(x as usize, y as usize);
328        }
329    }
330
331    /// Get the pixel width of the canvas.
332    pub fn width(&self) -> usize {
333        self.px_w
334    }
335
336    /// Get the pixel height of the canvas.
337    pub fn height(&self) -> usize {
338        self.px_h
339    }
340
341    /// Set a single pixel at `(x, y)`.
342    pub fn dot(&mut self, x: usize, y: usize) {
343        self.dot_with_color(x, y, self.current_color);
344    }
345
346    /// Draw a line from `(x0, y0)` to `(x1, y1)` using Bresenham's algorithm.
347    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
348        let (mut x, mut y) = (x0 as isize, y0 as isize);
349        let (x1, y1) = (x1 as isize, y1 as isize);
350        let dx = (x1 - x).abs();
351        let dy = -(y1 - y).abs();
352        let sx = if x < x1 { 1 } else { -1 };
353        let sy = if y < y1 { 1 } else { -1 };
354        let mut err = dx + dy;
355
356        loop {
357            self.dot_isize(x, y);
358            if x == x1 && y == y1 {
359                break;
360            }
361            let e2 = 2 * err;
362            if e2 >= dy {
363                err += dy;
364                x += sx;
365            }
366            if e2 <= dx {
367                err += dx;
368                y += sy;
369            }
370        }
371    }
372
373    /// Draw a rectangle outline from `(x, y)` with `w` width and `h` height.
374    pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
375        if w == 0 || h == 0 {
376            return;
377        }
378
379        self.line(x, y, x + w.saturating_sub(1), y);
380        self.line(
381            x + w.saturating_sub(1),
382            y,
383            x + w.saturating_sub(1),
384            y + h.saturating_sub(1),
385        );
386        self.line(
387            x + w.saturating_sub(1),
388            y + h.saturating_sub(1),
389            x,
390            y + h.saturating_sub(1),
391        );
392        self.line(x, y + h.saturating_sub(1), x, y);
393    }
394
395    /// Draw a circle outline centered at `(cx, cy)` with radius `r`.
396    pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
397        let mut x = r as isize;
398        let mut y: isize = 0;
399        let mut err: isize = 1 - x;
400        let (cx, cy) = (cx as isize, cy as isize);
401
402        while x >= y {
403            for &(dx, dy) in &[
404                (x, y),
405                (y, x),
406                (-x, y),
407                (-y, x),
408                (x, -y),
409                (y, -x),
410                (-x, -y),
411                (-y, -x),
412            ] {
413                let px = cx + dx;
414                let py = cy + dy;
415                self.dot_isize(px, py);
416            }
417
418            y += 1;
419            if err < 0 {
420                err += 2 * y + 1;
421            } else {
422                x -= 1;
423                err += 2 * (y - x) + 1;
424            }
425        }
426    }
427
428    /// Set the drawing color for subsequent shapes.
429    pub fn set_color(&mut self, color: Color) {
430        self.current_color = color;
431    }
432
433    /// Get the current drawing color.
434    pub fn color(&self) -> Color {
435        self.current_color
436    }
437
438    /// Draw a filled rectangle.
439    pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
440        if w == 0 || h == 0 {
441            return;
442        }
443
444        let x_end = x.saturating_add(w).min(self.px_w);
445        let y_end = y.saturating_add(h).min(self.px_h);
446        if x >= x_end || y >= y_end {
447            return;
448        }
449
450        for yy in y..y_end {
451            self.line(x, yy, x_end.saturating_sub(1), yy);
452        }
453    }
454
455    /// Draw a filled circle.
456    pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
457        let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
458        for y in (cy - r)..=(cy + r) {
459            let dy = y - cy;
460            let span_sq = (r * r - dy * dy).max(0);
461            let dx = (span_sq as f64).sqrt() as isize;
462            for x in (cx - dx)..=(cx + dx) {
463                self.dot_isize(x, y);
464            }
465        }
466    }
467
468    /// Draw a triangle outline.
469    pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
470        self.line(x0, y0, x1, y1);
471        self.line(x1, y1, x2, y2);
472        self.line(x2, y2, x0, y0);
473    }
474
475    /// Draw a filled triangle.
476    pub fn filled_triangle(
477        &mut self,
478        x0: usize,
479        y0: usize,
480        x1: usize,
481        y1: usize,
482        x2: usize,
483        y2: usize,
484    ) {
485        let vertices = [
486            (x0 as isize, y0 as isize),
487            (x1 as isize, y1 as isize),
488            (x2 as isize, y2 as isize),
489        ];
490        let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
491        let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
492
493        for y in min_y..=max_y {
494            let mut intersections: Vec<f64> = Vec::new();
495
496            for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
497                let (x_a, y_a) = vertices[edge.0];
498                let (x_b, y_b) = vertices[edge.1];
499                if y_a == y_b {
500                    continue;
501                }
502
503                let (x_start, y_start, x_end, y_end) = if y_a < y_b {
504                    (x_a, y_a, x_b, y_b)
505                } else {
506                    (x_b, y_b, x_a, y_a)
507                };
508
509                if y < y_start || y >= y_end {
510                    continue;
511                }
512
513                let t = (y - y_start) as f64 / (y_end - y_start) as f64;
514                intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
515            }
516
517            intersections.sort_by(|a, b| a.total_cmp(b));
518            let mut i = 0usize;
519            while i + 1 < intersections.len() {
520                let x_start = intersections[i].ceil() as isize;
521                let x_end = intersections[i + 1].floor() as isize;
522                for x in x_start..=x_end {
523                    self.dot_isize(x, y);
524                }
525                i += 2;
526            }
527        }
528
529        self.triangle(x0, y0, x1, y1, x2, y2);
530    }
531
532    /// Draw multiple points at once.
533    pub fn points(&mut self, pts: &[(usize, usize)]) {
534        for &(x, y) in pts {
535            self.dot(x, y);
536        }
537    }
538
539    /// Draw a polyline connecting the given points in order.
540    pub fn polyline(&mut self, pts: &[(usize, usize)]) {
541        for window in pts.windows(2) {
542            if let [(x0, y0), (x1, y1)] = window {
543                self.line(*x0, *y0, *x1, *y1);
544            }
545        }
546    }
547
548    /// Place a text label at pixel position `(x, y)`.
549    /// Text is rendered in regular characters overlaying the braille grid.
550    pub fn print(&mut self, x: usize, y: usize, text: &str) {
551        if text.is_empty() {
552            return;
553        }
554
555        let color = self.current_color;
556        if let Some(layer) = self.current_layer_mut() {
557            layer.labels.push(CanvasLabel {
558                x,
559                y,
560                text: text.to_string(),
561                color,
562            });
563        }
564    }
565
566    /// Start a new drawing layer. Shapes on later layers overlay earlier ones.
567    pub fn layer(&mut self) {
568        self.layers.push(Self::new_layer(self.cols, self.rows));
569    }
570
571    pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
572        let mut final_grid = vec![
573            vec![
574                CanvasPixel {
575                    bits: 0,
576                    color: Color::Reset,
577                };
578                self.cols
579            ];
580            self.rows
581        ];
582        let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
583            vec![vec![None; self.cols]; self.rows];
584
585        for layer in &self.layers {
586            for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
587                for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
588                    let src = layer.grid[row][col];
589                    if src.bits == 0 {
590                        continue;
591                    }
592
593                    let merged = dst.bits | src.bits;
594                    if merged != dst.bits {
595                        dst.bits = merged;
596                        dst.color = src.color;
597                    }
598                }
599            }
600
601            for label in &layer.labels {
602                let row = label.y / 4;
603                if row >= self.rows {
604                    continue;
605                }
606                let start_col = label.x / 2;
607                for (offset, ch) in label.text.chars().enumerate() {
608                    let col = start_col + offset;
609                    if col >= self.cols {
610                        break;
611                    }
612                    labels_overlay[row][col] = Some((ch, label.color));
613                }
614            }
615        }
616
617        let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
618        for row in 0..self.rows {
619            let mut segments: Vec<(String, Color)> = Vec::new();
620            let mut current_color: Option<Color> = None;
621            let mut current_text = String::new();
622
623            for col in 0..self.cols {
624                let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
625                    (label_ch, label_color)
626                } else {
627                    let bits = final_grid[row][col].bits;
628                    let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
629                    (ch, final_grid[row][col].color)
630                };
631
632                match current_color {
633                    Some(c) if c == color => {
634                        current_text.push(ch);
635                    }
636                    Some(c) => {
637                        segments.push((std::mem::take(&mut current_text), c));
638                        current_text.push(ch);
639                        current_color = Some(color);
640                    }
641                    None => {
642                        current_text.push(ch);
643                        current_color = Some(color);
644                    }
645                }
646            }
647
648            if let Some(color) = current_color {
649                segments.push((current_text, color));
650            }
651            lines.push(segments);
652        }
653
654        lines
655    }
656}
657
658impl<'a> ContainerBuilder<'a> {
659    // ── border ───────────────────────────────────────────────────────
660
661    /// Set the border style.
662    pub fn border(mut self, border: Border) -> Self {
663        self.border = Some(border);
664        self
665    }
666
667    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
668    pub fn rounded(self) -> Self {
669        self.border(Border::Rounded)
670    }
671
672    /// Set the style applied to the border characters.
673    pub fn border_style(mut self, style: Style) -> Self {
674        self.border_style = style;
675        self
676    }
677
678    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
679
680    /// Set uniform padding on all sides. Alias for [`pad`](Self::pad).
681    pub fn p(self, value: u32) -> Self {
682        self.pad(value)
683    }
684
685    /// Set uniform padding on all sides.
686    pub fn pad(mut self, value: u32) -> Self {
687        self.padding = Padding::all(value);
688        self
689    }
690
691    /// Set horizontal padding (left and right).
692    pub fn px(mut self, value: u32) -> Self {
693        self.padding.left = value;
694        self.padding.right = value;
695        self
696    }
697
698    /// Set vertical padding (top and bottom).
699    pub fn py(mut self, value: u32) -> Self {
700        self.padding.top = value;
701        self.padding.bottom = value;
702        self
703    }
704
705    /// Set top padding.
706    pub fn pt(mut self, value: u32) -> Self {
707        self.padding.top = value;
708        self
709    }
710
711    /// Set right padding.
712    pub fn pr(mut self, value: u32) -> Self {
713        self.padding.right = value;
714        self
715    }
716
717    /// Set bottom padding.
718    pub fn pb(mut self, value: u32) -> Self {
719        self.padding.bottom = value;
720        self
721    }
722
723    /// Set left padding.
724    pub fn pl(mut self, value: u32) -> Self {
725        self.padding.left = value;
726        self
727    }
728
729    /// Set per-side padding using a [`Padding`] value.
730    pub fn padding(mut self, padding: Padding) -> Self {
731        self.padding = padding;
732        self
733    }
734
735    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
736
737    /// Set uniform margin on all sides.
738    pub fn m(mut self, value: u32) -> Self {
739        self.margin = Margin::all(value);
740        self
741    }
742
743    /// Set horizontal margin (left and right).
744    pub fn mx(mut self, value: u32) -> Self {
745        self.margin.left = value;
746        self.margin.right = value;
747        self
748    }
749
750    /// Set vertical margin (top and bottom).
751    pub fn my(mut self, value: u32) -> Self {
752        self.margin.top = value;
753        self.margin.bottom = value;
754        self
755    }
756
757    /// Set top margin.
758    pub fn mt(mut self, value: u32) -> Self {
759        self.margin.top = value;
760        self
761    }
762
763    /// Set right margin.
764    pub fn mr(mut self, value: u32) -> Self {
765        self.margin.right = value;
766        self
767    }
768
769    /// Set bottom margin.
770    pub fn mb(mut self, value: u32) -> Self {
771        self.margin.bottom = value;
772        self
773    }
774
775    /// Set left margin.
776    pub fn ml(mut self, value: u32) -> Self {
777        self.margin.left = value;
778        self
779    }
780
781    /// Set per-side margin using a [`Margin`] value.
782    pub fn margin(mut self, margin: Margin) -> Self {
783        self.margin = margin;
784        self
785    }
786
787    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
788
789    /// Set a fixed width (sets both min and max width).
790    pub fn w(mut self, value: u32) -> Self {
791        self.constraints.min_width = Some(value);
792        self.constraints.max_width = Some(value);
793        self
794    }
795
796    /// Set a fixed height (sets both min and max height).
797    pub fn h(mut self, value: u32) -> Self {
798        self.constraints.min_height = Some(value);
799        self.constraints.max_height = Some(value);
800        self
801    }
802
803    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
804    pub fn min_w(mut self, value: u32) -> Self {
805        self.constraints.min_width = Some(value);
806        self
807    }
808
809    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
810    pub fn max_w(mut self, value: u32) -> Self {
811        self.constraints.max_width = Some(value);
812        self
813    }
814
815    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
816    pub fn min_h(mut self, value: u32) -> Self {
817        self.constraints.min_height = Some(value);
818        self
819    }
820
821    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
822    pub fn max_h(mut self, value: u32) -> Self {
823        self.constraints.max_height = Some(value);
824        self
825    }
826
827    /// Set the minimum width constraint in cells.
828    pub fn min_width(mut self, value: u32) -> Self {
829        self.constraints.min_width = Some(value);
830        self
831    }
832
833    /// Set the maximum width constraint in cells.
834    pub fn max_width(mut self, value: u32) -> Self {
835        self.constraints.max_width = Some(value);
836        self
837    }
838
839    /// Set the minimum height constraint in rows.
840    pub fn min_height(mut self, value: u32) -> Self {
841        self.constraints.min_height = Some(value);
842        self
843    }
844
845    /// Set the maximum height constraint in rows.
846    pub fn max_height(mut self, value: u32) -> Self {
847        self.constraints.max_height = Some(value);
848        self
849    }
850
851    /// Set all size constraints at once using a [`Constraints`] value.
852    pub fn constraints(mut self, constraints: Constraints) -> Self {
853        self.constraints = constraints;
854        self
855    }
856
857    // ── flex ─────────────────────────────────────────────────────────
858
859    /// Set the gap (in cells) between child elements.
860    pub fn gap(mut self, gap: u32) -> Self {
861        self.gap = gap;
862        self
863    }
864
865    /// Set the flex-grow factor. `1` means the container expands to fill available space.
866    pub fn grow(mut self, grow: u16) -> Self {
867        self.grow = grow;
868        self
869    }
870
871    // ── alignment ───────────────────────────────────────────────────
872
873    /// Set the cross-axis alignment of child elements.
874    pub fn align(mut self, align: Align) -> Self {
875        self.align = align;
876        self
877    }
878
879    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
880    pub fn center(self) -> Self {
881        self.align(Align::Center)
882    }
883
884    // ── title ────────────────────────────────────────────────────────
885
886    /// Set a plain-text title rendered in the top border.
887    pub fn title(self, title: impl Into<String>) -> Self {
888        self.title_styled(title, Style::new())
889    }
890
891    /// Set a styled title rendered in the top border.
892    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
893        self.title = Some((title.into(), style));
894        self
895    }
896
897    // ── internal ─────────────────────────────────────────────────────
898
899    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
900    pub fn scroll_offset(mut self, offset: u32) -> Self {
901        self.scroll_offset = Some(offset);
902        self
903    }
904
905    /// Finalize the builder as a vertical (column) container.
906    ///
907    /// The closure receives a `&mut Context` for rendering children.
908    /// Returns a [`Response`] with click/hover state for this container.
909    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
910        self.finish(Direction::Column, f)
911    }
912
913    /// Finalize the builder as a horizontal (row) container.
914    ///
915    /// The closure receives a `&mut Context` for rendering children.
916    /// Returns a [`Response`] with click/hover state for this container.
917    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
918        self.finish(Direction::Row, f)
919    }
920
921    fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
922        let interaction_id = self.ctx.interaction_count;
923        self.ctx.interaction_count += 1;
924
925        if let Some(scroll_offset) = self.scroll_offset {
926            self.ctx.commands.push(Command::BeginScrollable {
927                grow: self.grow,
928                border: self.border,
929                border_style: self.border_style,
930                padding: self.padding,
931                margin: self.margin,
932                constraints: self.constraints,
933                title: self.title,
934                scroll_offset,
935            });
936        } else {
937            self.ctx.commands.push(Command::BeginContainer {
938                direction,
939                gap: self.gap,
940                align: self.align,
941                border: self.border,
942                border_style: self.border_style,
943                padding: self.padding,
944                margin: self.margin,
945                constraints: self.constraints,
946                title: self.title,
947                grow: self.grow,
948            });
949        }
950        f(self.ctx);
951        self.ctx.commands.push(Command::EndContainer);
952        self.ctx.last_text_idx = None;
953
954        self.ctx.response_for(interaction_id)
955    }
956}
957
958impl Context {
959    #[allow(clippy::too_many_arguments)]
960    pub(crate) fn new(
961        events: Vec<Event>,
962        width: u32,
963        height: u32,
964        tick: u64,
965        mut focus_index: usize,
966        prev_focus_count: usize,
967        prev_scroll_infos: Vec<(u32, u32)>,
968        prev_hit_map: Vec<Rect>,
969        prev_focus_rects: Vec<(usize, Rect)>,
970        debug: bool,
971        theme: Theme,
972        last_mouse_pos: Option<(u32, u32)>,
973    ) -> Self {
974        let consumed = vec![false; events.len()];
975
976        let mut mouse_pos = last_mouse_pos;
977        let mut click_pos = None;
978        for event in &events {
979            if let Event::Mouse(mouse) = event {
980                mouse_pos = Some((mouse.x, mouse.y));
981                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
982                    click_pos = Some((mouse.x, mouse.y));
983                }
984            }
985        }
986
987        if let Some((mx, my)) = click_pos {
988            let mut best: Option<(usize, u64)> = None;
989            for &(fid, rect) in &prev_focus_rects {
990                if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
991                    let area = rect.width as u64 * rect.height as u64;
992                    if best.map_or(true, |(_, ba)| area < ba) {
993                        best = Some((fid, area));
994                    }
995                }
996            }
997            if let Some((fid, _)) = best {
998                focus_index = fid;
999            }
1000        }
1001
1002        Self {
1003            commands: Vec::new(),
1004            events,
1005            consumed,
1006            should_quit: false,
1007            area_width: width,
1008            area_height: height,
1009            tick,
1010            focus_index,
1011            focus_count: 0,
1012            prev_focus_count,
1013            scroll_count: 0,
1014            prev_scroll_infos,
1015            interaction_count: 0,
1016            prev_hit_map,
1017            _prev_focus_rects: prev_focus_rects,
1018            mouse_pos,
1019            click_pos,
1020            last_text_idx: None,
1021            debug,
1022            theme,
1023        }
1024    }
1025
1026    pub(crate) fn process_focus_keys(&mut self) {
1027        for (i, event) in self.events.iter().enumerate() {
1028            if let Event::Key(key) = event {
1029                if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1030                    if self.prev_focus_count > 0 {
1031                        self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1032                    }
1033                    self.consumed[i] = true;
1034                } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1035                    || key.code == KeyCode::BackTab
1036                {
1037                    if self.prev_focus_count > 0 {
1038                        self.focus_index = if self.focus_index == 0 {
1039                            self.prev_focus_count - 1
1040                        } else {
1041                            self.focus_index - 1
1042                        };
1043                    }
1044                    self.consumed[i] = true;
1045                }
1046            }
1047        }
1048    }
1049
1050    /// Render a custom [`Widget`].
1051    ///
1052    /// Calls [`Widget::ui`] with this context and returns the widget's response.
1053    pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1054        w.ui(self)
1055    }
1056
1057    /// Wrap child widgets in a panic boundary.
1058    ///
1059    /// If the closure panics, the panic is caught and an error message is
1060    /// rendered in place of the children. The app continues running.
1061    ///
1062    /// # Example
1063    ///
1064    /// ```no_run
1065    /// # slt::run(|ui: &mut slt::Context| {
1066    /// ui.error_boundary(|ui| {
1067    ///     ui.text("risky widget");
1068    /// });
1069    /// # });
1070    /// ```
1071    pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1072        self.error_boundary_with(f, |ui, msg| {
1073            ui.styled(
1074                format!("⚠ Error: {msg}"),
1075                Style::new().fg(ui.theme.error).bold(),
1076            );
1077        });
1078    }
1079
1080    /// Like [`error_boundary`](Self::error_boundary), but renders a custom
1081    /// fallback instead of the default error message.
1082    ///
1083    /// The fallback closure receives the panic message as a [`String`].
1084    ///
1085    /// # Example
1086    ///
1087    /// ```no_run
1088    /// # slt::run(|ui: &mut slt::Context| {
1089    /// ui.error_boundary_with(
1090    ///     |ui| {
1091    ///         ui.text("risky widget");
1092    ///     },
1093    ///     |ui, msg| {
1094    ///         ui.text(format!("Recovered from panic: {msg}"));
1095    ///     },
1096    /// );
1097    /// # });
1098    /// ```
1099    pub fn error_boundary_with(
1100        &mut self,
1101        f: impl FnOnce(&mut Context),
1102        fallback: impl FnOnce(&mut Context, String),
1103    ) {
1104        let cmd_count = self.commands.len();
1105        let last_text_idx = self.last_text_idx;
1106
1107        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1108            f(self);
1109        }));
1110
1111        match result {
1112            Ok(()) => {}
1113            Err(panic_info) => {
1114                self.commands.truncate(cmd_count);
1115                self.last_text_idx = last_text_idx;
1116
1117                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1118                    (*s).to_string()
1119                } else if let Some(s) = panic_info.downcast_ref::<String>() {
1120                    s.clone()
1121                } else {
1122                    "widget panicked".to_string()
1123                };
1124
1125                fallback(self, msg);
1126            }
1127        }
1128    }
1129
1130    /// Allocate a click/hover interaction slot and return the [`Response`].
1131    ///
1132    /// Use this in custom widgets to detect mouse clicks and hovers without
1133    /// wrapping content in a container. Each call reserves one slot in the
1134    /// hit-test map, so the call order must be stable across frames.
1135    pub fn interaction(&mut self) -> Response {
1136        let id = self.interaction_count;
1137        self.interaction_count += 1;
1138        self.response_for(id)
1139    }
1140
1141    /// Register a widget as focusable and return whether it currently has focus.
1142    ///
1143    /// Call this in custom widgets that need keyboard focus. Each call increments
1144    /// the internal focus counter, so the call order must be stable across frames.
1145    pub fn register_focusable(&mut self) -> bool {
1146        let id = self.focus_count;
1147        self.focus_count += 1;
1148        self.commands.push(Command::FocusMarker(id));
1149        if self.prev_focus_count == 0 {
1150            return true;
1151        }
1152        self.focus_index % self.prev_focus_count == id
1153    }
1154
1155    // ── text ──────────────────────────────────────────────────────────
1156
1157    /// Render a text element. Returns `&mut Self` for style chaining.
1158    ///
1159    /// # Example
1160    ///
1161    /// ```no_run
1162    /// # slt::run(|ui: &mut slt::Context| {
1163    /// use slt::Color;
1164    /// ui.text("hello").bold().fg(Color::Cyan);
1165    /// # });
1166    /// ```
1167    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1168        let content = s.into();
1169        self.commands.push(Command::Text {
1170            content,
1171            style: Style::new(),
1172            grow: 0,
1173            align: Align::Start,
1174            wrap: false,
1175            margin: Margin::default(),
1176            constraints: Constraints::default(),
1177        });
1178        self.last_text_idx = Some(self.commands.len() - 1);
1179        self
1180    }
1181
1182    /// Render a text element with word-boundary wrapping.
1183    ///
1184    /// Long lines are broken at word boundaries to fit the container width.
1185    /// Style chaining works the same as [`Context::text`].
1186    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1187        let content = s.into();
1188        self.commands.push(Command::Text {
1189            content,
1190            style: Style::new(),
1191            grow: 0,
1192            align: Align::Start,
1193            wrap: true,
1194            margin: Margin::default(),
1195            constraints: Constraints::default(),
1196        });
1197        self.last_text_idx = Some(self.commands.len() - 1);
1198        self
1199    }
1200
1201    // ── style chain (applies to last text) ───────────────────────────
1202
1203    /// Apply bold to the last rendered text element.
1204    pub fn bold(&mut self) -> &mut Self {
1205        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1206        self
1207    }
1208
1209    /// Apply dim styling to the last rendered text element.
1210    ///
1211    /// Also sets the foreground color to the theme's `text_dim` color if no
1212    /// explicit foreground has been set.
1213    pub fn dim(&mut self) -> &mut Self {
1214        let text_dim = self.theme.text_dim;
1215        self.modify_last_style(|s| {
1216            s.modifiers |= Modifiers::DIM;
1217            if s.fg.is_none() {
1218                s.fg = Some(text_dim);
1219            }
1220        });
1221        self
1222    }
1223
1224    /// Apply italic to the last rendered text element.
1225    pub fn italic(&mut self) -> &mut Self {
1226        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1227        self
1228    }
1229
1230    /// Apply underline to the last rendered text element.
1231    pub fn underline(&mut self) -> &mut Self {
1232        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1233        self
1234    }
1235
1236    /// Apply reverse-video to the last rendered text element.
1237    pub fn reversed(&mut self) -> &mut Self {
1238        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1239        self
1240    }
1241
1242    /// Apply strikethrough to the last rendered text element.
1243    pub fn strikethrough(&mut self) -> &mut Self {
1244        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1245        self
1246    }
1247
1248    /// Set the foreground color of the last rendered text element.
1249    pub fn fg(&mut self, color: Color) -> &mut Self {
1250        self.modify_last_style(|s| s.fg = Some(color));
1251        self
1252    }
1253
1254    /// Set the background color of the last rendered text element.
1255    pub fn bg(&mut self, color: Color) -> &mut Self {
1256        self.modify_last_style(|s| s.bg = Some(color));
1257        self
1258    }
1259
1260    /// Render a text element with an explicit [`Style`] applied immediately.
1261    ///
1262    /// Equivalent to calling `text(s)` followed by style-chain methods, but
1263    /// more concise when you already have a `Style` value.
1264    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
1265        self.commands.push(Command::Text {
1266            content: s.into(),
1267            style,
1268            grow: 0,
1269            align: Align::Start,
1270            wrap: false,
1271            margin: Margin::default(),
1272            constraints: Constraints::default(),
1273        });
1274        self.last_text_idx = Some(self.commands.len() - 1);
1275        self
1276    }
1277
1278    /// Enable word-boundary wrapping on the last rendered text element.
1279    pub fn wrap(&mut self) -> &mut Self {
1280        if let Some(idx) = self.last_text_idx {
1281            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1282                *wrap = true;
1283            }
1284        }
1285        self
1286    }
1287
1288    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1289        if let Some(idx) = self.last_text_idx {
1290            if let Command::Text { style, .. } = &mut self.commands[idx] {
1291                f(style);
1292            }
1293        }
1294    }
1295
1296    // ── containers ───────────────────────────────────────────────────
1297
1298    /// Create a vertical (column) container.
1299    ///
1300    /// Children are stacked top-to-bottom. Returns a [`Response`] with
1301    /// click/hover state for the container area.
1302    ///
1303    /// # Example
1304    ///
1305    /// ```no_run
1306    /// # slt::run(|ui: &mut slt::Context| {
1307    /// ui.col(|ui| {
1308    ///     ui.text("line one");
1309    ///     ui.text("line two");
1310    /// });
1311    /// # });
1312    /// ```
1313    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1314        self.push_container(Direction::Column, 0, f)
1315    }
1316
1317    /// Create a vertical (column) container with a gap between children.
1318    ///
1319    /// `gap` is the number of blank rows inserted between each child.
1320    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1321        self.push_container(Direction::Column, gap, f)
1322    }
1323
1324    /// Create a horizontal (row) container.
1325    ///
1326    /// Children are placed left-to-right. Returns a [`Response`] with
1327    /// click/hover state for the container area.
1328    ///
1329    /// # Example
1330    ///
1331    /// ```no_run
1332    /// # slt::run(|ui: &mut slt::Context| {
1333    /// ui.row(|ui| {
1334    ///     ui.text("left");
1335    ///     ui.spacer();
1336    ///     ui.text("right");
1337    /// });
1338    /// # });
1339    /// ```
1340    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1341        self.push_container(Direction::Row, 0, f)
1342    }
1343
1344    /// Create a horizontal (row) container with a gap between children.
1345    ///
1346    /// `gap` is the number of blank columns inserted between each child.
1347    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1348        self.push_container(Direction::Row, gap, f)
1349    }
1350
1351    /// Create a container with a fluent builder.
1352    ///
1353    /// Use this for borders, padding, grow, constraints, and titles. Chain
1354    /// configuration methods on the returned [`ContainerBuilder`], then call
1355    /// `.col()` or `.row()` to finalize.
1356    ///
1357    /// # Example
1358    ///
1359    /// ```no_run
1360    /// # slt::run(|ui: &mut slt::Context| {
1361    /// use slt::Border;
1362    /// ui.container()
1363    ///     .border(Border::Rounded)
1364    ///     .pad(1)
1365    ///     .title("My Panel")
1366    ///     .col(|ui| {
1367    ///         ui.text("content");
1368    ///     });
1369    /// # });
1370    /// ```
1371    pub fn container(&mut self) -> ContainerBuilder<'_> {
1372        let border = self.theme.border;
1373        ContainerBuilder {
1374            ctx: self,
1375            gap: 0,
1376            align: Align::Start,
1377            border: None,
1378            border_style: Style::new().fg(border),
1379            padding: Padding::default(),
1380            margin: Margin::default(),
1381            constraints: Constraints::default(),
1382            title: None,
1383            grow: 0,
1384            scroll_offset: None,
1385        }
1386    }
1387
1388    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
1389    ///
1390    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
1391    /// is updated in-place with the current scroll offset and bounds.
1392    ///
1393    /// # Example
1394    ///
1395    /// ```no_run
1396    /// # use slt::widgets::ScrollState;
1397    /// # slt::run(|ui: &mut slt::Context| {
1398    /// let mut scroll = ScrollState::new();
1399    /// ui.scrollable(&mut scroll).col(|ui| {
1400    ///     for i in 0..100 {
1401    ///         ui.text(format!("Line {i}"));
1402    ///     }
1403    /// });
1404    /// # });
1405    /// ```
1406    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1407        let index = self.scroll_count;
1408        self.scroll_count += 1;
1409        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1410            state.set_bounds(ch, vh);
1411            let max = ch.saturating_sub(vh) as usize;
1412            state.offset = state.offset.min(max);
1413        }
1414
1415        let next_id = self.interaction_count;
1416        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1417            self.auto_scroll(&rect, state);
1418        }
1419
1420        self.container().scroll_offset(state.offset as u32)
1421    }
1422
1423    fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1424        let mut to_consume: Vec<usize> = Vec::new();
1425
1426        for (i, event) in self.events.iter().enumerate() {
1427            if self.consumed[i] {
1428                continue;
1429            }
1430            if let Event::Mouse(mouse) = event {
1431                let in_bounds = mouse.x >= rect.x
1432                    && mouse.x < rect.right()
1433                    && mouse.y >= rect.y
1434                    && mouse.y < rect.bottom();
1435                if !in_bounds {
1436                    continue;
1437                }
1438                match mouse.kind {
1439                    MouseKind::ScrollUp => {
1440                        state.scroll_up(1);
1441                        to_consume.push(i);
1442                    }
1443                    MouseKind::ScrollDown => {
1444                        state.scroll_down(1);
1445                        to_consume.push(i);
1446                    }
1447                    MouseKind::Drag(MouseButton::Left) => {
1448                        // Left-drag is reserved for text selection.
1449                        // Scroll via mouse wheel instead.
1450                    }
1451                    _ => {}
1452                }
1453            }
1454        }
1455
1456        for i in to_consume {
1457            self.consumed[i] = true;
1458        }
1459    }
1460
1461    /// Shortcut for `container().border(border)`.
1462    ///
1463    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
1464    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1465        self.container().border(border)
1466    }
1467
1468    fn push_container(
1469        &mut self,
1470        direction: Direction,
1471        gap: u32,
1472        f: impl FnOnce(&mut Context),
1473    ) -> Response {
1474        let interaction_id = self.interaction_count;
1475        self.interaction_count += 1;
1476        let border = self.theme.border;
1477
1478        self.commands.push(Command::BeginContainer {
1479            direction,
1480            gap,
1481            align: Align::Start,
1482            border: None,
1483            border_style: Style::new().fg(border),
1484            padding: Padding::default(),
1485            margin: Margin::default(),
1486            constraints: Constraints::default(),
1487            title: None,
1488            grow: 0,
1489        });
1490        f(self);
1491        self.commands.push(Command::EndContainer);
1492        self.last_text_idx = None;
1493
1494        self.response_for(interaction_id)
1495    }
1496
1497    fn response_for(&self, interaction_id: usize) -> Response {
1498        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1499            let clicked = self
1500                .click_pos
1501                .map(|(mx, my)| {
1502                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1503                })
1504                .unwrap_or(false);
1505            let hovered = self
1506                .mouse_pos
1507                .map(|(mx, my)| {
1508                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1509                })
1510                .unwrap_or(false);
1511            Response { clicked, hovered }
1512        } else {
1513            Response::default()
1514        }
1515    }
1516
1517    /// Set the flex-grow factor of the last rendered text element.
1518    ///
1519    /// A value of `1` causes the element to expand and fill remaining space
1520    /// along the main axis.
1521    pub fn grow(&mut self, value: u16) -> &mut Self {
1522        if let Some(idx) = self.last_text_idx {
1523            if let Command::Text { grow, .. } = &mut self.commands[idx] {
1524                *grow = value;
1525            }
1526        }
1527        self
1528    }
1529
1530    /// Set the text alignment of the last rendered text element.
1531    pub fn align(&mut self, align: Align) -> &mut Self {
1532        if let Some(idx) = self.last_text_idx {
1533            if let Command::Text {
1534                align: text_align, ..
1535            } = &mut self.commands[idx]
1536            {
1537                *text_align = align;
1538            }
1539        }
1540        self
1541    }
1542
1543    /// Render an invisible spacer that expands to fill available space.
1544    ///
1545    /// Useful for pushing siblings to opposite ends of a row or column.
1546    pub fn spacer(&mut self) -> &mut Self {
1547        self.commands.push(Command::Spacer { grow: 1 });
1548        self.last_text_idx = None;
1549        self
1550    }
1551
1552    /// Render a single-line text input. Auto-handles cursor, typing, and backspace.
1553    ///
1554    /// The widget claims focus via [`Context::register_focusable`]. When focused,
1555    /// it consumes character, backspace, arrow, Home, and End key events.
1556    ///
1557    /// # Example
1558    ///
1559    /// ```no_run
1560    /// # use slt::widgets::TextInputState;
1561    /// # slt::run(|ui: &mut slt::Context| {
1562    /// let mut input = TextInputState::with_placeholder("Search...");
1563    /// ui.text_input(&mut input);
1564    /// // input.value holds the current text
1565    /// # });
1566    /// ```
1567    pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1568        let focused = self.register_focusable();
1569        state.cursor = state.cursor.min(state.value.chars().count());
1570
1571        if focused {
1572            let mut consumed_indices = Vec::new();
1573            for (i, event) in self.events.iter().enumerate() {
1574                if let Event::Key(key) = event {
1575                    match key.code {
1576                        KeyCode::Char(ch) => {
1577                            if let Some(max) = state.max_length {
1578                                if state.value.chars().count() >= max {
1579                                    continue;
1580                                }
1581                            }
1582                            let index = byte_index_for_char(&state.value, state.cursor);
1583                            state.value.insert(index, ch);
1584                            state.cursor += 1;
1585                            consumed_indices.push(i);
1586                        }
1587                        KeyCode::Backspace => {
1588                            if state.cursor > 0 {
1589                                let start = byte_index_for_char(&state.value, state.cursor - 1);
1590                                let end = byte_index_for_char(&state.value, state.cursor);
1591                                state.value.replace_range(start..end, "");
1592                                state.cursor -= 1;
1593                            }
1594                            consumed_indices.push(i);
1595                        }
1596                        KeyCode::Left => {
1597                            state.cursor = state.cursor.saturating_sub(1);
1598                            consumed_indices.push(i);
1599                        }
1600                        KeyCode::Right => {
1601                            state.cursor = (state.cursor + 1).min(state.value.chars().count());
1602                            consumed_indices.push(i);
1603                        }
1604                        KeyCode::Home => {
1605                            state.cursor = 0;
1606                            consumed_indices.push(i);
1607                        }
1608                        KeyCode::Delete => {
1609                            let len = state.value.chars().count();
1610                            if state.cursor < len {
1611                                let start = byte_index_for_char(&state.value, state.cursor);
1612                                let end = byte_index_for_char(&state.value, state.cursor + 1);
1613                                state.value.replace_range(start..end, "");
1614                            }
1615                            consumed_indices.push(i);
1616                        }
1617                        KeyCode::End => {
1618                            state.cursor = state.value.chars().count();
1619                            consumed_indices.push(i);
1620                        }
1621                        _ => {}
1622                    }
1623                }
1624                if let Event::Paste(ref text) = event {
1625                    for ch in text.chars() {
1626                        if let Some(max) = state.max_length {
1627                            if state.value.chars().count() >= max {
1628                                break;
1629                            }
1630                        }
1631                        let index = byte_index_for_char(&state.value, state.cursor);
1632                        state.value.insert(index, ch);
1633                        state.cursor += 1;
1634                    }
1635                    consumed_indices.push(i);
1636                }
1637            }
1638
1639            for index in consumed_indices {
1640                self.consumed[index] = true;
1641            }
1642        }
1643
1644        if state.value.is_empty() {
1645            self.styled(
1646                state.placeholder.clone(),
1647                Style::new().dim().fg(self.theme.text_dim),
1648            )
1649        } else {
1650            let mut rendered = String::new();
1651            for (idx, ch) in state.value.chars().enumerate() {
1652                if focused && idx == state.cursor {
1653                    rendered.push('▎');
1654                }
1655                rendered.push(ch);
1656            }
1657            if focused && state.cursor >= state.value.chars().count() {
1658                rendered.push('▎');
1659            }
1660            self.styled(rendered, Style::new().fg(self.theme.text))
1661        }
1662    }
1663
1664    /// Render an animated spinner.
1665    ///
1666    /// The spinner advances one frame per tick. Use [`SpinnerState::dots`] or
1667    /// [`SpinnerState::line`] to create the state, then chain style methods to
1668    /// color it.
1669    pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1670        self.styled(
1671            state.frame(self.tick).to_string(),
1672            Style::new().fg(self.theme.primary),
1673        )
1674    }
1675
1676    /// Render toast notifications. Calls `state.cleanup(tick)` automatically.
1677    ///
1678    /// Expired messages are removed before rendering. If there are no active
1679    /// messages, nothing is rendered and `self` is returned unchanged.
1680    pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1681        state.cleanup(self.tick);
1682        if state.messages.is_empty() {
1683            return self;
1684        }
1685
1686        self.interaction_count += 1;
1687        self.commands.push(Command::BeginContainer {
1688            direction: Direction::Column,
1689            gap: 0,
1690            align: Align::Start,
1691            border: None,
1692            border_style: Style::new().fg(self.theme.border),
1693            padding: Padding::default(),
1694            margin: Margin::default(),
1695            constraints: Constraints::default(),
1696            title: None,
1697            grow: 0,
1698        });
1699        for message in state.messages.iter().rev() {
1700            let color = match message.level {
1701                ToastLevel::Info => self.theme.primary,
1702                ToastLevel::Success => self.theme.success,
1703                ToastLevel::Warning => self.theme.warning,
1704                ToastLevel::Error => self.theme.error,
1705            };
1706            self.styled(format!("  ● {}", message.text), Style::new().fg(color));
1707        }
1708        self.commands.push(Command::EndContainer);
1709        self.last_text_idx = None;
1710
1711        self
1712    }
1713
1714    /// Render a multi-line text area with the given number of visible rows.
1715    ///
1716    /// When focused, handles character input, Enter (new line), Backspace,
1717    /// arrow keys, Home, and End. The cursor is rendered as a block character.
1718    pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1719        if state.lines.is_empty() {
1720            state.lines.push(String::new());
1721        }
1722        state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1723        state.cursor_col = state
1724            .cursor_col
1725            .min(state.lines[state.cursor_row].chars().count());
1726
1727        let focused = self.register_focusable();
1728
1729        if focused {
1730            let mut consumed_indices = Vec::new();
1731            for (i, event) in self.events.iter().enumerate() {
1732                if let Event::Key(key) = event {
1733                    match key.code {
1734                        KeyCode::Char(ch) => {
1735                            if let Some(max) = state.max_length {
1736                                let total: usize =
1737                                    state.lines.iter().map(|line| line.chars().count()).sum();
1738                                if total >= max {
1739                                    continue;
1740                                }
1741                            }
1742                            let index = byte_index_for_char(
1743                                &state.lines[state.cursor_row],
1744                                state.cursor_col,
1745                            );
1746                            state.lines[state.cursor_row].insert(index, ch);
1747                            state.cursor_col += 1;
1748                            consumed_indices.push(i);
1749                        }
1750                        KeyCode::Enter => {
1751                            let split_index = byte_index_for_char(
1752                                &state.lines[state.cursor_row],
1753                                state.cursor_col,
1754                            );
1755                            let remainder = state.lines[state.cursor_row].split_off(split_index);
1756                            state.cursor_row += 1;
1757                            state.lines.insert(state.cursor_row, remainder);
1758                            state.cursor_col = 0;
1759                            consumed_indices.push(i);
1760                        }
1761                        KeyCode::Backspace => {
1762                            if state.cursor_col > 0 {
1763                                let start = byte_index_for_char(
1764                                    &state.lines[state.cursor_row],
1765                                    state.cursor_col - 1,
1766                                );
1767                                let end = byte_index_for_char(
1768                                    &state.lines[state.cursor_row],
1769                                    state.cursor_col,
1770                                );
1771                                state.lines[state.cursor_row].replace_range(start..end, "");
1772                                state.cursor_col -= 1;
1773                            } else if state.cursor_row > 0 {
1774                                let current = state.lines.remove(state.cursor_row);
1775                                state.cursor_row -= 1;
1776                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1777                                state.lines[state.cursor_row].push_str(&current);
1778                            }
1779                            consumed_indices.push(i);
1780                        }
1781                        KeyCode::Left => {
1782                            if state.cursor_col > 0 {
1783                                state.cursor_col -= 1;
1784                            } else if state.cursor_row > 0 {
1785                                state.cursor_row -= 1;
1786                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1787                            }
1788                            consumed_indices.push(i);
1789                        }
1790                        KeyCode::Right => {
1791                            let line_len = state.lines[state.cursor_row].chars().count();
1792                            if state.cursor_col < line_len {
1793                                state.cursor_col += 1;
1794                            } else if state.cursor_row + 1 < state.lines.len() {
1795                                state.cursor_row += 1;
1796                                state.cursor_col = 0;
1797                            }
1798                            consumed_indices.push(i);
1799                        }
1800                        KeyCode::Up => {
1801                            if state.cursor_row > 0 {
1802                                state.cursor_row -= 1;
1803                                state.cursor_col = state
1804                                    .cursor_col
1805                                    .min(state.lines[state.cursor_row].chars().count());
1806                            }
1807                            consumed_indices.push(i);
1808                        }
1809                        KeyCode::Down => {
1810                            if state.cursor_row + 1 < state.lines.len() {
1811                                state.cursor_row += 1;
1812                                state.cursor_col = state
1813                                    .cursor_col
1814                                    .min(state.lines[state.cursor_row].chars().count());
1815                            }
1816                            consumed_indices.push(i);
1817                        }
1818                        KeyCode::Home => {
1819                            state.cursor_col = 0;
1820                            consumed_indices.push(i);
1821                        }
1822                        KeyCode::Delete => {
1823                            let line_len = state.lines[state.cursor_row].chars().count();
1824                            if state.cursor_col < line_len {
1825                                let start = byte_index_for_char(
1826                                    &state.lines[state.cursor_row],
1827                                    state.cursor_col,
1828                                );
1829                                let end = byte_index_for_char(
1830                                    &state.lines[state.cursor_row],
1831                                    state.cursor_col + 1,
1832                                );
1833                                state.lines[state.cursor_row].replace_range(start..end, "");
1834                            } else if state.cursor_row + 1 < state.lines.len() {
1835                                let next = state.lines.remove(state.cursor_row + 1);
1836                                state.lines[state.cursor_row].push_str(&next);
1837                            }
1838                            consumed_indices.push(i);
1839                        }
1840                        KeyCode::End => {
1841                            state.cursor_col = state.lines[state.cursor_row].chars().count();
1842                            consumed_indices.push(i);
1843                        }
1844                        _ => {}
1845                    }
1846                }
1847                if let Event::Paste(ref text) = event {
1848                    for ch in text.chars() {
1849                        if ch == '\n' || ch == '\r' {
1850                            let split_index = byte_index_for_char(
1851                                &state.lines[state.cursor_row],
1852                                state.cursor_col,
1853                            );
1854                            let remainder = state.lines[state.cursor_row].split_off(split_index);
1855                            state.cursor_row += 1;
1856                            state.lines.insert(state.cursor_row, remainder);
1857                            state.cursor_col = 0;
1858                        } else {
1859                            if let Some(max) = state.max_length {
1860                                let total: usize =
1861                                    state.lines.iter().map(|l| l.chars().count()).sum();
1862                                if total >= max {
1863                                    break;
1864                                }
1865                            }
1866                            let index = byte_index_for_char(
1867                                &state.lines[state.cursor_row],
1868                                state.cursor_col,
1869                            );
1870                            state.lines[state.cursor_row].insert(index, ch);
1871                            state.cursor_col += 1;
1872                        }
1873                    }
1874                    consumed_indices.push(i);
1875                }
1876            }
1877
1878            for index in consumed_indices {
1879                self.consumed[index] = true;
1880            }
1881        }
1882
1883        self.interaction_count += 1;
1884        self.commands.push(Command::BeginContainer {
1885            direction: Direction::Column,
1886            gap: 0,
1887            align: Align::Start,
1888            border: None,
1889            border_style: Style::new().fg(self.theme.border),
1890            padding: Padding::default(),
1891            margin: Margin::default(),
1892            constraints: Constraints::default(),
1893            title: None,
1894            grow: 0,
1895        });
1896        for row in 0..visible_rows as usize {
1897            let line = state.lines.get(row).cloned().unwrap_or_default();
1898            let mut rendered = line.clone();
1899            let mut style = if line.is_empty() {
1900                Style::new().fg(self.theme.text_dim)
1901            } else {
1902                Style::new().fg(self.theme.text)
1903            };
1904
1905            if focused && row == state.cursor_row {
1906                rendered.clear();
1907                for (idx, ch) in line.chars().enumerate() {
1908                    if idx == state.cursor_col {
1909                        rendered.push('▎');
1910                    }
1911                    rendered.push(ch);
1912                }
1913                if state.cursor_col >= line.chars().count() {
1914                    rendered.push('▎');
1915                }
1916                style = Style::new().fg(self.theme.text);
1917            }
1918
1919            self.styled(rendered, style);
1920        }
1921        self.commands.push(Command::EndContainer);
1922        self.last_text_idx = None;
1923
1924        self
1925    }
1926
1927    /// Render a progress bar (20 chars wide). `ratio` is clamped to `0.0..=1.0`.
1928    ///
1929    /// Uses block characters (`█` filled, `░` empty). For a custom width use
1930    /// [`Context::progress_bar`].
1931    pub fn progress(&mut self, ratio: f64) -> &mut Self {
1932        self.progress_bar(ratio, 20)
1933    }
1934
1935    /// Render a progress bar with a custom character width.
1936    ///
1937    /// `ratio` is clamped to `0.0..=1.0`. `width` is the total number of
1938    /// characters rendered.
1939    pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1940        let clamped = ratio.clamp(0.0, 1.0);
1941        let filled = (clamped * width as f64).round() as u32;
1942        let empty = width.saturating_sub(filled);
1943        let mut bar = String::new();
1944        for _ in 0..filled {
1945            bar.push('█');
1946        }
1947        for _ in 0..empty {
1948            bar.push('░');
1949        }
1950        self.text(bar)
1951    }
1952
1953    /// Render a horizontal bar chart from `(label, value)` pairs.
1954    ///
1955    /// Bars are normalized against the largest value and rendered with `█` up to
1956    /// `max_width` characters.
1957    ///
1958    /// # Example
1959    ///
1960    /// ```ignore
1961    /// # slt::run(|ui: &mut slt::Context| {
1962    /// let data = [
1963    ///     ("Sales", 160.0),
1964    ///     ("Revenue", 120.0),
1965    ///     ("Users", 220.0),
1966    ///     ("Costs", 60.0),
1967    /// ];
1968    /// ui.bar_chart(&data, 24);
1969    ///
1970    /// For styled bars with per-bar colors, see [`bar_chart_styled`].
1971    /// # });
1972    /// ```
1973    pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
1974        if data.is_empty() {
1975            return self;
1976        }
1977
1978        let max_label_width = data
1979            .iter()
1980            .map(|(label, _)| UnicodeWidthStr::width(*label))
1981            .max()
1982            .unwrap_or(0);
1983        let max_value = data
1984            .iter()
1985            .map(|(_, value)| *value)
1986            .fold(f64::NEG_INFINITY, f64::max);
1987        let denom = if max_value > 0.0 { max_value } else { 1.0 };
1988
1989        self.interaction_count += 1;
1990        self.commands.push(Command::BeginContainer {
1991            direction: Direction::Column,
1992            gap: 0,
1993            align: Align::Start,
1994            border: None,
1995            border_style: Style::new().fg(self.theme.border),
1996            padding: Padding::default(),
1997            margin: Margin::default(),
1998            constraints: Constraints::default(),
1999            title: None,
2000            grow: 0,
2001        });
2002
2003        for (label, value) in data {
2004            let label_width = UnicodeWidthStr::width(*label);
2005            let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2006            let normalized = (*value / denom).clamp(0.0, 1.0);
2007            let bar_len = (normalized * max_width as f64).round() as usize;
2008            let bar = "█".repeat(bar_len);
2009
2010            self.interaction_count += 1;
2011            self.commands.push(Command::BeginContainer {
2012                direction: Direction::Row,
2013                gap: 1,
2014                align: Align::Start,
2015                border: None,
2016                border_style: Style::new().fg(self.theme.border),
2017                padding: Padding::default(),
2018                margin: Margin::default(),
2019                constraints: Constraints::default(),
2020                title: None,
2021                grow: 0,
2022            });
2023            self.styled(
2024                format!("{label}{label_padding}"),
2025                Style::new().fg(self.theme.text),
2026            );
2027            self.styled(bar, Style::new().fg(self.theme.primary));
2028            self.styled(
2029                format_compact_number(*value),
2030                Style::new().fg(self.theme.text_dim),
2031            );
2032            self.commands.push(Command::EndContainer);
2033            self.last_text_idx = None;
2034        }
2035
2036        self.commands.push(Command::EndContainer);
2037        self.last_text_idx = None;
2038
2039        self
2040    }
2041
2042    /// Render a styled bar chart with per-bar colors, grouping, and direction control.
2043    ///
2044    /// # Example
2045    /// ```ignore
2046    /// # slt::run(|ui: &mut slt::Context| {
2047    /// use slt::{Bar, Color};
2048    /// let bars = vec![
2049    ///     Bar::new("Q1", 32.0).color(Color::Cyan),
2050    ///     Bar::new("Q2", 46.0).color(Color::Green),
2051    ///     Bar::new("Q3", 28.0).color(Color::Yellow),
2052    ///     Bar::new("Q4", 54.0).color(Color::Red),
2053    /// ];
2054    /// ui.bar_chart_styled(&bars, 30, slt::BarDirection::Horizontal);
2055    /// # });
2056    /// ```
2057    pub fn bar_chart_styled(
2058        &mut self,
2059        bars: &[Bar],
2060        max_width: u32,
2061        direction: BarDirection,
2062    ) -> &mut Self {
2063        if bars.is_empty() {
2064            return self;
2065        }
2066
2067        let max_value = bars
2068            .iter()
2069            .map(|bar| bar.value)
2070            .fold(f64::NEG_INFINITY, f64::max);
2071        let denom = if max_value > 0.0 { max_value } else { 1.0 };
2072
2073        match direction {
2074            BarDirection::Horizontal => {
2075                let max_label_width = bars
2076                    .iter()
2077                    .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2078                    .max()
2079                    .unwrap_or(0);
2080
2081                self.interaction_count += 1;
2082                self.commands.push(Command::BeginContainer {
2083                    direction: Direction::Column,
2084                    gap: 0,
2085                    align: Align::Start,
2086                    border: None,
2087                    border_style: Style::new().fg(self.theme.border),
2088                    padding: Padding::default(),
2089                    margin: Margin::default(),
2090                    constraints: Constraints::default(),
2091                    title: None,
2092                    grow: 0,
2093                });
2094
2095                for bar in bars {
2096                    let label_width = UnicodeWidthStr::width(bar.label.as_str());
2097                    let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2098                    let normalized = (bar.value / denom).clamp(0.0, 1.0);
2099                    let bar_len = (normalized * max_width as f64).round() as usize;
2100                    let bar_text = "█".repeat(bar_len);
2101                    let color = bar.color.unwrap_or(self.theme.primary);
2102
2103                    self.interaction_count += 1;
2104                    self.commands.push(Command::BeginContainer {
2105                        direction: Direction::Row,
2106                        gap: 1,
2107                        align: Align::Start,
2108                        border: None,
2109                        border_style: Style::new().fg(self.theme.border),
2110                        padding: Padding::default(),
2111                        margin: Margin::default(),
2112                        constraints: Constraints::default(),
2113                        title: None,
2114                        grow: 0,
2115                    });
2116                    self.styled(
2117                        format!("{}{label_padding}", bar.label),
2118                        Style::new().fg(self.theme.text),
2119                    );
2120                    self.styled(bar_text, Style::new().fg(color));
2121                    self.styled(
2122                        format_compact_number(bar.value),
2123                        Style::new().fg(self.theme.text_dim),
2124                    );
2125                    self.commands.push(Command::EndContainer);
2126                    self.last_text_idx = None;
2127                }
2128
2129                self.commands.push(Command::EndContainer);
2130                self.last_text_idx = None;
2131            }
2132            BarDirection::Vertical => {
2133                const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
2134
2135                let chart_height = max_width.max(1) as usize;
2136                let value_labels: Vec<String> = bars
2137                    .iter()
2138                    .map(|bar| format_compact_number(bar.value))
2139                    .collect();
2140                let col_width = bars
2141                    .iter()
2142                    .zip(value_labels.iter())
2143                    .map(|(bar, value)| {
2144                        UnicodeWidthStr::width(bar.label.as_str())
2145                            .max(UnicodeWidthStr::width(value.as_str()))
2146                            .max(1)
2147                    })
2148                    .max()
2149                    .unwrap_or(1);
2150
2151                let bar_units: Vec<usize> = bars
2152                    .iter()
2153                    .map(|bar| {
2154                        let normalized = (bar.value / denom).clamp(0.0, 1.0);
2155                        (normalized * chart_height as f64 * 8.0).round() as usize
2156                    })
2157                    .collect();
2158
2159                self.interaction_count += 1;
2160                self.commands.push(Command::BeginContainer {
2161                    direction: Direction::Column,
2162                    gap: 0,
2163                    align: Align::Start,
2164                    border: None,
2165                    border_style: Style::new().fg(self.theme.border),
2166                    padding: Padding::default(),
2167                    margin: Margin::default(),
2168                    constraints: Constraints::default(),
2169                    title: None,
2170                    grow: 0,
2171                });
2172
2173                self.interaction_count += 1;
2174                self.commands.push(Command::BeginContainer {
2175                    direction: Direction::Row,
2176                    gap: 1,
2177                    align: Align::Start,
2178                    border: None,
2179                    border_style: Style::new().fg(self.theme.border),
2180                    padding: Padding::default(),
2181                    margin: Margin::default(),
2182                    constraints: Constraints::default(),
2183                    title: None,
2184                    grow: 0,
2185                });
2186                for value in &value_labels {
2187                    self.styled(
2188                        center_text(value, col_width),
2189                        Style::new().fg(self.theme.text_dim),
2190                    );
2191                }
2192                self.commands.push(Command::EndContainer);
2193                self.last_text_idx = None;
2194
2195                for row in (0..chart_height).rev() {
2196                    self.interaction_count += 1;
2197                    self.commands.push(Command::BeginContainer {
2198                        direction: Direction::Row,
2199                        gap: 1,
2200                        align: Align::Start,
2201                        border: None,
2202                        border_style: Style::new().fg(self.theme.border),
2203                        padding: Padding::default(),
2204                        margin: Margin::default(),
2205                        constraints: Constraints::default(),
2206                        title: None,
2207                        grow: 0,
2208                    });
2209
2210                    let row_base = row * 8;
2211                    for (bar, units) in bars.iter().zip(bar_units.iter()) {
2212                        let fill = if *units <= row_base {
2213                            ' '
2214                        } else {
2215                            let delta = *units - row_base;
2216                            if delta >= 8 {
2217                                '█'
2218                            } else {
2219                                FRACTION_BLOCKS[delta]
2220                            }
2221                        };
2222
2223                        self.styled(
2224                            center_text(&fill.to_string(), col_width),
2225                            Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2226                        );
2227                    }
2228
2229                    self.commands.push(Command::EndContainer);
2230                    self.last_text_idx = None;
2231                }
2232
2233                self.interaction_count += 1;
2234                self.commands.push(Command::BeginContainer {
2235                    direction: Direction::Row,
2236                    gap: 1,
2237                    align: Align::Start,
2238                    border: None,
2239                    border_style: Style::new().fg(self.theme.border),
2240                    padding: Padding::default(),
2241                    margin: Margin::default(),
2242                    constraints: Constraints::default(),
2243                    title: None,
2244                    grow: 0,
2245                });
2246                for bar in bars {
2247                    self.styled(
2248                        center_text(&bar.label, col_width),
2249                        Style::new().fg(self.theme.text),
2250                    );
2251                }
2252                self.commands.push(Command::EndContainer);
2253                self.last_text_idx = None;
2254
2255                self.commands.push(Command::EndContainer);
2256                self.last_text_idx = None;
2257            }
2258        }
2259
2260        self
2261    }
2262
2263    /// Render a grouped bar chart.
2264    ///
2265    /// Each group contains multiple bars rendered side by side. Useful for
2266    /// comparing categories across groups (e.g., quarterly revenue by product).
2267    ///
2268    /// # Example
2269    /// ```ignore
2270    /// # slt::run(|ui: &mut slt::Context| {
2271    /// use slt::{Bar, BarGroup, Color};
2272    /// let groups = vec![
2273    ///     BarGroup::new("2023", vec![Bar::new("Rev", 100.0).color(Color::Cyan), Bar::new("Cost", 60.0).color(Color::Red)]),
2274    ///     BarGroup::new("2024", vec![Bar::new("Rev", 140.0).color(Color::Cyan), Bar::new("Cost", 80.0).color(Color::Red)]),
2275    /// ];
2276    /// ui.bar_chart_grouped(&groups, 40);
2277    /// # });
2278    /// ```
2279    pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
2280        if groups.is_empty() {
2281            return self;
2282        }
2283
2284        let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
2285        if all_bars.is_empty() {
2286            return self;
2287        }
2288
2289        let max_label_width = all_bars
2290            .iter()
2291            .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2292            .max()
2293            .unwrap_or(0);
2294        let max_value = all_bars
2295            .iter()
2296            .map(|bar| bar.value)
2297            .fold(f64::NEG_INFINITY, f64::max);
2298        let denom = if max_value > 0.0 { max_value } else { 1.0 };
2299
2300        self.interaction_count += 1;
2301        self.commands.push(Command::BeginContainer {
2302            direction: Direction::Column,
2303            gap: 1,
2304            align: Align::Start,
2305            border: None,
2306            border_style: Style::new().fg(self.theme.border),
2307            padding: Padding::default(),
2308            margin: Margin::default(),
2309            constraints: Constraints::default(),
2310            title: None,
2311            grow: 0,
2312        });
2313
2314        for group in groups {
2315            self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
2316
2317            for bar in &group.bars {
2318                let label_width = UnicodeWidthStr::width(bar.label.as_str());
2319                let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2320                let normalized = (bar.value / denom).clamp(0.0, 1.0);
2321                let bar_len = (normalized * max_width as f64).round() as usize;
2322                let bar_text = "█".repeat(bar_len);
2323
2324                self.interaction_count += 1;
2325                self.commands.push(Command::BeginContainer {
2326                    direction: Direction::Row,
2327                    gap: 1,
2328                    align: Align::Start,
2329                    border: None,
2330                    border_style: Style::new().fg(self.theme.border),
2331                    padding: Padding::default(),
2332                    margin: Margin::default(),
2333                    constraints: Constraints::default(),
2334                    title: None,
2335                    grow: 0,
2336                });
2337                self.styled(
2338                    format!("  {}{label_padding}", bar.label),
2339                    Style::new().fg(self.theme.text),
2340                );
2341                self.styled(
2342                    bar_text,
2343                    Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2344                );
2345                self.styled(
2346                    format_compact_number(bar.value),
2347                    Style::new().fg(self.theme.text_dim),
2348                );
2349                self.commands.push(Command::EndContainer);
2350                self.last_text_idx = None;
2351            }
2352        }
2353
2354        self.commands.push(Command::EndContainer);
2355        self.last_text_idx = None;
2356
2357        self
2358    }
2359
2360    /// Render a single-line sparkline from numeric data.
2361    ///
2362    /// Uses the last `width` points (or fewer if the data is shorter) and maps
2363    /// each point to one of `▁▂▃▄▅▆▇█`.
2364    ///
2365    /// # Example
2366    ///
2367    /// ```ignore
2368    /// # slt::run(|ui: &mut slt::Context| {
2369    /// let samples = [12.0, 9.0, 14.0, 18.0, 16.0, 21.0, 20.0, 24.0];
2370    /// ui.sparkline(&samples, 16);
2371    ///
2372    /// For per-point colors and missing values, see [`sparkline_styled`].
2373    /// # });
2374    /// ```
2375    pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
2376        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2377
2378        let w = width as usize;
2379        let window = if data.len() > w {
2380            &data[data.len() - w..]
2381        } else {
2382            data
2383        };
2384
2385        if window.is_empty() {
2386            return self;
2387        }
2388
2389        let min = window.iter().copied().fold(f64::INFINITY, f64::min);
2390        let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2391        let range = max - min;
2392
2393        let line: String = window
2394            .iter()
2395            .map(|&value| {
2396                let normalized = if range == 0.0 {
2397                    0.5
2398                } else {
2399                    (value - min) / range
2400                };
2401                let idx = (normalized * 7.0).round() as usize;
2402                BLOCKS[idx.min(7)]
2403            })
2404            .collect();
2405
2406        self.styled(line, Style::new().fg(self.theme.primary))
2407    }
2408
2409    /// Render a sparkline with per-point colors.
2410    ///
2411    /// Each point can have its own color via `(f64, Option<Color>)` tuples.
2412    /// Use `f64::NAN` for absent values (rendered as spaces).
2413    ///
2414    /// # Example
2415    /// ```ignore
2416    /// # slt::run(|ui: &mut slt::Context| {
2417    /// use slt::Color;
2418    /// let data: Vec<(f64, Option<Color>)> = vec![
2419    ///     (12.0, Some(Color::Green)),
2420    ///     (9.0, Some(Color::Red)),
2421    ///     (14.0, Some(Color::Green)),
2422    ///     (f64::NAN, None),
2423    ///     (18.0, Some(Color::Cyan)),
2424    /// ];
2425    /// ui.sparkline_styled(&data, 16);
2426    /// # });
2427    /// ```
2428    pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
2429        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2430
2431        let w = width as usize;
2432        let window = if data.len() > w {
2433            &data[data.len() - w..]
2434        } else {
2435            data
2436        };
2437
2438        if window.is_empty() {
2439            return self;
2440        }
2441
2442        let mut finite_values = window
2443            .iter()
2444            .map(|(value, _)| *value)
2445            .filter(|value| !value.is_nan());
2446        let Some(first) = finite_values.next() else {
2447            return self.styled(
2448                " ".repeat(window.len()),
2449                Style::new().fg(self.theme.text_dim),
2450            );
2451        };
2452
2453        let mut min = first;
2454        let mut max = first;
2455        for value in finite_values {
2456            min = f64::min(min, value);
2457            max = f64::max(max, value);
2458        }
2459        let range = max - min;
2460
2461        let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
2462        for (value, color) in window {
2463            if value.is_nan() {
2464                cells.push((' ', self.theme.text_dim));
2465                continue;
2466            }
2467
2468            let normalized = if range == 0.0 {
2469                0.5
2470            } else {
2471                ((*value - min) / range).clamp(0.0, 1.0)
2472            };
2473            let idx = (normalized * 7.0).round() as usize;
2474            cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
2475        }
2476
2477        self.interaction_count += 1;
2478        self.commands.push(Command::BeginContainer {
2479            direction: Direction::Row,
2480            gap: 0,
2481            align: Align::Start,
2482            border: None,
2483            border_style: Style::new().fg(self.theme.border),
2484            padding: Padding::default(),
2485            margin: Margin::default(),
2486            constraints: Constraints::default(),
2487            title: None,
2488            grow: 0,
2489        });
2490
2491        let mut seg = String::new();
2492        let mut seg_color = cells[0].1;
2493        for (ch, color) in cells {
2494            if color != seg_color {
2495                self.styled(seg, Style::new().fg(seg_color));
2496                seg = String::new();
2497                seg_color = color;
2498            }
2499            seg.push(ch);
2500        }
2501        if !seg.is_empty() {
2502            self.styled(seg, Style::new().fg(seg_color));
2503        }
2504
2505        self.commands.push(Command::EndContainer);
2506        self.last_text_idx = None;
2507
2508        self
2509    }
2510
2511    /// Render a multi-row line chart using braille characters.
2512    ///
2513    /// `width` and `height` are terminal cell dimensions. Internally this uses
2514    /// braille dot resolution (`width*2` x `height*4`) for smoother plotting.
2515    ///
2516    /// # Example
2517    ///
2518    /// ```ignore
2519    /// # slt::run(|ui: &mut slt::Context| {
2520    /// let data = [1.0, 3.0, 2.0, 5.0, 4.0, 6.0, 3.0, 7.0];
2521    /// ui.line_chart(&data, 40, 8);
2522    /// # });
2523    /// ```
2524    pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2525        if data.is_empty() || width == 0 || height == 0 {
2526            return self;
2527        }
2528
2529        let cols = width as usize;
2530        let rows = height as usize;
2531        let px_w = cols * 2;
2532        let px_h = rows * 4;
2533
2534        let min = data.iter().copied().fold(f64::INFINITY, f64::min);
2535        let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2536        let range = if (max - min).abs() < f64::EPSILON {
2537            1.0
2538        } else {
2539            max - min
2540        };
2541
2542        let points: Vec<usize> = (0..px_w)
2543            .map(|px| {
2544                let data_idx = if px_w <= 1 {
2545                    0.0
2546                } else {
2547                    px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
2548                };
2549                let idx = data_idx.floor() as usize;
2550                let frac = data_idx - idx as f64;
2551                let value = if idx + 1 < data.len() {
2552                    data[idx] * (1.0 - frac) + data[idx + 1] * frac
2553                } else {
2554                    data[idx.min(data.len() - 1)]
2555                };
2556
2557                let normalized = (value - min) / range;
2558                let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
2559                py.min(px_h - 1)
2560            })
2561            .collect();
2562
2563        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
2564        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
2565
2566        let mut grid = vec![vec![0u32; cols]; rows];
2567
2568        for i in 0..points.len() {
2569            let px = i;
2570            let py = points[i];
2571            let char_col = px / 2;
2572            let char_row = py / 4;
2573            let sub_col = px % 2;
2574            let sub_row = py % 4;
2575
2576            if char_col < cols && char_row < rows {
2577                grid[char_row][char_col] |= if sub_col == 0 {
2578                    LEFT_BITS[sub_row]
2579                } else {
2580                    RIGHT_BITS[sub_row]
2581                };
2582            }
2583
2584            if i + 1 < points.len() {
2585                let py_next = points[i + 1];
2586                let (y_start, y_end) = if py <= py_next {
2587                    (py, py_next)
2588                } else {
2589                    (py_next, py)
2590                };
2591                for y in y_start..=y_end {
2592                    let cell_row = y / 4;
2593                    let sub_y = y % 4;
2594                    if char_col < cols && cell_row < rows {
2595                        grid[cell_row][char_col] |= if sub_col == 0 {
2596                            LEFT_BITS[sub_y]
2597                        } else {
2598                            RIGHT_BITS[sub_y]
2599                        };
2600                    }
2601                }
2602            }
2603        }
2604
2605        let style = Style::new().fg(self.theme.primary);
2606        for row in grid {
2607            let line: String = row
2608                .iter()
2609                .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
2610                .collect();
2611            self.styled(line, style);
2612        }
2613
2614        self
2615    }
2616
2617    /// Render a braille drawing canvas.
2618    ///
2619    /// The closure receives a [`CanvasContext`] for pixel-level drawing. Each
2620    /// terminal cell maps to a 2x4 braille dot matrix, giving `width*2` x
2621    /// `height*4` pixel resolution.
2622    ///
2623    /// # Example
2624    ///
2625    /// ```ignore
2626    /// # slt::run(|ui: &mut slt::Context| {
2627    /// ui.canvas(40, 10, |cv| {
2628    ///     cv.line(0, 0, cv.width() - 1, cv.height() - 1);
2629    ///     cv.circle(40, 20, 15);
2630    /// });
2631    /// # });
2632    /// ```
2633    pub fn canvas(
2634        &mut self,
2635        width: u32,
2636        height: u32,
2637        draw: impl FnOnce(&mut CanvasContext),
2638    ) -> &mut Self {
2639        if width == 0 || height == 0 {
2640            return self;
2641        }
2642
2643        let mut canvas = CanvasContext::new(width as usize, height as usize);
2644        draw(&mut canvas);
2645
2646        for segments in canvas.render() {
2647            self.interaction_count += 1;
2648            self.commands.push(Command::BeginContainer {
2649                direction: Direction::Row,
2650                gap: 0,
2651                align: Align::Start,
2652                border: None,
2653                border_style: Style::new(),
2654                padding: Padding::default(),
2655                margin: Margin::default(),
2656                constraints: Constraints::default(),
2657                title: None,
2658                grow: 0,
2659            });
2660            for (text, color) in segments {
2661                let c = if color == Color::Reset {
2662                    self.theme.primary
2663                } else {
2664                    color
2665                };
2666                self.styled(text, Style::new().fg(c));
2667            }
2668            self.commands.push(Command::EndContainer);
2669            self.last_text_idx = None;
2670        }
2671
2672        self
2673    }
2674
2675    /// Render a multi-series chart with axes, legend, and auto-scaling.
2676    pub fn chart(
2677        &mut self,
2678        configure: impl FnOnce(&mut ChartBuilder),
2679        width: u32,
2680        height: u32,
2681    ) -> &mut Self {
2682        if width == 0 || height == 0 {
2683            return self;
2684        }
2685
2686        let axis_style = Style::new().fg(self.theme.text_dim);
2687        let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
2688        configure(&mut builder);
2689
2690        let config = builder.build();
2691        let rows = render_chart(&config);
2692
2693        for row in rows {
2694            self.interaction_count += 1;
2695            self.commands.push(Command::BeginContainer {
2696                direction: Direction::Row,
2697                gap: 0,
2698                align: Align::Start,
2699                border: None,
2700                border_style: Style::new().fg(self.theme.border),
2701                padding: Padding::default(),
2702                margin: Margin::default(),
2703                constraints: Constraints::default(),
2704                title: None,
2705                grow: 0,
2706            });
2707            for (text, style) in row.segments {
2708                self.styled(text, style);
2709            }
2710            self.commands.push(Command::EndContainer);
2711            self.last_text_idx = None;
2712        }
2713
2714        self
2715    }
2716
2717    /// Render a histogram from raw data with auto-binning.
2718    pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2719        self.histogram_with(data, |_| {}, width, height)
2720    }
2721
2722    /// Render a histogram with configuration options.
2723    pub fn histogram_with(
2724        &mut self,
2725        data: &[f64],
2726        configure: impl FnOnce(&mut HistogramBuilder),
2727        width: u32,
2728        height: u32,
2729    ) -> &mut Self {
2730        if width == 0 || height == 0 {
2731            return self;
2732        }
2733
2734        let mut options = HistogramBuilder::default();
2735        configure(&mut options);
2736        let axis_style = Style::new().fg(self.theme.text_dim);
2737        let config = build_histogram_config(data, &options, width, height, axis_style);
2738        let rows = render_chart(&config);
2739
2740        for row in rows {
2741            self.interaction_count += 1;
2742            self.commands.push(Command::BeginContainer {
2743                direction: Direction::Row,
2744                gap: 0,
2745                align: Align::Start,
2746                border: None,
2747                border_style: Style::new().fg(self.theme.border),
2748                padding: Padding::default(),
2749                margin: Margin::default(),
2750                constraints: Constraints::default(),
2751                title: None,
2752                grow: 0,
2753            });
2754            for (text, style) in row.segments {
2755                self.styled(text, style);
2756            }
2757            self.commands.push(Command::EndContainer);
2758            self.last_text_idx = None;
2759        }
2760
2761        self
2762    }
2763
2764    /// Render children in a fixed grid with the given number of columns.
2765    ///
2766    /// Children are placed left-to-right, top-to-bottom. Each cell has equal
2767    /// width (`area_width / cols`). Rows wrap automatically.
2768    ///
2769    /// # Example
2770    ///
2771    /// ```no_run
2772    /// # slt::run(|ui: &mut slt::Context| {
2773    /// ui.grid(3, |ui| {
2774    ///     for i in 0..9 {
2775    ///         ui.text(format!("Cell {i}"));
2776    ///     }
2777    /// });
2778    /// # });
2779    /// ```
2780    pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
2781        let interaction_id = self.interaction_count;
2782        self.interaction_count += 1;
2783        let border = self.theme.border;
2784
2785        self.commands.push(Command::BeginContainer {
2786            direction: Direction::Column,
2787            gap: 0,
2788            align: Align::Start,
2789            border: None,
2790            border_style: Style::new().fg(border),
2791            padding: Padding::default(),
2792            margin: Margin::default(),
2793            constraints: Constraints::default(),
2794            title: None,
2795            grow: 0,
2796        });
2797
2798        let children_start = self.commands.len();
2799        f(self);
2800        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
2801
2802        let mut elements: Vec<Vec<Command>> = Vec::new();
2803        let mut iter = child_commands.into_iter().peekable();
2804        while let Some(cmd) = iter.next() {
2805            match cmd {
2806                Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2807                    let mut depth = 1_u32;
2808                    let mut element = vec![cmd];
2809                    for next in iter.by_ref() {
2810                        match next {
2811                            Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2812                                depth += 1;
2813                            }
2814                            Command::EndContainer => {
2815                                depth = depth.saturating_sub(1);
2816                            }
2817                            _ => {}
2818                        }
2819                        let at_end = matches!(next, Command::EndContainer) && depth == 0;
2820                        element.push(next);
2821                        if at_end {
2822                            break;
2823                        }
2824                    }
2825                    elements.push(element);
2826                }
2827                Command::EndContainer => {}
2828                _ => elements.push(vec![cmd]),
2829            }
2830        }
2831
2832        let cols = cols.max(1) as usize;
2833        for row in elements.chunks(cols) {
2834            self.interaction_count += 1;
2835            self.commands.push(Command::BeginContainer {
2836                direction: Direction::Row,
2837                gap: 0,
2838                align: Align::Start,
2839                border: None,
2840                border_style: Style::new().fg(border),
2841                padding: Padding::default(),
2842                margin: Margin::default(),
2843                constraints: Constraints::default(),
2844                title: None,
2845                grow: 0,
2846            });
2847
2848            for element in row {
2849                self.interaction_count += 1;
2850                self.commands.push(Command::BeginContainer {
2851                    direction: Direction::Column,
2852                    gap: 0,
2853                    align: Align::Start,
2854                    border: None,
2855                    border_style: Style::new().fg(border),
2856                    padding: Padding::default(),
2857                    margin: Margin::default(),
2858                    constraints: Constraints::default(),
2859                    title: None,
2860                    grow: 1,
2861                });
2862                self.commands.extend(element.iter().cloned());
2863                self.commands.push(Command::EndContainer);
2864            }
2865
2866            self.commands.push(Command::EndContainer);
2867        }
2868
2869        self.commands.push(Command::EndContainer);
2870        self.last_text_idx = None;
2871
2872        self.response_for(interaction_id)
2873    }
2874
2875    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
2876    ///
2877    /// The selected item is highlighted with the theme's primary color. If the
2878    /// list is empty, nothing is rendered.
2879    pub fn list(&mut self, state: &mut ListState) -> &mut Self {
2880        if state.items.is_empty() {
2881            state.selected = 0;
2882            return self;
2883        }
2884
2885        state.selected = state.selected.min(state.items.len().saturating_sub(1));
2886
2887        let focused = self.register_focusable();
2888
2889        if focused {
2890            let mut consumed_indices = Vec::new();
2891            for (i, event) in self.events.iter().enumerate() {
2892                if let Event::Key(key) = event {
2893                    match key.code {
2894                        KeyCode::Up | KeyCode::Char('k') => {
2895                            state.selected = state.selected.saturating_sub(1);
2896                            consumed_indices.push(i);
2897                        }
2898                        KeyCode::Down | KeyCode::Char('j') => {
2899                            state.selected =
2900                                (state.selected + 1).min(state.items.len().saturating_sub(1));
2901                            consumed_indices.push(i);
2902                        }
2903                        _ => {}
2904                    }
2905                }
2906            }
2907
2908            for index in consumed_indices {
2909                self.consumed[index] = true;
2910            }
2911        }
2912
2913        for (idx, item) in state.items.iter().enumerate() {
2914            if idx == state.selected {
2915                if focused {
2916                    self.styled(
2917                        format!("▸ {item}"),
2918                        Style::new().bold().fg(self.theme.primary),
2919                    );
2920                } else {
2921                    self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
2922                }
2923            } else {
2924                self.styled(format!("  {item}"), Style::new().fg(self.theme.text));
2925            }
2926        }
2927
2928        self
2929    }
2930
2931    /// Render a data table with column headers. Handles Up/Down selection when focused.
2932    ///
2933    /// Column widths are computed automatically from header and cell content.
2934    /// The selected row is highlighted with the theme's selection colors.
2935    pub fn table(&mut self, state: &mut TableState) -> &mut Self {
2936        if state.is_dirty() {
2937            state.recompute_widths();
2938        }
2939
2940        let focused = self.register_focusable();
2941
2942        if focused && !state.rows.is_empty() {
2943            let mut consumed_indices = Vec::new();
2944            for (i, event) in self.events.iter().enumerate() {
2945                if let Event::Key(key) = event {
2946                    match key.code {
2947                        KeyCode::Up | KeyCode::Char('k') => {
2948                            state.selected = state.selected.saturating_sub(1);
2949                            consumed_indices.push(i);
2950                        }
2951                        KeyCode::Down | KeyCode::Char('j') => {
2952                            state.selected =
2953                                (state.selected + 1).min(state.rows.len().saturating_sub(1));
2954                            consumed_indices.push(i);
2955                        }
2956                        _ => {}
2957                    }
2958                }
2959            }
2960            for index in consumed_indices {
2961                self.consumed[index] = true;
2962            }
2963        }
2964
2965        state.selected = state.selected.min(state.rows.len().saturating_sub(1));
2966
2967        let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
2968        self.styled(header_line, Style::new().bold().fg(self.theme.text));
2969
2970        let separator = state
2971            .column_widths()
2972            .iter()
2973            .map(|w| "─".repeat(*w as usize))
2974            .collect::<Vec<_>>()
2975            .join("─┼─");
2976        self.text(separator);
2977
2978        for (idx, row) in state.rows.iter().enumerate() {
2979            let line = format_table_row(row, state.column_widths(), " │ ");
2980            if idx == state.selected {
2981                let mut style = Style::new()
2982                    .bg(self.theme.selected_bg)
2983                    .fg(self.theme.selected_fg);
2984                if focused {
2985                    style = style.bold();
2986                }
2987                self.styled(line, style);
2988            } else {
2989                self.styled(line, Style::new().fg(self.theme.text));
2990            }
2991        }
2992
2993        self
2994    }
2995
2996    /// Render a tab bar. Handles Left/Right navigation when focused.
2997    ///
2998    /// The active tab is rendered in the theme's primary color. If the labels
2999    /// list is empty, nothing is rendered.
3000    pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
3001        if state.labels.is_empty() {
3002            state.selected = 0;
3003            return self;
3004        }
3005
3006        state.selected = state.selected.min(state.labels.len().saturating_sub(1));
3007        let focused = self.register_focusable();
3008
3009        if focused {
3010            let mut consumed_indices = Vec::new();
3011            for (i, event) in self.events.iter().enumerate() {
3012                if let Event::Key(key) = event {
3013                    match key.code {
3014                        KeyCode::Left => {
3015                            state.selected = if state.selected == 0 {
3016                                state.labels.len().saturating_sub(1)
3017                            } else {
3018                                state.selected - 1
3019                            };
3020                            consumed_indices.push(i);
3021                        }
3022                        KeyCode::Right => {
3023                            state.selected = (state.selected + 1) % state.labels.len();
3024                            consumed_indices.push(i);
3025                        }
3026                        _ => {}
3027                    }
3028                }
3029            }
3030
3031            for index in consumed_indices {
3032                self.consumed[index] = true;
3033            }
3034        }
3035
3036        self.interaction_count += 1;
3037        self.commands.push(Command::BeginContainer {
3038            direction: Direction::Row,
3039            gap: 1,
3040            align: Align::Start,
3041            border: None,
3042            border_style: Style::new().fg(self.theme.border),
3043            padding: Padding::default(),
3044            margin: Margin::default(),
3045            constraints: Constraints::default(),
3046            title: None,
3047            grow: 0,
3048        });
3049        for (idx, label) in state.labels.iter().enumerate() {
3050            let style = if idx == state.selected {
3051                let s = Style::new().fg(self.theme.primary).bold();
3052                if focused {
3053                    s.underline()
3054                } else {
3055                    s
3056                }
3057            } else {
3058                Style::new().fg(self.theme.text_dim)
3059            };
3060            self.styled(format!("[ {label} ]"), style);
3061        }
3062        self.commands.push(Command::EndContainer);
3063        self.last_text_idx = None;
3064
3065        self
3066    }
3067
3068    /// Render a clickable button. Returns `true` when activated via Enter, Space, or mouse click.
3069    ///
3070    /// The button is styled with the theme's primary color when focused and the
3071    /// accent color when hovered.
3072    pub fn button(&mut self, label: impl Into<String>) -> bool {
3073        let focused = self.register_focusable();
3074        let interaction_id = self.interaction_count;
3075        self.interaction_count += 1;
3076        let response = self.response_for(interaction_id);
3077
3078        let mut activated = response.clicked;
3079        if focused {
3080            let mut consumed_indices = Vec::new();
3081            for (i, event) in self.events.iter().enumerate() {
3082                if let Event::Key(key) = event {
3083                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3084                        activated = true;
3085                        consumed_indices.push(i);
3086                    }
3087                }
3088            }
3089
3090            for index in consumed_indices {
3091                self.consumed[index] = true;
3092            }
3093        }
3094
3095        let style = if focused {
3096            Style::new().fg(self.theme.primary).bold()
3097        } else if response.hovered {
3098            Style::new().fg(self.theme.accent)
3099        } else {
3100            Style::new().fg(self.theme.text)
3101        };
3102
3103        self.commands.push(Command::BeginContainer {
3104            direction: Direction::Row,
3105            gap: 0,
3106            align: Align::Start,
3107            border: None,
3108            border_style: Style::new().fg(self.theme.border),
3109            padding: Padding::default(),
3110            margin: Margin::default(),
3111            constraints: Constraints::default(),
3112            title: None,
3113            grow: 0,
3114        });
3115        self.styled(format!("[ {} ]", label.into()), style);
3116        self.commands.push(Command::EndContainer);
3117        self.last_text_idx = None;
3118
3119        activated
3120    }
3121
3122    /// Render a checkbox. Toggles the bool on Enter, Space, or click.
3123    ///
3124    /// The checked state is shown with the theme's success color. When focused,
3125    /// a `▸` prefix is added.
3126    pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
3127        let focused = self.register_focusable();
3128        let interaction_id = self.interaction_count;
3129        self.interaction_count += 1;
3130        let response = self.response_for(interaction_id);
3131        let mut should_toggle = response.clicked;
3132
3133        if focused {
3134            let mut consumed_indices = Vec::new();
3135            for (i, event) in self.events.iter().enumerate() {
3136                if let Event::Key(key) = event {
3137                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3138                        should_toggle = true;
3139                        consumed_indices.push(i);
3140                    }
3141                }
3142            }
3143
3144            for index in consumed_indices {
3145                self.consumed[index] = true;
3146            }
3147        }
3148
3149        if should_toggle {
3150            *checked = !*checked;
3151        }
3152
3153        self.commands.push(Command::BeginContainer {
3154            direction: Direction::Row,
3155            gap: 1,
3156            align: Align::Start,
3157            border: None,
3158            border_style: Style::new().fg(self.theme.border),
3159            padding: Padding::default(),
3160            margin: Margin::default(),
3161            constraints: Constraints::default(),
3162            title: None,
3163            grow: 0,
3164        });
3165        let marker_style = if *checked {
3166            Style::new().fg(self.theme.success)
3167        } else {
3168            Style::new().fg(self.theme.text_dim)
3169        };
3170        let marker = if *checked { "[x]" } else { "[ ]" };
3171        let label_text = label.into();
3172        if focused {
3173            self.styled(format!("▸ {marker}"), marker_style.bold());
3174            self.styled(label_text, Style::new().fg(self.theme.text).bold());
3175        } else {
3176            self.styled(marker, marker_style);
3177            self.styled(label_text, Style::new().fg(self.theme.text));
3178        }
3179        self.commands.push(Command::EndContainer);
3180        self.last_text_idx = None;
3181
3182        self
3183    }
3184
3185    /// Render an on/off toggle switch.
3186    ///
3187    /// Toggles `on` when activated via Enter, Space, or click. The switch
3188    /// renders as `●━━ ON` or `━━● OFF` colored with the theme's success or
3189    /// dim color respectively.
3190    pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
3191        let focused = self.register_focusable();
3192        let interaction_id = self.interaction_count;
3193        self.interaction_count += 1;
3194        let response = self.response_for(interaction_id);
3195        let mut should_toggle = response.clicked;
3196
3197        if focused {
3198            let mut consumed_indices = Vec::new();
3199            for (i, event) in self.events.iter().enumerate() {
3200                if let Event::Key(key) = event {
3201                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3202                        should_toggle = true;
3203                        consumed_indices.push(i);
3204                    }
3205                }
3206            }
3207
3208            for index in consumed_indices {
3209                self.consumed[index] = true;
3210            }
3211        }
3212
3213        if should_toggle {
3214            *on = !*on;
3215        }
3216
3217        self.commands.push(Command::BeginContainer {
3218            direction: Direction::Row,
3219            gap: 2,
3220            align: Align::Start,
3221            border: None,
3222            border_style: Style::new().fg(self.theme.border),
3223            padding: Padding::default(),
3224            margin: Margin::default(),
3225            constraints: Constraints::default(),
3226            title: None,
3227            grow: 0,
3228        });
3229        let label_text = label.into();
3230        let switch = if *on { "●━━ ON" } else { "━━● OFF" };
3231        let switch_style = if *on {
3232            Style::new().fg(self.theme.success)
3233        } else {
3234            Style::new().fg(self.theme.text_dim)
3235        };
3236        if focused {
3237            self.styled(
3238                format!("▸ {label_text}"),
3239                Style::new().fg(self.theme.text).bold(),
3240            );
3241            self.styled(switch, switch_style.bold());
3242        } else {
3243            self.styled(label_text, Style::new().fg(self.theme.text));
3244            self.styled(switch, switch_style);
3245        }
3246        self.commands.push(Command::EndContainer);
3247        self.last_text_idx = None;
3248
3249        self
3250    }
3251
3252    /// Render a horizontal divider line.
3253    ///
3254    /// The line is drawn with the theme's border color and expands to fill the
3255    /// container width.
3256    pub fn separator(&mut self) -> &mut Self {
3257        self.commands.push(Command::Text {
3258            content: "─".repeat(200),
3259            style: Style::new().fg(self.theme.border).dim(),
3260            grow: 0,
3261            align: Align::Start,
3262            wrap: false,
3263            margin: Margin::default(),
3264            constraints: Constraints::default(),
3265        });
3266        self.last_text_idx = Some(self.commands.len() - 1);
3267        self
3268    }
3269
3270    /// Render a help bar showing keybinding hints.
3271    ///
3272    /// `bindings` is a slice of `(key, action)` pairs. Keys are rendered in the
3273    /// theme's primary color; actions in the dim text color. Pairs are separated
3274    /// by a `·` character.
3275    pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
3276        if bindings.is_empty() {
3277            return self;
3278        }
3279
3280        self.interaction_count += 1;
3281        self.commands.push(Command::BeginContainer {
3282            direction: Direction::Row,
3283            gap: 2,
3284            align: Align::Start,
3285            border: None,
3286            border_style: Style::new().fg(self.theme.border),
3287            padding: Padding::default(),
3288            margin: Margin::default(),
3289            constraints: Constraints::default(),
3290            title: None,
3291            grow: 0,
3292        });
3293        for (idx, (key, action)) in bindings.iter().enumerate() {
3294            if idx > 0 {
3295                self.styled("·", Style::new().fg(self.theme.text_dim));
3296            }
3297            self.styled(*key, Style::new().bold().fg(self.theme.primary));
3298            self.styled(*action, Style::new().fg(self.theme.text_dim));
3299        }
3300        self.commands.push(Command::EndContainer);
3301        self.last_text_idx = None;
3302
3303        self
3304    }
3305
3306    // ── events ───────────────────────────────────────────────────────
3307
3308    /// Check if a character key was pressed this frame.
3309    ///
3310    /// Returns `true` if the key event has not been consumed by another widget.
3311    pub fn key(&self, c: char) -> bool {
3312        self.events.iter().enumerate().any(|(i, e)| {
3313            !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
3314        })
3315    }
3316
3317    /// Check if a specific key code was pressed this frame.
3318    ///
3319    /// Returns `true` if the key event has not been consumed by another widget.
3320    pub fn key_code(&self, code: KeyCode) -> bool {
3321        self.events
3322            .iter()
3323            .enumerate()
3324            .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
3325    }
3326
3327    /// Check if a character key with specific modifiers was pressed this frame.
3328    ///
3329    /// Returns `true` if the key event has not been consumed by another widget.
3330    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3331        self.events.iter().enumerate().any(|(i, e)| {
3332            !self.consumed[i]
3333                && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3334        })
3335    }
3336
3337    /// Return the position of a left mouse button down event this frame, if any.
3338    ///
3339    /// Returns `None` if no unconsumed mouse-down event occurred.
3340    pub fn mouse_down(&self) -> Option<(u32, u32)> {
3341        self.events.iter().enumerate().find_map(|(i, event)| {
3342            if self.consumed[i] {
3343                return None;
3344            }
3345            if let Event::Mouse(mouse) = event {
3346                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3347                    return Some((mouse.x, mouse.y));
3348                }
3349            }
3350            None
3351        })
3352    }
3353
3354    /// Return the current mouse cursor position, if known.
3355    ///
3356    /// The position is updated on every mouse move or click event. Returns
3357    /// `None` until the first mouse event is received.
3358    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3359        self.mouse_pos
3360    }
3361
3362    /// Return the first unconsumed paste event text, if any.
3363    pub fn paste(&self) -> Option<&str> {
3364        self.events.iter().enumerate().find_map(|(i, event)| {
3365            if self.consumed[i] {
3366                return None;
3367            }
3368            if let Event::Paste(ref text) = event {
3369                return Some(text.as_str());
3370            }
3371            None
3372        })
3373    }
3374
3375    /// Check if an unconsumed scroll-up event occurred this frame.
3376    pub fn scroll_up(&self) -> bool {
3377        self.events.iter().enumerate().any(|(i, event)| {
3378            !self.consumed[i]
3379                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3380        })
3381    }
3382
3383    /// Check if an unconsumed scroll-down event occurred this frame.
3384    pub fn scroll_down(&self) -> bool {
3385        self.events.iter().enumerate().any(|(i, event)| {
3386            !self.consumed[i]
3387                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3388        })
3389    }
3390
3391    /// Signal the run loop to exit after this frame.
3392    pub fn quit(&mut self) {
3393        self.should_quit = true;
3394    }
3395
3396    /// Get the current theme.
3397    pub fn theme(&self) -> &Theme {
3398        &self.theme
3399    }
3400
3401    /// Change the theme for subsequent rendering.
3402    ///
3403    /// All widgets rendered after this call will use the new theme's colors.
3404    pub fn set_theme(&mut self, theme: Theme) {
3405        self.theme = theme;
3406    }
3407
3408    // ── info ─────────────────────────────────────────────────────────
3409
3410    /// Get the terminal width in cells.
3411    pub fn width(&self) -> u32 {
3412        self.area_width
3413    }
3414
3415    /// Get the terminal height in cells.
3416    pub fn height(&self) -> u32 {
3417        self.area_height
3418    }
3419
3420    /// Get the current tick count (increments each frame).
3421    ///
3422    /// Useful for animations and time-based logic. The tick starts at 0 and
3423    /// increases by 1 on every rendered frame.
3424    pub fn tick(&self) -> u64 {
3425        self.tick
3426    }
3427
3428    /// Return whether the layout debugger is enabled.
3429    ///
3430    /// The debugger is toggled with F12 at runtime.
3431    pub fn debug_enabled(&self) -> bool {
3432        self.debug
3433    }
3434}
3435
3436#[inline]
3437fn byte_index_for_char(value: &str, char_index: usize) -> usize {
3438    if char_index == 0 {
3439        return 0;
3440    }
3441    value
3442        .char_indices()
3443        .nth(char_index)
3444        .map_or(value.len(), |(idx, _)| idx)
3445}
3446
3447fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
3448    let mut parts: Vec<String> = Vec::new();
3449    for (i, width) in widths.iter().enumerate() {
3450        let cell = cells.get(i).map(String::as_str).unwrap_or("");
3451        let cell_width = UnicodeWidthStr::width(cell) as u32;
3452        let padding = (*width).saturating_sub(cell_width) as usize;
3453        parts.push(format!("{cell}{}", " ".repeat(padding)));
3454    }
3455    parts.join(separator)
3456}
3457
3458fn format_compact_number(value: f64) -> String {
3459    if value.fract().abs() < f64::EPSILON {
3460        return format!("{value:.0}");
3461    }
3462
3463    let mut s = format!("{value:.2}");
3464    while s.contains('.') && s.ends_with('0') {
3465        s.pop();
3466    }
3467    if s.ends_with('.') {
3468        s.pop();
3469    }
3470    s
3471}
3472
3473fn center_text(text: &str, width: usize) -> String {
3474    let text_width = UnicodeWidthStr::width(text);
3475    if text_width >= width {
3476        return text.to_string();
3477    }
3478
3479    let total = width - text_width;
3480    let left = total / 2;
3481    let right = total - left;
3482    format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
3483}