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