Skip to main content

slt/
context.rs

1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseKind};
3use crate::halfblock::HalfBlockImage;
4use crate::layout::{Command, Direction};
5use crate::rect::Rect;
6use crate::style::{
7    Align, Border, BorderSides, Breakpoint, Color, Constraints, Justify, Margin, Modifiers,
8    Padding, Style, Theme,
9};
10use crate::widgets::{
11    ApprovalAction, ButtonVariant, CommandPaletteState, ContextItem, FormField, FormState,
12    ListState, MultiSelectState, RadioState, ScrollState, SelectState, SpinnerState,
13    StreamingTextState, TableState, TabsState, TextInputState, TextareaState, ToastLevel,
14    ToastState, ToolApprovalState, TreeState,
15};
16use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
17
18#[allow(dead_code)]
19fn slt_assert(condition: bool, msg: &str) {
20    if !condition {
21        panic!("[SLT] {}", msg);
22    }
23}
24
25#[cfg(debug_assertions)]
26#[allow(dead_code)]
27fn slt_warn(msg: &str) {
28    eprintln!("\x1b[33m[SLT warning]\x1b[0m {}", msg);
29}
30
31#[cfg(not(debug_assertions))]
32#[allow(dead_code)]
33fn slt_warn(_msg: &str) {}
34
35/// Handle to state created by `use_state()`. Access via `.get(ui)` / `.get_mut(ui)`.
36pub struct State<T> {
37    idx: usize,
38    _marker: std::marker::PhantomData<T>,
39}
40
41impl<T: 'static> State<T> {
42    /// Read the current value.
43    pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
44        ui.hook_states[self.idx]
45            .downcast_ref::<T>()
46            .expect("use_state type mismatch")
47    }
48
49    /// Mutably access the current value.
50    pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
51        ui.hook_states[self.idx]
52            .downcast_mut::<T>()
53            .expect("use_state type mismatch")
54    }
55}
56
57/// Result of a container mouse interaction.
58///
59/// Returned by [`Context::col`], [`Context::row`], and [`ContainerBuilder::col`] /
60/// [`ContainerBuilder::row`] so you can react to clicks and hover without a separate
61/// event loop.
62#[derive(Debug, Clone, Copy, Default)]
63pub struct Response {
64    /// Whether the container was clicked this frame.
65    pub clicked: bool,
66    /// Whether the mouse is over the container.
67    pub hovered: bool,
68}
69
70/// Direction for bar chart rendering.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum BarDirection {
73    /// Bars grow horizontally (default, current behavior).
74    Horizontal,
75    /// Bars grow vertically from bottom to top.
76    Vertical,
77}
78
79/// A single bar in a styled bar chart.
80#[derive(Debug, Clone)]
81pub struct Bar {
82    /// Display label for this bar.
83    pub label: String,
84    /// Numeric value.
85    pub value: f64,
86    /// Bar color. If None, uses theme.primary.
87    pub color: Option<Color>,
88}
89
90impl Bar {
91    /// Create a new bar with a label and value.
92    pub fn new(label: impl Into<String>, value: f64) -> Self {
93        Self {
94            label: label.into(),
95            value,
96            color: None,
97        }
98    }
99
100    /// Set the bar color.
101    pub fn color(mut self, color: Color) -> Self {
102        self.color = Some(color);
103        self
104    }
105}
106
107/// A group of bars rendered together (for grouped bar charts).
108#[derive(Debug, Clone)]
109pub struct BarGroup {
110    /// Group label displayed below the bars.
111    pub label: String,
112    /// Bars in this group.
113    pub bars: Vec<Bar>,
114}
115
116impl BarGroup {
117    /// Create a new bar group with a label and bars.
118    pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
119        Self {
120            label: label.into(),
121            bars,
122        }
123    }
124}
125
126/// Trait for creating custom widgets.
127///
128/// Implement this trait to build reusable, composable widgets with full access
129/// to the [`Context`] API — focus, events, theming, layout, and mouse interaction.
130///
131/// # Examples
132///
133/// A simple rating widget:
134///
135/// ```no_run
136/// use slt::{Context, Widget, Color};
137///
138/// struct Rating {
139///     value: u8,
140///     max: u8,
141/// }
142///
143/// impl Rating {
144///     fn new(value: u8, max: u8) -> Self {
145///         Self { value, max }
146///     }
147/// }
148///
149/// impl Widget for Rating {
150///     type Response = bool;
151///
152///     fn ui(&mut self, ui: &mut Context) -> bool {
153///         let focused = ui.register_focusable();
154///         let mut changed = false;
155///
156///         if focused {
157///             if ui.key('+') && self.value < self.max {
158///                 self.value += 1;
159///                 changed = true;
160///             }
161///             if ui.key('-') && self.value > 0 {
162///                 self.value -= 1;
163///                 changed = true;
164///             }
165///         }
166///
167///         let stars: String = (0..self.max).map(|i| {
168///             if i < self.value { '★' } else { '☆' }
169///         }).collect();
170///
171///         let color = if focused { Color::Yellow } else { Color::White };
172///         ui.styled(stars, slt::Style::new().fg(color));
173///
174///         changed
175///     }
176/// }
177///
178/// fn main() -> std::io::Result<()> {
179///     let mut rating = Rating::new(3, 5);
180///     slt::run(|ui| {
181///         if ui.key('q') { ui.quit(); }
182///         ui.text("Rate this:");
183///         ui.widget(&mut rating);
184///     })
185/// }
186/// ```
187pub trait Widget {
188    /// The value returned after rendering. Use `()` for widgets with no return,
189    /// `bool` for widgets that report changes, or [`Response`] for click/hover.
190    type Response;
191
192    /// Render the widget into the given context.
193    ///
194    /// Use [`Context::register_focusable`] to participate in Tab focus cycling,
195    /// [`Context::key`] / [`Context::key_code`] to handle keyboard input,
196    /// and [`Context::interaction`] to detect clicks and hovers.
197    fn ui(&mut self, ctx: &mut Context) -> Self::Response;
198}
199
200/// The main rendering context passed to your closure each frame.
201///
202/// Provides all methods for building UI: text, containers, widgets, and event
203/// handling. You receive a `&mut Context` on every frame and describe what to
204/// render by calling its methods. SLT collects those calls, lays them out with
205/// flexbox, diffs against the previous frame, and flushes only changed cells.
206///
207/// # Example
208///
209/// ```no_run
210/// slt::run(|ui: &mut slt::Context| {
211///     if ui.key('q') { ui.quit(); }
212///     ui.text("Hello, world!").bold();
213/// });
214/// ```
215pub struct Context {
216    pub(crate) commands: Vec<Command>,
217    pub(crate) events: Vec<Event>,
218    pub(crate) consumed: Vec<bool>,
219    pub(crate) should_quit: bool,
220    pub(crate) area_width: u32,
221    pub(crate) area_height: u32,
222    pub(crate) tick: u64,
223    pub(crate) focus_index: usize,
224    pub(crate) focus_count: usize,
225    pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
226    pub(crate) hook_cursor: usize,
227    prev_focus_count: usize,
228    scroll_count: usize,
229    prev_scroll_infos: Vec<(u32, u32)>,
230    prev_scroll_rects: Vec<Rect>,
231    interaction_count: usize,
232    pub(crate) prev_hit_map: Vec<Rect>,
233    pub(crate) group_stack: Vec<String>,
234    pub(crate) prev_group_rects: Vec<(String, Rect)>,
235    group_count: usize,
236    prev_focus_groups: Vec<Option<String>>,
237    _prev_focus_rects: Vec<(usize, Rect)>,
238    mouse_pos: Option<(u32, u32)>,
239    click_pos: Option<(u32, u32)>,
240    last_text_idx: Option<usize>,
241    overlay_depth: usize,
242    pub(crate) modal_active: bool,
243    prev_modal_active: bool,
244    pub(crate) clipboard_text: Option<String>,
245    debug: bool,
246    theme: Theme,
247    pub(crate) dark_mode: bool,
248}
249
250/// Fluent builder for configuring containers before calling `.col()` or `.row()`.
251///
252/// Obtain one via [`Context::container`] or [`Context::bordered`]. Chain the
253/// configuration methods you need, then finalize with `.col(|ui| { ... })` or
254/// `.row(|ui| { ... })`.
255///
256/// # Example
257///
258/// ```no_run
259/// # slt::run(|ui: &mut slt::Context| {
260/// use slt::{Border, Color};
261/// ui.container()
262///     .border(Border::Rounded)
263///     .pad(1)
264///     .grow(1)
265///     .col(|ui| {
266///         ui.text("inside a bordered, padded, growing column");
267///     });
268/// # });
269/// ```
270#[must_use = "configure and finalize with .col() or .row()"]
271pub struct ContainerBuilder<'a> {
272    ctx: &'a mut Context,
273    gap: u32,
274    align: Align,
275    justify: Justify,
276    border: Option<Border>,
277    border_sides: BorderSides,
278    border_style: Style,
279    bg_color: Option<Color>,
280    dark_bg_color: Option<Color>,
281    dark_border_style: Option<Style>,
282    group_hover_bg: Option<Color>,
283    group_hover_border_style: Option<Style>,
284    group_name: Option<String>,
285    padding: Padding,
286    margin: Margin,
287    constraints: Constraints,
288    title: Option<(String, Style)>,
289    grow: u16,
290    scroll_offset: Option<u32>,
291}
292
293/// Drawing context for the [`Context::canvas`] widget.
294///
295/// Provides pixel-level drawing on a braille character grid. Each terminal
296/// cell maps to a 2x4 dot matrix, so a canvas of `width` columns x `height`
297/// rows gives `width*2` x `height*4` pixel resolution.
298/// A colored pixel in the canvas grid.
299#[derive(Debug, Clone, Copy)]
300struct CanvasPixel {
301    bits: u32,
302    color: Color,
303}
304
305/// Text label placed on the canvas.
306#[derive(Debug, Clone)]
307struct CanvasLabel {
308    x: usize,
309    y: usize,
310    text: String,
311    color: Color,
312}
313
314/// A layer in the canvas, supporting z-ordering.
315#[derive(Debug, Clone)]
316struct CanvasLayer {
317    grid: Vec<Vec<CanvasPixel>>,
318    labels: Vec<CanvasLabel>,
319}
320
321pub struct CanvasContext {
322    layers: Vec<CanvasLayer>,
323    cols: usize,
324    rows: usize,
325    px_w: usize,
326    px_h: usize,
327    current_color: Color,
328}
329
330impl CanvasContext {
331    fn new(cols: usize, rows: usize) -> Self {
332        Self {
333            layers: vec![Self::new_layer(cols, rows)],
334            cols,
335            rows,
336            px_w: cols * 2,
337            px_h: rows * 4,
338            current_color: Color::Reset,
339        }
340    }
341
342    fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
343        CanvasLayer {
344            grid: vec![
345                vec![
346                    CanvasPixel {
347                        bits: 0,
348                        color: Color::Reset,
349                    };
350                    cols
351                ];
352                rows
353            ],
354            labels: Vec::new(),
355        }
356    }
357
358    fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
359        self.layers.last_mut()
360    }
361
362    fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
363        if x >= self.px_w || y >= self.px_h {
364            return;
365        }
366
367        let char_col = x / 2;
368        let char_row = y / 4;
369        let sub_col = x % 2;
370        let sub_row = y % 4;
371        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
372        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
373
374        let bit = if sub_col == 0 {
375            LEFT_BITS[sub_row]
376        } else {
377            RIGHT_BITS[sub_row]
378        };
379
380        if let Some(layer) = self.current_layer_mut() {
381            let cell = &mut layer.grid[char_row][char_col];
382            let new_bits = cell.bits | bit;
383            if new_bits != cell.bits {
384                cell.bits = new_bits;
385                cell.color = color;
386            }
387        }
388    }
389
390    fn dot_isize(&mut self, x: isize, y: isize) {
391        if x >= 0 && y >= 0 {
392            self.dot(x as usize, y as usize);
393        }
394    }
395
396    /// Get the pixel width of the canvas.
397    pub fn width(&self) -> usize {
398        self.px_w
399    }
400
401    /// Get the pixel height of the canvas.
402    pub fn height(&self) -> usize {
403        self.px_h
404    }
405
406    /// Set a single pixel at `(x, y)`.
407    pub fn dot(&mut self, x: usize, y: usize) {
408        self.dot_with_color(x, y, self.current_color);
409    }
410
411    /// Draw a line from `(x0, y0)` to `(x1, y1)` using Bresenham's algorithm.
412    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
413        let (mut x, mut y) = (x0 as isize, y0 as isize);
414        let (x1, y1) = (x1 as isize, y1 as isize);
415        let dx = (x1 - x).abs();
416        let dy = -(y1 - y).abs();
417        let sx = if x < x1 { 1 } else { -1 };
418        let sy = if y < y1 { 1 } else { -1 };
419        let mut err = dx + dy;
420
421        loop {
422            self.dot_isize(x, y);
423            if x == x1 && y == y1 {
424                break;
425            }
426            let e2 = 2 * err;
427            if e2 >= dy {
428                err += dy;
429                x += sx;
430            }
431            if e2 <= dx {
432                err += dx;
433                y += sy;
434            }
435        }
436    }
437
438    /// Draw a rectangle outline from `(x, y)` with `w` width and `h` height.
439    pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
440        if w == 0 || h == 0 {
441            return;
442        }
443
444        self.line(x, y, x + w.saturating_sub(1), y);
445        self.line(
446            x + w.saturating_sub(1),
447            y,
448            x + w.saturating_sub(1),
449            y + h.saturating_sub(1),
450        );
451        self.line(
452            x + w.saturating_sub(1),
453            y + h.saturating_sub(1),
454            x,
455            y + h.saturating_sub(1),
456        );
457        self.line(x, y + h.saturating_sub(1), x, y);
458    }
459
460    /// Draw a circle outline centered at `(cx, cy)` with radius `r`.
461    pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
462        let mut x = r as isize;
463        let mut y: isize = 0;
464        let mut err: isize = 1 - x;
465        let (cx, cy) = (cx as isize, cy as isize);
466
467        while x >= y {
468            for &(dx, dy) in &[
469                (x, y),
470                (y, x),
471                (-x, y),
472                (-y, x),
473                (x, -y),
474                (y, -x),
475                (-x, -y),
476                (-y, -x),
477            ] {
478                let px = cx + dx;
479                let py = cy + dy;
480                self.dot_isize(px, py);
481            }
482
483            y += 1;
484            if err < 0 {
485                err += 2 * y + 1;
486            } else {
487                x -= 1;
488                err += 2 * (y - x) + 1;
489            }
490        }
491    }
492
493    /// Set the drawing color for subsequent shapes.
494    pub fn set_color(&mut self, color: Color) {
495        self.current_color = color;
496    }
497
498    /// Get the current drawing color.
499    pub fn color(&self) -> Color {
500        self.current_color
501    }
502
503    /// Draw a filled rectangle.
504    pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
505        if w == 0 || h == 0 {
506            return;
507        }
508
509        let x_end = x.saturating_add(w).min(self.px_w);
510        let y_end = y.saturating_add(h).min(self.px_h);
511        if x >= x_end || y >= y_end {
512            return;
513        }
514
515        for yy in y..y_end {
516            self.line(x, yy, x_end.saturating_sub(1), yy);
517        }
518    }
519
520    /// Draw a filled circle.
521    pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
522        let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
523        for y in (cy - r)..=(cy + r) {
524            let dy = y - cy;
525            let span_sq = (r * r - dy * dy).max(0);
526            let dx = (span_sq as f64).sqrt() as isize;
527            for x in (cx - dx)..=(cx + dx) {
528                self.dot_isize(x, y);
529            }
530        }
531    }
532
533    /// Draw a triangle outline.
534    pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
535        self.line(x0, y0, x1, y1);
536        self.line(x1, y1, x2, y2);
537        self.line(x2, y2, x0, y0);
538    }
539
540    /// Draw a filled triangle.
541    pub fn filled_triangle(
542        &mut self,
543        x0: usize,
544        y0: usize,
545        x1: usize,
546        y1: usize,
547        x2: usize,
548        y2: usize,
549    ) {
550        let vertices = [
551            (x0 as isize, y0 as isize),
552            (x1 as isize, y1 as isize),
553            (x2 as isize, y2 as isize),
554        ];
555        let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
556        let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
557
558        for y in min_y..=max_y {
559            let mut intersections: Vec<f64> = Vec::new();
560
561            for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
562                let (x_a, y_a) = vertices[edge.0];
563                let (x_b, y_b) = vertices[edge.1];
564                if y_a == y_b {
565                    continue;
566                }
567
568                let (x_start, y_start, x_end, y_end) = if y_a < y_b {
569                    (x_a, y_a, x_b, y_b)
570                } else {
571                    (x_b, y_b, x_a, y_a)
572                };
573
574                if y < y_start || y >= y_end {
575                    continue;
576                }
577
578                let t = (y - y_start) as f64 / (y_end - y_start) as f64;
579                intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
580            }
581
582            intersections.sort_by(|a, b| a.total_cmp(b));
583            let mut i = 0usize;
584            while i + 1 < intersections.len() {
585                let x_start = intersections[i].ceil() as isize;
586                let x_end = intersections[i + 1].floor() as isize;
587                for x in x_start..=x_end {
588                    self.dot_isize(x, y);
589                }
590                i += 2;
591            }
592        }
593
594        self.triangle(x0, y0, x1, y1, x2, y2);
595    }
596
597    /// Draw multiple points at once.
598    pub fn points(&mut self, pts: &[(usize, usize)]) {
599        for &(x, y) in pts {
600            self.dot(x, y);
601        }
602    }
603
604    /// Draw a polyline connecting the given points in order.
605    pub fn polyline(&mut self, pts: &[(usize, usize)]) {
606        for window in pts.windows(2) {
607            if let [(x0, y0), (x1, y1)] = window {
608                self.line(*x0, *y0, *x1, *y1);
609            }
610        }
611    }
612
613    /// Place a text label at pixel position `(x, y)`.
614    /// Text is rendered in regular characters overlaying the braille grid.
615    pub fn print(&mut self, x: usize, y: usize, text: &str) {
616        if text.is_empty() {
617            return;
618        }
619
620        let color = self.current_color;
621        if let Some(layer) = self.current_layer_mut() {
622            layer.labels.push(CanvasLabel {
623                x,
624                y,
625                text: text.to_string(),
626                color,
627            });
628        }
629    }
630
631    /// Start a new drawing layer. Shapes on later layers overlay earlier ones.
632    pub fn layer(&mut self) {
633        self.layers.push(Self::new_layer(self.cols, self.rows));
634    }
635
636    pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
637        let mut final_grid = vec![
638            vec![
639                CanvasPixel {
640                    bits: 0,
641                    color: Color::Reset,
642                };
643                self.cols
644            ];
645            self.rows
646        ];
647        let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
648            vec![vec![None; self.cols]; self.rows];
649
650        for layer in &self.layers {
651            for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
652                for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
653                    let src = layer.grid[row][col];
654                    if src.bits == 0 {
655                        continue;
656                    }
657
658                    let merged = dst.bits | src.bits;
659                    if merged != dst.bits {
660                        dst.bits = merged;
661                        dst.color = src.color;
662                    }
663                }
664            }
665
666            for label in &layer.labels {
667                let row = label.y / 4;
668                if row >= self.rows {
669                    continue;
670                }
671                let start_col = label.x / 2;
672                for (offset, ch) in label.text.chars().enumerate() {
673                    let col = start_col + offset;
674                    if col >= self.cols {
675                        break;
676                    }
677                    labels_overlay[row][col] = Some((ch, label.color));
678                }
679            }
680        }
681
682        let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
683        for row in 0..self.rows {
684            let mut segments: Vec<(String, Color)> = Vec::new();
685            let mut current_color: Option<Color> = None;
686            let mut current_text = String::new();
687
688            for col in 0..self.cols {
689                let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
690                    (label_ch, label_color)
691                } else {
692                    let bits = final_grid[row][col].bits;
693                    let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
694                    (ch, final_grid[row][col].color)
695                };
696
697                match current_color {
698                    Some(c) if c == color => {
699                        current_text.push(ch);
700                    }
701                    Some(c) => {
702                        segments.push((std::mem::take(&mut current_text), c));
703                        current_text.push(ch);
704                        current_color = Some(color);
705                    }
706                    None => {
707                        current_text.push(ch);
708                        current_color = Some(color);
709                    }
710                }
711            }
712
713            if let Some(color) = current_color {
714                segments.push((current_text, color));
715            }
716            lines.push(segments);
717        }
718
719        lines
720    }
721}
722
723impl<'a> ContainerBuilder<'a> {
724    // ── border ───────────────────────────────────────────────────────
725
726    /// Set the border style.
727    pub fn border(mut self, border: Border) -> Self {
728        self.border = Some(border);
729        self
730    }
731
732    /// Show or hide the top border.
733    pub fn border_top(mut self, show: bool) -> Self {
734        self.border_sides.top = show;
735        self
736    }
737
738    /// Show or hide the right border.
739    pub fn border_right(mut self, show: bool) -> Self {
740        self.border_sides.right = show;
741        self
742    }
743
744    /// Show or hide the bottom border.
745    pub fn border_bottom(mut self, show: bool) -> Self {
746        self.border_sides.bottom = show;
747        self
748    }
749
750    /// Show or hide the left border.
751    pub fn border_left(mut self, show: bool) -> Self {
752        self.border_sides.left = show;
753        self
754    }
755
756    /// Set which border sides are visible.
757    pub fn border_sides(mut self, sides: BorderSides) -> Self {
758        self.border_sides = sides;
759        self
760    }
761
762    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
763    pub fn rounded(self) -> Self {
764        self.border(Border::Rounded)
765    }
766
767    /// Set the style applied to the border characters.
768    pub fn border_style(mut self, style: Style) -> Self {
769        self.border_style = style;
770        self
771    }
772
773    /// Border style used when dark mode is active.
774    pub fn dark_border_style(mut self, style: Style) -> Self {
775        self.dark_border_style = Some(style);
776        self
777    }
778
779    pub fn bg(mut self, color: Color) -> Self {
780        self.bg_color = Some(color);
781        self
782    }
783
784    /// Background color used when dark mode is active.
785    pub fn dark_bg(mut self, color: Color) -> Self {
786        self.dark_bg_color = Some(color);
787        self
788    }
789
790    /// Background color applied when the parent group is hovered.
791    pub fn group_hover_bg(mut self, color: Color) -> Self {
792        self.group_hover_bg = Some(color);
793        self
794    }
795
796    /// Border style applied when the parent group is hovered.
797    pub fn group_hover_border_style(mut self, style: Style) -> Self {
798        self.group_hover_border_style = Some(style);
799        self
800    }
801
802    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
803
804    /// Set uniform padding on all sides. Alias for [`pad`](Self::pad).
805    pub fn p(self, value: u32) -> Self {
806        self.pad(value)
807    }
808
809    /// Set uniform padding on all sides.
810    pub fn pad(mut self, value: u32) -> Self {
811        self.padding = Padding::all(value);
812        self
813    }
814
815    /// Set horizontal padding (left and right).
816    pub fn px(mut self, value: u32) -> Self {
817        self.padding.left = value;
818        self.padding.right = value;
819        self
820    }
821
822    /// Set vertical padding (top and bottom).
823    pub fn py(mut self, value: u32) -> Self {
824        self.padding.top = value;
825        self.padding.bottom = value;
826        self
827    }
828
829    /// Set top padding.
830    pub fn pt(mut self, value: u32) -> Self {
831        self.padding.top = value;
832        self
833    }
834
835    /// Set right padding.
836    pub fn pr(mut self, value: u32) -> Self {
837        self.padding.right = value;
838        self
839    }
840
841    /// Set bottom padding.
842    pub fn pb(mut self, value: u32) -> Self {
843        self.padding.bottom = value;
844        self
845    }
846
847    /// Set left padding.
848    pub fn pl(mut self, value: u32) -> Self {
849        self.padding.left = value;
850        self
851    }
852
853    /// Set per-side padding using a [`Padding`] value.
854    pub fn padding(mut self, padding: Padding) -> Self {
855        self.padding = padding;
856        self
857    }
858
859    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
860
861    /// Set uniform margin on all sides.
862    pub fn m(mut self, value: u32) -> Self {
863        self.margin = Margin::all(value);
864        self
865    }
866
867    /// Set horizontal margin (left and right).
868    pub fn mx(mut self, value: u32) -> Self {
869        self.margin.left = value;
870        self.margin.right = value;
871        self
872    }
873
874    /// Set vertical margin (top and bottom).
875    pub fn my(mut self, value: u32) -> Self {
876        self.margin.top = value;
877        self.margin.bottom = value;
878        self
879    }
880
881    /// Set top margin.
882    pub fn mt(mut self, value: u32) -> Self {
883        self.margin.top = value;
884        self
885    }
886
887    /// Set right margin.
888    pub fn mr(mut self, value: u32) -> Self {
889        self.margin.right = value;
890        self
891    }
892
893    /// Set bottom margin.
894    pub fn mb(mut self, value: u32) -> Self {
895        self.margin.bottom = value;
896        self
897    }
898
899    /// Set left margin.
900    pub fn ml(mut self, value: u32) -> Self {
901        self.margin.left = value;
902        self
903    }
904
905    /// Set per-side margin using a [`Margin`] value.
906    pub fn margin(mut self, margin: Margin) -> Self {
907        self.margin = margin;
908        self
909    }
910
911    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
912
913    /// Set a fixed width (sets both min and max width).
914    pub fn w(mut self, value: u32) -> Self {
915        self.constraints.min_width = Some(value);
916        self.constraints.max_width = Some(value);
917        self
918    }
919
920    /// Width applied only at Xs breakpoint (< 40 cols).
921    ///
922    /// # Example
923    /// ```ignore
924    /// ui.container().w(20).md_w(40).lg_w(60).col(|ui| { ... });
925    /// ```
926    pub fn xs_w(self, value: u32) -> Self {
927        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
928        if is_xs {
929            self.w(value)
930        } else {
931            self
932        }
933    }
934
935    /// Width applied only at Sm breakpoint (40-79 cols).
936    pub fn sm_w(self, value: u32) -> Self {
937        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
938        if is_sm {
939            self.w(value)
940        } else {
941            self
942        }
943    }
944
945    /// Width applied only at Md breakpoint (80-119 cols).
946    pub fn md_w(self, value: u32) -> Self {
947        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
948        if is_md {
949            self.w(value)
950        } else {
951            self
952        }
953    }
954
955    /// Width applied only at Lg breakpoint (120-159 cols).
956    pub fn lg_w(self, value: u32) -> Self {
957        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
958        if is_lg {
959            self.w(value)
960        } else {
961            self
962        }
963    }
964
965    /// Width applied only at Xl breakpoint (>= 160 cols).
966    pub fn xl_w(self, value: u32) -> Self {
967        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
968        if is_xl {
969            self.w(value)
970        } else {
971            self
972        }
973    }
974
975    /// Set a fixed height (sets both min and max height).
976    pub fn h(mut self, value: u32) -> Self {
977        self.constraints.min_height = Some(value);
978        self.constraints.max_height = Some(value);
979        self
980    }
981
982    /// Height applied only at Xs breakpoint (< 40 cols).
983    pub fn xs_h(self, value: u32) -> Self {
984        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
985        if is_xs {
986            self.h(value)
987        } else {
988            self
989        }
990    }
991
992    /// Height applied only at Sm breakpoint (40-79 cols).
993    pub fn sm_h(self, value: u32) -> Self {
994        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
995        if is_sm {
996            self.h(value)
997        } else {
998            self
999        }
1000    }
1001
1002    /// Height applied only at Md breakpoint (80-119 cols).
1003    pub fn md_h(self, value: u32) -> Self {
1004        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1005        if is_md {
1006            self.h(value)
1007        } else {
1008            self
1009        }
1010    }
1011
1012    /// Height applied only at Lg breakpoint (120-159 cols).
1013    pub fn lg_h(self, value: u32) -> Self {
1014        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1015        if is_lg {
1016            self.h(value)
1017        } else {
1018            self
1019        }
1020    }
1021
1022    /// Height applied only at Xl breakpoint (>= 160 cols).
1023    pub fn xl_h(self, value: u32) -> Self {
1024        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1025        if is_xl {
1026            self.h(value)
1027        } else {
1028            self
1029        }
1030    }
1031
1032    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
1033    pub fn min_w(mut self, value: u32) -> Self {
1034        self.constraints.min_width = Some(value);
1035        self
1036    }
1037
1038    /// Minimum width applied only at Xs breakpoint (< 40 cols).
1039    pub fn xs_min_w(self, value: u32) -> Self {
1040        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1041        if is_xs {
1042            self.min_w(value)
1043        } else {
1044            self
1045        }
1046    }
1047
1048    /// Minimum width applied only at Sm breakpoint (40-79 cols).
1049    pub fn sm_min_w(self, value: u32) -> Self {
1050        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1051        if is_sm {
1052            self.min_w(value)
1053        } else {
1054            self
1055        }
1056    }
1057
1058    /// Minimum width applied only at Md breakpoint (80-119 cols).
1059    pub fn md_min_w(self, value: u32) -> Self {
1060        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1061        if is_md {
1062            self.min_w(value)
1063        } else {
1064            self
1065        }
1066    }
1067
1068    /// Minimum width applied only at Lg breakpoint (120-159 cols).
1069    pub fn lg_min_w(self, value: u32) -> Self {
1070        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1071        if is_lg {
1072            self.min_w(value)
1073        } else {
1074            self
1075        }
1076    }
1077
1078    /// Minimum width applied only at Xl breakpoint (>= 160 cols).
1079    pub fn xl_min_w(self, value: u32) -> Self {
1080        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1081        if is_xl {
1082            self.min_w(value)
1083        } else {
1084            self
1085        }
1086    }
1087
1088    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
1089    pub fn max_w(mut self, value: u32) -> Self {
1090        self.constraints.max_width = Some(value);
1091        self
1092    }
1093
1094    /// Maximum width applied only at Xs breakpoint (< 40 cols).
1095    pub fn xs_max_w(self, value: u32) -> Self {
1096        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1097        if is_xs {
1098            self.max_w(value)
1099        } else {
1100            self
1101        }
1102    }
1103
1104    /// Maximum width applied only at Sm breakpoint (40-79 cols).
1105    pub fn sm_max_w(self, value: u32) -> Self {
1106        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1107        if is_sm {
1108            self.max_w(value)
1109        } else {
1110            self
1111        }
1112    }
1113
1114    /// Maximum width applied only at Md breakpoint (80-119 cols).
1115    pub fn md_max_w(self, value: u32) -> Self {
1116        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1117        if is_md {
1118            self.max_w(value)
1119        } else {
1120            self
1121        }
1122    }
1123
1124    /// Maximum width applied only at Lg breakpoint (120-159 cols).
1125    pub fn lg_max_w(self, value: u32) -> Self {
1126        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1127        if is_lg {
1128            self.max_w(value)
1129        } else {
1130            self
1131        }
1132    }
1133
1134    /// Maximum width applied only at Xl breakpoint (>= 160 cols).
1135    pub fn xl_max_w(self, value: u32) -> Self {
1136        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1137        if is_xl {
1138            self.max_w(value)
1139        } else {
1140            self
1141        }
1142    }
1143
1144    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
1145    pub fn min_h(mut self, value: u32) -> Self {
1146        self.constraints.min_height = Some(value);
1147        self
1148    }
1149
1150    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
1151    pub fn max_h(mut self, value: u32) -> Self {
1152        self.constraints.max_height = Some(value);
1153        self
1154    }
1155
1156    /// Set the minimum width constraint in cells.
1157    pub fn min_width(mut self, value: u32) -> Self {
1158        self.constraints.min_width = Some(value);
1159        self
1160    }
1161
1162    /// Set the maximum width constraint in cells.
1163    pub fn max_width(mut self, value: u32) -> Self {
1164        self.constraints.max_width = Some(value);
1165        self
1166    }
1167
1168    /// Set the minimum height constraint in rows.
1169    pub fn min_height(mut self, value: u32) -> Self {
1170        self.constraints.min_height = Some(value);
1171        self
1172    }
1173
1174    /// Set the maximum height constraint in rows.
1175    pub fn max_height(mut self, value: u32) -> Self {
1176        self.constraints.max_height = Some(value);
1177        self
1178    }
1179
1180    /// Set width as a percentage (1-100) of the parent container.
1181    pub fn w_pct(mut self, pct: u8) -> Self {
1182        self.constraints.width_pct = Some(pct.min(100));
1183        self
1184    }
1185
1186    /// Set height as a percentage (1-100) of the parent container.
1187    pub fn h_pct(mut self, pct: u8) -> Self {
1188        self.constraints.height_pct = Some(pct.min(100));
1189        self
1190    }
1191
1192    /// Set all size constraints at once using a [`Constraints`] value.
1193    pub fn constraints(mut self, constraints: Constraints) -> Self {
1194        self.constraints = constraints;
1195        self
1196    }
1197
1198    // ── flex ─────────────────────────────────────────────────────────
1199
1200    /// Set the gap (in cells) between child elements.
1201    pub fn gap(mut self, gap: u32) -> Self {
1202        self.gap = gap;
1203        self
1204    }
1205
1206    /// Gap applied only at Xs breakpoint (< 40 cols).
1207    pub fn xs_gap(self, value: u32) -> Self {
1208        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1209        if is_xs {
1210            self.gap(value)
1211        } else {
1212            self
1213        }
1214    }
1215
1216    /// Gap applied only at Sm breakpoint (40-79 cols).
1217    pub fn sm_gap(self, value: u32) -> Self {
1218        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1219        if is_sm {
1220            self.gap(value)
1221        } else {
1222            self
1223        }
1224    }
1225
1226    /// Gap applied only at Md breakpoint (80-119 cols).
1227    ///
1228    /// # Example
1229    /// ```ignore
1230    /// ui.container().gap(0).md_gap(2).col(|ui| { ... });
1231    /// ```
1232    pub fn md_gap(self, value: u32) -> Self {
1233        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1234        if is_md {
1235            self.gap(value)
1236        } else {
1237            self
1238        }
1239    }
1240
1241    /// Gap applied only at Lg breakpoint (120-159 cols).
1242    pub fn lg_gap(self, value: u32) -> Self {
1243        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1244        if is_lg {
1245            self.gap(value)
1246        } else {
1247            self
1248        }
1249    }
1250
1251    /// Gap applied only at Xl breakpoint (>= 160 cols).
1252    pub fn xl_gap(self, value: u32) -> Self {
1253        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1254        if is_xl {
1255            self.gap(value)
1256        } else {
1257            self
1258        }
1259    }
1260
1261    /// Set the flex-grow factor. `1` means the container expands to fill available space.
1262    pub fn grow(mut self, grow: u16) -> Self {
1263        self.grow = grow;
1264        self
1265    }
1266
1267    /// Grow factor applied only at Xs breakpoint (< 40 cols).
1268    pub fn xs_grow(self, value: u16) -> Self {
1269        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1270        if is_xs {
1271            self.grow(value)
1272        } else {
1273            self
1274        }
1275    }
1276
1277    /// Grow factor applied only at Sm breakpoint (40-79 cols).
1278    pub fn sm_grow(self, value: u16) -> Self {
1279        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1280        if is_sm {
1281            self.grow(value)
1282        } else {
1283            self
1284        }
1285    }
1286
1287    /// Grow factor applied only at Md breakpoint (80-119 cols).
1288    pub fn md_grow(self, value: u16) -> Self {
1289        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1290        if is_md {
1291            self.grow(value)
1292        } else {
1293            self
1294        }
1295    }
1296
1297    /// Grow factor applied only at Lg breakpoint (120-159 cols).
1298    pub fn lg_grow(self, value: u16) -> Self {
1299        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1300        if is_lg {
1301            self.grow(value)
1302        } else {
1303            self
1304        }
1305    }
1306
1307    /// Grow factor applied only at Xl breakpoint (>= 160 cols).
1308    pub fn xl_grow(self, value: u16) -> Self {
1309        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1310        if is_xl {
1311            self.grow(value)
1312        } else {
1313            self
1314        }
1315    }
1316
1317    /// Uniform padding applied only at Xs breakpoint (< 40 cols).
1318    pub fn xs_p(self, value: u32) -> Self {
1319        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1320        if is_xs {
1321            self.p(value)
1322        } else {
1323            self
1324        }
1325    }
1326
1327    /// Uniform padding applied only at Sm breakpoint (40-79 cols).
1328    pub fn sm_p(self, value: u32) -> Self {
1329        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1330        if is_sm {
1331            self.p(value)
1332        } else {
1333            self
1334        }
1335    }
1336
1337    /// Uniform padding applied only at Md breakpoint (80-119 cols).
1338    pub fn md_p(self, value: u32) -> Self {
1339        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1340        if is_md {
1341            self.p(value)
1342        } else {
1343            self
1344        }
1345    }
1346
1347    /// Uniform padding applied only at Lg breakpoint (120-159 cols).
1348    pub fn lg_p(self, value: u32) -> Self {
1349        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1350        if is_lg {
1351            self.p(value)
1352        } else {
1353            self
1354        }
1355    }
1356
1357    /// Uniform padding applied only at Xl breakpoint (>= 160 cols).
1358    pub fn xl_p(self, value: u32) -> Self {
1359        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1360        if is_xl {
1361            self.p(value)
1362        } else {
1363            self
1364        }
1365    }
1366
1367    // ── alignment ───────────────────────────────────────────────────
1368
1369    /// Set the cross-axis alignment of child elements.
1370    pub fn align(mut self, align: Align) -> Self {
1371        self.align = align;
1372        self
1373    }
1374
1375    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
1376    pub fn center(self) -> Self {
1377        self.align(Align::Center)
1378    }
1379
1380    /// Set the main-axis content distribution mode.
1381    pub fn justify(mut self, justify: Justify) -> Self {
1382        self.justify = justify;
1383        self
1384    }
1385
1386    /// Distribute children with equal space between; first at start, last at end.
1387    pub fn space_between(self) -> Self {
1388        self.justify(Justify::SpaceBetween)
1389    }
1390
1391    /// Distribute children with equal space around each child.
1392    pub fn space_around(self) -> Self {
1393        self.justify(Justify::SpaceAround)
1394    }
1395
1396    /// Distribute children with equal space between all children and edges.
1397    pub fn space_evenly(self) -> Self {
1398        self.justify(Justify::SpaceEvenly)
1399    }
1400
1401    // ── title ────────────────────────────────────────────────────────
1402
1403    /// Set a plain-text title rendered in the top border.
1404    pub fn title(self, title: impl Into<String>) -> Self {
1405        self.title_styled(title, Style::new())
1406    }
1407
1408    /// Set a styled title rendered in the top border.
1409    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1410        self.title = Some((title.into(), style));
1411        self
1412    }
1413
1414    // ── internal ─────────────────────────────────────────────────────
1415
1416    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
1417    pub fn scroll_offset(mut self, offset: u32) -> Self {
1418        self.scroll_offset = Some(offset);
1419        self
1420    }
1421
1422    fn group_name(mut self, name: String) -> Self {
1423        self.group_name = Some(name);
1424        self
1425    }
1426
1427    /// Finalize the builder as a vertical (column) container.
1428    ///
1429    /// The closure receives a `&mut Context` for rendering children.
1430    /// Returns a [`Response`] with click/hover state for this container.
1431    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1432        self.finish(Direction::Column, f)
1433    }
1434
1435    /// Finalize the builder as a horizontal (row) container.
1436    ///
1437    /// The closure receives a `&mut Context` for rendering children.
1438    /// Returns a [`Response`] with click/hover state for this container.
1439    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1440        self.finish(Direction::Row, f)
1441    }
1442
1443    /// Finalize the builder as an inline text line.
1444    ///
1445    /// Like [`row`](ContainerBuilder::row) but gap is forced to zero
1446    /// for seamless inline rendering of mixed-style text.
1447    pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1448        self.gap = 0;
1449        self.finish(Direction::Row, f)
1450    }
1451
1452    fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1453        let interaction_id = self.ctx.interaction_count;
1454        self.ctx.interaction_count += 1;
1455
1456        let in_hovered_group = self
1457            .group_name
1458            .as_ref()
1459            .map(|name| self.ctx.is_group_hovered(name))
1460            .unwrap_or(false)
1461            || self
1462                .ctx
1463                .group_stack
1464                .last()
1465                .map(|name| self.ctx.is_group_hovered(name))
1466                .unwrap_or(false);
1467        let in_focused_group = self
1468            .group_name
1469            .as_ref()
1470            .map(|name| self.ctx.is_group_focused(name))
1471            .unwrap_or(false)
1472            || self
1473                .ctx
1474                .group_stack
1475                .last()
1476                .map(|name| self.ctx.is_group_focused(name))
1477                .unwrap_or(false);
1478
1479        let resolved_bg = if self.ctx.dark_mode {
1480            self.dark_bg_color.or(self.bg_color)
1481        } else {
1482            self.bg_color
1483        };
1484        let resolved_border_style = if self.ctx.dark_mode {
1485            self.dark_border_style.unwrap_or(self.border_style)
1486        } else {
1487            self.border_style
1488        };
1489        let bg_color = if in_hovered_group || in_focused_group {
1490            self.group_hover_bg.or(resolved_bg)
1491        } else {
1492            resolved_bg
1493        };
1494        let border_style = if in_hovered_group || in_focused_group {
1495            self.group_hover_border_style
1496                .unwrap_or(resolved_border_style)
1497        } else {
1498            resolved_border_style
1499        };
1500        let group_name = self.group_name.clone();
1501        let is_group_container = group_name.is_some();
1502
1503        if let Some(scroll_offset) = self.scroll_offset {
1504            self.ctx.commands.push(Command::BeginScrollable {
1505                grow: self.grow,
1506                border: self.border,
1507                border_sides: self.border_sides,
1508                border_style,
1509                padding: self.padding,
1510                margin: self.margin,
1511                constraints: self.constraints,
1512                title: self.title,
1513                scroll_offset,
1514            });
1515        } else {
1516            self.ctx.commands.push(Command::BeginContainer {
1517                direction,
1518                gap: self.gap,
1519                align: self.align,
1520                justify: self.justify,
1521                border: self.border,
1522                border_sides: self.border_sides,
1523                border_style,
1524                bg_color,
1525                padding: self.padding,
1526                margin: self.margin,
1527                constraints: self.constraints,
1528                title: self.title,
1529                grow: self.grow,
1530                group_name,
1531            });
1532        }
1533        f(self.ctx);
1534        self.ctx.commands.push(Command::EndContainer);
1535        self.ctx.last_text_idx = None;
1536
1537        if is_group_container {
1538            self.ctx.group_stack.pop();
1539            self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1540        }
1541
1542        self.ctx.response_for(interaction_id)
1543    }
1544}
1545
1546impl Context {
1547    #[allow(clippy::too_many_arguments)]
1548    pub(crate) fn new(
1549        events: Vec<Event>,
1550        width: u32,
1551        height: u32,
1552        tick: u64,
1553        mut focus_index: usize,
1554        prev_focus_count: usize,
1555        prev_scroll_infos: Vec<(u32, u32)>,
1556        prev_scroll_rects: Vec<Rect>,
1557        prev_hit_map: Vec<Rect>,
1558        prev_group_rects: Vec<(String, Rect)>,
1559        prev_focus_rects: Vec<(usize, Rect)>,
1560        prev_focus_groups: Vec<Option<String>>,
1561        prev_hook_states: Vec<Box<dyn std::any::Any>>,
1562        debug: bool,
1563        theme: Theme,
1564        last_mouse_pos: Option<(u32, u32)>,
1565        prev_modal_active: bool,
1566    ) -> Self {
1567        let consumed = vec![false; events.len()];
1568
1569        let mut mouse_pos = last_mouse_pos;
1570        let mut click_pos = None;
1571        for event in &events {
1572            if let Event::Mouse(mouse) = event {
1573                mouse_pos = Some((mouse.x, mouse.y));
1574                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1575                    click_pos = Some((mouse.x, mouse.y));
1576                }
1577            }
1578        }
1579
1580        if let Some((mx, my)) = click_pos {
1581            let mut best: Option<(usize, u64)> = None;
1582            for &(fid, rect) in &prev_focus_rects {
1583                if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1584                    let area = rect.width as u64 * rect.height as u64;
1585                    if best.map_or(true, |(_, ba)| area < ba) {
1586                        best = Some((fid, area));
1587                    }
1588                }
1589            }
1590            if let Some((fid, _)) = best {
1591                focus_index = fid;
1592            }
1593        }
1594
1595        Self {
1596            commands: Vec::new(),
1597            events,
1598            consumed,
1599            should_quit: false,
1600            area_width: width,
1601            area_height: height,
1602            tick,
1603            focus_index,
1604            focus_count: 0,
1605            hook_states: prev_hook_states,
1606            hook_cursor: 0,
1607            prev_focus_count,
1608            scroll_count: 0,
1609            prev_scroll_infos,
1610            prev_scroll_rects,
1611            interaction_count: 0,
1612            prev_hit_map,
1613            group_stack: Vec::new(),
1614            prev_group_rects,
1615            group_count: 0,
1616            prev_focus_groups,
1617            _prev_focus_rects: prev_focus_rects,
1618            mouse_pos,
1619            click_pos,
1620            last_text_idx: None,
1621            overlay_depth: 0,
1622            modal_active: false,
1623            prev_modal_active,
1624            clipboard_text: None,
1625            debug,
1626            theme,
1627            dark_mode: true,
1628        }
1629    }
1630
1631    pub(crate) fn process_focus_keys(&mut self) {
1632        for (i, event) in self.events.iter().enumerate() {
1633            if let Event::Key(key) = event {
1634                if key.kind != KeyEventKind::Press {
1635                    continue;
1636                }
1637                if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1638                    if self.prev_focus_count > 0 {
1639                        self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1640                    }
1641                    self.consumed[i] = true;
1642                } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1643                    || key.code == KeyCode::BackTab
1644                {
1645                    if self.prev_focus_count > 0 {
1646                        self.focus_index = if self.focus_index == 0 {
1647                            self.prev_focus_count - 1
1648                        } else {
1649                            self.focus_index - 1
1650                        };
1651                    }
1652                    self.consumed[i] = true;
1653                }
1654            }
1655        }
1656    }
1657
1658    /// Render a custom [`Widget`].
1659    ///
1660    /// Calls [`Widget::ui`] with this context and returns the widget's response.
1661    pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1662        w.ui(self)
1663    }
1664
1665    /// Wrap child widgets in a panic boundary.
1666    ///
1667    /// If the closure panics, the panic is caught and an error message is
1668    /// rendered in place of the children. The app continues running.
1669    ///
1670    /// # Example
1671    ///
1672    /// ```no_run
1673    /// # slt::run(|ui: &mut slt::Context| {
1674    /// ui.error_boundary(|ui| {
1675    ///     ui.text("risky widget");
1676    /// });
1677    /// # });
1678    /// ```
1679    pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1680        self.error_boundary_with(f, |ui, msg| {
1681            ui.styled(
1682                format!("⚠ Error: {msg}"),
1683                Style::new().fg(ui.theme.error).bold(),
1684            );
1685        });
1686    }
1687
1688    /// Like [`error_boundary`](Self::error_boundary), but renders a custom
1689    /// fallback instead of the default error message.
1690    ///
1691    /// The fallback closure receives the panic message as a [`String`].
1692    ///
1693    /// # Example
1694    ///
1695    /// ```no_run
1696    /// # slt::run(|ui: &mut slt::Context| {
1697    /// ui.error_boundary_with(
1698    ///     |ui| {
1699    ///         ui.text("risky widget");
1700    ///     },
1701    ///     |ui, msg| {
1702    ///         ui.text(format!("Recovered from panic: {msg}"));
1703    ///     },
1704    /// );
1705    /// # });
1706    /// ```
1707    pub fn error_boundary_with(
1708        &mut self,
1709        f: impl FnOnce(&mut Context),
1710        fallback: impl FnOnce(&mut Context, String),
1711    ) {
1712        let cmd_count = self.commands.len();
1713        let last_text_idx = self.last_text_idx;
1714
1715        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1716            f(self);
1717        }));
1718
1719        match result {
1720            Ok(()) => {}
1721            Err(panic_info) => {
1722                self.commands.truncate(cmd_count);
1723                self.last_text_idx = last_text_idx;
1724
1725                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1726                    (*s).to_string()
1727                } else if let Some(s) = panic_info.downcast_ref::<String>() {
1728                    s.clone()
1729                } else {
1730                    "widget panicked".to_string()
1731                };
1732
1733                fallback(self, msg);
1734            }
1735        }
1736    }
1737
1738    /// Allocate a click/hover interaction slot and return the [`Response`].
1739    ///
1740    /// Use this in custom widgets to detect mouse clicks and hovers without
1741    /// wrapping content in a container. Each call reserves one slot in the
1742    /// hit-test map, so the call order must be stable across frames.
1743    pub fn interaction(&mut self) -> Response {
1744        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1745            return Response::default();
1746        }
1747        let id = self.interaction_count;
1748        self.interaction_count += 1;
1749        self.response_for(id)
1750    }
1751
1752    /// Register a widget as focusable and return whether it currently has focus.
1753    ///
1754    /// Call this in custom widgets that need keyboard focus. Each call increments
1755    /// the internal focus counter, so the call order must be stable across frames.
1756    pub fn register_focusable(&mut self) -> bool {
1757        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1758            return false;
1759        }
1760        let id = self.focus_count;
1761        self.focus_count += 1;
1762        self.commands.push(Command::FocusMarker(id));
1763        if self.prev_focus_count == 0 {
1764            return true;
1765        }
1766        self.focus_index % self.prev_focus_count == id
1767    }
1768
1769    /// Create persistent state that survives across frames.
1770    ///
1771    /// Returns a `State<T>` handle. Access with `state.get(ui)` / `state.get_mut(ui)`.
1772    ///
1773    /// # Rules
1774    /// - Must be called in the same order every frame (like React hooks)
1775    /// - Do NOT call inside if/else that changes between frames
1776    ///
1777    /// # Example
1778    /// ```ignore
1779    /// let count = ui.use_state(|| 0i32);
1780    /// let val = count.get(ui);
1781    /// ui.text(format!("Count: {val}"));
1782    /// if ui.button("+1") {
1783    ///     *count.get_mut(ui) += 1;
1784    /// }
1785    /// ```
1786    pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
1787        let idx = self.hook_cursor;
1788        self.hook_cursor += 1;
1789
1790        if idx >= self.hook_states.len() {
1791            self.hook_states.push(Box::new(init()));
1792        }
1793
1794        State {
1795            idx,
1796            _marker: std::marker::PhantomData,
1797        }
1798    }
1799
1800    /// Memoize a computed value. Recomputes only when `deps` changes.
1801    ///
1802    /// # Example
1803    /// ```ignore
1804    /// let doubled = ui.use_memo(&count, |c| c * 2);
1805    /// ui.text(format!("Doubled: {doubled}"));
1806    /// ```
1807    pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1808        &mut self,
1809        deps: &D,
1810        compute: impl FnOnce(&D) -> T,
1811    ) -> &T {
1812        let idx = self.hook_cursor;
1813        self.hook_cursor += 1;
1814
1815        let should_recompute = if idx >= self.hook_states.len() {
1816            true
1817        } else {
1818            let (stored_deps, _) = self.hook_states[idx]
1819                .downcast_ref::<(D, T)>()
1820                .expect("use_memo type mismatch");
1821            stored_deps != deps
1822        };
1823
1824        if should_recompute {
1825            let value = compute(deps);
1826            let slot = Box::new((deps.clone(), value));
1827            if idx < self.hook_states.len() {
1828                self.hook_states[idx] = slot;
1829            } else {
1830                self.hook_states.push(slot);
1831            }
1832        }
1833
1834        let (_, value) = self.hook_states[idx]
1835            .downcast_ref::<(D, T)>()
1836            .expect("use_memo type mismatch");
1837        value
1838    }
1839
1840    // ── text ──────────────────────────────────────────────────────────
1841
1842    /// Render a text element. Returns `&mut Self` for style chaining.
1843    ///
1844    /// # Example
1845    ///
1846    /// ```no_run
1847    /// # slt::run(|ui: &mut slt::Context| {
1848    /// use slt::Color;
1849    /// ui.text("hello").bold().fg(Color::Cyan);
1850    /// # });
1851    /// ```
1852    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1853        let content = s.into();
1854        self.commands.push(Command::Text {
1855            content,
1856            style: Style::new(),
1857            grow: 0,
1858            align: Align::Start,
1859            wrap: false,
1860            margin: Margin::default(),
1861            constraints: Constraints::default(),
1862        });
1863        self.last_text_idx = Some(self.commands.len() - 1);
1864        self
1865    }
1866
1867    /// Render a clickable hyperlink.
1868    ///
1869    /// The link is interactive: clicking it (or pressing Enter/Space when
1870    /// focused) opens the URL in the system browser. OSC 8 is also emitted
1871    /// for terminals that support native hyperlinks.
1872    pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
1873        let url_str = url.into();
1874        let focused = self.register_focusable();
1875        let interaction_id = self.interaction_count;
1876        self.interaction_count += 1;
1877        let response = self.response_for(interaction_id);
1878
1879        let mut activated = response.clicked;
1880        if focused {
1881            for (i, event) in self.events.iter().enumerate() {
1882                if let Event::Key(key) = event {
1883                    if key.kind != KeyEventKind::Press {
1884                        continue;
1885                    }
1886                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1887                        activated = true;
1888                        self.consumed[i] = true;
1889                    }
1890                }
1891            }
1892        }
1893
1894        if activated {
1895            let _ = open_url(&url_str);
1896        }
1897
1898        let style = if focused {
1899            Style::new()
1900                .fg(self.theme.primary)
1901                .bg(self.theme.surface_hover)
1902                .underline()
1903                .bold()
1904        } else if response.hovered {
1905            Style::new()
1906                .fg(self.theme.accent)
1907                .bg(self.theme.surface_hover)
1908                .underline()
1909        } else {
1910            Style::new().fg(self.theme.primary).underline()
1911        };
1912
1913        self.commands.push(Command::Link {
1914            text: text.into(),
1915            url: url_str,
1916            style,
1917            margin: Margin::default(),
1918            constraints: Constraints::default(),
1919        });
1920        self.last_text_idx = Some(self.commands.len() - 1);
1921        self
1922    }
1923
1924    /// Render a text element with word-boundary wrapping.
1925    ///
1926    /// Long lines are broken at word boundaries to fit the container width.
1927    /// Style chaining works the same as [`Context::text`].
1928    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1929        let content = s.into();
1930        self.commands.push(Command::Text {
1931            content,
1932            style: Style::new(),
1933            grow: 0,
1934            align: Align::Start,
1935            wrap: true,
1936            margin: Margin::default(),
1937            constraints: Constraints::default(),
1938        });
1939        self.last_text_idx = Some(self.commands.len() - 1);
1940        self
1941    }
1942
1943    // ── style chain (applies to last text) ───────────────────────────
1944
1945    /// Apply bold to the last rendered text element.
1946    pub fn bold(&mut self) -> &mut Self {
1947        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1948        self
1949    }
1950
1951    /// Apply dim styling to the last rendered text element.
1952    ///
1953    /// Also sets the foreground color to the theme's `text_dim` color if no
1954    /// explicit foreground has been set.
1955    pub fn dim(&mut self) -> &mut Self {
1956        let text_dim = self.theme.text_dim;
1957        self.modify_last_style(|s| {
1958            s.modifiers |= Modifiers::DIM;
1959            if s.fg.is_none() {
1960                s.fg = Some(text_dim);
1961            }
1962        });
1963        self
1964    }
1965
1966    /// Apply italic to the last rendered text element.
1967    pub fn italic(&mut self) -> &mut Self {
1968        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1969        self
1970    }
1971
1972    /// Apply underline to the last rendered text element.
1973    pub fn underline(&mut self) -> &mut Self {
1974        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1975        self
1976    }
1977
1978    /// Apply reverse-video to the last rendered text element.
1979    pub fn reversed(&mut self) -> &mut Self {
1980        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1981        self
1982    }
1983
1984    /// Apply strikethrough to the last rendered text element.
1985    pub fn strikethrough(&mut self) -> &mut Self {
1986        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1987        self
1988    }
1989
1990    /// Set the foreground color of the last rendered text element.
1991    pub fn fg(&mut self, color: Color) -> &mut Self {
1992        self.modify_last_style(|s| s.fg = Some(color));
1993        self
1994    }
1995
1996    /// Set the background color of the last rendered text element.
1997    pub fn bg(&mut self, color: Color) -> &mut Self {
1998        self.modify_last_style(|s| s.bg = Some(color));
1999        self
2000    }
2001
2002    pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
2003        let apply_group_style = self
2004            .group_stack
2005            .last()
2006            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2007            .unwrap_or(false);
2008        if apply_group_style {
2009            self.modify_last_style(|s| s.fg = Some(color));
2010        }
2011        self
2012    }
2013
2014    pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
2015        let apply_group_style = self
2016            .group_stack
2017            .last()
2018            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2019            .unwrap_or(false);
2020        if apply_group_style {
2021            self.modify_last_style(|s| s.bg = Some(color));
2022        }
2023        self
2024    }
2025
2026    /// Render a text element with an explicit [`Style`] applied immediately.
2027    ///
2028    /// Equivalent to calling `text(s)` followed by style-chain methods, but
2029    /// more concise when you already have a `Style` value.
2030    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
2031        self.commands.push(Command::Text {
2032            content: s.into(),
2033            style,
2034            grow: 0,
2035            align: Align::Start,
2036            wrap: false,
2037            margin: Margin::default(),
2038            constraints: Constraints::default(),
2039        });
2040        self.last_text_idx = Some(self.commands.len() - 1);
2041        self
2042    }
2043
2044    /// Render a half-block image in the terminal.
2045    ///
2046    /// Each terminal cell displays two vertical pixels using the `▀` character
2047    /// with foreground (upper pixel) and background (lower pixel) colors.
2048    ///
2049    /// Create a [`HalfBlockImage`] from a file (requires `image` feature):
2050    /// ```ignore
2051    /// let img = image::open("photo.png").unwrap();
2052    /// let half = HalfBlockImage::from_dynamic(&img, 40, 20);
2053    /// ui.image(&half);
2054    /// ```
2055    ///
2056    /// Or from raw RGB data (no feature needed):
2057    /// ```no_run
2058    /// # use slt::{Context, HalfBlockImage};
2059    /// # slt::run(|ui: &mut Context| {
2060    /// let rgb = vec![255u8; 30 * 20 * 3];
2061    /// let half = HalfBlockImage::from_rgb(&rgb, 30, 10);
2062    /// ui.image(&half);
2063    /// # });
2064    /// ```
2065    pub fn image(&mut self, img: &HalfBlockImage) {
2066        let width = img.width;
2067        let height = img.height;
2068
2069        self.container().w(width).h(height).gap(0).col(|ui| {
2070            for row in 0..height {
2071                ui.container().gap(0).row(|ui| {
2072                    for col in 0..width {
2073                        let idx = (row * width + col) as usize;
2074                        if let Some(&(upper, lower)) = img.pixels.get(idx) {
2075                            ui.styled("▀", Style::new().fg(upper).bg(lower));
2076                        }
2077                    }
2078                });
2079            }
2080        });
2081    }
2082
2083    /// Render streaming text with a typing cursor indicator.
2084    ///
2085    /// Displays the accumulated text content. While `streaming` is true,
2086    /// shows a blinking cursor (`▌`) at the end.
2087    ///
2088    /// ```no_run
2089    /// # use slt::widgets::StreamingTextState;
2090    /// # slt::run(|ui: &mut slt::Context| {
2091    /// let mut stream = StreamingTextState::new();
2092    /// stream.start();
2093    /// stream.push("Hello from ");
2094    /// stream.push("the AI!");
2095    /// ui.streaming_text(&mut stream);
2096    /// # });
2097    /// ```
2098    pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
2099        if state.streaming {
2100            state.cursor_tick = state.cursor_tick.wrapping_add(1);
2101            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
2102        }
2103
2104        if state.content.is_empty() && state.streaming {
2105            let cursor = if state.cursor_visible { "▌" } else { " " };
2106            let primary = self.theme.primary;
2107            self.text(cursor).fg(primary);
2108            return;
2109        }
2110
2111        if !state.content.is_empty() {
2112            if state.streaming && state.cursor_visible {
2113                self.text_wrap(format!("{}▌", state.content));
2114            } else {
2115                self.text_wrap(&state.content);
2116            }
2117        }
2118    }
2119
2120    /// Render a tool approval widget with approve/reject buttons.
2121    ///
2122    /// Shows the tool name, description, and two action buttons.
2123    /// Returns the updated [`ApprovalAction`] each frame.
2124    ///
2125    /// ```no_run
2126    /// # use slt::widgets::{ApprovalAction, ToolApprovalState};
2127    /// # slt::run(|ui: &mut slt::Context| {
2128    /// let mut tool = ToolApprovalState::new("read_file", "Read contents of config.toml");
2129    /// ui.tool_approval(&mut tool);
2130    /// if tool.action == ApprovalAction::Approved {
2131    /// }
2132    /// # });
2133    /// ```
2134    pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
2135        let theme = self.theme;
2136        self.bordered(Border::Rounded).col(|ui| {
2137            ui.row(|ui| {
2138                ui.text("⚡").fg(theme.warning);
2139                ui.text(&state.tool_name).bold().fg(theme.primary);
2140            });
2141            ui.text(&state.description).dim();
2142
2143            if state.action == ApprovalAction::Pending {
2144                ui.row(|ui| {
2145                    if ui.button("✓ Approve") {
2146                        state.action = ApprovalAction::Approved;
2147                    }
2148                    if ui.button("✗ Reject") {
2149                        state.action = ApprovalAction::Rejected;
2150                    }
2151                });
2152            } else {
2153                let (label, color) = match state.action {
2154                    ApprovalAction::Approved => ("✓ Approved", theme.success),
2155                    ApprovalAction::Rejected => ("✗ Rejected", theme.error),
2156                    ApprovalAction::Pending => unreachable!(),
2157                };
2158                ui.text(label).fg(color).bold();
2159            }
2160        });
2161    }
2162
2163    /// Render a context bar showing active context items with token counts.
2164    ///
2165    /// Displays a horizontal bar of context sources (files, URLs, etc.)
2166    /// with their token counts, useful for AI chat interfaces.
2167    ///
2168    /// ```no_run
2169    /// # use slt::widgets::ContextItem;
2170    /// # slt::run(|ui: &mut slt::Context| {
2171    /// let items = vec![ContextItem::new("main.rs", 1200), ContextItem::new("lib.rs", 800)];
2172    /// ui.context_bar(&items);
2173    /// # });
2174    /// ```
2175    pub fn context_bar(&mut self, items: &[ContextItem]) {
2176        if items.is_empty() {
2177            return;
2178        }
2179
2180        let theme = self.theme;
2181        let total: usize = items.iter().map(|item| item.tokens).sum();
2182
2183        self.container().row(|ui| {
2184            ui.text("📎").dim();
2185            for item in items {
2186                ui.text(format!(
2187                    "{} ({})",
2188                    item.label,
2189                    format_token_count(item.tokens)
2190                ))
2191                .fg(theme.secondary);
2192            }
2193            ui.spacer();
2194            ui.text(format!("Σ {}", format_token_count(total))).dim();
2195        });
2196    }
2197
2198    /// Enable word-boundary wrapping on the last rendered text element.
2199    pub fn wrap(&mut self) -> &mut Self {
2200        if let Some(idx) = self.last_text_idx {
2201            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
2202                *wrap = true;
2203            }
2204        }
2205        self
2206    }
2207
2208    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
2209        if let Some(idx) = self.last_text_idx {
2210            match &mut self.commands[idx] {
2211                Command::Text { style, .. } | Command::Link { style, .. } => f(style),
2212                _ => {}
2213            }
2214        }
2215    }
2216
2217    // ── containers ───────────────────────────────────────────────────
2218
2219    /// Create a vertical (column) container.
2220    ///
2221    /// Children are stacked top-to-bottom. Returns a [`Response`] with
2222    /// click/hover state for the container area.
2223    ///
2224    /// # Example
2225    ///
2226    /// ```no_run
2227    /// # slt::run(|ui: &mut slt::Context| {
2228    /// ui.col(|ui| {
2229    ///     ui.text("line one");
2230    ///     ui.text("line two");
2231    /// });
2232    /// # });
2233    /// ```
2234    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2235        self.push_container(Direction::Column, 0, f)
2236    }
2237
2238    /// Create a vertical (column) container with a gap between children.
2239    ///
2240    /// `gap` is the number of blank rows inserted between each child.
2241    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2242        self.push_container(Direction::Column, gap, f)
2243    }
2244
2245    /// Create a horizontal (row) container.
2246    ///
2247    /// Children are placed left-to-right. Returns a [`Response`] with
2248    /// click/hover state for the container area.
2249    ///
2250    /// # Example
2251    ///
2252    /// ```no_run
2253    /// # slt::run(|ui: &mut slt::Context| {
2254    /// ui.row(|ui| {
2255    ///     ui.text("left");
2256    ///     ui.spacer();
2257    ///     ui.text("right");
2258    /// });
2259    /// # });
2260    /// ```
2261    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2262        self.push_container(Direction::Row, 0, f)
2263    }
2264
2265    /// Create a horizontal (row) container with a gap between children.
2266    ///
2267    /// `gap` is the number of blank columns inserted between each child.
2268    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2269        self.push_container(Direction::Row, gap, f)
2270    }
2271
2272    /// Render inline text with mixed styles on a single line.
2273    ///
2274    /// Unlike [`row`](Context::row), `line()` is designed for rich text —
2275    /// children are rendered as continuous inline text without gaps.
2276    ///
2277    /// # Example
2278    ///
2279    /// ```no_run
2280    /// # use slt::Color;
2281    /// # slt::run(|ui: &mut slt::Context| {
2282    /// ui.line(|ui| {
2283    ///     ui.text("Status: ");
2284    ///     ui.text("Online").bold().fg(Color::Green);
2285    /// });
2286    /// # });
2287    /// ```
2288    pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2289        let _ = self.push_container(Direction::Row, 0, f);
2290        self
2291    }
2292
2293    /// Render inline text with mixed styles, wrapping at word boundaries.
2294    ///
2295    /// Like [`line`](Context::line), but when the combined text exceeds
2296    /// the container width it wraps across multiple lines while
2297    /// preserving per-segment styles.
2298    ///
2299    /// # Example
2300    ///
2301    /// ```no_run
2302    /// # use slt::{Color, Style};
2303    /// # slt::run(|ui: &mut slt::Context| {
2304    /// ui.line_wrap(|ui| {
2305    ///     ui.text("This is a long ");
2306    ///     ui.text("important").bold().fg(Color::Red);
2307    ///     ui.text(" message that wraps across lines");
2308    /// });
2309    /// # });
2310    /// ```
2311    pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2312        let start = self.commands.len();
2313        f(self);
2314        let mut segments: Vec<(String, Style)> = Vec::new();
2315        for cmd in self.commands.drain(start..) {
2316            if let Command::Text { content, style, .. } = cmd {
2317                segments.push((content, style));
2318            }
2319        }
2320        self.commands.push(Command::RichText {
2321            segments,
2322            wrap: true,
2323            align: Align::Start,
2324            margin: Margin::default(),
2325            constraints: Constraints::default(),
2326        });
2327        self.last_text_idx = None;
2328        self
2329    }
2330
2331    /// Render content in a modal overlay with dimmed background.
2332    pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
2333        self.commands.push(Command::BeginOverlay { modal: true });
2334        self.overlay_depth += 1;
2335        self.modal_active = true;
2336        f(self);
2337        self.overlay_depth = self.overlay_depth.saturating_sub(1);
2338        self.commands.push(Command::EndOverlay);
2339        self.last_text_idx = None;
2340    }
2341
2342    /// Render floating content without dimming the background.
2343    pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
2344        self.commands.push(Command::BeginOverlay { modal: false });
2345        self.overlay_depth += 1;
2346        f(self);
2347        self.overlay_depth = self.overlay_depth.saturating_sub(1);
2348        self.commands.push(Command::EndOverlay);
2349        self.last_text_idx = None;
2350    }
2351
2352    /// Create a named group container for shared hover/focus styling.
2353    pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
2354        self.group_count = self.group_count.saturating_add(1);
2355        self.group_stack.push(name.to_string());
2356        self.container().group_name(name.to_string())
2357    }
2358
2359    /// Create a container with a fluent builder.
2360    ///
2361    /// Use this for borders, padding, grow, constraints, and titles. Chain
2362    /// configuration methods on the returned [`ContainerBuilder`], then call
2363    /// `.col()` or `.row()` to finalize.
2364    ///
2365    /// # Example
2366    ///
2367    /// ```no_run
2368    /// # slt::run(|ui: &mut slt::Context| {
2369    /// use slt::Border;
2370    /// ui.container()
2371    ///     .border(Border::Rounded)
2372    ///     .pad(1)
2373    ///     .title("My Panel")
2374    ///     .col(|ui| {
2375    ///         ui.text("content");
2376    ///     });
2377    /// # });
2378    /// ```
2379    pub fn container(&mut self) -> ContainerBuilder<'_> {
2380        let border = self.theme.border;
2381        ContainerBuilder {
2382            ctx: self,
2383            gap: 0,
2384            align: Align::Start,
2385            justify: Justify::Start,
2386            border: None,
2387            border_sides: BorderSides::all(),
2388            border_style: Style::new().fg(border),
2389            bg_color: None,
2390            dark_bg_color: None,
2391            dark_border_style: None,
2392            group_hover_bg: None,
2393            group_hover_border_style: None,
2394            group_name: None,
2395            padding: Padding::default(),
2396            margin: Margin::default(),
2397            constraints: Constraints::default(),
2398            title: None,
2399            grow: 0,
2400            scroll_offset: None,
2401        }
2402    }
2403
2404    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
2405    ///
2406    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
2407    /// is updated in-place with the current scroll offset and bounds.
2408    ///
2409    /// # Example
2410    ///
2411    /// ```no_run
2412    /// # use slt::widgets::ScrollState;
2413    /// # slt::run(|ui: &mut slt::Context| {
2414    /// let mut scroll = ScrollState::new();
2415    /// ui.scrollable(&mut scroll).col(|ui| {
2416    ///     for i in 0..100 {
2417    ///         ui.text(format!("Line {i}"));
2418    ///     }
2419    /// });
2420    /// # });
2421    /// ```
2422    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
2423        let index = self.scroll_count;
2424        self.scroll_count += 1;
2425        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
2426            state.set_bounds(ch, vh);
2427            let max = ch.saturating_sub(vh) as usize;
2428            state.offset = state.offset.min(max);
2429        }
2430
2431        let next_id = self.interaction_count;
2432        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
2433            let inner_rects: Vec<Rect> = self
2434                .prev_scroll_rects
2435                .iter()
2436                .enumerate()
2437                .filter(|&(j, sr)| {
2438                    j != index
2439                        && sr.width > 0
2440                        && sr.height > 0
2441                        && sr.x >= rect.x
2442                        && sr.right() <= rect.right()
2443                        && sr.y >= rect.y
2444                        && sr.bottom() <= rect.bottom()
2445                })
2446                .map(|(_, sr)| *sr)
2447                .collect();
2448            self.auto_scroll_nested(&rect, state, &inner_rects);
2449        }
2450
2451        self.container().scroll_offset(state.offset as u32)
2452    }
2453
2454    /// Render a scrollbar track for a [`ScrollState`].
2455    ///
2456    /// Displays a track (`│`) with a proportional thumb (`█`). The thumb size
2457    /// and position are calculated from the scroll state's content height,
2458    /// viewport height, and current offset.
2459    ///
2460    /// Typically placed beside a `scrollable()` container in a `row()`:
2461    /// ```no_run
2462    /// # use slt::widgets::ScrollState;
2463    /// # slt::run(|ui: &mut slt::Context| {
2464    /// let mut scroll = ScrollState::new();
2465    /// ui.row(|ui| {
2466    ///     ui.scrollable(&mut scroll).grow(1).col(|ui| {
2467    ///         for i in 0..100 { ui.text(format!("Line {i}")); }
2468    ///     });
2469    ///     ui.scrollbar(&scroll);
2470    /// });
2471    /// # });
2472    /// ```
2473    pub fn scrollbar(&mut self, state: &ScrollState) {
2474        let vh = state.viewport_height();
2475        let ch = state.content_height();
2476        if vh == 0 || ch <= vh {
2477            return;
2478        }
2479
2480        let track_height = vh;
2481        let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
2482        let max_offset = ch.saturating_sub(vh);
2483        let thumb_pos = if max_offset == 0 {
2484            0
2485        } else {
2486            ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
2487                .round() as u32
2488        };
2489
2490        let theme = self.theme;
2491        let track_char = '│';
2492        let thumb_char = '█';
2493
2494        self.container().w(1).h(track_height).col(|ui| {
2495            for i in 0..track_height {
2496                if i >= thumb_pos && i < thumb_pos + thumb_height {
2497                    ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
2498                } else {
2499                    ui.styled(
2500                        track_char.to_string(),
2501                        Style::new().fg(theme.text_dim).dim(),
2502                    );
2503                }
2504            }
2505        });
2506    }
2507
2508    fn auto_scroll_nested(
2509        &mut self,
2510        rect: &Rect,
2511        state: &mut ScrollState,
2512        inner_scroll_rects: &[Rect],
2513    ) {
2514        let mut to_consume: Vec<usize> = Vec::new();
2515
2516        for (i, event) in self.events.iter().enumerate() {
2517            if self.consumed[i] {
2518                continue;
2519            }
2520            if let Event::Mouse(mouse) = event {
2521                let in_bounds = mouse.x >= rect.x
2522                    && mouse.x < rect.right()
2523                    && mouse.y >= rect.y
2524                    && mouse.y < rect.bottom();
2525                if !in_bounds {
2526                    continue;
2527                }
2528                let in_inner = inner_scroll_rects.iter().any(|sr| {
2529                    mouse.x >= sr.x
2530                        && mouse.x < sr.right()
2531                        && mouse.y >= sr.y
2532                        && mouse.y < sr.bottom()
2533                });
2534                if in_inner {
2535                    continue;
2536                }
2537                match mouse.kind {
2538                    MouseKind::ScrollUp => {
2539                        state.scroll_up(1);
2540                        to_consume.push(i);
2541                    }
2542                    MouseKind::ScrollDown => {
2543                        state.scroll_down(1);
2544                        to_consume.push(i);
2545                    }
2546                    MouseKind::Drag(MouseButton::Left) => {}
2547                    _ => {}
2548                }
2549            }
2550        }
2551
2552        for i in to_consume {
2553            self.consumed[i] = true;
2554        }
2555    }
2556
2557    /// Shortcut for `container().border(border)`.
2558    ///
2559    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
2560    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
2561        self.container()
2562            .border(border)
2563            .border_sides(BorderSides::all())
2564    }
2565
2566    fn push_container(
2567        &mut self,
2568        direction: Direction,
2569        gap: u32,
2570        f: impl FnOnce(&mut Context),
2571    ) -> Response {
2572        let interaction_id = self.interaction_count;
2573        self.interaction_count += 1;
2574        let border = self.theme.border;
2575
2576        self.commands.push(Command::BeginContainer {
2577            direction,
2578            gap,
2579            align: Align::Start,
2580            justify: Justify::Start,
2581            border: None,
2582            border_sides: BorderSides::all(),
2583            border_style: Style::new().fg(border),
2584            bg_color: None,
2585            padding: Padding::default(),
2586            margin: Margin::default(),
2587            constraints: Constraints::default(),
2588            title: None,
2589            grow: 0,
2590            group_name: None,
2591        });
2592        f(self);
2593        self.commands.push(Command::EndContainer);
2594        self.last_text_idx = None;
2595
2596        self.response_for(interaction_id)
2597    }
2598
2599    fn response_for(&self, interaction_id: usize) -> Response {
2600        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2601            return Response::default();
2602        }
2603        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
2604            let clicked = self
2605                .click_pos
2606                .map(|(mx, my)| {
2607                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2608                })
2609                .unwrap_or(false);
2610            let hovered = self
2611                .mouse_pos
2612                .map(|(mx, my)| {
2613                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2614                })
2615                .unwrap_or(false);
2616            Response { clicked, hovered }
2617        } else {
2618            Response::default()
2619        }
2620    }
2621
2622    /// Returns true if the named group is currently hovered by the mouse.
2623    pub fn is_group_hovered(&self, name: &str) -> bool {
2624        if let Some(pos) = self.mouse_pos {
2625            self.prev_group_rects.iter().any(|(n, rect)| {
2626                n == name
2627                    && pos.0 >= rect.x
2628                    && pos.0 < rect.x + rect.width
2629                    && pos.1 >= rect.y
2630                    && pos.1 < rect.y + rect.height
2631            })
2632        } else {
2633            false
2634        }
2635    }
2636
2637    /// Returns true if the named group contains the currently focused widget.
2638    pub fn is_group_focused(&self, name: &str) -> bool {
2639        if self.prev_focus_count == 0 {
2640            return false;
2641        }
2642        let focused_index = self.focus_index % self.prev_focus_count;
2643        self.prev_focus_groups
2644            .get(focused_index)
2645            .and_then(|group| group.as_deref())
2646            .map(|group| group == name)
2647            .unwrap_or(false)
2648    }
2649
2650    /// Set the flex-grow factor of the last rendered text element.
2651    ///
2652    /// A value of `1` causes the element to expand and fill remaining space
2653    /// along the main axis.
2654    pub fn grow(&mut self, value: u16) -> &mut Self {
2655        if let Some(idx) = self.last_text_idx {
2656            if let Command::Text { grow, .. } = &mut self.commands[idx] {
2657                *grow = value;
2658            }
2659        }
2660        self
2661    }
2662
2663    /// Set the text alignment of the last rendered text element.
2664    pub fn align(&mut self, align: Align) -> &mut Self {
2665        if let Some(idx) = self.last_text_idx {
2666            if let Command::Text {
2667                align: text_align, ..
2668            } = &mut self.commands[idx]
2669            {
2670                *text_align = align;
2671            }
2672        }
2673        self
2674    }
2675
2676    /// Render an invisible spacer that expands to fill available space.
2677    ///
2678    /// Useful for pushing siblings to opposite ends of a row or column.
2679    pub fn spacer(&mut self) -> &mut Self {
2680        self.commands.push(Command::Spacer { grow: 1 });
2681        self.last_text_idx = None;
2682        self
2683    }
2684
2685    /// Render a form that groups input fields vertically.
2686    ///
2687    /// Use [`Context::form_field`] inside the closure to render each field.
2688    pub fn form(
2689        &mut self,
2690        state: &mut FormState,
2691        f: impl FnOnce(&mut Context, &mut FormState),
2692    ) -> &mut Self {
2693        self.col(|ui| {
2694            f(ui, state);
2695        });
2696        self
2697    }
2698
2699    /// Render a single form field with label and input.
2700    ///
2701    /// Shows a validation error below the input when present.
2702    pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
2703        self.col(|ui| {
2704            ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
2705            ui.text_input(&mut field.input);
2706            if let Some(error) = field.error.as_deref() {
2707                ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
2708            }
2709        });
2710        self
2711    }
2712
2713    /// Render a submit button.
2714    ///
2715    /// Returns `true` when the button is clicked or activated.
2716    pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
2717        self.button(label)
2718    }
2719
2720    /// Render a single-line text input. Auto-handles cursor, typing, and backspace.
2721    ///
2722    /// The widget claims focus via [`Context::register_focusable`]. When focused,
2723    /// it consumes character, backspace, arrow, Home, and End key events.
2724    ///
2725    /// # Example
2726    ///
2727    /// ```no_run
2728    /// # use slt::widgets::TextInputState;
2729    /// # slt::run(|ui: &mut slt::Context| {
2730    /// let mut input = TextInputState::with_placeholder("Search...");
2731    /// ui.text_input(&mut input);
2732    /// // input.value holds the current text
2733    /// # });
2734    /// ```
2735    pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
2736        slt_assert(
2737            !state.value.contains('\n'),
2738            "text_input got a newline — use textarea instead",
2739        );
2740        let focused = self.register_focusable();
2741        state.cursor = state.cursor.min(state.value.chars().count());
2742
2743        if focused {
2744            let mut consumed_indices = Vec::new();
2745            for (i, event) in self.events.iter().enumerate() {
2746                if let Event::Key(key) = event {
2747                    if key.kind != KeyEventKind::Press {
2748                        continue;
2749                    }
2750                    match key.code {
2751                        KeyCode::Char(ch) => {
2752                            if let Some(max) = state.max_length {
2753                                if state.value.chars().count() >= max {
2754                                    continue;
2755                                }
2756                            }
2757                            let index = byte_index_for_char(&state.value, state.cursor);
2758                            state.value.insert(index, ch);
2759                            state.cursor += 1;
2760                            consumed_indices.push(i);
2761                        }
2762                        KeyCode::Backspace => {
2763                            if state.cursor > 0 {
2764                                let start = byte_index_for_char(&state.value, state.cursor - 1);
2765                                let end = byte_index_for_char(&state.value, state.cursor);
2766                                state.value.replace_range(start..end, "");
2767                                state.cursor -= 1;
2768                            }
2769                            consumed_indices.push(i);
2770                        }
2771                        KeyCode::Left => {
2772                            state.cursor = state.cursor.saturating_sub(1);
2773                            consumed_indices.push(i);
2774                        }
2775                        KeyCode::Right => {
2776                            state.cursor = (state.cursor + 1).min(state.value.chars().count());
2777                            consumed_indices.push(i);
2778                        }
2779                        KeyCode::Home => {
2780                            state.cursor = 0;
2781                            consumed_indices.push(i);
2782                        }
2783                        KeyCode::Delete => {
2784                            let len = state.value.chars().count();
2785                            if state.cursor < len {
2786                                let start = byte_index_for_char(&state.value, state.cursor);
2787                                let end = byte_index_for_char(&state.value, state.cursor + 1);
2788                                state.value.replace_range(start..end, "");
2789                            }
2790                            consumed_indices.push(i);
2791                        }
2792                        KeyCode::End => {
2793                            state.cursor = state.value.chars().count();
2794                            consumed_indices.push(i);
2795                        }
2796                        _ => {}
2797                    }
2798                }
2799                if let Event::Paste(ref text) = event {
2800                    for ch in text.chars() {
2801                        if let Some(max) = state.max_length {
2802                            if state.value.chars().count() >= max {
2803                                break;
2804                            }
2805                        }
2806                        let index = byte_index_for_char(&state.value, state.cursor);
2807                        state.value.insert(index, ch);
2808                        state.cursor += 1;
2809                    }
2810                    consumed_indices.push(i);
2811                }
2812            }
2813
2814            for index in consumed_indices {
2815                self.consumed[index] = true;
2816            }
2817        }
2818
2819        let visible_width = self.area_width.saturating_sub(4) as usize;
2820        let input_text = if state.value.is_empty() {
2821            if state.placeholder.len() > 100 {
2822                slt_warn(
2823                    "text_input placeholder is very long (>100 chars) — consider shortening it",
2824                );
2825            }
2826            let mut ph = state.placeholder.clone();
2827            if focused {
2828                ph.insert(0, '▎');
2829            }
2830            ph
2831        } else {
2832            let chars: Vec<char> = state.value.chars().collect();
2833            let display_chars: Vec<char> = if state.masked {
2834                vec!['•'; chars.len()]
2835            } else {
2836                chars.clone()
2837            };
2838
2839            let cursor_display_pos: usize = display_chars[..state.cursor.min(display_chars.len())]
2840                .iter()
2841                .map(|c| UnicodeWidthChar::width(*c).unwrap_or(1))
2842                .sum();
2843
2844            let scroll_offset = if cursor_display_pos >= visible_width {
2845                cursor_display_pos - visible_width + 1
2846            } else {
2847                0
2848            };
2849
2850            let mut rendered = String::new();
2851            let mut current_width: usize = 0;
2852            for (idx, &ch) in display_chars.iter().enumerate() {
2853                let cw = UnicodeWidthChar::width(ch).unwrap_or(1);
2854                if current_width + cw <= scroll_offset {
2855                    current_width += cw;
2856                    continue;
2857                }
2858                if current_width - scroll_offset >= visible_width {
2859                    break;
2860                }
2861                if focused && idx == state.cursor {
2862                    rendered.push('▎');
2863                }
2864                rendered.push(ch);
2865                current_width += cw;
2866            }
2867            if focused && state.cursor >= display_chars.len() {
2868                rendered.push('▎');
2869            }
2870            rendered
2871        };
2872        let input_style = if state.value.is_empty() && !focused {
2873            Style::new().dim().fg(self.theme.text_dim)
2874        } else {
2875            Style::new().fg(self.theme.text)
2876        };
2877
2878        let border_color = if focused {
2879            self.theme.primary
2880        } else if state.validation_error.is_some() {
2881            self.theme.error
2882        } else {
2883            self.theme.border
2884        };
2885
2886        self.bordered(Border::Rounded)
2887            .border_style(Style::new().fg(border_color))
2888            .px(1)
2889            .col(|ui| {
2890                ui.styled(input_text, input_style);
2891            });
2892
2893        if let Some(error) = state.validation_error.clone() {
2894            self.styled(
2895                format!("⚠ {error}"),
2896                Style::new().dim().fg(self.theme.error),
2897            );
2898        }
2899        self
2900    }
2901
2902    /// Render an animated spinner.
2903    ///
2904    /// The spinner advances one frame per tick. Use [`SpinnerState::dots`] or
2905    /// [`SpinnerState::line`] to create the state, then chain style methods to
2906    /// color it.
2907    pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
2908        self.styled(
2909            state.frame(self.tick).to_string(),
2910            Style::new().fg(self.theme.primary),
2911        )
2912    }
2913
2914    /// Render toast notifications. Calls `state.cleanup(tick)` automatically.
2915    ///
2916    /// Expired messages are removed before rendering. If there are no active
2917    /// messages, nothing is rendered and `self` is returned unchanged.
2918    pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
2919        state.cleanup(self.tick);
2920        if state.messages.is_empty() {
2921            return self;
2922        }
2923
2924        self.interaction_count += 1;
2925        self.commands.push(Command::BeginContainer {
2926            direction: Direction::Column,
2927            gap: 0,
2928            align: Align::Start,
2929            justify: Justify::Start,
2930            border: None,
2931            border_sides: BorderSides::all(),
2932            border_style: Style::new().fg(self.theme.border),
2933            bg_color: None,
2934            padding: Padding::default(),
2935            margin: Margin::default(),
2936            constraints: Constraints::default(),
2937            title: None,
2938            grow: 0,
2939            group_name: None,
2940        });
2941        for message in state.messages.iter().rev() {
2942            let color = match message.level {
2943                ToastLevel::Info => self.theme.primary,
2944                ToastLevel::Success => self.theme.success,
2945                ToastLevel::Warning => self.theme.warning,
2946                ToastLevel::Error => self.theme.error,
2947            };
2948            self.styled(format!("  ● {}", message.text), Style::new().fg(color));
2949        }
2950        self.commands.push(Command::EndContainer);
2951        self.last_text_idx = None;
2952
2953        self
2954    }
2955
2956    /// Render a multi-line text area with the given number of visible rows.
2957    ///
2958    /// When focused, handles character input, Enter (new line), Backspace,
2959    /// arrow keys, Home, and End. The cursor is rendered as a block character.
2960    ///
2961    /// Set [`TextareaState::word_wrap`] to enable soft-wrapping at a given
2962    /// display-column width. Up/Down then navigate visual lines.
2963    pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
2964        if state.lines.is_empty() {
2965            state.lines.push(String::new());
2966        }
2967        state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
2968        state.cursor_col = state
2969            .cursor_col
2970            .min(state.lines[state.cursor_row].chars().count());
2971
2972        let focused = self.register_focusable();
2973        let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
2974        let wrapping = state.wrap_width.is_some();
2975
2976        let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
2977
2978        if focused {
2979            let mut consumed_indices = Vec::new();
2980            for (i, event) in self.events.iter().enumerate() {
2981                if let Event::Key(key) = event {
2982                    if key.kind != KeyEventKind::Press {
2983                        continue;
2984                    }
2985                    match key.code {
2986                        KeyCode::Char(ch) => {
2987                            if let Some(max) = state.max_length {
2988                                let total: usize =
2989                                    state.lines.iter().map(|line| line.chars().count()).sum();
2990                                if total >= max {
2991                                    continue;
2992                                }
2993                            }
2994                            let index = byte_index_for_char(
2995                                &state.lines[state.cursor_row],
2996                                state.cursor_col,
2997                            );
2998                            state.lines[state.cursor_row].insert(index, ch);
2999                            state.cursor_col += 1;
3000                            consumed_indices.push(i);
3001                        }
3002                        KeyCode::Enter => {
3003                            let split_index = byte_index_for_char(
3004                                &state.lines[state.cursor_row],
3005                                state.cursor_col,
3006                            );
3007                            let remainder = state.lines[state.cursor_row].split_off(split_index);
3008                            state.cursor_row += 1;
3009                            state.lines.insert(state.cursor_row, remainder);
3010                            state.cursor_col = 0;
3011                            consumed_indices.push(i);
3012                        }
3013                        KeyCode::Backspace => {
3014                            if state.cursor_col > 0 {
3015                                let start = byte_index_for_char(
3016                                    &state.lines[state.cursor_row],
3017                                    state.cursor_col - 1,
3018                                );
3019                                let end = byte_index_for_char(
3020                                    &state.lines[state.cursor_row],
3021                                    state.cursor_col,
3022                                );
3023                                state.lines[state.cursor_row].replace_range(start..end, "");
3024                                state.cursor_col -= 1;
3025                            } else if state.cursor_row > 0 {
3026                                let current = state.lines.remove(state.cursor_row);
3027                                state.cursor_row -= 1;
3028                                state.cursor_col = state.lines[state.cursor_row].chars().count();
3029                                state.lines[state.cursor_row].push_str(&current);
3030                            }
3031                            consumed_indices.push(i);
3032                        }
3033                        KeyCode::Left => {
3034                            if state.cursor_col > 0 {
3035                                state.cursor_col -= 1;
3036                            } else if state.cursor_row > 0 {
3037                                state.cursor_row -= 1;
3038                                state.cursor_col = state.lines[state.cursor_row].chars().count();
3039                            }
3040                            consumed_indices.push(i);
3041                        }
3042                        KeyCode::Right => {
3043                            let line_len = state.lines[state.cursor_row].chars().count();
3044                            if state.cursor_col < line_len {
3045                                state.cursor_col += 1;
3046                            } else if state.cursor_row + 1 < state.lines.len() {
3047                                state.cursor_row += 1;
3048                                state.cursor_col = 0;
3049                            }
3050                            consumed_indices.push(i);
3051                        }
3052                        KeyCode::Up => {
3053                            if wrapping {
3054                                let (vrow, vcol) = textarea_logical_to_visual(
3055                                    &pre_vlines,
3056                                    state.cursor_row,
3057                                    state.cursor_col,
3058                                );
3059                                if vrow > 0 {
3060                                    let (lr, lc) =
3061                                        textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
3062                                    state.cursor_row = lr;
3063                                    state.cursor_col = lc;
3064                                }
3065                            } else if state.cursor_row > 0 {
3066                                state.cursor_row -= 1;
3067                                state.cursor_col = state
3068                                    .cursor_col
3069                                    .min(state.lines[state.cursor_row].chars().count());
3070                            }
3071                            consumed_indices.push(i);
3072                        }
3073                        KeyCode::Down => {
3074                            if wrapping {
3075                                let (vrow, vcol) = textarea_logical_to_visual(
3076                                    &pre_vlines,
3077                                    state.cursor_row,
3078                                    state.cursor_col,
3079                                );
3080                                if vrow + 1 < pre_vlines.len() {
3081                                    let (lr, lc) =
3082                                        textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
3083                                    state.cursor_row = lr;
3084                                    state.cursor_col = lc;
3085                                }
3086                            } else if state.cursor_row + 1 < state.lines.len() {
3087                                state.cursor_row += 1;
3088                                state.cursor_col = state
3089                                    .cursor_col
3090                                    .min(state.lines[state.cursor_row].chars().count());
3091                            }
3092                            consumed_indices.push(i);
3093                        }
3094                        KeyCode::Home => {
3095                            state.cursor_col = 0;
3096                            consumed_indices.push(i);
3097                        }
3098                        KeyCode::Delete => {
3099                            let line_len = state.lines[state.cursor_row].chars().count();
3100                            if state.cursor_col < line_len {
3101                                let start = byte_index_for_char(
3102                                    &state.lines[state.cursor_row],
3103                                    state.cursor_col,
3104                                );
3105                                let end = byte_index_for_char(
3106                                    &state.lines[state.cursor_row],
3107                                    state.cursor_col + 1,
3108                                );
3109                                state.lines[state.cursor_row].replace_range(start..end, "");
3110                            } else if state.cursor_row + 1 < state.lines.len() {
3111                                let next = state.lines.remove(state.cursor_row + 1);
3112                                state.lines[state.cursor_row].push_str(&next);
3113                            }
3114                            consumed_indices.push(i);
3115                        }
3116                        KeyCode::End => {
3117                            state.cursor_col = state.lines[state.cursor_row].chars().count();
3118                            consumed_indices.push(i);
3119                        }
3120                        _ => {}
3121                    }
3122                }
3123                if let Event::Paste(ref text) = event {
3124                    for ch in text.chars() {
3125                        if ch == '\n' || ch == '\r' {
3126                            let split_index = byte_index_for_char(
3127                                &state.lines[state.cursor_row],
3128                                state.cursor_col,
3129                            );
3130                            let remainder = state.lines[state.cursor_row].split_off(split_index);
3131                            state.cursor_row += 1;
3132                            state.lines.insert(state.cursor_row, remainder);
3133                            state.cursor_col = 0;
3134                        } else {
3135                            if let Some(max) = state.max_length {
3136                                let total: usize =
3137                                    state.lines.iter().map(|l| l.chars().count()).sum();
3138                                if total >= max {
3139                                    break;
3140                                }
3141                            }
3142                            let index = byte_index_for_char(
3143                                &state.lines[state.cursor_row],
3144                                state.cursor_col,
3145                            );
3146                            state.lines[state.cursor_row].insert(index, ch);
3147                            state.cursor_col += 1;
3148                        }
3149                    }
3150                    consumed_indices.push(i);
3151                }
3152            }
3153
3154            for index in consumed_indices {
3155                self.consumed[index] = true;
3156            }
3157        }
3158
3159        let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
3160        let (cursor_vrow, cursor_vcol) =
3161            textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
3162
3163        if cursor_vrow < state.scroll_offset {
3164            state.scroll_offset = cursor_vrow;
3165        }
3166        if cursor_vrow >= state.scroll_offset + visible_rows as usize {
3167            state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
3168        }
3169
3170        self.interaction_count += 1;
3171        self.commands.push(Command::BeginContainer {
3172            direction: Direction::Column,
3173            gap: 0,
3174            align: Align::Start,
3175            justify: Justify::Start,
3176            border: None,
3177            border_sides: BorderSides::all(),
3178            border_style: Style::new().fg(self.theme.border),
3179            bg_color: None,
3180            padding: Padding::default(),
3181            margin: Margin::default(),
3182            constraints: Constraints::default(),
3183            title: None,
3184            grow: 0,
3185            group_name: None,
3186        });
3187
3188        for vi in 0..visible_rows as usize {
3189            let actual_vi = state.scroll_offset + vi;
3190            let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
3191                let line = &state.lines[vl.logical_row];
3192                let text: String = line
3193                    .chars()
3194                    .skip(vl.char_start)
3195                    .take(vl.char_count)
3196                    .collect();
3197                (text, actual_vi == cursor_vrow)
3198            } else {
3199                (String::new(), false)
3200            };
3201
3202            let mut rendered = seg_text.clone();
3203            let mut style = if seg_text.is_empty() {
3204                Style::new().fg(self.theme.text_dim)
3205            } else {
3206                Style::new().fg(self.theme.text)
3207            };
3208
3209            if is_cursor_line && focused {
3210                rendered.clear();
3211                for (idx, ch) in seg_text.chars().enumerate() {
3212                    if idx == cursor_vcol {
3213                        rendered.push('▎');
3214                    }
3215                    rendered.push(ch);
3216                }
3217                if cursor_vcol >= seg_text.chars().count() {
3218                    rendered.push('▎');
3219                }
3220                style = Style::new().fg(self.theme.text);
3221            }
3222
3223            self.styled(rendered, style);
3224        }
3225        self.commands.push(Command::EndContainer);
3226        self.last_text_idx = None;
3227
3228        self
3229    }
3230
3231    /// Render a progress bar (20 chars wide). `ratio` is clamped to `0.0..=1.0`.
3232    ///
3233    /// Uses block characters (`█` filled, `░` empty). For a custom width use
3234    /// [`Context::progress_bar`].
3235    pub fn progress(&mut self, ratio: f64) -> &mut Self {
3236        self.progress_bar(ratio, 20)
3237    }
3238
3239    /// Render a progress bar with a custom character width.
3240    ///
3241    /// `ratio` is clamped to `0.0..=1.0`. `width` is the total number of
3242    /// characters rendered.
3243    pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
3244        let clamped = ratio.clamp(0.0, 1.0);
3245        let filled = (clamped * width as f64).round() as u32;
3246        let empty = width.saturating_sub(filled);
3247        let mut bar = String::new();
3248        for _ in 0..filled {
3249            bar.push('█');
3250        }
3251        for _ in 0..empty {
3252            bar.push('░');
3253        }
3254        self.text(bar)
3255    }
3256
3257    /// Render a horizontal bar chart from `(label, value)` pairs.
3258    ///
3259    /// Bars are normalized against the largest value and rendered with `█` up to
3260    /// `max_width` characters.
3261    ///
3262    /// # Example
3263    ///
3264    /// ```ignore
3265    /// # slt::run(|ui: &mut slt::Context| {
3266    /// let data = [
3267    ///     ("Sales", 160.0),
3268    ///     ("Revenue", 120.0),
3269    ///     ("Users", 220.0),
3270    ///     ("Costs", 60.0),
3271    /// ];
3272    /// ui.bar_chart(&data, 24);
3273    ///
3274    /// For styled bars with per-bar colors, see [`bar_chart_styled`].
3275    /// # });
3276    /// ```
3277    pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
3278        if data.is_empty() {
3279            return self;
3280        }
3281
3282        let max_label_width = data
3283            .iter()
3284            .map(|(label, _)| UnicodeWidthStr::width(*label))
3285            .max()
3286            .unwrap_or(0);
3287        let max_value = data
3288            .iter()
3289            .map(|(_, value)| *value)
3290            .fold(f64::NEG_INFINITY, f64::max);
3291        let denom = if max_value > 0.0 { max_value } else { 1.0 };
3292
3293        self.interaction_count += 1;
3294        self.commands.push(Command::BeginContainer {
3295            direction: Direction::Column,
3296            gap: 0,
3297            align: Align::Start,
3298            justify: Justify::Start,
3299            border: None,
3300            border_sides: BorderSides::all(),
3301            border_style: Style::new().fg(self.theme.border),
3302            bg_color: None,
3303            padding: Padding::default(),
3304            margin: Margin::default(),
3305            constraints: Constraints::default(),
3306            title: None,
3307            grow: 0,
3308            group_name: None,
3309        });
3310
3311        for (label, value) in data {
3312            let label_width = UnicodeWidthStr::width(*label);
3313            let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3314            let normalized = (*value / denom).clamp(0.0, 1.0);
3315            let bar_len = (normalized * max_width as f64).round() as usize;
3316            let bar = "█".repeat(bar_len);
3317
3318            self.interaction_count += 1;
3319            self.commands.push(Command::BeginContainer {
3320                direction: Direction::Row,
3321                gap: 1,
3322                align: Align::Start,
3323                justify: Justify::Start,
3324                border: None,
3325                border_sides: BorderSides::all(),
3326                border_style: Style::new().fg(self.theme.border),
3327                bg_color: None,
3328                padding: Padding::default(),
3329                margin: Margin::default(),
3330                constraints: Constraints::default(),
3331                title: None,
3332                grow: 0,
3333                group_name: None,
3334            });
3335            self.styled(
3336                format!("{label}{label_padding}"),
3337                Style::new().fg(self.theme.text),
3338            );
3339            self.styled(bar, Style::new().fg(self.theme.primary));
3340            self.styled(
3341                format_compact_number(*value),
3342                Style::new().fg(self.theme.text_dim),
3343            );
3344            self.commands.push(Command::EndContainer);
3345            self.last_text_idx = None;
3346        }
3347
3348        self.commands.push(Command::EndContainer);
3349        self.last_text_idx = None;
3350
3351        self
3352    }
3353
3354    /// Render a styled bar chart with per-bar colors, grouping, and direction control.
3355    ///
3356    /// # Example
3357    /// ```ignore
3358    /// # slt::run(|ui: &mut slt::Context| {
3359    /// use slt::{Bar, Color};
3360    /// let bars = vec![
3361    ///     Bar::new("Q1", 32.0).color(Color::Cyan),
3362    ///     Bar::new("Q2", 46.0).color(Color::Green),
3363    ///     Bar::new("Q3", 28.0).color(Color::Yellow),
3364    ///     Bar::new("Q4", 54.0).color(Color::Red),
3365    /// ];
3366    /// ui.bar_chart_styled(&bars, 30, slt::BarDirection::Horizontal);
3367    /// # });
3368    /// ```
3369    pub fn bar_chart_styled(
3370        &mut self,
3371        bars: &[Bar],
3372        max_width: u32,
3373        direction: BarDirection,
3374    ) -> &mut Self {
3375        if bars.is_empty() {
3376            return self;
3377        }
3378
3379        let max_value = bars
3380            .iter()
3381            .map(|bar| bar.value)
3382            .fold(f64::NEG_INFINITY, f64::max);
3383        let denom = if max_value > 0.0 { max_value } else { 1.0 };
3384
3385        match direction {
3386            BarDirection::Horizontal => {
3387                let max_label_width = bars
3388                    .iter()
3389                    .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3390                    .max()
3391                    .unwrap_or(0);
3392
3393                self.interaction_count += 1;
3394                self.commands.push(Command::BeginContainer {
3395                    direction: Direction::Column,
3396                    gap: 0,
3397                    align: Align::Start,
3398                    justify: Justify::Start,
3399                    border: None,
3400                    border_sides: BorderSides::all(),
3401                    border_style: Style::new().fg(self.theme.border),
3402                    bg_color: None,
3403                    padding: Padding::default(),
3404                    margin: Margin::default(),
3405                    constraints: Constraints::default(),
3406                    title: None,
3407                    grow: 0,
3408                    group_name: None,
3409                });
3410
3411                for bar in bars {
3412                    let label_width = UnicodeWidthStr::width(bar.label.as_str());
3413                    let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3414                    let normalized = (bar.value / denom).clamp(0.0, 1.0);
3415                    let bar_len = (normalized * max_width as f64).round() as usize;
3416                    let bar_text = "█".repeat(bar_len);
3417                    let color = bar.color.unwrap_or(self.theme.primary);
3418
3419                    self.interaction_count += 1;
3420                    self.commands.push(Command::BeginContainer {
3421                        direction: Direction::Row,
3422                        gap: 1,
3423                        align: Align::Start,
3424                        justify: Justify::Start,
3425                        border: None,
3426                        border_sides: BorderSides::all(),
3427                        border_style: Style::new().fg(self.theme.border),
3428                        bg_color: None,
3429                        padding: Padding::default(),
3430                        margin: Margin::default(),
3431                        constraints: Constraints::default(),
3432                        title: None,
3433                        grow: 0,
3434                        group_name: None,
3435                    });
3436                    self.styled(
3437                        format!("{}{label_padding}", bar.label),
3438                        Style::new().fg(self.theme.text),
3439                    );
3440                    self.styled(bar_text, Style::new().fg(color));
3441                    self.styled(
3442                        format_compact_number(bar.value),
3443                        Style::new().fg(self.theme.text_dim),
3444                    );
3445                    self.commands.push(Command::EndContainer);
3446                    self.last_text_idx = None;
3447                }
3448
3449                self.commands.push(Command::EndContainer);
3450                self.last_text_idx = None;
3451            }
3452            BarDirection::Vertical => {
3453                const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
3454
3455                let chart_height = max_width.max(1) as usize;
3456                let value_labels: Vec<String> = bars
3457                    .iter()
3458                    .map(|bar| format_compact_number(bar.value))
3459                    .collect();
3460                let col_width = bars
3461                    .iter()
3462                    .zip(value_labels.iter())
3463                    .map(|(bar, value)| {
3464                        UnicodeWidthStr::width(bar.label.as_str())
3465                            .max(UnicodeWidthStr::width(value.as_str()))
3466                            .max(1)
3467                    })
3468                    .max()
3469                    .unwrap_or(1);
3470
3471                let bar_units: Vec<usize> = bars
3472                    .iter()
3473                    .map(|bar| {
3474                        let normalized = (bar.value / denom).clamp(0.0, 1.0);
3475                        (normalized * chart_height as f64 * 8.0).round() as usize
3476                    })
3477                    .collect();
3478
3479                self.interaction_count += 1;
3480                self.commands.push(Command::BeginContainer {
3481                    direction: Direction::Column,
3482                    gap: 0,
3483                    align: Align::Start,
3484                    justify: Justify::Start,
3485                    border: None,
3486                    border_sides: BorderSides::all(),
3487                    border_style: Style::new().fg(self.theme.border),
3488                    bg_color: None,
3489                    padding: Padding::default(),
3490                    margin: Margin::default(),
3491                    constraints: Constraints::default(),
3492                    title: None,
3493                    grow: 0,
3494                    group_name: None,
3495                });
3496
3497                self.interaction_count += 1;
3498                self.commands.push(Command::BeginContainer {
3499                    direction: Direction::Row,
3500                    gap: 1,
3501                    align: Align::Start,
3502                    justify: Justify::Start,
3503                    border: None,
3504                    border_sides: BorderSides::all(),
3505                    border_style: Style::new().fg(self.theme.border),
3506                    bg_color: None,
3507                    padding: Padding::default(),
3508                    margin: Margin::default(),
3509                    constraints: Constraints::default(),
3510                    title: None,
3511                    grow: 0,
3512                    group_name: None,
3513                });
3514                for value in &value_labels {
3515                    self.styled(
3516                        center_text(value, col_width),
3517                        Style::new().fg(self.theme.text_dim),
3518                    );
3519                }
3520                self.commands.push(Command::EndContainer);
3521                self.last_text_idx = None;
3522
3523                for row in (0..chart_height).rev() {
3524                    self.interaction_count += 1;
3525                    self.commands.push(Command::BeginContainer {
3526                        direction: Direction::Row,
3527                        gap: 1,
3528                        align: Align::Start,
3529                        justify: Justify::Start,
3530                        border: None,
3531                        border_sides: BorderSides::all(),
3532                        border_style: Style::new().fg(self.theme.border),
3533                        bg_color: None,
3534                        padding: Padding::default(),
3535                        margin: Margin::default(),
3536                        constraints: Constraints::default(),
3537                        title: None,
3538                        grow: 0,
3539                        group_name: None,
3540                    });
3541
3542                    let row_base = row * 8;
3543                    for (bar, units) in bars.iter().zip(bar_units.iter()) {
3544                        let fill = if *units <= row_base {
3545                            ' '
3546                        } else {
3547                            let delta = *units - row_base;
3548                            if delta >= 8 {
3549                                '█'
3550                            } else {
3551                                FRACTION_BLOCKS[delta]
3552                            }
3553                        };
3554
3555                        self.styled(
3556                            center_text(&fill.to_string(), col_width),
3557                            Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3558                        );
3559                    }
3560
3561                    self.commands.push(Command::EndContainer);
3562                    self.last_text_idx = None;
3563                }
3564
3565                self.interaction_count += 1;
3566                self.commands.push(Command::BeginContainer {
3567                    direction: Direction::Row,
3568                    gap: 1,
3569                    align: Align::Start,
3570                    justify: Justify::Start,
3571                    border: None,
3572                    border_sides: BorderSides::all(),
3573                    border_style: Style::new().fg(self.theme.border),
3574                    bg_color: None,
3575                    padding: Padding::default(),
3576                    margin: Margin::default(),
3577                    constraints: Constraints::default(),
3578                    title: None,
3579                    grow: 0,
3580                    group_name: None,
3581                });
3582                for bar in bars {
3583                    self.styled(
3584                        center_text(&bar.label, col_width),
3585                        Style::new().fg(self.theme.text),
3586                    );
3587                }
3588                self.commands.push(Command::EndContainer);
3589                self.last_text_idx = None;
3590
3591                self.commands.push(Command::EndContainer);
3592                self.last_text_idx = None;
3593            }
3594        }
3595
3596        self
3597    }
3598
3599    /// Render a grouped bar chart.
3600    ///
3601    /// Each group contains multiple bars rendered side by side. Useful for
3602    /// comparing categories across groups (e.g., quarterly revenue by product).
3603    ///
3604    /// # Example
3605    /// ```ignore
3606    /// # slt::run(|ui: &mut slt::Context| {
3607    /// use slt::{Bar, BarGroup, Color};
3608    /// let groups = vec![
3609    ///     BarGroup::new("2023", vec![Bar::new("Rev", 100.0).color(Color::Cyan), Bar::new("Cost", 60.0).color(Color::Red)]),
3610    ///     BarGroup::new("2024", vec![Bar::new("Rev", 140.0).color(Color::Cyan), Bar::new("Cost", 80.0).color(Color::Red)]),
3611    /// ];
3612    /// ui.bar_chart_grouped(&groups, 40);
3613    /// # });
3614    /// ```
3615    pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
3616        if groups.is_empty() {
3617            return self;
3618        }
3619
3620        let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
3621        if all_bars.is_empty() {
3622            return self;
3623        }
3624
3625        let max_label_width = all_bars
3626            .iter()
3627            .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3628            .max()
3629            .unwrap_or(0);
3630        let max_value = all_bars
3631            .iter()
3632            .map(|bar| bar.value)
3633            .fold(f64::NEG_INFINITY, f64::max);
3634        let denom = if max_value > 0.0 { max_value } else { 1.0 };
3635
3636        self.interaction_count += 1;
3637        self.commands.push(Command::BeginContainer {
3638            direction: Direction::Column,
3639            gap: 1,
3640            align: Align::Start,
3641            justify: Justify::Start,
3642            border: None,
3643            border_sides: BorderSides::all(),
3644            border_style: Style::new().fg(self.theme.border),
3645            bg_color: None,
3646            padding: Padding::default(),
3647            margin: Margin::default(),
3648            constraints: Constraints::default(),
3649            title: None,
3650            grow: 0,
3651            group_name: None,
3652        });
3653
3654        for group in groups {
3655            self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
3656
3657            for bar in &group.bars {
3658                let label_width = UnicodeWidthStr::width(bar.label.as_str());
3659                let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3660                let normalized = (bar.value / denom).clamp(0.0, 1.0);
3661                let bar_len = (normalized * max_width as f64).round() as usize;
3662                let bar_text = "█".repeat(bar_len);
3663
3664                self.interaction_count += 1;
3665                self.commands.push(Command::BeginContainer {
3666                    direction: Direction::Row,
3667                    gap: 1,
3668                    align: Align::Start,
3669                    justify: Justify::Start,
3670                    border: None,
3671                    border_sides: BorderSides::all(),
3672                    border_style: Style::new().fg(self.theme.border),
3673                    bg_color: None,
3674                    padding: Padding::default(),
3675                    margin: Margin::default(),
3676                    constraints: Constraints::default(),
3677                    title: None,
3678                    grow: 0,
3679                    group_name: None,
3680                });
3681                self.styled(
3682                    format!("  {}{label_padding}", bar.label),
3683                    Style::new().fg(self.theme.text),
3684                );
3685                self.styled(
3686                    bar_text,
3687                    Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3688                );
3689                self.styled(
3690                    format_compact_number(bar.value),
3691                    Style::new().fg(self.theme.text_dim),
3692                );
3693                self.commands.push(Command::EndContainer);
3694                self.last_text_idx = None;
3695            }
3696        }
3697
3698        self.commands.push(Command::EndContainer);
3699        self.last_text_idx = None;
3700
3701        self
3702    }
3703
3704    /// Render a single-line sparkline from numeric data.
3705    ///
3706    /// Uses the last `width` points (or fewer if the data is shorter) and maps
3707    /// each point to one of `▁▂▃▄▅▆▇█`.
3708    ///
3709    /// # Example
3710    ///
3711    /// ```ignore
3712    /// # slt::run(|ui: &mut slt::Context| {
3713    /// let samples = [12.0, 9.0, 14.0, 18.0, 16.0, 21.0, 20.0, 24.0];
3714    /// ui.sparkline(&samples, 16);
3715    ///
3716    /// For per-point colors and missing values, see [`sparkline_styled`].
3717    /// # });
3718    /// ```
3719    pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
3720        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3721
3722        let w = width as usize;
3723        let window = if data.len() > w {
3724            &data[data.len() - w..]
3725        } else {
3726            data
3727        };
3728
3729        if window.is_empty() {
3730            return self;
3731        }
3732
3733        let min = window.iter().copied().fold(f64::INFINITY, f64::min);
3734        let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3735        let range = max - min;
3736
3737        let line: String = window
3738            .iter()
3739            .map(|&value| {
3740                let normalized = if range == 0.0 {
3741                    0.5
3742                } else {
3743                    (value - min) / range
3744                };
3745                let idx = (normalized * 7.0).round() as usize;
3746                BLOCKS[idx.min(7)]
3747            })
3748            .collect();
3749
3750        self.styled(line, Style::new().fg(self.theme.primary))
3751    }
3752
3753    /// Render a sparkline with per-point colors.
3754    ///
3755    /// Each point can have its own color via `(f64, Option<Color>)` tuples.
3756    /// Use `f64::NAN` for absent values (rendered as spaces).
3757    ///
3758    /// # Example
3759    /// ```ignore
3760    /// # slt::run(|ui: &mut slt::Context| {
3761    /// use slt::Color;
3762    /// let data: Vec<(f64, Option<Color>)> = vec![
3763    ///     (12.0, Some(Color::Green)),
3764    ///     (9.0, Some(Color::Red)),
3765    ///     (14.0, Some(Color::Green)),
3766    ///     (f64::NAN, None),
3767    ///     (18.0, Some(Color::Cyan)),
3768    /// ];
3769    /// ui.sparkline_styled(&data, 16);
3770    /// # });
3771    /// ```
3772    pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
3773        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3774
3775        let w = width as usize;
3776        let window = if data.len() > w {
3777            &data[data.len() - w..]
3778        } else {
3779            data
3780        };
3781
3782        if window.is_empty() {
3783            return self;
3784        }
3785
3786        let mut finite_values = window
3787            .iter()
3788            .map(|(value, _)| *value)
3789            .filter(|value| !value.is_nan());
3790        let Some(first) = finite_values.next() else {
3791            return self.styled(
3792                " ".repeat(window.len()),
3793                Style::new().fg(self.theme.text_dim),
3794            );
3795        };
3796
3797        let mut min = first;
3798        let mut max = first;
3799        for value in finite_values {
3800            min = f64::min(min, value);
3801            max = f64::max(max, value);
3802        }
3803        let range = max - min;
3804
3805        let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
3806        for (value, color) in window {
3807            if value.is_nan() {
3808                cells.push((' ', self.theme.text_dim));
3809                continue;
3810            }
3811
3812            let normalized = if range == 0.0 {
3813                0.5
3814            } else {
3815                ((*value - min) / range).clamp(0.0, 1.0)
3816            };
3817            let idx = (normalized * 7.0).round() as usize;
3818            cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
3819        }
3820
3821        self.interaction_count += 1;
3822        self.commands.push(Command::BeginContainer {
3823            direction: Direction::Row,
3824            gap: 0,
3825            align: Align::Start,
3826            justify: Justify::Start,
3827            border: None,
3828            border_sides: BorderSides::all(),
3829            border_style: Style::new().fg(self.theme.border),
3830            bg_color: None,
3831            padding: Padding::default(),
3832            margin: Margin::default(),
3833            constraints: Constraints::default(),
3834            title: None,
3835            grow: 0,
3836            group_name: None,
3837        });
3838
3839        let mut seg = String::new();
3840        let mut seg_color = cells[0].1;
3841        for (ch, color) in cells {
3842            if color != seg_color {
3843                self.styled(seg, Style::new().fg(seg_color));
3844                seg = String::new();
3845                seg_color = color;
3846            }
3847            seg.push(ch);
3848        }
3849        if !seg.is_empty() {
3850            self.styled(seg, Style::new().fg(seg_color));
3851        }
3852
3853        self.commands.push(Command::EndContainer);
3854        self.last_text_idx = None;
3855
3856        self
3857    }
3858
3859    /// Render a multi-row line chart using braille characters.
3860    ///
3861    /// `width` and `height` are terminal cell dimensions. Internally this uses
3862    /// braille dot resolution (`width*2` x `height*4`) for smoother plotting.
3863    ///
3864    /// # Example
3865    ///
3866    /// ```ignore
3867    /// # slt::run(|ui: &mut slt::Context| {
3868    /// let data = [1.0, 3.0, 2.0, 5.0, 4.0, 6.0, 3.0, 7.0];
3869    /// ui.line_chart(&data, 40, 8);
3870    /// # });
3871    /// ```
3872    pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
3873        if data.is_empty() || width == 0 || height == 0 {
3874            return self;
3875        }
3876
3877        let cols = width as usize;
3878        let rows = height as usize;
3879        let px_w = cols * 2;
3880        let px_h = rows * 4;
3881
3882        let min = data.iter().copied().fold(f64::INFINITY, f64::min);
3883        let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3884        let range = if (max - min).abs() < f64::EPSILON {
3885            1.0
3886        } else {
3887            max - min
3888        };
3889
3890        let points: Vec<usize> = (0..px_w)
3891            .map(|px| {
3892                let data_idx = if px_w <= 1 {
3893                    0.0
3894                } else {
3895                    px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
3896                };
3897                let idx = data_idx.floor() as usize;
3898                let frac = data_idx - idx as f64;
3899                let value = if idx + 1 < data.len() {
3900                    data[idx] * (1.0 - frac) + data[idx + 1] * frac
3901                } else {
3902                    data[idx.min(data.len() - 1)]
3903                };
3904
3905                let normalized = (value - min) / range;
3906                let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
3907                py.min(px_h - 1)
3908            })
3909            .collect();
3910
3911        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
3912        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
3913
3914        let mut grid = vec![vec![0u32; cols]; rows];
3915
3916        for i in 0..points.len() {
3917            let px = i;
3918            let py = points[i];
3919            let char_col = px / 2;
3920            let char_row = py / 4;
3921            let sub_col = px % 2;
3922            let sub_row = py % 4;
3923
3924            if char_col < cols && char_row < rows {
3925                grid[char_row][char_col] |= if sub_col == 0 {
3926                    LEFT_BITS[sub_row]
3927                } else {
3928                    RIGHT_BITS[sub_row]
3929                };
3930            }
3931
3932            if i + 1 < points.len() {
3933                let py_next = points[i + 1];
3934                let (y_start, y_end) = if py <= py_next {
3935                    (py, py_next)
3936                } else {
3937                    (py_next, py)
3938                };
3939                for y in y_start..=y_end {
3940                    let cell_row = y / 4;
3941                    let sub_y = y % 4;
3942                    if char_col < cols && cell_row < rows {
3943                        grid[cell_row][char_col] |= if sub_col == 0 {
3944                            LEFT_BITS[sub_y]
3945                        } else {
3946                            RIGHT_BITS[sub_y]
3947                        };
3948                    }
3949                }
3950            }
3951        }
3952
3953        let style = Style::new().fg(self.theme.primary);
3954        for row in grid {
3955            let line: String = row
3956                .iter()
3957                .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
3958                .collect();
3959            self.styled(line, style);
3960        }
3961
3962        self
3963    }
3964
3965    /// Render a braille drawing canvas.
3966    ///
3967    /// The closure receives a [`CanvasContext`] for pixel-level drawing. Each
3968    /// terminal cell maps to a 2x4 braille dot matrix, giving `width*2` x
3969    /// `height*4` pixel resolution.
3970    ///
3971    /// # Example
3972    ///
3973    /// ```ignore
3974    /// # slt::run(|ui: &mut slt::Context| {
3975    /// ui.canvas(40, 10, |cv| {
3976    ///     cv.line(0, 0, cv.width() - 1, cv.height() - 1);
3977    ///     cv.circle(40, 20, 15);
3978    /// });
3979    /// # });
3980    /// ```
3981    pub fn canvas(
3982        &mut self,
3983        width: u32,
3984        height: u32,
3985        draw: impl FnOnce(&mut CanvasContext),
3986    ) -> &mut Self {
3987        if width == 0 || height == 0 {
3988            return self;
3989        }
3990
3991        let mut canvas = CanvasContext::new(width as usize, height as usize);
3992        draw(&mut canvas);
3993
3994        for segments in canvas.render() {
3995            self.interaction_count += 1;
3996            self.commands.push(Command::BeginContainer {
3997                direction: Direction::Row,
3998                gap: 0,
3999                align: Align::Start,
4000                justify: Justify::Start,
4001                border: None,
4002                border_sides: BorderSides::all(),
4003                border_style: Style::new(),
4004                bg_color: None,
4005                padding: Padding::default(),
4006                margin: Margin::default(),
4007                constraints: Constraints::default(),
4008                title: None,
4009                grow: 0,
4010                group_name: None,
4011            });
4012            for (text, color) in segments {
4013                let c = if color == Color::Reset {
4014                    self.theme.primary
4015                } else {
4016                    color
4017                };
4018                self.styled(text, Style::new().fg(c));
4019            }
4020            self.commands.push(Command::EndContainer);
4021            self.last_text_idx = None;
4022        }
4023
4024        self
4025    }
4026
4027    /// Render a multi-series chart with axes, legend, and auto-scaling.
4028    pub fn chart(
4029        &mut self,
4030        configure: impl FnOnce(&mut ChartBuilder),
4031        width: u32,
4032        height: u32,
4033    ) -> &mut Self {
4034        if width == 0 || height == 0 {
4035            return self;
4036        }
4037
4038        let axis_style = Style::new().fg(self.theme.text_dim);
4039        let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
4040        configure(&mut builder);
4041
4042        let config = builder.build();
4043        let rows = render_chart(&config);
4044
4045        for row in rows {
4046            self.interaction_count += 1;
4047            self.commands.push(Command::BeginContainer {
4048                direction: Direction::Row,
4049                gap: 0,
4050                align: Align::Start,
4051                justify: Justify::Start,
4052                border: None,
4053                border_sides: BorderSides::all(),
4054                border_style: Style::new().fg(self.theme.border),
4055                bg_color: None,
4056                padding: Padding::default(),
4057                margin: Margin::default(),
4058                constraints: Constraints::default(),
4059                title: None,
4060                grow: 0,
4061                group_name: None,
4062            });
4063            for (text, style) in row.segments {
4064                self.styled(text, style);
4065            }
4066            self.commands.push(Command::EndContainer);
4067            self.last_text_idx = None;
4068        }
4069
4070        self
4071    }
4072
4073    /// Renders a scatter plot.
4074    ///
4075    /// Each point is a (x, y) tuple. Uses braille markers.
4076    pub fn scatter(&mut self, data: &[(f64, f64)], width: u32, height: u32) -> &mut Self {
4077        self.chart(
4078            |c| {
4079                c.scatter(data);
4080                c.grid(true);
4081            },
4082            width,
4083            height,
4084        )
4085    }
4086
4087    /// Render a histogram from raw data with auto-binning.
4088    pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
4089        self.histogram_with(data, |_| {}, width, height)
4090    }
4091
4092    /// Render a histogram with configuration options.
4093    pub fn histogram_with(
4094        &mut self,
4095        data: &[f64],
4096        configure: impl FnOnce(&mut HistogramBuilder),
4097        width: u32,
4098        height: u32,
4099    ) -> &mut Self {
4100        if width == 0 || height == 0 {
4101            return self;
4102        }
4103
4104        let mut options = HistogramBuilder::default();
4105        configure(&mut options);
4106        let axis_style = Style::new().fg(self.theme.text_dim);
4107        let config = build_histogram_config(data, &options, width, height, axis_style);
4108        let rows = render_chart(&config);
4109
4110        for row in rows {
4111            self.interaction_count += 1;
4112            self.commands.push(Command::BeginContainer {
4113                direction: Direction::Row,
4114                gap: 0,
4115                align: Align::Start,
4116                justify: Justify::Start,
4117                border: None,
4118                border_sides: BorderSides::all(),
4119                border_style: Style::new().fg(self.theme.border),
4120                bg_color: None,
4121                padding: Padding::default(),
4122                margin: Margin::default(),
4123                constraints: Constraints::default(),
4124                title: None,
4125                grow: 0,
4126                group_name: None,
4127            });
4128            for (text, style) in row.segments {
4129                self.styled(text, style);
4130            }
4131            self.commands.push(Command::EndContainer);
4132            self.last_text_idx = None;
4133        }
4134
4135        self
4136    }
4137
4138    /// Render children in a fixed grid with the given number of columns.
4139    ///
4140    /// Children are placed left-to-right, top-to-bottom. Each cell has equal
4141    /// width (`area_width / cols`). Rows wrap automatically.
4142    ///
4143    /// # Example
4144    ///
4145    /// ```no_run
4146    /// # slt::run(|ui: &mut slt::Context| {
4147    /// ui.grid(3, |ui| {
4148    ///     for i in 0..9 {
4149    ///         ui.text(format!("Cell {i}"));
4150    ///     }
4151    /// });
4152    /// # });
4153    /// ```
4154    pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
4155        slt_assert(cols > 0, "grid() requires at least 1 column");
4156        let interaction_id = self.interaction_count;
4157        self.interaction_count += 1;
4158        let border = self.theme.border;
4159
4160        self.commands.push(Command::BeginContainer {
4161            direction: Direction::Column,
4162            gap: 0,
4163            align: Align::Start,
4164            justify: Justify::Start,
4165            border: None,
4166            border_sides: BorderSides::all(),
4167            border_style: Style::new().fg(border),
4168            bg_color: None,
4169            padding: Padding::default(),
4170            margin: Margin::default(),
4171            constraints: Constraints::default(),
4172            title: None,
4173            grow: 0,
4174            group_name: None,
4175        });
4176
4177        let children_start = self.commands.len();
4178        f(self);
4179        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
4180
4181        let mut elements: Vec<Vec<Command>> = Vec::new();
4182        let mut iter = child_commands.into_iter().peekable();
4183        while let Some(cmd) = iter.next() {
4184            match cmd {
4185                Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4186                    let mut depth = 1_u32;
4187                    let mut element = vec![cmd];
4188                    for next in iter.by_ref() {
4189                        match next {
4190                            Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4191                                depth += 1;
4192                            }
4193                            Command::EndContainer => {
4194                                depth = depth.saturating_sub(1);
4195                            }
4196                            _ => {}
4197                        }
4198                        let at_end = matches!(next, Command::EndContainer) && depth == 0;
4199                        element.push(next);
4200                        if at_end {
4201                            break;
4202                        }
4203                    }
4204                    elements.push(element);
4205                }
4206                Command::EndContainer => {}
4207                _ => elements.push(vec![cmd]),
4208            }
4209        }
4210
4211        let cols = cols.max(1) as usize;
4212        for row in elements.chunks(cols) {
4213            self.interaction_count += 1;
4214            self.commands.push(Command::BeginContainer {
4215                direction: Direction::Row,
4216                gap: 0,
4217                align: Align::Start,
4218                justify: Justify::Start,
4219                border: None,
4220                border_sides: BorderSides::all(),
4221                border_style: Style::new().fg(border),
4222                bg_color: None,
4223                padding: Padding::default(),
4224                margin: Margin::default(),
4225                constraints: Constraints::default(),
4226                title: None,
4227                grow: 0,
4228                group_name: None,
4229            });
4230
4231            for element in row {
4232                self.interaction_count += 1;
4233                self.commands.push(Command::BeginContainer {
4234                    direction: Direction::Column,
4235                    gap: 0,
4236                    align: Align::Start,
4237                    justify: Justify::Start,
4238                    border: None,
4239                    border_sides: BorderSides::all(),
4240                    border_style: Style::new().fg(border),
4241                    bg_color: None,
4242                    padding: Padding::default(),
4243                    margin: Margin::default(),
4244                    constraints: Constraints::default(),
4245                    title: None,
4246                    grow: 1,
4247                    group_name: None,
4248                });
4249                self.commands.extend(element.iter().cloned());
4250                self.commands.push(Command::EndContainer);
4251            }
4252
4253            self.commands.push(Command::EndContainer);
4254        }
4255
4256        self.commands.push(Command::EndContainer);
4257        self.last_text_idx = None;
4258
4259        self.response_for(interaction_id)
4260    }
4261
4262    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
4263    ///
4264    /// The selected item is highlighted with the theme's primary color. If the
4265    /// list is empty, nothing is rendered.
4266    pub fn list(&mut self, state: &mut ListState) -> &mut Self {
4267        let visible = state.visible_indices().to_vec();
4268        if visible.is_empty() && state.items.is_empty() {
4269            state.selected = 0;
4270            return self;
4271        }
4272
4273        if !visible.is_empty() {
4274            state.selected = state.selected.min(visible.len().saturating_sub(1));
4275        }
4276
4277        let focused = self.register_focusable();
4278        let interaction_id = self.interaction_count;
4279        self.interaction_count += 1;
4280
4281        if focused {
4282            let mut consumed_indices = Vec::new();
4283            for (i, event) in self.events.iter().enumerate() {
4284                if let Event::Key(key) = event {
4285                    if key.kind != KeyEventKind::Press {
4286                        continue;
4287                    }
4288                    match key.code {
4289                        KeyCode::Up | KeyCode::Char('k') => {
4290                            state.selected = state.selected.saturating_sub(1);
4291                            consumed_indices.push(i);
4292                        }
4293                        KeyCode::Down | KeyCode::Char('j') => {
4294                            state.selected =
4295                                (state.selected + 1).min(visible.len().saturating_sub(1));
4296                            consumed_indices.push(i);
4297                        }
4298                        _ => {}
4299                    }
4300                }
4301            }
4302
4303            for index in consumed_indices {
4304                self.consumed[index] = true;
4305            }
4306        }
4307
4308        if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4309            for (i, event) in self.events.iter().enumerate() {
4310                if self.consumed[i] {
4311                    continue;
4312                }
4313                if let Event::Mouse(mouse) = event {
4314                    if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4315                        continue;
4316                    }
4317                    let in_bounds = mouse.x >= rect.x
4318                        && mouse.x < rect.right()
4319                        && mouse.y >= rect.y
4320                        && mouse.y < rect.bottom();
4321                    if !in_bounds {
4322                        continue;
4323                    }
4324                    let clicked_idx = (mouse.y - rect.y) as usize;
4325                    if clicked_idx < visible.len() {
4326                        state.selected = clicked_idx;
4327                        self.consumed[i] = true;
4328                    }
4329                }
4330            }
4331        }
4332
4333        self.commands.push(Command::BeginContainer {
4334            direction: Direction::Column,
4335            gap: 0,
4336            align: Align::Start,
4337            justify: Justify::Start,
4338            border: None,
4339            border_sides: BorderSides::all(),
4340            border_style: Style::new().fg(self.theme.border),
4341            bg_color: None,
4342            padding: Padding::default(),
4343            margin: Margin::default(),
4344            constraints: Constraints::default(),
4345            title: None,
4346            grow: 0,
4347            group_name: None,
4348        });
4349
4350        for (view_idx, &item_idx) in visible.iter().enumerate() {
4351            let item = &state.items[item_idx];
4352            if view_idx == state.selected {
4353                if focused {
4354                    self.styled(
4355                        format!("▸ {item}"),
4356                        Style::new().bold().fg(self.theme.primary),
4357                    );
4358                } else {
4359                    self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
4360                }
4361            } else {
4362                self.styled(format!("  {item}"), Style::new().fg(self.theme.text));
4363            }
4364        }
4365
4366        self.commands.push(Command::EndContainer);
4367        self.last_text_idx = None;
4368
4369        self
4370    }
4371
4372    /// Render a data table with column headers. Handles Up/Down selection when focused.
4373    ///
4374    /// Column widths are computed automatically from header and cell content.
4375    /// The selected row is highlighted with the theme's selection colors.
4376    pub fn table(&mut self, state: &mut TableState) -> &mut Self {
4377        if state.is_dirty() {
4378            state.recompute_widths();
4379        }
4380
4381        let focused = self.register_focusable();
4382        let interaction_id = self.interaction_count;
4383        self.interaction_count += 1;
4384
4385        if focused && !state.visible_indices().is_empty() {
4386            let mut consumed_indices = Vec::new();
4387            for (i, event) in self.events.iter().enumerate() {
4388                if let Event::Key(key) = event {
4389                    if key.kind != KeyEventKind::Press {
4390                        continue;
4391                    }
4392                    match key.code {
4393                        KeyCode::Up | KeyCode::Char('k') => {
4394                            let visible_len = if state.page_size > 0 {
4395                                let start = state
4396                                    .page
4397                                    .saturating_mul(state.page_size)
4398                                    .min(state.visible_indices().len());
4399                                let end =
4400                                    (start + state.page_size).min(state.visible_indices().len());
4401                                end.saturating_sub(start)
4402                            } else {
4403                                state.visible_indices().len()
4404                            };
4405                            state.selected = state.selected.min(visible_len.saturating_sub(1));
4406                            state.selected = state.selected.saturating_sub(1);
4407                            consumed_indices.push(i);
4408                        }
4409                        KeyCode::Down | KeyCode::Char('j') => {
4410                            let visible_len = if state.page_size > 0 {
4411                                let start = state
4412                                    .page
4413                                    .saturating_mul(state.page_size)
4414                                    .min(state.visible_indices().len());
4415                                let end =
4416                                    (start + state.page_size).min(state.visible_indices().len());
4417                                end.saturating_sub(start)
4418                            } else {
4419                                state.visible_indices().len()
4420                            };
4421                            state.selected =
4422                                (state.selected + 1).min(visible_len.saturating_sub(1));
4423                            consumed_indices.push(i);
4424                        }
4425                        KeyCode::PageUp => {
4426                            let old_page = state.page;
4427                            state.prev_page();
4428                            if state.page != old_page {
4429                                state.selected = 0;
4430                            }
4431                            consumed_indices.push(i);
4432                        }
4433                        KeyCode::PageDown => {
4434                            let old_page = state.page;
4435                            state.next_page();
4436                            if state.page != old_page {
4437                                state.selected = 0;
4438                            }
4439                            consumed_indices.push(i);
4440                        }
4441                        _ => {}
4442                    }
4443                }
4444            }
4445            for index in consumed_indices {
4446                self.consumed[index] = true;
4447            }
4448        }
4449
4450        if !state.visible_indices().is_empty() || !state.headers.is_empty() {
4451            if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4452                for (i, event) in self.events.iter().enumerate() {
4453                    if self.consumed[i] {
4454                        continue;
4455                    }
4456                    if let Event::Mouse(mouse) = event {
4457                        if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4458                            continue;
4459                        }
4460                        let in_bounds = mouse.x >= rect.x
4461                            && mouse.x < rect.right()
4462                            && mouse.y >= rect.y
4463                            && mouse.y < rect.bottom();
4464                        if !in_bounds {
4465                            continue;
4466                        }
4467
4468                        if mouse.y == rect.y {
4469                            let rel_x = mouse.x.saturating_sub(rect.x);
4470                            let mut x_offset = 0u32;
4471                            for (col_idx, width) in state.column_widths().iter().enumerate() {
4472                                if rel_x >= x_offset && rel_x < x_offset + *width {
4473                                    state.toggle_sort(col_idx);
4474                                    state.selected = 0;
4475                                    self.consumed[i] = true;
4476                                    break;
4477                                }
4478                                x_offset += *width;
4479                                if col_idx + 1 < state.column_widths().len() {
4480                                    x_offset += 3;
4481                                }
4482                            }
4483                            continue;
4484                        }
4485
4486                        if mouse.y < rect.y + 2 {
4487                            continue;
4488                        }
4489
4490                        let visible_len = if state.page_size > 0 {
4491                            let start = state
4492                                .page
4493                                .saturating_mul(state.page_size)
4494                                .min(state.visible_indices().len());
4495                            let end = (start + state.page_size).min(state.visible_indices().len());
4496                            end.saturating_sub(start)
4497                        } else {
4498                            state.visible_indices().len()
4499                        };
4500                        let clicked_idx = (mouse.y - rect.y - 2) as usize;
4501                        if clicked_idx < visible_len {
4502                            state.selected = clicked_idx;
4503                            self.consumed[i] = true;
4504                        }
4505                    }
4506                }
4507            }
4508        }
4509
4510        if state.is_dirty() {
4511            state.recompute_widths();
4512        }
4513
4514        let total_visible = state.visible_indices().len();
4515        let page_start = if state.page_size > 0 {
4516            state
4517                .page
4518                .saturating_mul(state.page_size)
4519                .min(total_visible)
4520        } else {
4521            0
4522        };
4523        let page_end = if state.page_size > 0 {
4524            (page_start + state.page_size).min(total_visible)
4525        } else {
4526            total_visible
4527        };
4528        let visible_len = page_end.saturating_sub(page_start);
4529        state.selected = state.selected.min(visible_len.saturating_sub(1));
4530
4531        self.commands.push(Command::BeginContainer {
4532            direction: Direction::Column,
4533            gap: 0,
4534            align: Align::Start,
4535            justify: Justify::Start,
4536            border: None,
4537            border_sides: BorderSides::all(),
4538            border_style: Style::new().fg(self.theme.border),
4539            bg_color: None,
4540            padding: Padding::default(),
4541            margin: Margin::default(),
4542            constraints: Constraints::default(),
4543            title: None,
4544            grow: 0,
4545            group_name: None,
4546        });
4547
4548        let header_cells = state
4549            .headers
4550            .iter()
4551            .enumerate()
4552            .map(|(i, header)| {
4553                if state.sort_column == Some(i) {
4554                    if state.sort_ascending {
4555                        format!("{header} ▲")
4556                    } else {
4557                        format!("{header} ▼")
4558                    }
4559                } else {
4560                    header.clone()
4561                }
4562            })
4563            .collect::<Vec<_>>();
4564        let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
4565        self.styled(header_line, Style::new().bold().fg(self.theme.text));
4566
4567        let separator = state
4568            .column_widths()
4569            .iter()
4570            .map(|w| "─".repeat(*w as usize))
4571            .collect::<Vec<_>>()
4572            .join("─┼─");
4573        self.text(separator);
4574
4575        for idx in 0..visible_len {
4576            let data_idx = state.visible_indices()[page_start + idx];
4577            let Some(row) = state.rows.get(data_idx) else {
4578                continue;
4579            };
4580            let line = format_table_row(row, state.column_widths(), " │ ");
4581            if idx == state.selected {
4582                let mut style = Style::new()
4583                    .bg(self.theme.selected_bg)
4584                    .fg(self.theme.selected_fg);
4585                if focused {
4586                    style = style.bold();
4587                }
4588                self.styled(line, style);
4589            } else {
4590                self.styled(line, Style::new().fg(self.theme.text));
4591            }
4592        }
4593
4594        if state.page_size > 0 && state.total_pages() > 1 {
4595            self.styled(
4596                format!("Page {}/{}", state.page + 1, state.total_pages()),
4597                Style::new().dim().fg(self.theme.text_dim),
4598            );
4599        }
4600
4601        self.commands.push(Command::EndContainer);
4602        self.last_text_idx = None;
4603
4604        self
4605    }
4606
4607    /// Render a tab bar. Handles Left/Right navigation when focused.
4608    ///
4609    /// The active tab is rendered in the theme's primary color. If the labels
4610    /// list is empty, nothing is rendered.
4611    pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
4612        if state.labels.is_empty() {
4613            state.selected = 0;
4614            return self;
4615        }
4616
4617        state.selected = state.selected.min(state.labels.len().saturating_sub(1));
4618        let focused = self.register_focusable();
4619        let interaction_id = self.interaction_count;
4620
4621        if focused {
4622            let mut consumed_indices = Vec::new();
4623            for (i, event) in self.events.iter().enumerate() {
4624                if let Event::Key(key) = event {
4625                    if key.kind != KeyEventKind::Press {
4626                        continue;
4627                    }
4628                    match key.code {
4629                        KeyCode::Left => {
4630                            state.selected = if state.selected == 0 {
4631                                state.labels.len().saturating_sub(1)
4632                            } else {
4633                                state.selected - 1
4634                            };
4635                            consumed_indices.push(i);
4636                        }
4637                        KeyCode::Right => {
4638                            state.selected = (state.selected + 1) % state.labels.len();
4639                            consumed_indices.push(i);
4640                        }
4641                        _ => {}
4642                    }
4643                }
4644            }
4645
4646            for index in consumed_indices {
4647                self.consumed[index] = true;
4648            }
4649        }
4650
4651        if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4652            for (i, event) in self.events.iter().enumerate() {
4653                if self.consumed[i] {
4654                    continue;
4655                }
4656                if let Event::Mouse(mouse) = event {
4657                    if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4658                        continue;
4659                    }
4660                    let in_bounds = mouse.x >= rect.x
4661                        && mouse.x < rect.right()
4662                        && mouse.y >= rect.y
4663                        && mouse.y < rect.bottom();
4664                    if !in_bounds {
4665                        continue;
4666                    }
4667
4668                    let mut x_offset = 0u32;
4669                    let rel_x = mouse.x - rect.x;
4670                    for (idx, label) in state.labels.iter().enumerate() {
4671                        let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
4672                        if rel_x >= x_offset && rel_x < x_offset + tab_width {
4673                            state.selected = idx;
4674                            self.consumed[i] = true;
4675                            break;
4676                        }
4677                        x_offset += tab_width + 1;
4678                    }
4679                }
4680            }
4681        }
4682
4683        self.interaction_count += 1;
4684        self.commands.push(Command::BeginContainer {
4685            direction: Direction::Row,
4686            gap: 1,
4687            align: Align::Start,
4688            justify: Justify::Start,
4689            border: None,
4690            border_sides: BorderSides::all(),
4691            border_style: Style::new().fg(self.theme.border),
4692            bg_color: None,
4693            padding: Padding::default(),
4694            margin: Margin::default(),
4695            constraints: Constraints::default(),
4696            title: None,
4697            grow: 0,
4698            group_name: None,
4699        });
4700        for (idx, label) in state.labels.iter().enumerate() {
4701            let style = if idx == state.selected {
4702                let s = Style::new().fg(self.theme.primary).bold();
4703                if focused {
4704                    s.underline()
4705                } else {
4706                    s
4707                }
4708            } else {
4709                Style::new().fg(self.theme.text_dim)
4710            };
4711            self.styled(format!("[ {label} ]"), style);
4712        }
4713        self.commands.push(Command::EndContainer);
4714        self.last_text_idx = None;
4715
4716        self
4717    }
4718
4719    /// Render a clickable button. Returns `true` when activated via Enter, Space, or mouse click.
4720    ///
4721    /// The button is styled with the theme's primary color when focused and the
4722    /// accent color when hovered.
4723    pub fn button(&mut self, label: impl Into<String>) -> bool {
4724        let focused = self.register_focusable();
4725        let interaction_id = self.interaction_count;
4726        self.interaction_count += 1;
4727        let response = self.response_for(interaction_id);
4728
4729        let mut activated = response.clicked;
4730        if focused {
4731            let mut consumed_indices = Vec::new();
4732            for (i, event) in self.events.iter().enumerate() {
4733                if let Event::Key(key) = event {
4734                    if key.kind != KeyEventKind::Press {
4735                        continue;
4736                    }
4737                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4738                        activated = true;
4739                        consumed_indices.push(i);
4740                    }
4741                }
4742            }
4743
4744            for index in consumed_indices {
4745                self.consumed[index] = true;
4746            }
4747        }
4748
4749        let hovered = response.hovered;
4750        let style = if focused {
4751            Style::new().fg(self.theme.primary).bold()
4752        } else if hovered {
4753            Style::new().fg(self.theme.accent)
4754        } else {
4755            Style::new().fg(self.theme.text)
4756        };
4757        let hover_bg = if hovered || focused {
4758            Some(self.theme.surface_hover)
4759        } else {
4760            None
4761        };
4762
4763        self.commands.push(Command::BeginContainer {
4764            direction: Direction::Row,
4765            gap: 0,
4766            align: Align::Start,
4767            justify: Justify::Start,
4768            border: None,
4769            border_sides: BorderSides::all(),
4770            border_style: Style::new().fg(self.theme.border),
4771            bg_color: hover_bg,
4772            padding: Padding::default(),
4773            margin: Margin::default(),
4774            constraints: Constraints::default(),
4775            title: None,
4776            grow: 0,
4777            group_name: None,
4778        });
4779        self.styled(format!("[ {} ]", label.into()), style);
4780        self.commands.push(Command::EndContainer);
4781        self.last_text_idx = None;
4782
4783        activated
4784    }
4785
4786    /// Render a styled button variant. Returns `true` when activated.
4787    ///
4788    /// Use [`ButtonVariant::Primary`] for call-to-action, [`ButtonVariant::Danger`]
4789    /// for destructive actions, or [`ButtonVariant::Outline`] for secondary actions.
4790    pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> bool {
4791        let focused = self.register_focusable();
4792        let interaction_id = self.interaction_count;
4793        self.interaction_count += 1;
4794        let response = self.response_for(interaction_id);
4795
4796        let mut activated = response.clicked;
4797        if focused {
4798            let mut consumed_indices = Vec::new();
4799            for (i, event) in self.events.iter().enumerate() {
4800                if let Event::Key(key) = event {
4801                    if key.kind != KeyEventKind::Press {
4802                        continue;
4803                    }
4804                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4805                        activated = true;
4806                        consumed_indices.push(i);
4807                    }
4808                }
4809            }
4810            for index in consumed_indices {
4811                self.consumed[index] = true;
4812            }
4813        }
4814
4815        let label = label.into();
4816        let hover_bg = if response.hovered || focused {
4817            Some(self.theme.surface_hover)
4818        } else {
4819            None
4820        };
4821        let (text, style, bg_color, border) = match variant {
4822            ButtonVariant::Default => {
4823                let style = if focused {
4824                    Style::new().fg(self.theme.primary).bold()
4825                } else if response.hovered {
4826                    Style::new().fg(self.theme.accent)
4827                } else {
4828                    Style::new().fg(self.theme.text)
4829                };
4830                (format!("[ {label} ]"), style, hover_bg, None)
4831            }
4832            ButtonVariant::Primary => {
4833                let style = if focused {
4834                    Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
4835                } else if response.hovered {
4836                    Style::new().fg(self.theme.bg).bg(self.theme.accent)
4837                } else {
4838                    Style::new().fg(self.theme.bg).bg(self.theme.primary)
4839                };
4840                (format!(" {label} "), style, hover_bg, None)
4841            }
4842            ButtonVariant::Danger => {
4843                let style = if focused {
4844                    Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
4845                } else if response.hovered {
4846                    Style::new().fg(self.theme.bg).bg(self.theme.warning)
4847                } else {
4848                    Style::new().fg(self.theme.bg).bg(self.theme.error)
4849                };
4850                (format!(" {label} "), style, hover_bg, None)
4851            }
4852            ButtonVariant::Outline => {
4853                let border_color = if focused {
4854                    self.theme.primary
4855                } else if response.hovered {
4856                    self.theme.accent
4857                } else {
4858                    self.theme.border
4859                };
4860                let style = if focused {
4861                    Style::new().fg(self.theme.primary).bold()
4862                } else if response.hovered {
4863                    Style::new().fg(self.theme.accent)
4864                } else {
4865                    Style::new().fg(self.theme.text)
4866                };
4867                (
4868                    format!(" {label} "),
4869                    style,
4870                    hover_bg,
4871                    Some((Border::Rounded, Style::new().fg(border_color))),
4872                )
4873            }
4874        };
4875
4876        let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
4877        self.commands.push(Command::BeginContainer {
4878            direction: Direction::Row,
4879            gap: 0,
4880            align: Align::Center,
4881            justify: Justify::Center,
4882            border: if border.is_some() {
4883                Some(btn_border)
4884            } else {
4885                None
4886            },
4887            border_sides: BorderSides::all(),
4888            border_style: btn_border_style,
4889            bg_color,
4890            padding: Padding::default(),
4891            margin: Margin::default(),
4892            constraints: Constraints::default(),
4893            title: None,
4894            grow: 0,
4895            group_name: None,
4896        });
4897        self.styled(text, style);
4898        self.commands.push(Command::EndContainer);
4899        self.last_text_idx = None;
4900
4901        activated
4902    }
4903
4904    /// Render a checkbox. Toggles the bool on Enter, Space, or click.
4905    ///
4906    /// The checked state is shown with the theme's success color. When focused,
4907    /// a `▸` prefix is added.
4908    pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
4909        let focused = self.register_focusable();
4910        let interaction_id = self.interaction_count;
4911        self.interaction_count += 1;
4912        let response = self.response_for(interaction_id);
4913        let mut should_toggle = response.clicked;
4914
4915        if focused {
4916            let mut consumed_indices = Vec::new();
4917            for (i, event) in self.events.iter().enumerate() {
4918                if let Event::Key(key) = event {
4919                    if key.kind != KeyEventKind::Press {
4920                        continue;
4921                    }
4922                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4923                        should_toggle = true;
4924                        consumed_indices.push(i);
4925                    }
4926                }
4927            }
4928
4929            for index in consumed_indices {
4930                self.consumed[index] = true;
4931            }
4932        }
4933
4934        if should_toggle {
4935            *checked = !*checked;
4936        }
4937
4938        let hover_bg = if response.hovered || focused {
4939            Some(self.theme.surface_hover)
4940        } else {
4941            None
4942        };
4943        self.commands.push(Command::BeginContainer {
4944            direction: Direction::Row,
4945            gap: 1,
4946            align: Align::Start,
4947            justify: Justify::Start,
4948            border: None,
4949            border_sides: BorderSides::all(),
4950            border_style: Style::new().fg(self.theme.border),
4951            bg_color: hover_bg,
4952            padding: Padding::default(),
4953            margin: Margin::default(),
4954            constraints: Constraints::default(),
4955            title: None,
4956            grow: 0,
4957            group_name: None,
4958        });
4959        let marker_style = if *checked {
4960            Style::new().fg(self.theme.success)
4961        } else {
4962            Style::new().fg(self.theme.text_dim)
4963        };
4964        let marker = if *checked { "[x]" } else { "[ ]" };
4965        let label_text = label.into();
4966        if focused {
4967            self.styled(format!("▸ {marker}"), marker_style.bold());
4968            self.styled(label_text, Style::new().fg(self.theme.text).bold());
4969        } else {
4970            self.styled(marker, marker_style);
4971            self.styled(label_text, Style::new().fg(self.theme.text));
4972        }
4973        self.commands.push(Command::EndContainer);
4974        self.last_text_idx = None;
4975
4976        self
4977    }
4978
4979    /// Render an on/off toggle switch.
4980    ///
4981    /// Toggles `on` when activated via Enter, Space, or click. The switch
4982    /// renders as `●━━ ON` or `━━● OFF` colored with the theme's success or
4983    /// dim color respectively.
4984    pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
4985        let focused = self.register_focusable();
4986        let interaction_id = self.interaction_count;
4987        self.interaction_count += 1;
4988        let response = self.response_for(interaction_id);
4989        let mut should_toggle = response.clicked;
4990
4991        if focused {
4992            let mut consumed_indices = Vec::new();
4993            for (i, event) in self.events.iter().enumerate() {
4994                if let Event::Key(key) = event {
4995                    if key.kind != KeyEventKind::Press {
4996                        continue;
4997                    }
4998                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4999                        should_toggle = true;
5000                        consumed_indices.push(i);
5001                    }
5002                }
5003            }
5004
5005            for index in consumed_indices {
5006                self.consumed[index] = true;
5007            }
5008        }
5009
5010        if should_toggle {
5011            *on = !*on;
5012        }
5013
5014        let hover_bg = if response.hovered || focused {
5015            Some(self.theme.surface_hover)
5016        } else {
5017            None
5018        };
5019        self.commands.push(Command::BeginContainer {
5020            direction: Direction::Row,
5021            gap: 2,
5022            align: Align::Start,
5023            justify: Justify::Start,
5024            border: None,
5025            border_sides: BorderSides::all(),
5026            border_style: Style::new().fg(self.theme.border),
5027            bg_color: hover_bg,
5028            padding: Padding::default(),
5029            margin: Margin::default(),
5030            constraints: Constraints::default(),
5031            title: None,
5032            grow: 0,
5033            group_name: None,
5034        });
5035        let label_text = label.into();
5036        let switch = if *on { "●━━ ON" } else { "━━● OFF" };
5037        let switch_style = if *on {
5038            Style::new().fg(self.theme.success)
5039        } else {
5040            Style::new().fg(self.theme.text_dim)
5041        };
5042        if focused {
5043            self.styled(
5044                format!("▸ {label_text}"),
5045                Style::new().fg(self.theme.text).bold(),
5046            );
5047            self.styled(switch, switch_style.bold());
5048        } else {
5049            self.styled(label_text, Style::new().fg(self.theme.text));
5050            self.styled(switch, switch_style);
5051        }
5052        self.commands.push(Command::EndContainer);
5053        self.last_text_idx = None;
5054
5055        self
5056    }
5057
5058    // ── select / dropdown ─────────────────────────────────────────────
5059
5060    /// Render a dropdown select. Shows the selected item; expands on activation.
5061    ///
5062    /// Returns `true` when the selection changed this frame.
5063    pub fn select(&mut self, state: &mut SelectState) -> bool {
5064        if state.items.is_empty() {
5065            return false;
5066        }
5067        state.selected = state.selected.min(state.items.len().saturating_sub(1));
5068
5069        let focused = self.register_focusable();
5070        let interaction_id = self.interaction_count;
5071        self.interaction_count += 1;
5072        let response = self.response_for(interaction_id);
5073        let old_selected = state.selected;
5074
5075        if response.clicked {
5076            state.open = !state.open;
5077            if state.open {
5078                state.set_cursor(state.selected);
5079            }
5080        }
5081
5082        if focused {
5083            let mut consumed_indices = Vec::new();
5084            for (i, event) in self.events.iter().enumerate() {
5085                if self.consumed[i] {
5086                    continue;
5087                }
5088                if let Event::Key(key) = event {
5089                    if key.kind != KeyEventKind::Press {
5090                        continue;
5091                    }
5092                    if state.open {
5093                        match key.code {
5094                            KeyCode::Up | KeyCode::Char('k') => {
5095                                let c = state.cursor();
5096                                state.set_cursor(c.saturating_sub(1));
5097                                consumed_indices.push(i);
5098                            }
5099                            KeyCode::Down | KeyCode::Char('j') => {
5100                                let c = state.cursor();
5101                                state.set_cursor((c + 1).min(state.items.len().saturating_sub(1)));
5102                                consumed_indices.push(i);
5103                            }
5104                            KeyCode::Enter | KeyCode::Char(' ') => {
5105                                state.selected = state.cursor();
5106                                state.open = false;
5107                                consumed_indices.push(i);
5108                            }
5109                            KeyCode::Esc => {
5110                                state.open = false;
5111                                consumed_indices.push(i);
5112                            }
5113                            _ => {}
5114                        }
5115                    } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5116                        state.open = true;
5117                        state.set_cursor(state.selected);
5118                        consumed_indices.push(i);
5119                    }
5120                }
5121            }
5122            for idx in consumed_indices {
5123                self.consumed[idx] = true;
5124            }
5125        }
5126
5127        let changed = state.selected != old_selected;
5128
5129        let border_color = if focused {
5130            self.theme.primary
5131        } else {
5132            self.theme.border
5133        };
5134        let display_text = state
5135            .items
5136            .get(state.selected)
5137            .cloned()
5138            .unwrap_or_else(|| state.placeholder.clone());
5139        let arrow = if state.open { "▲" } else { "▼" };
5140
5141        self.commands.push(Command::BeginContainer {
5142            direction: Direction::Column,
5143            gap: 0,
5144            align: Align::Start,
5145            justify: Justify::Start,
5146            border: None,
5147            border_sides: BorderSides::all(),
5148            border_style: Style::new().fg(self.theme.border),
5149            bg_color: None,
5150            padding: Padding::default(),
5151            margin: Margin::default(),
5152            constraints: Constraints::default(),
5153            title: None,
5154            grow: 0,
5155            group_name: None,
5156        });
5157
5158        self.commands.push(Command::BeginContainer {
5159            direction: Direction::Row,
5160            gap: 1,
5161            align: Align::Start,
5162            justify: Justify::Start,
5163            border: Some(Border::Rounded),
5164            border_sides: BorderSides::all(),
5165            border_style: Style::new().fg(border_color),
5166            bg_color: None,
5167            padding: Padding {
5168                left: 1,
5169                right: 1,
5170                top: 0,
5171                bottom: 0,
5172            },
5173            margin: Margin::default(),
5174            constraints: Constraints::default(),
5175            title: None,
5176            grow: 0,
5177            group_name: None,
5178        });
5179        self.interaction_count += 1;
5180        self.styled(&display_text, Style::new().fg(self.theme.text));
5181        self.styled(arrow, Style::new().fg(self.theme.text_dim));
5182        self.commands.push(Command::EndContainer);
5183        self.last_text_idx = None;
5184
5185        if state.open {
5186            for (idx, item) in state.items.iter().enumerate() {
5187                let is_cursor = idx == state.cursor();
5188                let style = if is_cursor {
5189                    Style::new().bold().fg(self.theme.primary)
5190                } else {
5191                    Style::new().fg(self.theme.text)
5192                };
5193                let prefix = if is_cursor { "▸ " } else { "  " };
5194                self.styled(format!("{prefix}{item}"), style);
5195            }
5196        }
5197
5198        self.commands.push(Command::EndContainer);
5199        self.last_text_idx = None;
5200        changed
5201    }
5202
5203    // ── radio ────────────────────────────────────────────────────────
5204
5205    /// Render a radio button group. Returns `true` when selection changed.
5206    pub fn radio(&mut self, state: &mut RadioState) -> bool {
5207        if state.items.is_empty() {
5208            return false;
5209        }
5210        state.selected = state.selected.min(state.items.len().saturating_sub(1));
5211        let focused = self.register_focusable();
5212        let old_selected = state.selected;
5213
5214        if focused {
5215            let mut consumed_indices = Vec::new();
5216            for (i, event) in self.events.iter().enumerate() {
5217                if self.consumed[i] {
5218                    continue;
5219                }
5220                if let Event::Key(key) = event {
5221                    if key.kind != KeyEventKind::Press {
5222                        continue;
5223                    }
5224                    match key.code {
5225                        KeyCode::Up | KeyCode::Char('k') => {
5226                            state.selected = state.selected.saturating_sub(1);
5227                            consumed_indices.push(i);
5228                        }
5229                        KeyCode::Down | KeyCode::Char('j') => {
5230                            state.selected =
5231                                (state.selected + 1).min(state.items.len().saturating_sub(1));
5232                            consumed_indices.push(i);
5233                        }
5234                        KeyCode::Enter | KeyCode::Char(' ') => {
5235                            consumed_indices.push(i);
5236                        }
5237                        _ => {}
5238                    }
5239                }
5240            }
5241            for idx in consumed_indices {
5242                self.consumed[idx] = true;
5243            }
5244        }
5245
5246        let interaction_id = self.interaction_count;
5247        self.interaction_count += 1;
5248
5249        if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5250            for (i, event) in self.events.iter().enumerate() {
5251                if self.consumed[i] {
5252                    continue;
5253                }
5254                if let Event::Mouse(mouse) = event {
5255                    if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5256                        continue;
5257                    }
5258                    let in_bounds = mouse.x >= rect.x
5259                        && mouse.x < rect.right()
5260                        && mouse.y >= rect.y
5261                        && mouse.y < rect.bottom();
5262                    if !in_bounds {
5263                        continue;
5264                    }
5265                    let clicked_idx = (mouse.y - rect.y) as usize;
5266                    if clicked_idx < state.items.len() {
5267                        state.selected = clicked_idx;
5268                        self.consumed[i] = true;
5269                    }
5270                }
5271            }
5272        }
5273
5274        self.commands.push(Command::BeginContainer {
5275            direction: Direction::Column,
5276            gap: 0,
5277            align: Align::Start,
5278            justify: Justify::Start,
5279            border: None,
5280            border_sides: BorderSides::all(),
5281            border_style: Style::new().fg(self.theme.border),
5282            bg_color: None,
5283            padding: Padding::default(),
5284            margin: Margin::default(),
5285            constraints: Constraints::default(),
5286            title: None,
5287            grow: 0,
5288            group_name: None,
5289        });
5290
5291        for (idx, item) in state.items.iter().enumerate() {
5292            let is_selected = idx == state.selected;
5293            let marker = if is_selected { "●" } else { "○" };
5294            let style = if is_selected {
5295                if focused {
5296                    Style::new().bold().fg(self.theme.primary)
5297                } else {
5298                    Style::new().fg(self.theme.primary)
5299                }
5300            } else {
5301                Style::new().fg(self.theme.text)
5302            };
5303            let prefix = if focused && idx == state.selected {
5304                "▸ "
5305            } else {
5306                "  "
5307            };
5308            self.styled(format!("{prefix}{marker} {item}"), style);
5309        }
5310
5311        self.commands.push(Command::EndContainer);
5312        self.last_text_idx = None;
5313        state.selected != old_selected
5314    }
5315
5316    // ── multi-select ─────────────────────────────────────────────────
5317
5318    /// Render a multi-select list. Space toggles, Up/Down navigates.
5319    pub fn multi_select(&mut self, state: &mut MultiSelectState) -> &mut Self {
5320        if state.items.is_empty() {
5321            return self;
5322        }
5323        state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
5324        let focused = self.register_focusable();
5325
5326        if focused {
5327            let mut consumed_indices = Vec::new();
5328            for (i, event) in self.events.iter().enumerate() {
5329                if self.consumed[i] {
5330                    continue;
5331                }
5332                if let Event::Key(key) = event {
5333                    if key.kind != KeyEventKind::Press {
5334                        continue;
5335                    }
5336                    match key.code {
5337                        KeyCode::Up | KeyCode::Char('k') => {
5338                            state.cursor = state.cursor.saturating_sub(1);
5339                            consumed_indices.push(i);
5340                        }
5341                        KeyCode::Down | KeyCode::Char('j') => {
5342                            state.cursor =
5343                                (state.cursor + 1).min(state.items.len().saturating_sub(1));
5344                            consumed_indices.push(i);
5345                        }
5346                        KeyCode::Char(' ') | KeyCode::Enter => {
5347                            state.toggle(state.cursor);
5348                            consumed_indices.push(i);
5349                        }
5350                        _ => {}
5351                    }
5352                }
5353            }
5354            for idx in consumed_indices {
5355                self.consumed[idx] = true;
5356            }
5357        }
5358
5359        let interaction_id = self.interaction_count;
5360        self.interaction_count += 1;
5361
5362        if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5363            for (i, event) in self.events.iter().enumerate() {
5364                if self.consumed[i] {
5365                    continue;
5366                }
5367                if let Event::Mouse(mouse) = event {
5368                    if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5369                        continue;
5370                    }
5371                    let in_bounds = mouse.x >= rect.x
5372                        && mouse.x < rect.right()
5373                        && mouse.y >= rect.y
5374                        && mouse.y < rect.bottom();
5375                    if !in_bounds {
5376                        continue;
5377                    }
5378                    let clicked_idx = (mouse.y - rect.y) as usize;
5379                    if clicked_idx < state.items.len() {
5380                        state.toggle(clicked_idx);
5381                        state.cursor = clicked_idx;
5382                        self.consumed[i] = true;
5383                    }
5384                }
5385            }
5386        }
5387
5388        self.commands.push(Command::BeginContainer {
5389            direction: Direction::Column,
5390            gap: 0,
5391            align: Align::Start,
5392            justify: Justify::Start,
5393            border: None,
5394            border_sides: BorderSides::all(),
5395            border_style: Style::new().fg(self.theme.border),
5396            bg_color: None,
5397            padding: Padding::default(),
5398            margin: Margin::default(),
5399            constraints: Constraints::default(),
5400            title: None,
5401            grow: 0,
5402            group_name: None,
5403        });
5404
5405        for (idx, item) in state.items.iter().enumerate() {
5406            let checked = state.selected.contains(&idx);
5407            let marker = if checked { "[x]" } else { "[ ]" };
5408            let is_cursor = idx == state.cursor;
5409            let style = if is_cursor && focused {
5410                Style::new().bold().fg(self.theme.primary)
5411            } else if checked {
5412                Style::new().fg(self.theme.success)
5413            } else {
5414                Style::new().fg(self.theme.text)
5415            };
5416            let prefix = if is_cursor && focused { "▸ " } else { "  " };
5417            self.styled(format!("{prefix}{marker} {item}"), style);
5418        }
5419
5420        self.commands.push(Command::EndContainer);
5421        self.last_text_idx = None;
5422        self
5423    }
5424
5425    // ── tree ─────────────────────────────────────────────────────────
5426
5427    /// Render a tree view. Left/Right to collapse/expand, Up/Down to navigate.
5428    pub fn tree(&mut self, state: &mut TreeState) -> &mut Self {
5429        let entries = state.flatten();
5430        if entries.is_empty() {
5431            return self;
5432        }
5433        state.selected = state.selected.min(entries.len().saturating_sub(1));
5434        let focused = self.register_focusable();
5435
5436        if focused {
5437            let mut consumed_indices = Vec::new();
5438            for (i, event) in self.events.iter().enumerate() {
5439                if self.consumed[i] {
5440                    continue;
5441                }
5442                if let Event::Key(key) = event {
5443                    if key.kind != KeyEventKind::Press {
5444                        continue;
5445                    }
5446                    match key.code {
5447                        KeyCode::Up | KeyCode::Char('k') => {
5448                            state.selected = state.selected.saturating_sub(1);
5449                            consumed_indices.push(i);
5450                        }
5451                        KeyCode::Down | KeyCode::Char('j') => {
5452                            let max = state.flatten().len().saturating_sub(1);
5453                            state.selected = (state.selected + 1).min(max);
5454                            consumed_indices.push(i);
5455                        }
5456                        KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
5457                            state.toggle_at(state.selected);
5458                            consumed_indices.push(i);
5459                        }
5460                        KeyCode::Left => {
5461                            let entry = &entries[state.selected.min(entries.len() - 1)];
5462                            if entry.expanded {
5463                                state.toggle_at(state.selected);
5464                            }
5465                            consumed_indices.push(i);
5466                        }
5467                        _ => {}
5468                    }
5469                }
5470            }
5471            for idx in consumed_indices {
5472                self.consumed[idx] = true;
5473            }
5474        }
5475
5476        self.interaction_count += 1;
5477        self.commands.push(Command::BeginContainer {
5478            direction: Direction::Column,
5479            gap: 0,
5480            align: Align::Start,
5481            justify: Justify::Start,
5482            border: None,
5483            border_sides: BorderSides::all(),
5484            border_style: Style::new().fg(self.theme.border),
5485            bg_color: None,
5486            padding: Padding::default(),
5487            margin: Margin::default(),
5488            constraints: Constraints::default(),
5489            title: None,
5490            grow: 0,
5491            group_name: None,
5492        });
5493
5494        let entries = state.flatten();
5495        for (idx, entry) in entries.iter().enumerate() {
5496            let indent = "  ".repeat(entry.depth);
5497            let icon = if entry.is_leaf {
5498                "  "
5499            } else if entry.expanded {
5500                "▾ "
5501            } else {
5502                "▸ "
5503            };
5504            let is_selected = idx == state.selected;
5505            let style = if is_selected && focused {
5506                Style::new().bold().fg(self.theme.primary)
5507            } else if is_selected {
5508                Style::new().fg(self.theme.primary)
5509            } else {
5510                Style::new().fg(self.theme.text)
5511            };
5512            let cursor = if is_selected && focused { "▸" } else { " " };
5513            self.styled(format!("{cursor}{indent}{icon}{}", entry.label), style);
5514        }
5515
5516        self.commands.push(Command::EndContainer);
5517        self.last_text_idx = None;
5518        self
5519    }
5520
5521    // ── virtual list ─────────────────────────────────────────────────
5522
5523    /// Render a virtual list that only renders visible items.
5524    ///
5525    /// `total` is the number of items. `visible_height` limits how many rows
5526    /// are rendered. The closure `f` is called only for visible indices.
5527    pub fn virtual_list(
5528        &mut self,
5529        state: &mut ListState,
5530        visible_height: usize,
5531        f: impl Fn(&mut Context, usize),
5532    ) -> &mut Self {
5533        if state.items.is_empty() {
5534            return self;
5535        }
5536        state.selected = state.selected.min(state.items.len().saturating_sub(1));
5537        let focused = self.register_focusable();
5538
5539        if focused {
5540            let mut consumed_indices = Vec::new();
5541            for (i, event) in self.events.iter().enumerate() {
5542                if self.consumed[i] {
5543                    continue;
5544                }
5545                if let Event::Key(key) = event {
5546                    if key.kind != KeyEventKind::Press {
5547                        continue;
5548                    }
5549                    match key.code {
5550                        KeyCode::Up | KeyCode::Char('k') => {
5551                            state.selected = state.selected.saturating_sub(1);
5552                            consumed_indices.push(i);
5553                        }
5554                        KeyCode::Down | KeyCode::Char('j') => {
5555                            state.selected =
5556                                (state.selected + 1).min(state.items.len().saturating_sub(1));
5557                            consumed_indices.push(i);
5558                        }
5559                        KeyCode::PageUp => {
5560                            state.selected = state.selected.saturating_sub(visible_height);
5561                            consumed_indices.push(i);
5562                        }
5563                        KeyCode::PageDown => {
5564                            state.selected = (state.selected + visible_height)
5565                                .min(state.items.len().saturating_sub(1));
5566                            consumed_indices.push(i);
5567                        }
5568                        KeyCode::Home => {
5569                            state.selected = 0;
5570                            consumed_indices.push(i);
5571                        }
5572                        KeyCode::End => {
5573                            state.selected = state.items.len().saturating_sub(1);
5574                            consumed_indices.push(i);
5575                        }
5576                        _ => {}
5577                    }
5578                }
5579            }
5580            for idx in consumed_indices {
5581                self.consumed[idx] = true;
5582            }
5583        }
5584
5585        let start = if state.selected >= visible_height {
5586            state.selected - visible_height + 1
5587        } else {
5588            0
5589        };
5590        let end = (start + visible_height).min(state.items.len());
5591
5592        self.interaction_count += 1;
5593        self.commands.push(Command::BeginContainer {
5594            direction: Direction::Column,
5595            gap: 0,
5596            align: Align::Start,
5597            justify: Justify::Start,
5598            border: None,
5599            border_sides: BorderSides::all(),
5600            border_style: Style::new().fg(self.theme.border),
5601            bg_color: None,
5602            padding: Padding::default(),
5603            margin: Margin::default(),
5604            constraints: Constraints::default(),
5605            title: None,
5606            grow: 0,
5607            group_name: None,
5608        });
5609
5610        if start > 0 {
5611            self.styled(
5612                format!("  ↑ {} more", start),
5613                Style::new().fg(self.theme.text_dim).dim(),
5614            );
5615        }
5616
5617        for idx in start..end {
5618            f(self, idx);
5619        }
5620
5621        let remaining = state.items.len().saturating_sub(end);
5622        if remaining > 0 {
5623            self.styled(
5624                format!("  ↓ {} more", remaining),
5625                Style::new().fg(self.theme.text_dim).dim(),
5626            );
5627        }
5628
5629        self.commands.push(Command::EndContainer);
5630        self.last_text_idx = None;
5631        self
5632    }
5633
5634    // ── command palette ──────────────────────────────────────────────
5635
5636    /// Render a command palette overlay. Returns `Some(index)` when a command is selected.
5637    pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Option<usize> {
5638        if !state.open {
5639            return None;
5640        }
5641
5642        let filtered = state.filtered_indices();
5643        let sel = state.selected().min(filtered.len().saturating_sub(1));
5644        state.set_selected(sel);
5645
5646        let mut consumed_indices = Vec::new();
5647        let mut result: Option<usize> = None;
5648
5649        for (i, event) in self.events.iter().enumerate() {
5650            if self.consumed[i] {
5651                continue;
5652            }
5653            if let Event::Key(key) = event {
5654                if key.kind != KeyEventKind::Press {
5655                    continue;
5656                }
5657                match key.code {
5658                    KeyCode::Esc => {
5659                        state.open = false;
5660                        consumed_indices.push(i);
5661                    }
5662                    KeyCode::Up => {
5663                        let s = state.selected();
5664                        state.set_selected(s.saturating_sub(1));
5665                        consumed_indices.push(i);
5666                    }
5667                    KeyCode::Down => {
5668                        let s = state.selected();
5669                        state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
5670                        consumed_indices.push(i);
5671                    }
5672                    KeyCode::Enter => {
5673                        if let Some(&cmd_idx) = filtered.get(state.selected()) {
5674                            result = Some(cmd_idx);
5675                            state.open = false;
5676                        }
5677                        consumed_indices.push(i);
5678                    }
5679                    KeyCode::Backspace => {
5680                        if state.cursor > 0 {
5681                            let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
5682                            let end_idx = byte_index_for_char(&state.input, state.cursor);
5683                            state.input.replace_range(byte_idx..end_idx, "");
5684                            state.cursor -= 1;
5685                            state.set_selected(0);
5686                        }
5687                        consumed_indices.push(i);
5688                    }
5689                    KeyCode::Char(ch) => {
5690                        let byte_idx = byte_index_for_char(&state.input, state.cursor);
5691                        state.input.insert(byte_idx, ch);
5692                        state.cursor += 1;
5693                        state.set_selected(0);
5694                        consumed_indices.push(i);
5695                    }
5696                    _ => {}
5697                }
5698            }
5699        }
5700        for idx in consumed_indices {
5701            self.consumed[idx] = true;
5702        }
5703
5704        let filtered = state.filtered_indices();
5705
5706        self.modal(|ui| {
5707            let primary = ui.theme.primary;
5708            ui.container()
5709                .border(Border::Rounded)
5710                .border_style(Style::new().fg(primary))
5711                .pad(1)
5712                .max_w(60)
5713                .col(|ui| {
5714                    let border_color = ui.theme.primary;
5715                    ui.bordered(Border::Rounded)
5716                        .border_style(Style::new().fg(border_color))
5717                        .px(1)
5718                        .col(|ui| {
5719                            let display = if state.input.is_empty() {
5720                                "Type to search...".to_string()
5721                            } else {
5722                                state.input.clone()
5723                            };
5724                            let style = if state.input.is_empty() {
5725                                Style::new().dim().fg(ui.theme.text_dim)
5726                            } else {
5727                                Style::new().fg(ui.theme.text)
5728                            };
5729                            ui.styled(display, style);
5730                        });
5731
5732                    for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
5733                        let cmd = &state.commands[cmd_idx];
5734                        let is_selected = list_idx == state.selected();
5735                        let style = if is_selected {
5736                            Style::new().bold().fg(ui.theme.primary)
5737                        } else {
5738                            Style::new().fg(ui.theme.text)
5739                        };
5740                        let prefix = if is_selected { "▸ " } else { "  " };
5741                        let shortcut_text = cmd
5742                            .shortcut
5743                            .as_deref()
5744                            .map(|s| format!("  ({s})"))
5745                            .unwrap_or_default();
5746                        ui.styled(format!("{prefix}{}{shortcut_text}", cmd.label), style);
5747                        if is_selected && !cmd.description.is_empty() {
5748                            ui.styled(
5749                                format!("    {}", cmd.description),
5750                                Style::new().dim().fg(ui.theme.text_dim),
5751                            );
5752                        }
5753                    }
5754
5755                    if filtered.is_empty() {
5756                        ui.styled(
5757                            "  No matching commands",
5758                            Style::new().dim().fg(ui.theme.text_dim),
5759                        );
5760                    }
5761                });
5762        });
5763
5764        result
5765    }
5766
5767    // ── markdown ─────────────────────────────────────────────────────
5768
5769    /// Render a markdown string with basic formatting.
5770    ///
5771    /// Supports headers (`#`), bold (`**`), italic (`*`), inline code (`` ` ``),
5772    /// unordered lists (`-`/`*`), ordered lists (`1.`), and horizontal rules (`---`).
5773    pub fn markdown(&mut self, text: &str) -> &mut Self {
5774        self.commands.push(Command::BeginContainer {
5775            direction: Direction::Column,
5776            gap: 0,
5777            align: Align::Start,
5778            justify: Justify::Start,
5779            border: None,
5780            border_sides: BorderSides::all(),
5781            border_style: Style::new().fg(self.theme.border),
5782            bg_color: None,
5783            padding: Padding::default(),
5784            margin: Margin::default(),
5785            constraints: Constraints::default(),
5786            title: None,
5787            grow: 0,
5788            group_name: None,
5789        });
5790        self.interaction_count += 1;
5791
5792        let text_style = Style::new().fg(self.theme.text);
5793        let bold_style = Style::new().fg(self.theme.text).bold();
5794        let code_style = Style::new().fg(self.theme.accent);
5795
5796        for line in text.lines() {
5797            let trimmed = line.trim();
5798            if trimmed.is_empty() {
5799                self.text(" ");
5800                continue;
5801            }
5802            if trimmed == "---" || trimmed == "***" || trimmed == "___" {
5803                self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
5804                continue;
5805            }
5806            if let Some(heading) = trimmed.strip_prefix("### ") {
5807                self.styled(heading, Style::new().bold().fg(self.theme.accent));
5808            } else if let Some(heading) = trimmed.strip_prefix("## ") {
5809                self.styled(heading, Style::new().bold().fg(self.theme.secondary));
5810            } else if let Some(heading) = trimmed.strip_prefix("# ") {
5811                self.styled(heading, Style::new().bold().fg(self.theme.primary));
5812            } else if let Some(item) = trimmed
5813                .strip_prefix("- ")
5814                .or_else(|| trimmed.strip_prefix("* "))
5815            {
5816                let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
5817                if segs.len() <= 1 {
5818                    self.styled(format!("  • {item}"), text_style);
5819                } else {
5820                    self.line(|ui| {
5821                        ui.styled("  • ", text_style);
5822                        for (s, st) in segs {
5823                            ui.styled(s, st);
5824                        }
5825                    });
5826                }
5827            } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
5828                let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
5829                if parts.len() == 2 {
5830                    let segs =
5831                        Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
5832                    if segs.len() <= 1 {
5833                        self.styled(format!("  {}. {}", parts[0], parts[1]), text_style);
5834                    } else {
5835                        self.line(|ui| {
5836                            ui.styled(format!("  {}. ", parts[0]), text_style);
5837                            for (s, st) in segs {
5838                                ui.styled(s, st);
5839                            }
5840                        });
5841                    }
5842                } else {
5843                    self.text(trimmed);
5844                }
5845            } else if let Some(code) = trimmed.strip_prefix("```") {
5846                let _ = code;
5847                self.styled("  ┌─code─", Style::new().fg(self.theme.border).dim());
5848            } else {
5849                let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
5850                if segs.len() <= 1 {
5851                    self.styled(trimmed, text_style);
5852                } else {
5853                    self.line(|ui| {
5854                        for (s, st) in segs {
5855                            ui.styled(s, st);
5856                        }
5857                    });
5858                }
5859            }
5860        }
5861
5862        self.commands.push(Command::EndContainer);
5863        self.last_text_idx = None;
5864        self
5865    }
5866
5867    fn parse_inline_segments(
5868        text: &str,
5869        base: Style,
5870        bold: Style,
5871        code: Style,
5872    ) -> Vec<(String, Style)> {
5873        let mut segments: Vec<(String, Style)> = Vec::new();
5874        let mut current = String::new();
5875        let chars: Vec<char> = text.chars().collect();
5876        let mut i = 0;
5877        while i < chars.len() {
5878            if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
5879                if let Some(end) = text[i + 2..].find("**") {
5880                    if !current.is_empty() {
5881                        segments.push((std::mem::take(&mut current), base));
5882                    }
5883                    segments.push((text[i + 2..i + 2 + end].to_string(), bold));
5884                    i += 4 + end;
5885                    continue;
5886                }
5887            }
5888            if chars[i] == '*'
5889                && (i + 1 >= chars.len() || chars[i + 1] != '*')
5890                && (i == 0 || chars[i - 1] != '*')
5891            {
5892                if let Some(end) = text[i + 1..].find('*') {
5893                    if !current.is_empty() {
5894                        segments.push((std::mem::take(&mut current), base));
5895                    }
5896                    segments.push((text[i + 1..i + 1 + end].to_string(), base.italic()));
5897                    i += 2 + end;
5898                    continue;
5899                }
5900            }
5901            if chars[i] == '`' {
5902                if let Some(end) = text[i + 1..].find('`') {
5903                    if !current.is_empty() {
5904                        segments.push((std::mem::take(&mut current), base));
5905                    }
5906                    segments.push((text[i + 1..i + 1 + end].to_string(), code));
5907                    i += 2 + end;
5908                    continue;
5909                }
5910            }
5911            current.push(chars[i]);
5912            i += 1;
5913        }
5914        if !current.is_empty() {
5915            segments.push((current, base));
5916        }
5917        segments
5918    }
5919
5920    // ── key sequence ─────────────────────────────────────────────────
5921
5922    /// Check if a sequence of character keys was pressed across recent frames.
5923    ///
5924    /// Matches when each character in `seq` appears in consecutive unconsumed
5925    /// key events within this frame. For single-frame sequences only (e.g., "gg").
5926    pub fn key_seq(&self, seq: &str) -> bool {
5927        if seq.is_empty() {
5928            return false;
5929        }
5930        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5931            return false;
5932        }
5933        let target: Vec<char> = seq.chars().collect();
5934        let mut matched = 0;
5935        for (i, event) in self.events.iter().enumerate() {
5936            if self.consumed[i] {
5937                continue;
5938            }
5939            if let Event::Key(key) = event {
5940                if key.kind != KeyEventKind::Press {
5941                    continue;
5942                }
5943                if let KeyCode::Char(c) = key.code {
5944                    if c == target[matched] {
5945                        matched += 1;
5946                        if matched == target.len() {
5947                            return true;
5948                        }
5949                    } else {
5950                        matched = 0;
5951                        if c == target[0] {
5952                            matched = 1;
5953                        }
5954                    }
5955                }
5956            }
5957        }
5958        false
5959    }
5960
5961    /// Render a horizontal divider line.
5962    ///
5963    /// The line is drawn with the theme's border color and expands to fill the
5964    /// container width.
5965    pub fn separator(&mut self) -> &mut Self {
5966        self.commands.push(Command::Text {
5967            content: "─".repeat(200),
5968            style: Style::new().fg(self.theme.border).dim(),
5969            grow: 0,
5970            align: Align::Start,
5971            wrap: false,
5972            margin: Margin::default(),
5973            constraints: Constraints::default(),
5974        });
5975        self.last_text_idx = Some(self.commands.len() - 1);
5976        self
5977    }
5978
5979    /// Render a help bar showing keybinding hints.
5980    ///
5981    /// `bindings` is a slice of `(key, action)` pairs. Keys are rendered in the
5982    /// theme's primary color; actions in the dim text color. Pairs are separated
5983    /// by a `·` character.
5984    pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
5985        if bindings.is_empty() {
5986            return self;
5987        }
5988
5989        self.interaction_count += 1;
5990        self.commands.push(Command::BeginContainer {
5991            direction: Direction::Row,
5992            gap: 2,
5993            align: Align::Start,
5994            justify: Justify::Start,
5995            border: None,
5996            border_sides: BorderSides::all(),
5997            border_style: Style::new().fg(self.theme.border),
5998            bg_color: None,
5999            padding: Padding::default(),
6000            margin: Margin::default(),
6001            constraints: Constraints::default(),
6002            title: None,
6003            grow: 0,
6004            group_name: None,
6005        });
6006        for (idx, (key, action)) in bindings.iter().enumerate() {
6007            if idx > 0 {
6008                self.styled("·", Style::new().fg(self.theme.text_dim));
6009            }
6010            self.styled(*key, Style::new().bold().fg(self.theme.primary));
6011            self.styled(*action, Style::new().fg(self.theme.text_dim));
6012        }
6013        self.commands.push(Command::EndContainer);
6014        self.last_text_idx = None;
6015
6016        self
6017    }
6018
6019    // ── events ───────────────────────────────────────────────────────
6020
6021    /// Check if a character key was pressed this frame.
6022    ///
6023    /// Returns `true` if the key event has not been consumed by another widget.
6024    pub fn key(&self, c: char) -> bool {
6025        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6026            return false;
6027        }
6028        self.events.iter().enumerate().any(|(i, e)| {
6029            !self.consumed[i]
6030                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
6031        })
6032    }
6033
6034    /// Check if a specific key code was pressed this frame.
6035    ///
6036    /// Returns `true` if the key event has not been consumed by another widget.
6037    pub fn key_code(&self, code: KeyCode) -> bool {
6038        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6039            return false;
6040        }
6041        self.events.iter().enumerate().any(|(i, e)| {
6042            !self.consumed[i]
6043                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
6044        })
6045    }
6046
6047    /// Check if a character key was released this frame.
6048    ///
6049    /// Returns `true` if the key release event has not been consumed by another widget.
6050    pub fn key_release(&self, c: char) -> bool {
6051        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6052            return false;
6053        }
6054        self.events.iter().enumerate().any(|(i, e)| {
6055            !self.consumed[i]
6056                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
6057        })
6058    }
6059
6060    /// Check if a specific key code was released this frame.
6061    ///
6062    /// Returns `true` if the key release event has not been consumed by another widget.
6063    pub fn key_code_release(&self, code: KeyCode) -> bool {
6064        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6065            return false;
6066        }
6067        self.events.iter().enumerate().any(|(i, e)| {
6068            !self.consumed[i]
6069                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
6070        })
6071    }
6072
6073    /// Check if a character key with specific modifiers was pressed this frame.
6074    ///
6075    /// Returns `true` if the key event has not been consumed by another widget.
6076    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
6077        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6078            return false;
6079        }
6080        self.events.iter().enumerate().any(|(i, e)| {
6081            !self.consumed[i]
6082                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
6083        })
6084    }
6085
6086    /// Return the position of a left mouse button down event this frame, if any.
6087    ///
6088    /// Returns `None` if no unconsumed mouse-down event occurred.
6089    pub fn mouse_down(&self) -> Option<(u32, u32)> {
6090        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6091            return None;
6092        }
6093        self.events.iter().enumerate().find_map(|(i, event)| {
6094            if self.consumed[i] {
6095                return None;
6096            }
6097            if let Event::Mouse(mouse) = event {
6098                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
6099                    return Some((mouse.x, mouse.y));
6100                }
6101            }
6102            None
6103        })
6104    }
6105
6106    /// Return the current mouse cursor position, if known.
6107    ///
6108    /// The position is updated on every mouse move or click event. Returns
6109    /// `None` until the first mouse event is received.
6110    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
6111        self.mouse_pos
6112    }
6113
6114    /// Return the first unconsumed paste event text, if any.
6115    pub fn paste(&self) -> Option<&str> {
6116        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6117            return None;
6118        }
6119        self.events.iter().enumerate().find_map(|(i, event)| {
6120            if self.consumed[i] {
6121                return None;
6122            }
6123            if let Event::Paste(ref text) = event {
6124                return Some(text.as_str());
6125            }
6126            None
6127        })
6128    }
6129
6130    /// Check if an unconsumed scroll-up event occurred this frame.
6131    pub fn scroll_up(&self) -> bool {
6132        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6133            return false;
6134        }
6135        self.events.iter().enumerate().any(|(i, event)| {
6136            !self.consumed[i]
6137                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
6138        })
6139    }
6140
6141    /// Check if an unconsumed scroll-down event occurred this frame.
6142    pub fn scroll_down(&self) -> bool {
6143        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6144            return false;
6145        }
6146        self.events.iter().enumerate().any(|(i, event)| {
6147            !self.consumed[i]
6148                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
6149        })
6150    }
6151
6152    /// Signal the run loop to exit after this frame.
6153    pub fn quit(&mut self) {
6154        self.should_quit = true;
6155    }
6156
6157    /// Copy text to the system clipboard via OSC 52.
6158    ///
6159    /// Works transparently over SSH connections. The text is queued and
6160    /// written to the terminal after the current frame renders.
6161    ///
6162    /// Requires a terminal that supports OSC 52 (most modern terminals:
6163    /// Ghostty, kitty, WezTerm, iTerm2, Windows Terminal).
6164    pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
6165        self.clipboard_text = Some(text.into());
6166    }
6167
6168    /// Get the current theme.
6169    pub fn theme(&self) -> &Theme {
6170        &self.theme
6171    }
6172
6173    /// Change the theme for subsequent rendering.
6174    ///
6175    /// All widgets rendered after this call will use the new theme's colors.
6176    pub fn set_theme(&mut self, theme: Theme) {
6177        self.theme = theme;
6178    }
6179
6180    /// Check if dark mode is active.
6181    pub fn is_dark_mode(&self) -> bool {
6182        self.dark_mode
6183    }
6184
6185    /// Set dark mode. When true, dark_* style variants are applied.
6186    pub fn set_dark_mode(&mut self, dark: bool) {
6187        self.dark_mode = dark;
6188    }
6189
6190    // ── info ─────────────────────────────────────────────────────────
6191
6192    /// Get the terminal width in cells.
6193    pub fn width(&self) -> u32 {
6194        self.area_width
6195    }
6196
6197    /// Get the current terminal width breakpoint.
6198    ///
6199    /// Returns a [`Breakpoint`] based on the terminal width:
6200    /// - `Xs`: < 40 columns
6201    /// - `Sm`: 40-79 columns
6202    /// - `Md`: 80-119 columns
6203    /// - `Lg`: 120-159 columns
6204    /// - `Xl`: >= 160 columns
6205    ///
6206    /// Use this for responsive layouts that adapt to terminal size:
6207    /// ```no_run
6208    /// # use slt::{Breakpoint, Context};
6209    /// # slt::run(|ui: &mut Context| {
6210    /// match ui.breakpoint() {
6211    ///     Breakpoint::Xs | Breakpoint::Sm => {
6212    ///         ui.col(|ui| { ui.text("Stacked layout"); });
6213    ///     }
6214    ///     _ => {
6215    ///         ui.row(|ui| { ui.text("Side-by-side layout"); });
6216    ///     }
6217    /// }
6218    /// # });
6219    /// ```
6220    pub fn breakpoint(&self) -> Breakpoint {
6221        let w = self.area_width;
6222        if w < 40 {
6223            Breakpoint::Xs
6224        } else if w < 80 {
6225            Breakpoint::Sm
6226        } else if w < 120 {
6227            Breakpoint::Md
6228        } else if w < 160 {
6229            Breakpoint::Lg
6230        } else {
6231            Breakpoint::Xl
6232        }
6233    }
6234
6235    /// Get the terminal height in cells.
6236    pub fn height(&self) -> u32 {
6237        self.area_height
6238    }
6239
6240    /// Get the current tick count (increments each frame).
6241    ///
6242    /// Useful for animations and time-based logic. The tick starts at 0 and
6243    /// increases by 1 on every rendered frame.
6244    pub fn tick(&self) -> u64 {
6245        self.tick
6246    }
6247
6248    /// Return whether the layout debugger is enabled.
6249    ///
6250    /// The debugger is toggled with F12 at runtime.
6251    pub fn debug_enabled(&self) -> bool {
6252        self.debug
6253    }
6254}
6255
6256#[inline]
6257fn byte_index_for_char(value: &str, char_index: usize) -> usize {
6258    if char_index == 0 {
6259        return 0;
6260    }
6261    value
6262        .char_indices()
6263        .nth(char_index)
6264        .map_or(value.len(), |(idx, _)| idx)
6265}
6266
6267fn format_token_count(count: usize) -> String {
6268    if count >= 1_000_000 {
6269        format!("{:.1}M", count as f64 / 1_000_000.0)
6270    } else if count >= 1_000 {
6271        format!("{:.1}k", count as f64 / 1_000.0)
6272    } else {
6273        format!("{count}")
6274    }
6275}
6276
6277fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
6278    let mut parts: Vec<String> = Vec::new();
6279    for (i, width) in widths.iter().enumerate() {
6280        let cell = cells.get(i).map(String::as_str).unwrap_or("");
6281        let cell_width = UnicodeWidthStr::width(cell) as u32;
6282        let padding = (*width).saturating_sub(cell_width) as usize;
6283        parts.push(format!("{cell}{}", " ".repeat(padding)));
6284    }
6285    parts.join(separator)
6286}
6287
6288fn format_compact_number(value: f64) -> String {
6289    if value.fract().abs() < f64::EPSILON {
6290        return format!("{value:.0}");
6291    }
6292
6293    let mut s = format!("{value:.2}");
6294    while s.contains('.') && s.ends_with('0') {
6295        s.pop();
6296    }
6297    if s.ends_with('.') {
6298        s.pop();
6299    }
6300    s
6301}
6302
6303fn center_text(text: &str, width: usize) -> String {
6304    let text_width = UnicodeWidthStr::width(text);
6305    if text_width >= width {
6306        return text.to_string();
6307    }
6308
6309    let total = width - text_width;
6310    let left = total / 2;
6311    let right = total - left;
6312    format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
6313}
6314
6315struct TextareaVLine {
6316    logical_row: usize,
6317    char_start: usize,
6318    char_count: usize,
6319}
6320
6321fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
6322    let mut out = Vec::new();
6323    for (row, line) in lines.iter().enumerate() {
6324        if line.is_empty() || wrap_width == u32::MAX {
6325            out.push(TextareaVLine {
6326                logical_row: row,
6327                char_start: 0,
6328                char_count: line.chars().count(),
6329            });
6330            continue;
6331        }
6332        let mut seg_start = 0usize;
6333        let mut seg_chars = 0usize;
6334        let mut seg_width = 0u32;
6335        for (idx, ch) in line.chars().enumerate() {
6336            let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
6337            if seg_width + cw > wrap_width && seg_chars > 0 {
6338                out.push(TextareaVLine {
6339                    logical_row: row,
6340                    char_start: seg_start,
6341                    char_count: seg_chars,
6342                });
6343                seg_start = idx;
6344                seg_chars = 0;
6345                seg_width = 0;
6346            }
6347            seg_chars += 1;
6348            seg_width += cw;
6349        }
6350        out.push(TextareaVLine {
6351            logical_row: row,
6352            char_start: seg_start,
6353            char_count: seg_chars,
6354        });
6355    }
6356    out
6357}
6358
6359fn textarea_logical_to_visual(
6360    vlines: &[TextareaVLine],
6361    logical_row: usize,
6362    logical_col: usize,
6363) -> (usize, usize) {
6364    for (i, vl) in vlines.iter().enumerate() {
6365        if vl.logical_row != logical_row {
6366            continue;
6367        }
6368        let seg_end = vl.char_start + vl.char_count;
6369        if logical_col >= vl.char_start && logical_col < seg_end {
6370            return (i, logical_col - vl.char_start);
6371        }
6372        if logical_col == seg_end {
6373            let is_last_seg = vlines
6374                .get(i + 1)
6375                .map_or(true, |next| next.logical_row != logical_row);
6376            if is_last_seg {
6377                return (i, logical_col - vl.char_start);
6378            }
6379        }
6380    }
6381    (vlines.len().saturating_sub(1), 0)
6382}
6383
6384fn textarea_visual_to_logical(
6385    vlines: &[TextareaVLine],
6386    visual_row: usize,
6387    visual_col: usize,
6388) -> (usize, usize) {
6389    if let Some(vl) = vlines.get(visual_row) {
6390        let logical_col = vl.char_start + visual_col.min(vl.char_count);
6391        (vl.logical_row, logical_col)
6392    } else {
6393        (0, 0)
6394    }
6395}
6396
6397fn open_url(url: &str) -> std::io::Result<()> {
6398    #[cfg(target_os = "macos")]
6399    {
6400        std::process::Command::new("open").arg(url).spawn()?;
6401    }
6402    #[cfg(target_os = "linux")]
6403    {
6404        std::process::Command::new("xdg-open").arg(url).spawn()?;
6405    }
6406    #[cfg(target_os = "windows")]
6407    {
6408        std::process::Command::new("cmd")
6409            .args(["/c", "start", "", url])
6410            .spawn()?;
6411    }
6412    Ok(())
6413}