Skip to main content

slt/
context.rs

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