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    mouse_pos: Option<(u32, u32)>,
186    click_pos: Option<(u32, u32)>,
187    last_mouse_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        focus_index: usize,
966        prev_focus_count: usize,
967        prev_scroll_infos: Vec<(u32, u32)>,
968        prev_hit_map: Vec<Rect>,
969        debug: bool,
970        theme: Theme,
971        last_mouse_pos: Option<(u32, u32)>,
972    ) -> Self {
973        let consumed = vec![false; events.len()];
974
975        let mut mouse_pos = last_mouse_pos;
976        let mut click_pos = None;
977        for event in &events {
978            if let Event::Mouse(mouse) = event {
979                mouse_pos = Some((mouse.x, mouse.y));
980                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
981                    click_pos = Some((mouse.x, mouse.y));
982                }
983            }
984        }
985
986        Self {
987            commands: Vec::new(),
988            events,
989            consumed,
990            should_quit: false,
991            area_width: width,
992            area_height: height,
993            tick,
994            focus_index,
995            focus_count: 0,
996            prev_focus_count,
997            scroll_count: 0,
998            prev_scroll_infos,
999            interaction_count: 0,
1000            prev_hit_map,
1001            mouse_pos,
1002            click_pos,
1003            last_mouse_pos,
1004            last_text_idx: None,
1005            debug,
1006            theme,
1007        }
1008    }
1009
1010    pub(crate) fn process_focus_keys(&mut self) {
1011        for (i, event) in self.events.iter().enumerate() {
1012            if let Event::Key(key) = event {
1013                if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1014                    if self.prev_focus_count > 0 {
1015                        self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1016                    }
1017                    self.consumed[i] = true;
1018                } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1019                    || key.code == KeyCode::BackTab
1020                {
1021                    if self.prev_focus_count > 0 {
1022                        self.focus_index = if self.focus_index == 0 {
1023                            self.prev_focus_count - 1
1024                        } else {
1025                            self.focus_index - 1
1026                        };
1027                    }
1028                    self.consumed[i] = true;
1029                }
1030            }
1031        }
1032    }
1033
1034    /// Render a custom [`Widget`].
1035    ///
1036    /// Calls [`Widget::ui`] with this context and returns the widget's response.
1037    pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1038        w.ui(self)
1039    }
1040
1041    /// Wrap child widgets in a panic boundary.
1042    ///
1043    /// If the closure panics, the panic is caught and an error message is
1044    /// rendered in place of the children. The app continues running.
1045    ///
1046    /// # Example
1047    ///
1048    /// ```no_run
1049    /// # slt::run(|ui: &mut slt::Context| {
1050    /// ui.error_boundary(|ui| {
1051    ///     ui.text("risky widget");
1052    /// });
1053    /// # });
1054    /// ```
1055    pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1056        self.error_boundary_with(f, |ui, msg| {
1057            ui.styled(
1058                format!("⚠ Error: {msg}"),
1059                Style::new().fg(ui.theme.error).bold(),
1060            );
1061        });
1062    }
1063
1064    /// Like [`error_boundary`](Self::error_boundary), but renders a custom
1065    /// fallback instead of the default error message.
1066    ///
1067    /// The fallback closure receives the panic message as a [`String`].
1068    ///
1069    /// # Example
1070    ///
1071    /// ```no_run
1072    /// # slt::run(|ui: &mut slt::Context| {
1073    /// ui.error_boundary_with(
1074    ///     |ui| {
1075    ///         ui.text("risky widget");
1076    ///     },
1077    ///     |ui, msg| {
1078    ///         ui.text(format!("Recovered from panic: {msg}"));
1079    ///     },
1080    /// );
1081    /// # });
1082    /// ```
1083    pub fn error_boundary_with(
1084        &mut self,
1085        f: impl FnOnce(&mut Context),
1086        fallback: impl FnOnce(&mut Context, String),
1087    ) {
1088        let cmd_count = self.commands.len();
1089        let last_text_idx = self.last_text_idx;
1090
1091        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1092            f(self);
1093        }));
1094
1095        match result {
1096            Ok(()) => {}
1097            Err(panic_info) => {
1098                self.commands.truncate(cmd_count);
1099                self.last_text_idx = last_text_idx;
1100
1101                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1102                    (*s).to_string()
1103                } else if let Some(s) = panic_info.downcast_ref::<String>() {
1104                    s.clone()
1105                } else {
1106                    "widget panicked".to_string()
1107                };
1108
1109                fallback(self, msg);
1110            }
1111        }
1112    }
1113
1114    /// Allocate a click/hover interaction slot and return the [`Response`].
1115    ///
1116    /// Use this in custom widgets to detect mouse clicks and hovers without
1117    /// wrapping content in a container. Each call reserves one slot in the
1118    /// hit-test map, so the call order must be stable across frames.
1119    pub fn interaction(&mut self) -> Response {
1120        let id = self.interaction_count;
1121        self.interaction_count += 1;
1122        self.response_for(id)
1123    }
1124
1125    /// Register a widget as focusable and return whether it currently has focus.
1126    ///
1127    /// Call this in custom widgets that need keyboard focus. Each call increments
1128    /// the internal focus counter, so the call order must be stable across frames.
1129    pub fn register_focusable(&mut self) -> bool {
1130        let id = self.focus_count;
1131        self.focus_count += 1;
1132        if self.prev_focus_count == 0 {
1133            return true;
1134        }
1135        self.focus_index % self.prev_focus_count == id
1136    }
1137
1138    // ── text ──────────────────────────────────────────────────────────
1139
1140    /// Render a text element. Returns `&mut Self` for style chaining.
1141    ///
1142    /// # Example
1143    ///
1144    /// ```no_run
1145    /// # slt::run(|ui: &mut slt::Context| {
1146    /// use slt::Color;
1147    /// ui.text("hello").bold().fg(Color::Cyan);
1148    /// # });
1149    /// ```
1150    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1151        let content = s.into();
1152        self.commands.push(Command::Text {
1153            content,
1154            style: Style::new(),
1155            grow: 0,
1156            align: Align::Start,
1157            wrap: false,
1158            margin: Margin::default(),
1159            constraints: Constraints::default(),
1160        });
1161        self.last_text_idx = Some(self.commands.len() - 1);
1162        self
1163    }
1164
1165    /// Render a text element with word-boundary wrapping.
1166    ///
1167    /// Long lines are broken at word boundaries to fit the container width.
1168    /// Style chaining works the same as [`Context::text`].
1169    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1170        let content = s.into();
1171        self.commands.push(Command::Text {
1172            content,
1173            style: Style::new(),
1174            grow: 0,
1175            align: Align::Start,
1176            wrap: true,
1177            margin: Margin::default(),
1178            constraints: Constraints::default(),
1179        });
1180        self.last_text_idx = Some(self.commands.len() - 1);
1181        self
1182    }
1183
1184    // ── style chain (applies to last text) ───────────────────────────
1185
1186    /// Apply bold to the last rendered text element.
1187    pub fn bold(&mut self) -> &mut Self {
1188        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1189        self
1190    }
1191
1192    /// Apply dim styling to the last rendered text element.
1193    ///
1194    /// Also sets the foreground color to the theme's `text_dim` color if no
1195    /// explicit foreground has been set.
1196    pub fn dim(&mut self) -> &mut Self {
1197        let text_dim = self.theme.text_dim;
1198        self.modify_last_style(|s| {
1199            s.modifiers |= Modifiers::DIM;
1200            if s.fg.is_none() {
1201                s.fg = Some(text_dim);
1202            }
1203        });
1204        self
1205    }
1206
1207    /// Apply italic to the last rendered text element.
1208    pub fn italic(&mut self) -> &mut Self {
1209        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1210        self
1211    }
1212
1213    /// Apply underline to the last rendered text element.
1214    pub fn underline(&mut self) -> &mut Self {
1215        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1216        self
1217    }
1218
1219    /// Apply reverse-video to the last rendered text element.
1220    pub fn reversed(&mut self) -> &mut Self {
1221        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1222        self
1223    }
1224
1225    /// Apply strikethrough to the last rendered text element.
1226    pub fn strikethrough(&mut self) -> &mut Self {
1227        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1228        self
1229    }
1230
1231    /// Set the foreground color of the last rendered text element.
1232    pub fn fg(&mut self, color: Color) -> &mut Self {
1233        self.modify_last_style(|s| s.fg = Some(color));
1234        self
1235    }
1236
1237    /// Set the background color of the last rendered text element.
1238    pub fn bg(&mut self, color: Color) -> &mut Self {
1239        self.modify_last_style(|s| s.bg = Some(color));
1240        self
1241    }
1242
1243    /// Render a text element with an explicit [`Style`] applied immediately.
1244    ///
1245    /// Equivalent to calling `text(s)` followed by style-chain methods, but
1246    /// more concise when you already have a `Style` value.
1247    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
1248        self.commands.push(Command::Text {
1249            content: s.into(),
1250            style,
1251            grow: 0,
1252            align: Align::Start,
1253            wrap: false,
1254            margin: Margin::default(),
1255            constraints: Constraints::default(),
1256        });
1257        self.last_text_idx = Some(self.commands.len() - 1);
1258        self
1259    }
1260
1261    /// Enable word-boundary wrapping on the last rendered text element.
1262    pub fn wrap(&mut self) -> &mut Self {
1263        if let Some(idx) = self.last_text_idx {
1264            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1265                *wrap = true;
1266            }
1267        }
1268        self
1269    }
1270
1271    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1272        if let Some(idx) = self.last_text_idx {
1273            if let Command::Text { style, .. } = &mut self.commands[idx] {
1274                f(style);
1275            }
1276        }
1277    }
1278
1279    // ── containers ───────────────────────────────────────────────────
1280
1281    /// Create a vertical (column) container.
1282    ///
1283    /// Children are stacked top-to-bottom. Returns a [`Response`] with
1284    /// click/hover state for the container area.
1285    ///
1286    /// # Example
1287    ///
1288    /// ```no_run
1289    /// # slt::run(|ui: &mut slt::Context| {
1290    /// ui.col(|ui| {
1291    ///     ui.text("line one");
1292    ///     ui.text("line two");
1293    /// });
1294    /// # });
1295    /// ```
1296    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1297        self.push_container(Direction::Column, 0, f)
1298    }
1299
1300    /// Create a vertical (column) container with a gap between children.
1301    ///
1302    /// `gap` is the number of blank rows inserted between each child.
1303    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1304        self.push_container(Direction::Column, gap, f)
1305    }
1306
1307    /// Create a horizontal (row) container.
1308    ///
1309    /// Children are placed left-to-right. Returns a [`Response`] with
1310    /// click/hover state for the container area.
1311    ///
1312    /// # Example
1313    ///
1314    /// ```no_run
1315    /// # slt::run(|ui: &mut slt::Context| {
1316    /// ui.row(|ui| {
1317    ///     ui.text("left");
1318    ///     ui.spacer();
1319    ///     ui.text("right");
1320    /// });
1321    /// # });
1322    /// ```
1323    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1324        self.push_container(Direction::Row, 0, f)
1325    }
1326
1327    /// Create a horizontal (row) container with a gap between children.
1328    ///
1329    /// `gap` is the number of blank columns inserted between each child.
1330    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1331        self.push_container(Direction::Row, gap, f)
1332    }
1333
1334    /// Create a container with a fluent builder.
1335    ///
1336    /// Use this for borders, padding, grow, constraints, and titles. Chain
1337    /// configuration methods on the returned [`ContainerBuilder`], then call
1338    /// `.col()` or `.row()` to finalize.
1339    ///
1340    /// # Example
1341    ///
1342    /// ```no_run
1343    /// # slt::run(|ui: &mut slt::Context| {
1344    /// use slt::Border;
1345    /// ui.container()
1346    ///     .border(Border::Rounded)
1347    ///     .pad(1)
1348    ///     .title("My Panel")
1349    ///     .col(|ui| {
1350    ///         ui.text("content");
1351    ///     });
1352    /// # });
1353    /// ```
1354    pub fn container(&mut self) -> ContainerBuilder<'_> {
1355        let border = self.theme.border;
1356        ContainerBuilder {
1357            ctx: self,
1358            gap: 0,
1359            align: Align::Start,
1360            border: None,
1361            border_style: Style::new().fg(border),
1362            padding: Padding::default(),
1363            margin: Margin::default(),
1364            constraints: Constraints::default(),
1365            title: None,
1366            grow: 0,
1367            scroll_offset: None,
1368        }
1369    }
1370
1371    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
1372    ///
1373    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
1374    /// is updated in-place with the current scroll offset and bounds.
1375    ///
1376    /// # Example
1377    ///
1378    /// ```no_run
1379    /// # use slt::widgets::ScrollState;
1380    /// # slt::run(|ui: &mut slt::Context| {
1381    /// let mut scroll = ScrollState::new();
1382    /// ui.scrollable(&mut scroll).col(|ui| {
1383    ///     for i in 0..100 {
1384    ///         ui.text(format!("Line {i}"));
1385    ///     }
1386    /// });
1387    /// # });
1388    /// ```
1389    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1390        let index = self.scroll_count;
1391        self.scroll_count += 1;
1392        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1393            state.set_bounds(ch, vh);
1394            let max = ch.saturating_sub(vh) as usize;
1395            state.offset = state.offset.min(max);
1396        }
1397
1398        let next_id = self.interaction_count;
1399        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1400            self.auto_scroll(&rect, state);
1401        }
1402
1403        self.container().scroll_offset(state.offset as u32)
1404    }
1405
1406    fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1407        let last_y = self.last_mouse_pos.map(|(_, y)| y);
1408        let mut to_consume: Vec<usize> = Vec::new();
1409
1410        for (i, event) in self.events.iter().enumerate() {
1411            if self.consumed[i] {
1412                continue;
1413            }
1414            if let Event::Mouse(mouse) = event {
1415                let in_bounds = mouse.x >= rect.x
1416                    && mouse.x < rect.right()
1417                    && mouse.y >= rect.y
1418                    && mouse.y < rect.bottom();
1419                if !in_bounds {
1420                    continue;
1421                }
1422                match mouse.kind {
1423                    MouseKind::ScrollUp => {
1424                        state.scroll_up(1);
1425                        to_consume.push(i);
1426                    }
1427                    MouseKind::ScrollDown => {
1428                        state.scroll_down(1);
1429                        to_consume.push(i);
1430                    }
1431                    MouseKind::Drag(MouseButton::Left) => {
1432                        if let Some(prev_y) = last_y {
1433                            let delta = mouse.y as i32 - prev_y as i32;
1434                            if delta < 0 {
1435                                state.scroll_down((-delta) as usize);
1436                            } else if delta > 0 {
1437                                state.scroll_up(delta as usize);
1438                            }
1439                        }
1440                        to_consume.push(i);
1441                    }
1442                    _ => {}
1443                }
1444            }
1445        }
1446
1447        for i in to_consume {
1448            self.consumed[i] = true;
1449        }
1450    }
1451
1452    /// Shortcut for `container().border(border)`.
1453    ///
1454    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
1455    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1456        self.container().border(border)
1457    }
1458
1459    fn push_container(
1460        &mut self,
1461        direction: Direction,
1462        gap: u32,
1463        f: impl FnOnce(&mut Context),
1464    ) -> Response {
1465        let interaction_id = self.interaction_count;
1466        self.interaction_count += 1;
1467        let border = self.theme.border;
1468
1469        self.commands.push(Command::BeginContainer {
1470            direction,
1471            gap,
1472            align: Align::Start,
1473            border: None,
1474            border_style: Style::new().fg(border),
1475            padding: Padding::default(),
1476            margin: Margin::default(),
1477            constraints: Constraints::default(),
1478            title: None,
1479            grow: 0,
1480        });
1481        f(self);
1482        self.commands.push(Command::EndContainer);
1483        self.last_text_idx = None;
1484
1485        self.response_for(interaction_id)
1486    }
1487
1488    fn response_for(&self, interaction_id: usize) -> Response {
1489        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1490            let clicked = self
1491                .click_pos
1492                .map(|(mx, my)| {
1493                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1494                })
1495                .unwrap_or(false);
1496            let hovered = self
1497                .mouse_pos
1498                .map(|(mx, my)| {
1499                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1500                })
1501                .unwrap_or(false);
1502            Response { clicked, hovered }
1503        } else {
1504            Response::default()
1505        }
1506    }
1507
1508    /// Set the flex-grow factor of the last rendered text element.
1509    ///
1510    /// A value of `1` causes the element to expand and fill remaining space
1511    /// along the main axis.
1512    pub fn grow(&mut self, value: u16) -> &mut Self {
1513        if let Some(idx) = self.last_text_idx {
1514            if let Command::Text { grow, .. } = &mut self.commands[idx] {
1515                *grow = value;
1516            }
1517        }
1518        self
1519    }
1520
1521    /// Set the text alignment of the last rendered text element.
1522    pub fn align(&mut self, align: Align) -> &mut Self {
1523        if let Some(idx) = self.last_text_idx {
1524            if let Command::Text {
1525                align: text_align, ..
1526            } = &mut self.commands[idx]
1527            {
1528                *text_align = align;
1529            }
1530        }
1531        self
1532    }
1533
1534    /// Render an invisible spacer that expands to fill available space.
1535    ///
1536    /// Useful for pushing siblings to opposite ends of a row or column.
1537    pub fn spacer(&mut self) -> &mut Self {
1538        self.commands.push(Command::Spacer { grow: 1 });
1539        self.last_text_idx = None;
1540        self
1541    }
1542
1543    /// Render a single-line text input. Auto-handles cursor, typing, and backspace.
1544    ///
1545    /// The widget claims focus via [`Context::register_focusable`]. When focused,
1546    /// it consumes character, backspace, arrow, Home, and End key events.
1547    ///
1548    /// # Example
1549    ///
1550    /// ```no_run
1551    /// # use slt::widgets::TextInputState;
1552    /// # slt::run(|ui: &mut slt::Context| {
1553    /// let mut input = TextInputState::with_placeholder("Search...");
1554    /// ui.text_input(&mut input);
1555    /// // input.value holds the current text
1556    /// # });
1557    /// ```
1558    pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1559        let focused = self.register_focusable();
1560        state.cursor = state.cursor.min(state.value.chars().count());
1561
1562        if focused {
1563            let mut consumed_indices = Vec::new();
1564            for (i, event) in self.events.iter().enumerate() {
1565                if let Event::Key(key) = event {
1566                    match key.code {
1567                        KeyCode::Char(ch) => {
1568                            if let Some(max) = state.max_length {
1569                                if state.value.chars().count() >= max {
1570                                    continue;
1571                                }
1572                            }
1573                            let index = byte_index_for_char(&state.value, state.cursor);
1574                            state.value.insert(index, ch);
1575                            state.cursor += 1;
1576                            consumed_indices.push(i);
1577                        }
1578                        KeyCode::Backspace => {
1579                            if state.cursor > 0 {
1580                                let start = byte_index_for_char(&state.value, state.cursor - 1);
1581                                let end = byte_index_for_char(&state.value, state.cursor);
1582                                state.value.replace_range(start..end, "");
1583                                state.cursor -= 1;
1584                            }
1585                            consumed_indices.push(i);
1586                        }
1587                        KeyCode::Left => {
1588                            state.cursor = state.cursor.saturating_sub(1);
1589                            consumed_indices.push(i);
1590                        }
1591                        KeyCode::Right => {
1592                            state.cursor = (state.cursor + 1).min(state.value.chars().count());
1593                            consumed_indices.push(i);
1594                        }
1595                        KeyCode::Home => {
1596                            state.cursor = 0;
1597                            consumed_indices.push(i);
1598                        }
1599                        KeyCode::Delete => {
1600                            let len = state.value.chars().count();
1601                            if state.cursor < len {
1602                                let start = byte_index_for_char(&state.value, state.cursor);
1603                                let end = byte_index_for_char(&state.value, state.cursor + 1);
1604                                state.value.replace_range(start..end, "");
1605                            }
1606                            consumed_indices.push(i);
1607                        }
1608                        KeyCode::End => {
1609                            state.cursor = state.value.chars().count();
1610                            consumed_indices.push(i);
1611                        }
1612                        _ => {}
1613                    }
1614                }
1615                if let Event::Paste(ref text) = event {
1616                    for ch in text.chars() {
1617                        if let Some(max) = state.max_length {
1618                            if state.value.chars().count() >= max {
1619                                break;
1620                            }
1621                        }
1622                        let index = byte_index_for_char(&state.value, state.cursor);
1623                        state.value.insert(index, ch);
1624                        state.cursor += 1;
1625                    }
1626                    consumed_indices.push(i);
1627                }
1628            }
1629
1630            for index in consumed_indices {
1631                self.consumed[index] = true;
1632            }
1633        }
1634
1635        if state.value.is_empty() {
1636            self.styled(
1637                state.placeholder.clone(),
1638                Style::new().dim().fg(self.theme.text_dim),
1639            )
1640        } else {
1641            let mut rendered = String::new();
1642            for (idx, ch) in state.value.chars().enumerate() {
1643                if focused && idx == state.cursor {
1644                    rendered.push('▎');
1645                }
1646                rendered.push(ch);
1647            }
1648            if focused && state.cursor >= state.value.chars().count() {
1649                rendered.push('▎');
1650            }
1651            self.styled(rendered, Style::new().fg(self.theme.text))
1652        }
1653    }
1654
1655    /// Render an animated spinner.
1656    ///
1657    /// The spinner advances one frame per tick. Use [`SpinnerState::dots`] or
1658    /// [`SpinnerState::line`] to create the state, then chain style methods to
1659    /// color it.
1660    pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1661        self.styled(
1662            state.frame(self.tick).to_string(),
1663            Style::new().fg(self.theme.primary),
1664        )
1665    }
1666
1667    /// Render toast notifications. Calls `state.cleanup(tick)` automatically.
1668    ///
1669    /// Expired messages are removed before rendering. If there are no active
1670    /// messages, nothing is rendered and `self` is returned unchanged.
1671    pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1672        state.cleanup(self.tick);
1673        if state.messages.is_empty() {
1674            return self;
1675        }
1676
1677        self.interaction_count += 1;
1678        self.commands.push(Command::BeginContainer {
1679            direction: Direction::Column,
1680            gap: 0,
1681            align: Align::Start,
1682            border: None,
1683            border_style: Style::new().fg(self.theme.border),
1684            padding: Padding::default(),
1685            margin: Margin::default(),
1686            constraints: Constraints::default(),
1687            title: None,
1688            grow: 0,
1689        });
1690        for message in state.messages.iter().rev() {
1691            let color = match message.level {
1692                ToastLevel::Info => self.theme.primary,
1693                ToastLevel::Success => self.theme.success,
1694                ToastLevel::Warning => self.theme.warning,
1695                ToastLevel::Error => self.theme.error,
1696            };
1697            self.styled(format!("  ● {}", message.text), Style::new().fg(color));
1698        }
1699        self.commands.push(Command::EndContainer);
1700        self.last_text_idx = None;
1701
1702        self
1703    }
1704
1705    /// Render a multi-line text area with the given number of visible rows.
1706    ///
1707    /// When focused, handles character input, Enter (new line), Backspace,
1708    /// arrow keys, Home, and End. The cursor is rendered as a block character.
1709    pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1710        if state.lines.is_empty() {
1711            state.lines.push(String::new());
1712        }
1713        state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1714        state.cursor_col = state
1715            .cursor_col
1716            .min(state.lines[state.cursor_row].chars().count());
1717
1718        let focused = self.register_focusable();
1719
1720        if focused {
1721            let mut consumed_indices = Vec::new();
1722            for (i, event) in self.events.iter().enumerate() {
1723                if let Event::Key(key) = event {
1724                    match key.code {
1725                        KeyCode::Char(ch) => {
1726                            if let Some(max) = state.max_length {
1727                                let total: usize =
1728                                    state.lines.iter().map(|line| line.chars().count()).sum();
1729                                if total >= max {
1730                                    continue;
1731                                }
1732                            }
1733                            let index = byte_index_for_char(
1734                                &state.lines[state.cursor_row],
1735                                state.cursor_col,
1736                            );
1737                            state.lines[state.cursor_row].insert(index, ch);
1738                            state.cursor_col += 1;
1739                            consumed_indices.push(i);
1740                        }
1741                        KeyCode::Enter => {
1742                            let split_index = byte_index_for_char(
1743                                &state.lines[state.cursor_row],
1744                                state.cursor_col,
1745                            );
1746                            let remainder = state.lines[state.cursor_row].split_off(split_index);
1747                            state.cursor_row += 1;
1748                            state.lines.insert(state.cursor_row, remainder);
1749                            state.cursor_col = 0;
1750                            consumed_indices.push(i);
1751                        }
1752                        KeyCode::Backspace => {
1753                            if state.cursor_col > 0 {
1754                                let start = byte_index_for_char(
1755                                    &state.lines[state.cursor_row],
1756                                    state.cursor_col - 1,
1757                                );
1758                                let end = byte_index_for_char(
1759                                    &state.lines[state.cursor_row],
1760                                    state.cursor_col,
1761                                );
1762                                state.lines[state.cursor_row].replace_range(start..end, "");
1763                                state.cursor_col -= 1;
1764                            } else if state.cursor_row > 0 {
1765                                let current = state.lines.remove(state.cursor_row);
1766                                state.cursor_row -= 1;
1767                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1768                                state.lines[state.cursor_row].push_str(&current);
1769                            }
1770                            consumed_indices.push(i);
1771                        }
1772                        KeyCode::Left => {
1773                            if state.cursor_col > 0 {
1774                                state.cursor_col -= 1;
1775                            } else if state.cursor_row > 0 {
1776                                state.cursor_row -= 1;
1777                                state.cursor_col = state.lines[state.cursor_row].chars().count();
1778                            }
1779                            consumed_indices.push(i);
1780                        }
1781                        KeyCode::Right => {
1782                            let line_len = state.lines[state.cursor_row].chars().count();
1783                            if state.cursor_col < line_len {
1784                                state.cursor_col += 1;
1785                            } else if state.cursor_row + 1 < state.lines.len() {
1786                                state.cursor_row += 1;
1787                                state.cursor_col = 0;
1788                            }
1789                            consumed_indices.push(i);
1790                        }
1791                        KeyCode::Up => {
1792                            if state.cursor_row > 0 {
1793                                state.cursor_row -= 1;
1794                                state.cursor_col = state
1795                                    .cursor_col
1796                                    .min(state.lines[state.cursor_row].chars().count());
1797                            }
1798                            consumed_indices.push(i);
1799                        }
1800                        KeyCode::Down => {
1801                            if state.cursor_row + 1 < state.lines.len() {
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::Home => {
1810                            state.cursor_col = 0;
1811                            consumed_indices.push(i);
1812                        }
1813                        KeyCode::Delete => {
1814                            let line_len = state.lines[state.cursor_row].chars().count();
1815                            if state.cursor_col < line_len {
1816                                let start = byte_index_for_char(
1817                                    &state.lines[state.cursor_row],
1818                                    state.cursor_col,
1819                                );
1820                                let end = byte_index_for_char(
1821                                    &state.lines[state.cursor_row],
1822                                    state.cursor_col + 1,
1823                                );
1824                                state.lines[state.cursor_row].replace_range(start..end, "");
1825                            } else if state.cursor_row + 1 < state.lines.len() {
1826                                let next = state.lines.remove(state.cursor_row + 1);
1827                                state.lines[state.cursor_row].push_str(&next);
1828                            }
1829                            consumed_indices.push(i);
1830                        }
1831                        KeyCode::End => {
1832                            state.cursor_col = state.lines[state.cursor_row].chars().count();
1833                            consumed_indices.push(i);
1834                        }
1835                        _ => {}
1836                    }
1837                }
1838                if let Event::Paste(ref text) = event {
1839                    for ch in text.chars() {
1840                        if ch == '\n' || ch == '\r' {
1841                            let split_index = byte_index_for_char(
1842                                &state.lines[state.cursor_row],
1843                                state.cursor_col,
1844                            );
1845                            let remainder = state.lines[state.cursor_row].split_off(split_index);
1846                            state.cursor_row += 1;
1847                            state.lines.insert(state.cursor_row, remainder);
1848                            state.cursor_col = 0;
1849                        } else {
1850                            if let Some(max) = state.max_length {
1851                                let total: usize =
1852                                    state.lines.iter().map(|l| l.chars().count()).sum();
1853                                if total >= max {
1854                                    break;
1855                                }
1856                            }
1857                            let index = byte_index_for_char(
1858                                &state.lines[state.cursor_row],
1859                                state.cursor_col,
1860                            );
1861                            state.lines[state.cursor_row].insert(index, ch);
1862                            state.cursor_col += 1;
1863                        }
1864                    }
1865                    consumed_indices.push(i);
1866                }
1867            }
1868
1869            for index in consumed_indices {
1870                self.consumed[index] = true;
1871            }
1872        }
1873
1874        self.interaction_count += 1;
1875        self.commands.push(Command::BeginContainer {
1876            direction: Direction::Column,
1877            gap: 0,
1878            align: Align::Start,
1879            border: None,
1880            border_style: Style::new().fg(self.theme.border),
1881            padding: Padding::default(),
1882            margin: Margin::default(),
1883            constraints: Constraints::default(),
1884            title: None,
1885            grow: 0,
1886        });
1887        for row in 0..visible_rows as usize {
1888            let line = state.lines.get(row).cloned().unwrap_or_default();
1889            let mut rendered = line.clone();
1890            let mut style = if line.is_empty() {
1891                Style::new().fg(self.theme.text_dim)
1892            } else {
1893                Style::new().fg(self.theme.text)
1894            };
1895
1896            if focused && row == state.cursor_row {
1897                rendered.clear();
1898                for (idx, ch) in line.chars().enumerate() {
1899                    if idx == state.cursor_col {
1900                        rendered.push('▎');
1901                    }
1902                    rendered.push(ch);
1903                }
1904                if state.cursor_col >= line.chars().count() {
1905                    rendered.push('▎');
1906                }
1907                style = Style::new().fg(self.theme.text);
1908            }
1909
1910            self.styled(rendered, style);
1911        }
1912        self.commands.push(Command::EndContainer);
1913        self.last_text_idx = None;
1914
1915        self
1916    }
1917
1918    /// Render a progress bar (20 chars wide). `ratio` is clamped to `0.0..=1.0`.
1919    ///
1920    /// Uses block characters (`█` filled, `░` empty). For a custom width use
1921    /// [`Context::progress_bar`].
1922    pub fn progress(&mut self, ratio: f64) -> &mut Self {
1923        self.progress_bar(ratio, 20)
1924    }
1925
1926    /// Render a progress bar with a custom character width.
1927    ///
1928    /// `ratio` is clamped to `0.0..=1.0`. `width` is the total number of
1929    /// characters rendered.
1930    pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1931        let clamped = ratio.clamp(0.0, 1.0);
1932        let filled = (clamped * width as f64).round() as u32;
1933        let empty = width.saturating_sub(filled);
1934        let mut bar = String::new();
1935        for _ in 0..filled {
1936            bar.push('█');
1937        }
1938        for _ in 0..empty {
1939            bar.push('░');
1940        }
1941        self.text(bar)
1942    }
1943
1944    /// Render a horizontal bar chart from `(label, value)` pairs.
1945    ///
1946    /// Bars are normalized against the largest value and rendered with `█` up to
1947    /// `max_width` characters.
1948    ///
1949    /// # Example
1950    ///
1951    /// ```ignore
1952    /// # slt::run(|ui: &mut slt::Context| {
1953    /// let data = [
1954    ///     ("Sales", 160.0),
1955    ///     ("Revenue", 120.0),
1956    ///     ("Users", 220.0),
1957    ///     ("Costs", 60.0),
1958    /// ];
1959    /// ui.bar_chart(&data, 24);
1960    ///
1961    /// For styled bars with per-bar colors, see [`bar_chart_styled`].
1962    /// # });
1963    /// ```
1964    pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
1965        if data.is_empty() {
1966            return self;
1967        }
1968
1969        let max_label_width = data
1970            .iter()
1971            .map(|(label, _)| UnicodeWidthStr::width(*label))
1972            .max()
1973            .unwrap_or(0);
1974        let max_value = data
1975            .iter()
1976            .map(|(_, value)| *value)
1977            .fold(f64::NEG_INFINITY, f64::max);
1978        let denom = if max_value > 0.0 { max_value } else { 1.0 };
1979
1980        self.interaction_count += 1;
1981        self.commands.push(Command::BeginContainer {
1982            direction: Direction::Column,
1983            gap: 0,
1984            align: Align::Start,
1985            border: None,
1986            border_style: Style::new().fg(self.theme.border),
1987            padding: Padding::default(),
1988            margin: Margin::default(),
1989            constraints: Constraints::default(),
1990            title: None,
1991            grow: 0,
1992        });
1993
1994        for (label, value) in data {
1995            let label_width = UnicodeWidthStr::width(*label);
1996            let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
1997            let normalized = (*value / denom).clamp(0.0, 1.0);
1998            let bar_len = (normalized * max_width as f64).round() as usize;
1999            let bar = "█".repeat(bar_len);
2000
2001            self.interaction_count += 1;
2002            self.commands.push(Command::BeginContainer {
2003                direction: Direction::Row,
2004                gap: 1,
2005                align: Align::Start,
2006                border: None,
2007                border_style: Style::new().fg(self.theme.border),
2008                padding: Padding::default(),
2009                margin: Margin::default(),
2010                constraints: Constraints::default(),
2011                title: None,
2012                grow: 0,
2013            });
2014            self.styled(
2015                format!("{label}{label_padding}"),
2016                Style::new().fg(self.theme.text),
2017            );
2018            self.styled(bar, Style::new().fg(self.theme.primary));
2019            self.styled(
2020                format_compact_number(*value),
2021                Style::new().fg(self.theme.text_dim),
2022            );
2023            self.commands.push(Command::EndContainer);
2024            self.last_text_idx = None;
2025        }
2026
2027        self.commands.push(Command::EndContainer);
2028        self.last_text_idx = None;
2029
2030        self
2031    }
2032
2033    /// Render a styled bar chart with per-bar colors, grouping, and direction control.
2034    ///
2035    /// # Example
2036    /// ```ignore
2037    /// # slt::run(|ui: &mut slt::Context| {
2038    /// use slt::{Bar, Color};
2039    /// let bars = vec![
2040    ///     Bar::new("Q1", 32.0).color(Color::Cyan),
2041    ///     Bar::new("Q2", 46.0).color(Color::Green),
2042    ///     Bar::new("Q3", 28.0).color(Color::Yellow),
2043    ///     Bar::new("Q4", 54.0).color(Color::Red),
2044    /// ];
2045    /// ui.bar_chart_styled(&bars, 30, slt::BarDirection::Horizontal);
2046    /// # });
2047    /// ```
2048    pub fn bar_chart_styled(
2049        &mut self,
2050        bars: &[Bar],
2051        max_width: u32,
2052        direction: BarDirection,
2053    ) -> &mut Self {
2054        if bars.is_empty() {
2055            return self;
2056        }
2057
2058        let max_value = bars
2059            .iter()
2060            .map(|bar| bar.value)
2061            .fold(f64::NEG_INFINITY, f64::max);
2062        let denom = if max_value > 0.0 { max_value } else { 1.0 };
2063
2064        match direction {
2065            BarDirection::Horizontal => {
2066                let max_label_width = bars
2067                    .iter()
2068                    .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2069                    .max()
2070                    .unwrap_or(0);
2071
2072                self.interaction_count += 1;
2073                self.commands.push(Command::BeginContainer {
2074                    direction: Direction::Column,
2075                    gap: 0,
2076                    align: Align::Start,
2077                    border: None,
2078                    border_style: Style::new().fg(self.theme.border),
2079                    padding: Padding::default(),
2080                    margin: Margin::default(),
2081                    constraints: Constraints::default(),
2082                    title: None,
2083                    grow: 0,
2084                });
2085
2086                for bar in bars {
2087                    let label_width = UnicodeWidthStr::width(bar.label.as_str());
2088                    let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2089                    let normalized = (bar.value / denom).clamp(0.0, 1.0);
2090                    let bar_len = (normalized * max_width as f64).round() as usize;
2091                    let bar_text = "█".repeat(bar_len);
2092                    let color = bar.color.unwrap_or(self.theme.primary);
2093
2094                    self.interaction_count += 1;
2095                    self.commands.push(Command::BeginContainer {
2096                        direction: Direction::Row,
2097                        gap: 1,
2098                        align: Align::Start,
2099                        border: None,
2100                        border_style: Style::new().fg(self.theme.border),
2101                        padding: Padding::default(),
2102                        margin: Margin::default(),
2103                        constraints: Constraints::default(),
2104                        title: None,
2105                        grow: 0,
2106                    });
2107                    self.styled(
2108                        format!("{}{label_padding}", bar.label),
2109                        Style::new().fg(self.theme.text),
2110                    );
2111                    self.styled(bar_text, Style::new().fg(color));
2112                    self.styled(
2113                        format_compact_number(bar.value),
2114                        Style::new().fg(self.theme.text_dim),
2115                    );
2116                    self.commands.push(Command::EndContainer);
2117                    self.last_text_idx = None;
2118                }
2119
2120                self.commands.push(Command::EndContainer);
2121                self.last_text_idx = None;
2122            }
2123            BarDirection::Vertical => {
2124                const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
2125
2126                let chart_height = max_width.max(1) as usize;
2127                let value_labels: Vec<String> = bars
2128                    .iter()
2129                    .map(|bar| format_compact_number(bar.value))
2130                    .collect();
2131                let col_width = bars
2132                    .iter()
2133                    .zip(value_labels.iter())
2134                    .map(|(bar, value)| {
2135                        UnicodeWidthStr::width(bar.label.as_str())
2136                            .max(UnicodeWidthStr::width(value.as_str()))
2137                            .max(1)
2138                    })
2139                    .max()
2140                    .unwrap_or(1);
2141
2142                let bar_units: Vec<usize> = bars
2143                    .iter()
2144                    .map(|bar| {
2145                        let normalized = (bar.value / denom).clamp(0.0, 1.0);
2146                        (normalized * chart_height as f64 * 8.0).round() as usize
2147                    })
2148                    .collect();
2149
2150                self.interaction_count += 1;
2151                self.commands.push(Command::BeginContainer {
2152                    direction: Direction::Column,
2153                    gap: 0,
2154                    align: Align::Start,
2155                    border: None,
2156                    border_style: Style::new().fg(self.theme.border),
2157                    padding: Padding::default(),
2158                    margin: Margin::default(),
2159                    constraints: Constraints::default(),
2160                    title: None,
2161                    grow: 0,
2162                });
2163
2164                self.interaction_count += 1;
2165                self.commands.push(Command::BeginContainer {
2166                    direction: Direction::Row,
2167                    gap: 1,
2168                    align: Align::Start,
2169                    border: None,
2170                    border_style: Style::new().fg(self.theme.border),
2171                    padding: Padding::default(),
2172                    margin: Margin::default(),
2173                    constraints: Constraints::default(),
2174                    title: None,
2175                    grow: 0,
2176                });
2177                for value in &value_labels {
2178                    self.styled(
2179                        center_text(value, col_width),
2180                        Style::new().fg(self.theme.text_dim),
2181                    );
2182                }
2183                self.commands.push(Command::EndContainer);
2184                self.last_text_idx = None;
2185
2186                for row in (0..chart_height).rev() {
2187                    self.interaction_count += 1;
2188                    self.commands.push(Command::BeginContainer {
2189                        direction: Direction::Row,
2190                        gap: 1,
2191                        align: Align::Start,
2192                        border: None,
2193                        border_style: Style::new().fg(self.theme.border),
2194                        padding: Padding::default(),
2195                        margin: Margin::default(),
2196                        constraints: Constraints::default(),
2197                        title: None,
2198                        grow: 0,
2199                    });
2200
2201                    let row_base = row * 8;
2202                    for (bar, units) in bars.iter().zip(bar_units.iter()) {
2203                        let fill = if *units <= row_base {
2204                            ' '
2205                        } else {
2206                            let delta = *units - row_base;
2207                            if delta >= 8 {
2208                                '█'
2209                            } else {
2210                                FRACTION_BLOCKS[delta]
2211                            }
2212                        };
2213
2214                        self.styled(
2215                            center_text(&fill.to_string(), col_width),
2216                            Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2217                        );
2218                    }
2219
2220                    self.commands.push(Command::EndContainer);
2221                    self.last_text_idx = None;
2222                }
2223
2224                self.interaction_count += 1;
2225                self.commands.push(Command::BeginContainer {
2226                    direction: Direction::Row,
2227                    gap: 1,
2228                    align: Align::Start,
2229                    border: None,
2230                    border_style: Style::new().fg(self.theme.border),
2231                    padding: Padding::default(),
2232                    margin: Margin::default(),
2233                    constraints: Constraints::default(),
2234                    title: None,
2235                    grow: 0,
2236                });
2237                for bar in bars {
2238                    self.styled(
2239                        center_text(&bar.label, col_width),
2240                        Style::new().fg(self.theme.text),
2241                    );
2242                }
2243                self.commands.push(Command::EndContainer);
2244                self.last_text_idx = None;
2245
2246                self.commands.push(Command::EndContainer);
2247                self.last_text_idx = None;
2248            }
2249        }
2250
2251        self
2252    }
2253
2254    /// Render a grouped bar chart.
2255    ///
2256    /// Each group contains multiple bars rendered side by side. Useful for
2257    /// comparing categories across groups (e.g., quarterly revenue by product).
2258    ///
2259    /// # Example
2260    /// ```ignore
2261    /// # slt::run(|ui: &mut slt::Context| {
2262    /// use slt::{Bar, BarGroup, Color};
2263    /// let groups = vec![
2264    ///     BarGroup::new("2023", vec![Bar::new("Rev", 100.0).color(Color::Cyan), Bar::new("Cost", 60.0).color(Color::Red)]),
2265    ///     BarGroup::new("2024", vec![Bar::new("Rev", 140.0).color(Color::Cyan), Bar::new("Cost", 80.0).color(Color::Red)]),
2266    /// ];
2267    /// ui.bar_chart_grouped(&groups, 40);
2268    /// # });
2269    /// ```
2270    pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
2271        if groups.is_empty() {
2272            return self;
2273        }
2274
2275        let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
2276        if all_bars.is_empty() {
2277            return self;
2278        }
2279
2280        let max_label_width = all_bars
2281            .iter()
2282            .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2283            .max()
2284            .unwrap_or(0);
2285        let max_value = all_bars
2286            .iter()
2287            .map(|bar| bar.value)
2288            .fold(f64::NEG_INFINITY, f64::max);
2289        let denom = if max_value > 0.0 { max_value } else { 1.0 };
2290
2291        self.interaction_count += 1;
2292        self.commands.push(Command::BeginContainer {
2293            direction: Direction::Column,
2294            gap: 1,
2295            align: Align::Start,
2296            border: None,
2297            border_style: Style::new().fg(self.theme.border),
2298            padding: Padding::default(),
2299            margin: Margin::default(),
2300            constraints: Constraints::default(),
2301            title: None,
2302            grow: 0,
2303        });
2304
2305        for group in groups {
2306            self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
2307
2308            for bar in &group.bars {
2309                let label_width = UnicodeWidthStr::width(bar.label.as_str());
2310                let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2311                let normalized = (bar.value / denom).clamp(0.0, 1.0);
2312                let bar_len = (normalized * max_width as f64).round() as usize;
2313                let bar_text = "█".repeat(bar_len);
2314
2315                self.interaction_count += 1;
2316                self.commands.push(Command::BeginContainer {
2317                    direction: Direction::Row,
2318                    gap: 1,
2319                    align: Align::Start,
2320                    border: None,
2321                    border_style: Style::new().fg(self.theme.border),
2322                    padding: Padding::default(),
2323                    margin: Margin::default(),
2324                    constraints: Constraints::default(),
2325                    title: None,
2326                    grow: 0,
2327                });
2328                self.styled(
2329                    format!("  {}{label_padding}", bar.label),
2330                    Style::new().fg(self.theme.text),
2331                );
2332                self.styled(
2333                    bar_text,
2334                    Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2335                );
2336                self.styled(
2337                    format_compact_number(bar.value),
2338                    Style::new().fg(self.theme.text_dim),
2339                );
2340                self.commands.push(Command::EndContainer);
2341                self.last_text_idx = None;
2342            }
2343        }
2344
2345        self.commands.push(Command::EndContainer);
2346        self.last_text_idx = None;
2347
2348        self
2349    }
2350
2351    /// Render a single-line sparkline from numeric data.
2352    ///
2353    /// Uses the last `width` points (or fewer if the data is shorter) and maps
2354    /// each point to one of `▁▂▃▄▅▆▇█`.
2355    ///
2356    /// # Example
2357    ///
2358    /// ```ignore
2359    /// # slt::run(|ui: &mut slt::Context| {
2360    /// let samples = [12.0, 9.0, 14.0, 18.0, 16.0, 21.0, 20.0, 24.0];
2361    /// ui.sparkline(&samples, 16);
2362    ///
2363    /// For per-point colors and missing values, see [`sparkline_styled`].
2364    /// # });
2365    /// ```
2366    pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
2367        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2368
2369        let w = width as usize;
2370        let window = if data.len() > w {
2371            &data[data.len() - w..]
2372        } else {
2373            data
2374        };
2375
2376        if window.is_empty() {
2377            return self;
2378        }
2379
2380        let min = window.iter().copied().fold(f64::INFINITY, f64::min);
2381        let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2382        let range = max - min;
2383
2384        let line: String = window
2385            .iter()
2386            .map(|&value| {
2387                let normalized = if range == 0.0 {
2388                    0.5
2389                } else {
2390                    (value - min) / range
2391                };
2392                let idx = (normalized * 7.0).round() as usize;
2393                BLOCKS[idx.min(7)]
2394            })
2395            .collect();
2396
2397        self.styled(line, Style::new().fg(self.theme.primary))
2398    }
2399
2400    /// Render a sparkline with per-point colors.
2401    ///
2402    /// Each point can have its own color via `(f64, Option<Color>)` tuples.
2403    /// Use `f64::NAN` for absent values (rendered as spaces).
2404    ///
2405    /// # Example
2406    /// ```ignore
2407    /// # slt::run(|ui: &mut slt::Context| {
2408    /// use slt::Color;
2409    /// let data: Vec<(f64, Option<Color>)> = vec![
2410    ///     (12.0, Some(Color::Green)),
2411    ///     (9.0, Some(Color::Red)),
2412    ///     (14.0, Some(Color::Green)),
2413    ///     (f64::NAN, None),
2414    ///     (18.0, Some(Color::Cyan)),
2415    /// ];
2416    /// ui.sparkline_styled(&data, 16);
2417    /// # });
2418    /// ```
2419    pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
2420        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2421
2422        let w = width as usize;
2423        let window = if data.len() > w {
2424            &data[data.len() - w..]
2425        } else {
2426            data
2427        };
2428
2429        if window.is_empty() {
2430            return self;
2431        }
2432
2433        let mut finite_values = window
2434            .iter()
2435            .map(|(value, _)| *value)
2436            .filter(|value| !value.is_nan());
2437        let Some(first) = finite_values.next() else {
2438            return self.styled(
2439                " ".repeat(window.len()),
2440                Style::new().fg(self.theme.text_dim),
2441            );
2442        };
2443
2444        let mut min = first;
2445        let mut max = first;
2446        for value in finite_values {
2447            min = f64::min(min, value);
2448            max = f64::max(max, value);
2449        }
2450        let range = max - min;
2451
2452        let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
2453        for (value, color) in window {
2454            if value.is_nan() {
2455                cells.push((' ', self.theme.text_dim));
2456                continue;
2457            }
2458
2459            let normalized = if range == 0.0 {
2460                0.5
2461            } else {
2462                ((*value - min) / range).clamp(0.0, 1.0)
2463            };
2464            let idx = (normalized * 7.0).round() as usize;
2465            cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
2466        }
2467
2468        self.interaction_count += 1;
2469        self.commands.push(Command::BeginContainer {
2470            direction: Direction::Row,
2471            gap: 0,
2472            align: Align::Start,
2473            border: None,
2474            border_style: Style::new().fg(self.theme.border),
2475            padding: Padding::default(),
2476            margin: Margin::default(),
2477            constraints: Constraints::default(),
2478            title: None,
2479            grow: 0,
2480        });
2481
2482        let mut seg = String::new();
2483        let mut seg_color = cells[0].1;
2484        for (ch, color) in cells {
2485            if color != seg_color {
2486                self.styled(seg, Style::new().fg(seg_color));
2487                seg = String::new();
2488                seg_color = color;
2489            }
2490            seg.push(ch);
2491        }
2492        if !seg.is_empty() {
2493            self.styled(seg, Style::new().fg(seg_color));
2494        }
2495
2496        self.commands.push(Command::EndContainer);
2497        self.last_text_idx = None;
2498
2499        self
2500    }
2501
2502    /// Render a multi-row line chart using braille characters.
2503    ///
2504    /// `width` and `height` are terminal cell dimensions. Internally this uses
2505    /// braille dot resolution (`width*2` x `height*4`) for smoother plotting.
2506    ///
2507    /// # Example
2508    ///
2509    /// ```ignore
2510    /// # slt::run(|ui: &mut slt::Context| {
2511    /// let data = [1.0, 3.0, 2.0, 5.0, 4.0, 6.0, 3.0, 7.0];
2512    /// ui.line_chart(&data, 40, 8);
2513    /// # });
2514    /// ```
2515    pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2516        if data.is_empty() || width == 0 || height == 0 {
2517            return self;
2518        }
2519
2520        let cols = width as usize;
2521        let rows = height as usize;
2522        let px_w = cols * 2;
2523        let px_h = rows * 4;
2524
2525        let min = data.iter().copied().fold(f64::INFINITY, f64::min);
2526        let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2527        let range = if (max - min).abs() < f64::EPSILON {
2528            1.0
2529        } else {
2530            max - min
2531        };
2532
2533        let points: Vec<usize> = (0..px_w)
2534            .map(|px| {
2535                let data_idx = if px_w <= 1 {
2536                    0.0
2537                } else {
2538                    px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
2539                };
2540                let idx = data_idx.floor() as usize;
2541                let frac = data_idx - idx as f64;
2542                let value = if idx + 1 < data.len() {
2543                    data[idx] * (1.0 - frac) + data[idx + 1] * frac
2544                } else {
2545                    data[idx.min(data.len() - 1)]
2546                };
2547
2548                let normalized = (value - min) / range;
2549                let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
2550                py.min(px_h - 1)
2551            })
2552            .collect();
2553
2554        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
2555        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
2556
2557        let mut grid = vec![vec![0u32; cols]; rows];
2558
2559        for i in 0..points.len() {
2560            let px = i;
2561            let py = points[i];
2562            let char_col = px / 2;
2563            let char_row = py / 4;
2564            let sub_col = px % 2;
2565            let sub_row = py % 4;
2566
2567            if char_col < cols && char_row < rows {
2568                grid[char_row][char_col] |= if sub_col == 0 {
2569                    LEFT_BITS[sub_row]
2570                } else {
2571                    RIGHT_BITS[sub_row]
2572                };
2573            }
2574
2575            if i + 1 < points.len() {
2576                let py_next = points[i + 1];
2577                let (y_start, y_end) = if py <= py_next {
2578                    (py, py_next)
2579                } else {
2580                    (py_next, py)
2581                };
2582                for y in y_start..=y_end {
2583                    let cell_row = y / 4;
2584                    let sub_y = y % 4;
2585                    if char_col < cols && cell_row < rows {
2586                        grid[cell_row][char_col] |= if sub_col == 0 {
2587                            LEFT_BITS[sub_y]
2588                        } else {
2589                            RIGHT_BITS[sub_y]
2590                        };
2591                    }
2592                }
2593            }
2594        }
2595
2596        let style = Style::new().fg(self.theme.primary);
2597        for row in grid {
2598            let line: String = row
2599                .iter()
2600                .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
2601                .collect();
2602            self.styled(line, style);
2603        }
2604
2605        self
2606    }
2607
2608    /// Render a braille drawing canvas.
2609    ///
2610    /// The closure receives a [`CanvasContext`] for pixel-level drawing. Each
2611    /// terminal cell maps to a 2x4 braille dot matrix, giving `width*2` x
2612    /// `height*4` pixel resolution.
2613    ///
2614    /// # Example
2615    ///
2616    /// ```ignore
2617    /// # slt::run(|ui: &mut slt::Context| {
2618    /// ui.canvas(40, 10, |cv| {
2619    ///     cv.line(0, 0, cv.width() - 1, cv.height() - 1);
2620    ///     cv.circle(40, 20, 15);
2621    /// });
2622    /// # });
2623    /// ```
2624    pub fn canvas(
2625        &mut self,
2626        width: u32,
2627        height: u32,
2628        draw: impl FnOnce(&mut CanvasContext),
2629    ) -> &mut Self {
2630        if width == 0 || height == 0 {
2631            return self;
2632        }
2633
2634        let mut canvas = CanvasContext::new(width as usize, height as usize);
2635        draw(&mut canvas);
2636
2637        for segments in canvas.render() {
2638            self.interaction_count += 1;
2639            self.commands.push(Command::BeginContainer {
2640                direction: Direction::Row,
2641                gap: 0,
2642                align: Align::Start,
2643                border: None,
2644                border_style: Style::new(),
2645                padding: Padding::default(),
2646                margin: Margin::default(),
2647                constraints: Constraints::default(),
2648                title: None,
2649                grow: 0,
2650            });
2651            for (text, color) in segments {
2652                let c = if color == Color::Reset {
2653                    self.theme.primary
2654                } else {
2655                    color
2656                };
2657                self.styled(text, Style::new().fg(c));
2658            }
2659            self.commands.push(Command::EndContainer);
2660            self.last_text_idx = None;
2661        }
2662
2663        self
2664    }
2665
2666    /// Render a multi-series chart with axes, legend, and auto-scaling.
2667    pub fn chart(
2668        &mut self,
2669        configure: impl FnOnce(&mut ChartBuilder),
2670        width: u32,
2671        height: u32,
2672    ) -> &mut Self {
2673        if width == 0 || height == 0 {
2674            return self;
2675        }
2676
2677        let axis_style = Style::new().fg(self.theme.text_dim);
2678        let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
2679        configure(&mut builder);
2680
2681        let config = builder.build();
2682        let rows = render_chart(&config);
2683
2684        for row in rows {
2685            self.interaction_count += 1;
2686            self.commands.push(Command::BeginContainer {
2687                direction: Direction::Row,
2688                gap: 0,
2689                align: Align::Start,
2690                border: None,
2691                border_style: Style::new().fg(self.theme.border),
2692                padding: Padding::default(),
2693                margin: Margin::default(),
2694                constraints: Constraints::default(),
2695                title: None,
2696                grow: 0,
2697            });
2698            for (text, style) in row.segments {
2699                self.styled(text, style);
2700            }
2701            self.commands.push(Command::EndContainer);
2702            self.last_text_idx = None;
2703        }
2704
2705        self
2706    }
2707
2708    /// Render a histogram from raw data with auto-binning.
2709    pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2710        self.histogram_with(data, |_| {}, width, height)
2711    }
2712
2713    /// Render a histogram with configuration options.
2714    pub fn histogram_with(
2715        &mut self,
2716        data: &[f64],
2717        configure: impl FnOnce(&mut HistogramBuilder),
2718        width: u32,
2719        height: u32,
2720    ) -> &mut Self {
2721        if width == 0 || height == 0 {
2722            return self;
2723        }
2724
2725        let mut options = HistogramBuilder::default();
2726        configure(&mut options);
2727        let axis_style = Style::new().fg(self.theme.text_dim);
2728        let config = build_histogram_config(data, &options, width, height, axis_style);
2729        let rows = render_chart(&config);
2730
2731        for row in rows {
2732            self.interaction_count += 1;
2733            self.commands.push(Command::BeginContainer {
2734                direction: Direction::Row,
2735                gap: 0,
2736                align: Align::Start,
2737                border: None,
2738                border_style: Style::new().fg(self.theme.border),
2739                padding: Padding::default(),
2740                margin: Margin::default(),
2741                constraints: Constraints::default(),
2742                title: None,
2743                grow: 0,
2744            });
2745            for (text, style) in row.segments {
2746                self.styled(text, style);
2747            }
2748            self.commands.push(Command::EndContainer);
2749            self.last_text_idx = None;
2750        }
2751
2752        self
2753    }
2754
2755    /// Render children in a fixed grid with the given number of columns.
2756    ///
2757    /// Children are placed left-to-right, top-to-bottom. Each cell has equal
2758    /// width (`area_width / cols`). Rows wrap automatically.
2759    ///
2760    /// # Example
2761    ///
2762    /// ```no_run
2763    /// # slt::run(|ui: &mut slt::Context| {
2764    /// ui.grid(3, |ui| {
2765    ///     for i in 0..9 {
2766    ///         ui.text(format!("Cell {i}"));
2767    ///     }
2768    /// });
2769    /// # });
2770    /// ```
2771    pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
2772        let interaction_id = self.interaction_count;
2773        self.interaction_count += 1;
2774        let border = self.theme.border;
2775
2776        self.commands.push(Command::BeginContainer {
2777            direction: Direction::Column,
2778            gap: 0,
2779            align: Align::Start,
2780            border: None,
2781            border_style: Style::new().fg(border),
2782            padding: Padding::default(),
2783            margin: Margin::default(),
2784            constraints: Constraints::default(),
2785            title: None,
2786            grow: 0,
2787        });
2788
2789        let children_start = self.commands.len();
2790        f(self);
2791        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
2792
2793        let mut elements: Vec<Vec<Command>> = Vec::new();
2794        let mut iter = child_commands.into_iter().peekable();
2795        while let Some(cmd) = iter.next() {
2796            match cmd {
2797                Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2798                    let mut depth = 1_u32;
2799                    let mut element = vec![cmd];
2800                    for next in iter.by_ref() {
2801                        match next {
2802                            Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2803                                depth += 1;
2804                            }
2805                            Command::EndContainer => {
2806                                depth = depth.saturating_sub(1);
2807                            }
2808                            _ => {}
2809                        }
2810                        let at_end = matches!(next, Command::EndContainer) && depth == 0;
2811                        element.push(next);
2812                        if at_end {
2813                            break;
2814                        }
2815                    }
2816                    elements.push(element);
2817                }
2818                Command::EndContainer => {}
2819                _ => elements.push(vec![cmd]),
2820            }
2821        }
2822
2823        let cols = cols.max(1) as usize;
2824        for row in elements.chunks(cols) {
2825            self.interaction_count += 1;
2826            self.commands.push(Command::BeginContainer {
2827                direction: Direction::Row,
2828                gap: 0,
2829                align: Align::Start,
2830                border: None,
2831                border_style: Style::new().fg(border),
2832                padding: Padding::default(),
2833                margin: Margin::default(),
2834                constraints: Constraints::default(),
2835                title: None,
2836                grow: 0,
2837            });
2838
2839            for element in row {
2840                self.interaction_count += 1;
2841                self.commands.push(Command::BeginContainer {
2842                    direction: Direction::Column,
2843                    gap: 0,
2844                    align: Align::Start,
2845                    border: None,
2846                    border_style: Style::new().fg(border),
2847                    padding: Padding::default(),
2848                    margin: Margin::default(),
2849                    constraints: Constraints::default(),
2850                    title: None,
2851                    grow: 1,
2852                });
2853                self.commands.extend(element.iter().cloned());
2854                self.commands.push(Command::EndContainer);
2855            }
2856
2857            self.commands.push(Command::EndContainer);
2858        }
2859
2860        self.commands.push(Command::EndContainer);
2861        self.last_text_idx = None;
2862
2863        self.response_for(interaction_id)
2864    }
2865
2866    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
2867    ///
2868    /// The selected item is highlighted with the theme's primary color. If the
2869    /// list is empty, nothing is rendered.
2870    pub fn list(&mut self, state: &mut ListState) -> &mut Self {
2871        if state.items.is_empty() {
2872            state.selected = 0;
2873            return self;
2874        }
2875
2876        state.selected = state.selected.min(state.items.len().saturating_sub(1));
2877
2878        let focused = self.register_focusable();
2879
2880        if focused {
2881            let mut consumed_indices = Vec::new();
2882            for (i, event) in self.events.iter().enumerate() {
2883                if let Event::Key(key) = event {
2884                    match key.code {
2885                        KeyCode::Up | KeyCode::Char('k') => {
2886                            state.selected = state.selected.saturating_sub(1);
2887                            consumed_indices.push(i);
2888                        }
2889                        KeyCode::Down | KeyCode::Char('j') => {
2890                            state.selected =
2891                                (state.selected + 1).min(state.items.len().saturating_sub(1));
2892                            consumed_indices.push(i);
2893                        }
2894                        _ => {}
2895                    }
2896                }
2897            }
2898
2899            for index in consumed_indices {
2900                self.consumed[index] = true;
2901            }
2902        }
2903
2904        for (idx, item) in state.items.iter().enumerate() {
2905            if idx == state.selected {
2906                if focused {
2907                    self.styled(
2908                        format!("▸ {item}"),
2909                        Style::new().bold().fg(self.theme.primary),
2910                    );
2911                } else {
2912                    self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
2913                }
2914            } else {
2915                self.styled(format!("  {item}"), Style::new().fg(self.theme.text));
2916            }
2917        }
2918
2919        self
2920    }
2921
2922    /// Render a data table with column headers. Handles Up/Down selection when focused.
2923    ///
2924    /// Column widths are computed automatically from header and cell content.
2925    /// The selected row is highlighted with the theme's selection colors.
2926    pub fn table(&mut self, state: &mut TableState) -> &mut Self {
2927        if state.is_dirty() {
2928            state.recompute_widths();
2929        }
2930
2931        let focused = self.register_focusable();
2932
2933        if focused && !state.rows.is_empty() {
2934            let mut consumed_indices = Vec::new();
2935            for (i, event) in self.events.iter().enumerate() {
2936                if let Event::Key(key) = event {
2937                    match key.code {
2938                        KeyCode::Up | KeyCode::Char('k') => {
2939                            state.selected = state.selected.saturating_sub(1);
2940                            consumed_indices.push(i);
2941                        }
2942                        KeyCode::Down | KeyCode::Char('j') => {
2943                            state.selected =
2944                                (state.selected + 1).min(state.rows.len().saturating_sub(1));
2945                            consumed_indices.push(i);
2946                        }
2947                        _ => {}
2948                    }
2949                }
2950            }
2951            for index in consumed_indices {
2952                self.consumed[index] = true;
2953            }
2954        }
2955
2956        state.selected = state.selected.min(state.rows.len().saturating_sub(1));
2957
2958        let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
2959        self.styled(header_line, Style::new().bold().fg(self.theme.text));
2960
2961        let separator = state
2962            .column_widths()
2963            .iter()
2964            .map(|w| "─".repeat(*w as usize))
2965            .collect::<Vec<_>>()
2966            .join("─┼─");
2967        self.text(separator);
2968
2969        for (idx, row) in state.rows.iter().enumerate() {
2970            let line = format_table_row(row, state.column_widths(), " │ ");
2971            if idx == state.selected {
2972                let mut style = Style::new()
2973                    .bg(self.theme.selected_bg)
2974                    .fg(self.theme.selected_fg);
2975                if focused {
2976                    style = style.bold();
2977                }
2978                self.styled(line, style);
2979            } else {
2980                self.styled(line, Style::new().fg(self.theme.text));
2981            }
2982        }
2983
2984        self
2985    }
2986
2987    /// Render a tab bar. Handles Left/Right navigation when focused.
2988    ///
2989    /// The active tab is rendered in the theme's primary color. If the labels
2990    /// list is empty, nothing is rendered.
2991    pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
2992        if state.labels.is_empty() {
2993            state.selected = 0;
2994            return self;
2995        }
2996
2997        state.selected = state.selected.min(state.labels.len().saturating_sub(1));
2998        let focused = self.register_focusable();
2999
3000        if focused {
3001            let mut consumed_indices = Vec::new();
3002            for (i, event) in self.events.iter().enumerate() {
3003                if let Event::Key(key) = event {
3004                    match key.code {
3005                        KeyCode::Left => {
3006                            state.selected = if state.selected == 0 {
3007                                state.labels.len().saturating_sub(1)
3008                            } else {
3009                                state.selected - 1
3010                            };
3011                            consumed_indices.push(i);
3012                        }
3013                        KeyCode::Right => {
3014                            state.selected = (state.selected + 1) % state.labels.len();
3015                            consumed_indices.push(i);
3016                        }
3017                        _ => {}
3018                    }
3019                }
3020            }
3021
3022            for index in consumed_indices {
3023                self.consumed[index] = true;
3024            }
3025        }
3026
3027        self.interaction_count += 1;
3028        self.commands.push(Command::BeginContainer {
3029            direction: Direction::Row,
3030            gap: 1,
3031            align: Align::Start,
3032            border: None,
3033            border_style: Style::new().fg(self.theme.border),
3034            padding: Padding::default(),
3035            margin: Margin::default(),
3036            constraints: Constraints::default(),
3037            title: None,
3038            grow: 0,
3039        });
3040        for (idx, label) in state.labels.iter().enumerate() {
3041            let style = if idx == state.selected {
3042                let s = Style::new().fg(self.theme.primary).bold();
3043                if focused {
3044                    s.underline()
3045                } else {
3046                    s
3047                }
3048            } else {
3049                Style::new().fg(self.theme.text_dim)
3050            };
3051            self.styled(format!("[ {label} ]"), style);
3052        }
3053        self.commands.push(Command::EndContainer);
3054        self.last_text_idx = None;
3055
3056        self
3057    }
3058
3059    /// Render a clickable button. Returns `true` when activated via Enter, Space, or mouse click.
3060    ///
3061    /// The button is styled with the theme's primary color when focused and the
3062    /// accent color when hovered.
3063    pub fn button(&mut self, label: impl Into<String>) -> bool {
3064        let focused = self.register_focusable();
3065        let interaction_id = self.interaction_count;
3066        self.interaction_count += 1;
3067        let response = self.response_for(interaction_id);
3068
3069        let mut activated = response.clicked;
3070        if focused {
3071            let mut consumed_indices = Vec::new();
3072            for (i, event) in self.events.iter().enumerate() {
3073                if let Event::Key(key) = event {
3074                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3075                        activated = true;
3076                        consumed_indices.push(i);
3077                    }
3078                }
3079            }
3080
3081            for index in consumed_indices {
3082                self.consumed[index] = true;
3083            }
3084        }
3085
3086        let style = if focused {
3087            Style::new().fg(self.theme.primary).bold()
3088        } else if response.hovered {
3089            Style::new().fg(self.theme.accent)
3090        } else {
3091            Style::new().fg(self.theme.text)
3092        };
3093
3094        self.commands.push(Command::BeginContainer {
3095            direction: Direction::Row,
3096            gap: 0,
3097            align: Align::Start,
3098            border: None,
3099            border_style: Style::new().fg(self.theme.border),
3100            padding: Padding::default(),
3101            margin: Margin::default(),
3102            constraints: Constraints::default(),
3103            title: None,
3104            grow: 0,
3105        });
3106        self.styled(format!("[ {} ]", label.into()), style);
3107        self.commands.push(Command::EndContainer);
3108        self.last_text_idx = None;
3109
3110        activated
3111    }
3112
3113    /// Render a checkbox. Toggles the bool on Enter, Space, or click.
3114    ///
3115    /// The checked state is shown with the theme's success color. When focused,
3116    /// a `▸` prefix is added.
3117    pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
3118        let focused = self.register_focusable();
3119        let interaction_id = self.interaction_count;
3120        self.interaction_count += 1;
3121        let response = self.response_for(interaction_id);
3122        let mut should_toggle = response.clicked;
3123
3124        if focused {
3125            let mut consumed_indices = Vec::new();
3126            for (i, event) in self.events.iter().enumerate() {
3127                if let Event::Key(key) = event {
3128                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3129                        should_toggle = true;
3130                        consumed_indices.push(i);
3131                    }
3132                }
3133            }
3134
3135            for index in consumed_indices {
3136                self.consumed[index] = true;
3137            }
3138        }
3139
3140        if should_toggle {
3141            *checked = !*checked;
3142        }
3143
3144        self.commands.push(Command::BeginContainer {
3145            direction: Direction::Row,
3146            gap: 1,
3147            align: Align::Start,
3148            border: None,
3149            border_style: Style::new().fg(self.theme.border),
3150            padding: Padding::default(),
3151            margin: Margin::default(),
3152            constraints: Constraints::default(),
3153            title: None,
3154            grow: 0,
3155        });
3156        let marker_style = if *checked {
3157            Style::new().fg(self.theme.success)
3158        } else {
3159            Style::new().fg(self.theme.text_dim)
3160        };
3161        let marker = if *checked { "[x]" } else { "[ ]" };
3162        let label_text = label.into();
3163        if focused {
3164            self.styled(format!("▸ {marker}"), marker_style.bold());
3165            self.styled(label_text, Style::new().fg(self.theme.text).bold());
3166        } else {
3167            self.styled(marker, marker_style);
3168            self.styled(label_text, Style::new().fg(self.theme.text));
3169        }
3170        self.commands.push(Command::EndContainer);
3171        self.last_text_idx = None;
3172
3173        self
3174    }
3175
3176    /// Render an on/off toggle switch.
3177    ///
3178    /// Toggles `on` when activated via Enter, Space, or click. The switch
3179    /// renders as `●━━ ON` or `━━● OFF` colored with the theme's success or
3180    /// dim color respectively.
3181    pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
3182        let focused = self.register_focusable();
3183        let interaction_id = self.interaction_count;
3184        self.interaction_count += 1;
3185        let response = self.response_for(interaction_id);
3186        let mut should_toggle = response.clicked;
3187
3188        if focused {
3189            let mut consumed_indices = Vec::new();
3190            for (i, event) in self.events.iter().enumerate() {
3191                if let Event::Key(key) = event {
3192                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3193                        should_toggle = true;
3194                        consumed_indices.push(i);
3195                    }
3196                }
3197            }
3198
3199            for index in consumed_indices {
3200                self.consumed[index] = true;
3201            }
3202        }
3203
3204        if should_toggle {
3205            *on = !*on;
3206        }
3207
3208        self.commands.push(Command::BeginContainer {
3209            direction: Direction::Row,
3210            gap: 2,
3211            align: Align::Start,
3212            border: None,
3213            border_style: Style::new().fg(self.theme.border),
3214            padding: Padding::default(),
3215            margin: Margin::default(),
3216            constraints: Constraints::default(),
3217            title: None,
3218            grow: 0,
3219        });
3220        let label_text = label.into();
3221        let switch = if *on { "●━━ ON" } else { "━━● OFF" };
3222        let switch_style = if *on {
3223            Style::new().fg(self.theme.success)
3224        } else {
3225            Style::new().fg(self.theme.text_dim)
3226        };
3227        if focused {
3228            self.styled(
3229                format!("▸ {label_text}"),
3230                Style::new().fg(self.theme.text).bold(),
3231            );
3232            self.styled(switch, switch_style.bold());
3233        } else {
3234            self.styled(label_text, Style::new().fg(self.theme.text));
3235            self.styled(switch, switch_style);
3236        }
3237        self.commands.push(Command::EndContainer);
3238        self.last_text_idx = None;
3239
3240        self
3241    }
3242
3243    /// Render a horizontal divider line.
3244    ///
3245    /// The line is drawn with the theme's border color and expands to fill the
3246    /// container width.
3247    pub fn separator(&mut self) -> &mut Self {
3248        self.commands.push(Command::Text {
3249            content: "─".repeat(200),
3250            style: Style::new().fg(self.theme.border).dim(),
3251            grow: 0,
3252            align: Align::Start,
3253            wrap: false,
3254            margin: Margin::default(),
3255            constraints: Constraints::default(),
3256        });
3257        self.last_text_idx = Some(self.commands.len() - 1);
3258        self
3259    }
3260
3261    /// Render a help bar showing keybinding hints.
3262    ///
3263    /// `bindings` is a slice of `(key, action)` pairs. Keys are rendered in the
3264    /// theme's primary color; actions in the dim text color. Pairs are separated
3265    /// by a `·` character.
3266    pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
3267        if bindings.is_empty() {
3268            return self;
3269        }
3270
3271        self.interaction_count += 1;
3272        self.commands.push(Command::BeginContainer {
3273            direction: Direction::Row,
3274            gap: 2,
3275            align: Align::Start,
3276            border: None,
3277            border_style: Style::new().fg(self.theme.border),
3278            padding: Padding::default(),
3279            margin: Margin::default(),
3280            constraints: Constraints::default(),
3281            title: None,
3282            grow: 0,
3283        });
3284        for (idx, (key, action)) in bindings.iter().enumerate() {
3285            if idx > 0 {
3286                self.styled("·", Style::new().fg(self.theme.text_dim));
3287            }
3288            self.styled(*key, Style::new().bold().fg(self.theme.primary));
3289            self.styled(*action, Style::new().fg(self.theme.text_dim));
3290        }
3291        self.commands.push(Command::EndContainer);
3292        self.last_text_idx = None;
3293
3294        self
3295    }
3296
3297    // ── events ───────────────────────────────────────────────────────
3298
3299    /// Check if a character key was pressed this frame.
3300    ///
3301    /// Returns `true` if the key event has not been consumed by another widget.
3302    pub fn key(&self, c: char) -> bool {
3303        self.events.iter().enumerate().any(|(i, e)| {
3304            !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
3305        })
3306    }
3307
3308    /// Check if a specific key code was pressed this frame.
3309    ///
3310    /// Returns `true` if the key event has not been consumed by another widget.
3311    pub fn key_code(&self, code: KeyCode) -> bool {
3312        self.events
3313            .iter()
3314            .enumerate()
3315            .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
3316    }
3317
3318    /// Check if a character key with specific modifiers was pressed this frame.
3319    ///
3320    /// Returns `true` if the key event has not been consumed by another widget.
3321    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3322        self.events.iter().enumerate().any(|(i, e)| {
3323            !self.consumed[i]
3324                && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3325        })
3326    }
3327
3328    /// Return the position of a left mouse button down event this frame, if any.
3329    ///
3330    /// Returns `None` if no unconsumed mouse-down event occurred.
3331    pub fn mouse_down(&self) -> Option<(u32, u32)> {
3332        self.events.iter().enumerate().find_map(|(i, event)| {
3333            if self.consumed[i] {
3334                return None;
3335            }
3336            if let Event::Mouse(mouse) = event {
3337                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3338                    return Some((mouse.x, mouse.y));
3339                }
3340            }
3341            None
3342        })
3343    }
3344
3345    /// Return the current mouse cursor position, if known.
3346    ///
3347    /// The position is updated on every mouse move or click event. Returns
3348    /// `None` until the first mouse event is received.
3349    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3350        self.mouse_pos
3351    }
3352
3353    /// Return the first unconsumed paste event text, if any.
3354    pub fn paste(&self) -> Option<&str> {
3355        self.events.iter().enumerate().find_map(|(i, event)| {
3356            if self.consumed[i] {
3357                return None;
3358            }
3359            if let Event::Paste(ref text) = event {
3360                return Some(text.as_str());
3361            }
3362            None
3363        })
3364    }
3365
3366    /// Check if an unconsumed scroll-up event occurred this frame.
3367    pub fn scroll_up(&self) -> bool {
3368        self.events.iter().enumerate().any(|(i, event)| {
3369            !self.consumed[i]
3370                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3371        })
3372    }
3373
3374    /// Check if an unconsumed scroll-down event occurred this frame.
3375    pub fn scroll_down(&self) -> bool {
3376        self.events.iter().enumerate().any(|(i, event)| {
3377            !self.consumed[i]
3378                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3379        })
3380    }
3381
3382    /// Signal the run loop to exit after this frame.
3383    pub fn quit(&mut self) {
3384        self.should_quit = true;
3385    }
3386
3387    /// Get the current theme.
3388    pub fn theme(&self) -> &Theme {
3389        &self.theme
3390    }
3391
3392    /// Change the theme for subsequent rendering.
3393    ///
3394    /// All widgets rendered after this call will use the new theme's colors.
3395    pub fn set_theme(&mut self, theme: Theme) {
3396        self.theme = theme;
3397    }
3398
3399    // ── info ─────────────────────────────────────────────────────────
3400
3401    /// Get the terminal width in cells.
3402    pub fn width(&self) -> u32 {
3403        self.area_width
3404    }
3405
3406    /// Get the terminal height in cells.
3407    pub fn height(&self) -> u32 {
3408        self.area_height
3409    }
3410
3411    /// Get the current tick count (increments each frame).
3412    ///
3413    /// Useful for animations and time-based logic. The tick starts at 0 and
3414    /// increases by 1 on every rendered frame.
3415    pub fn tick(&self) -> u64 {
3416        self.tick
3417    }
3418
3419    /// Return whether the layout debugger is enabled.
3420    ///
3421    /// The debugger is toggled with F12 at runtime.
3422    pub fn debug_enabled(&self) -> bool {
3423        self.debug
3424    }
3425}
3426
3427#[inline]
3428fn byte_index_for_char(value: &str, char_index: usize) -> usize {
3429    if char_index == 0 {
3430        return 0;
3431    }
3432    value
3433        .char_indices()
3434        .nth(char_index)
3435        .map_or(value.len(), |(idx, _)| idx)
3436}
3437
3438fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
3439    let mut parts: Vec<String> = Vec::new();
3440    for (i, width) in widths.iter().enumerate() {
3441        let cell = cells.get(i).map(String::as_str).unwrap_or("");
3442        let cell_width = UnicodeWidthStr::width(cell) as u32;
3443        let padding = (*width).saturating_sub(cell_width) as usize;
3444        parts.push(format!("{cell}{}", " ".repeat(padding)));
3445    }
3446    parts.join(separator)
3447}
3448
3449fn format_compact_number(value: f64) -> String {
3450    if value.fract().abs() < f64::EPSILON {
3451        return format!("{value:.0}");
3452    }
3453
3454    let mut s = format!("{value:.2}");
3455    while s.contains('.') && s.ends_with('0') {
3456        s.pop();
3457    }
3458    if s.ends_with('.') {
3459        s.pop();
3460    }
3461    s
3462}
3463
3464fn center_text(text: &str, width: usize) -> String {
3465    let text_width = UnicodeWidthStr::width(text);
3466    if text_width >= width {
3467        return text.to_string();
3468    }
3469
3470    let total = width - text_width;
3471    let left = total / 2;
3472    let right = total - left;
3473    format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
3474}