Skip to main content

slt/
context.rs

1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseKind};
3use crate::halfblock::HalfBlockImage;
4use crate::layout::{Command, Direction};
5use crate::rect::Rect;
6use crate::style::{
7    Align, Border, BorderSides, Breakpoint, Color, Constraints, ContainerStyle, Justify, Margin,
8    Modifiers, Padding, Style, Theme,
9};
10use crate::widgets::{
11    ApprovalAction, ButtonVariant, CommandPaletteState, ContextItem, FormField, FormState,
12    ListState, MultiSelectState, RadioState, ScrollState, SelectState, SpinnerState,
13    StreamingTextState, TableState, TabsState, TextInputState, TextareaState, ToastLevel,
14    ToastState, ToolApprovalState, TreeState,
15};
16use crate::FrameState;
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)`.
37#[derive(Debug, Copy, Clone, PartialEq, Eq)]
38pub struct State<T> {
39    idx: usize,
40    _marker: std::marker::PhantomData<T>,
41}
42
43impl<T: 'static> State<T> {
44    /// Read the current value.
45    pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
46        ui.hook_states[self.idx]
47            .downcast_ref::<T>()
48            .unwrap_or_else(|| {
49                panic!(
50                    "use_state type mismatch at hook index {} — expected {}",
51                    self.idx,
52                    std::any::type_name::<T>()
53                )
54            })
55    }
56
57    /// Mutably access the current value.
58    pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
59        ui.hook_states[self.idx]
60            .downcast_mut::<T>()
61            .unwrap_or_else(|| {
62                panic!(
63                    "use_state type mismatch at hook index {} — expected {}",
64                    self.idx,
65                    std::any::type_name::<T>()
66                )
67            })
68    }
69}
70
71/// Result of a container mouse interaction.
72///
73/// Returned by [`Context::col`], [`Context::row`], and [`ContainerBuilder::col`] /
74/// [`ContainerBuilder::row`] so you can react to clicks and hover without a separate
75/// event loop.
76#[derive(Debug, Clone, Copy, Default)]
77pub struct Response {
78    /// Whether the container was clicked this frame.
79    pub clicked: bool,
80    /// Whether the mouse is over the container.
81    pub hovered: bool,
82}
83
84/// Direction for bar chart rendering.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum BarDirection {
87    /// Bars grow horizontally (default, current behavior).
88    Horizontal,
89    /// Bars grow vertically from bottom to top.
90    Vertical,
91}
92
93/// A single bar in a styled bar chart.
94#[derive(Debug, Clone)]
95pub struct Bar {
96    /// Display label for this bar.
97    pub label: String,
98    /// Numeric value.
99    pub value: f64,
100    /// Bar color. If None, uses theme.primary.
101    pub color: Option<Color>,
102}
103
104impl Bar {
105    /// Create a new bar with a label and value.
106    pub fn new(label: impl Into<String>, value: f64) -> Self {
107        Self {
108            label: label.into(),
109            value,
110            color: None,
111        }
112    }
113
114    /// Set the bar color.
115    pub fn color(mut self, color: Color) -> Self {
116        self.color = Some(color);
117        self
118    }
119}
120
121/// A group of bars rendered together (for grouped bar charts).
122#[derive(Debug, Clone)]
123pub struct BarGroup {
124    /// Group label displayed below the bars.
125    pub label: String,
126    /// Bars in this group.
127    pub bars: Vec<Bar>,
128}
129
130impl BarGroup {
131    /// Create a new bar group with a label and bars.
132    pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
133        Self {
134            label: label.into(),
135            bars,
136        }
137    }
138}
139
140/// Trait for creating custom widgets.
141///
142/// Implement this trait to build reusable, composable widgets with full access
143/// to the [`Context`] API — focus, events, theming, layout, and mouse interaction.
144///
145/// # Examples
146///
147/// A simple rating widget:
148///
149/// ```no_run
150/// use slt::{Context, Widget, Color};
151///
152/// struct Rating {
153///     value: u8,
154///     max: u8,
155/// }
156///
157/// impl Rating {
158///     fn new(value: u8, max: u8) -> Self {
159///         Self { value, max }
160///     }
161/// }
162///
163/// impl Widget for Rating {
164///     type Response = bool;
165///
166///     fn ui(&mut self, ui: &mut Context) -> bool {
167///         let focused = ui.register_focusable();
168///         let mut changed = false;
169///
170///         if focused {
171///             if ui.key('+') && self.value < self.max {
172///                 self.value += 1;
173///                 changed = true;
174///             }
175///             if ui.key('-') && self.value > 0 {
176///                 self.value -= 1;
177///                 changed = true;
178///             }
179///         }
180///
181///         let stars: String = (0..self.max).map(|i| {
182///             if i < self.value { '★' } else { '☆' }
183///         }).collect();
184///
185///         let color = if focused { Color::Yellow } else { Color::White };
186///         ui.styled(stars, slt::Style::new().fg(color));
187///
188///         changed
189///     }
190/// }
191///
192/// fn main() -> std::io::Result<()> {
193///     let mut rating = Rating::new(3, 5);
194///     slt::run(|ui| {
195///         if ui.key('q') { ui.quit(); }
196///         ui.text("Rate this:");
197///         ui.widget(&mut rating);
198///     })
199/// }
200/// ```
201pub trait Widget {
202    /// The value returned after rendering. Use `()` for widgets with no return,
203    /// `bool` for widgets that report changes, or [`Response`] for click/hover.
204    type Response;
205
206    /// Render the widget into the given context.
207    ///
208    /// Use [`Context::register_focusable`] to participate in Tab focus cycling,
209    /// [`Context::key`] / [`Context::key_code`] to handle keyboard input,
210    /// and [`Context::interaction`] to detect clicks and hovers.
211    fn ui(&mut self, ctx: &mut Context) -> Self::Response;
212}
213
214/// The main rendering context passed to your closure each frame.
215///
216/// Provides all methods for building UI: text, containers, widgets, and event
217/// handling. You receive a `&mut Context` on every frame and describe what to
218/// render by calling its methods. SLT collects those calls, lays them out with
219/// flexbox, diffs against the previous frame, and flushes only changed cells.
220///
221/// # Example
222///
223/// ```no_run
224/// slt::run(|ui: &mut slt::Context| {
225///     if ui.key('q') { ui.quit(); }
226///     ui.text("Hello, world!").bold();
227/// });
228/// ```
229pub struct Context {
230    // NOTE: If you add a mutable per-frame field, also add it to ContextSnapshot in error_boundary_with
231    pub(crate) commands: Vec<Command>,
232    pub(crate) events: Vec<Event>,
233    pub(crate) consumed: Vec<bool>,
234    pub(crate) should_quit: bool,
235    pub(crate) area_width: u32,
236    pub(crate) area_height: u32,
237    pub(crate) tick: u64,
238    pub(crate) focus_index: usize,
239    pub(crate) focus_count: usize,
240    pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
241    pub(crate) hook_cursor: usize,
242    prev_focus_count: usize,
243    scroll_count: usize,
244    prev_scroll_infos: Vec<(u32, u32)>,
245    prev_scroll_rects: Vec<Rect>,
246    interaction_count: usize,
247    pub(crate) prev_hit_map: Vec<Rect>,
248    pub(crate) group_stack: Vec<String>,
249    pub(crate) prev_group_rects: Vec<(String, Rect)>,
250    group_count: usize,
251    prev_focus_groups: Vec<Option<String>>,
252    _prev_focus_rects: Vec<(usize, Rect)>,
253    mouse_pos: Option<(u32, u32)>,
254    click_pos: Option<(u32, u32)>,
255    last_text_idx: Option<usize>,
256    overlay_depth: usize,
257    pub(crate) modal_active: bool,
258    prev_modal_active: bool,
259    pub(crate) clipboard_text: Option<String>,
260    debug: bool,
261    theme: Theme,
262    pub(crate) dark_mode: bool,
263    pub(crate) is_real_terminal: bool,
264    pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
265}
266
267type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
268
269struct ContextSnapshot {
270    cmd_count: usize,
271    last_text_idx: Option<usize>,
272    focus_count: usize,
273    interaction_count: usize,
274    scroll_count: usize,
275    group_count: usize,
276    group_stack_len: usize,
277    overlay_depth: usize,
278    modal_active: bool,
279    hook_cursor: usize,
280    hook_states_len: usize,
281    dark_mode: bool,
282    deferred_draws_len: usize,
283}
284
285impl ContextSnapshot {
286    fn capture(ctx: &Context) -> Self {
287        Self {
288            cmd_count: ctx.commands.len(),
289            last_text_idx: ctx.last_text_idx,
290            focus_count: ctx.focus_count,
291            interaction_count: ctx.interaction_count,
292            scroll_count: ctx.scroll_count,
293            group_count: ctx.group_count,
294            group_stack_len: ctx.group_stack.len(),
295            overlay_depth: ctx.overlay_depth,
296            modal_active: ctx.modal_active,
297            hook_cursor: ctx.hook_cursor,
298            hook_states_len: ctx.hook_states.len(),
299            dark_mode: ctx.dark_mode,
300            deferred_draws_len: ctx.deferred_draws.len(),
301        }
302    }
303
304    fn restore(&self, ctx: &mut Context) {
305        ctx.commands.truncate(self.cmd_count);
306        ctx.last_text_idx = self.last_text_idx;
307        ctx.focus_count = self.focus_count;
308        ctx.interaction_count = self.interaction_count;
309        ctx.scroll_count = self.scroll_count;
310        ctx.group_count = self.group_count;
311        ctx.group_stack.truncate(self.group_stack_len);
312        ctx.overlay_depth = self.overlay_depth;
313        ctx.modal_active = self.modal_active;
314        ctx.hook_cursor = self.hook_cursor;
315        ctx.hook_states.truncate(self.hook_states_len);
316        ctx.dark_mode = self.dark_mode;
317        ctx.deferred_draws.truncate(self.deferred_draws_len);
318    }
319}
320
321/// Fluent builder for configuring containers before calling `.col()` or `.row()`.
322///
323/// Obtain one via [`Context::container`] or [`Context::bordered`]. Chain the
324/// configuration methods you need, then finalize with `.col(|ui| { ... })` or
325/// `.row(|ui| { ... })`.
326///
327/// # Example
328///
329/// ```no_run
330/// # slt::run(|ui: &mut slt::Context| {
331/// use slt::{Border, Color};
332/// ui.container()
333///     .border(Border::Rounded)
334///     .pad(1)
335///     .grow(1)
336///     .col(|ui| {
337///         ui.text("inside a bordered, padded, growing column");
338///     });
339/// # });
340/// ```
341#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
342pub struct ContainerBuilder<'a> {
343    ctx: &'a mut Context,
344    gap: u32,
345    align: Align,
346    justify: Justify,
347    border: Option<Border>,
348    border_sides: BorderSides,
349    border_style: Style,
350    bg: Option<Color>,
351    dark_bg: Option<Color>,
352    dark_border_style: Option<Style>,
353    group_hover_bg: Option<Color>,
354    group_hover_border_style: Option<Style>,
355    group_name: Option<String>,
356    padding: Padding,
357    margin: Margin,
358    constraints: Constraints,
359    title: Option<(String, Style)>,
360    grow: u16,
361    scroll_offset: Option<u32>,
362}
363
364/// Drawing context for the [`Context::canvas`] widget.
365///
366/// Provides pixel-level drawing on a braille character grid. Each terminal
367/// cell maps to a 2x4 dot matrix, so a canvas of `width` columns x `height`
368/// rows gives `width*2` x `height*4` pixel resolution.
369/// A colored pixel in the canvas grid.
370#[derive(Debug, Clone, Copy)]
371struct CanvasPixel {
372    bits: u32,
373    color: Color,
374}
375
376/// Text label placed on the canvas.
377#[derive(Debug, Clone)]
378struct CanvasLabel {
379    x: usize,
380    y: usize,
381    text: String,
382    color: Color,
383}
384
385/// A layer in the canvas, supporting z-ordering.
386#[derive(Debug, Clone)]
387struct CanvasLayer {
388    grid: Vec<Vec<CanvasPixel>>,
389    labels: Vec<CanvasLabel>,
390}
391
392pub struct CanvasContext {
393    layers: Vec<CanvasLayer>,
394    cols: usize,
395    rows: usize,
396    px_w: usize,
397    px_h: usize,
398    current_color: Color,
399}
400
401impl CanvasContext {
402    fn new(cols: usize, rows: usize) -> Self {
403        Self {
404            layers: vec![Self::new_layer(cols, rows)],
405            cols,
406            rows,
407            px_w: cols * 2,
408            px_h: rows * 4,
409            current_color: Color::Reset,
410        }
411    }
412
413    fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
414        CanvasLayer {
415            grid: vec![
416                vec![
417                    CanvasPixel {
418                        bits: 0,
419                        color: Color::Reset,
420                    };
421                    cols
422                ];
423                rows
424            ],
425            labels: Vec::new(),
426        }
427    }
428
429    fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
430        self.layers.last_mut()
431    }
432
433    fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
434        if x >= self.px_w || y >= self.px_h {
435            return;
436        }
437
438        let char_col = x / 2;
439        let char_row = y / 4;
440        let sub_col = x % 2;
441        let sub_row = y % 4;
442        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
443        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
444
445        let bit = if sub_col == 0 {
446            LEFT_BITS[sub_row]
447        } else {
448            RIGHT_BITS[sub_row]
449        };
450
451        if let Some(layer) = self.current_layer_mut() {
452            let cell = &mut layer.grid[char_row][char_col];
453            let new_bits = cell.bits | bit;
454            if new_bits != cell.bits {
455                cell.bits = new_bits;
456                cell.color = color;
457            }
458        }
459    }
460
461    fn dot_isize(&mut self, x: isize, y: isize) {
462        if x >= 0 && y >= 0 {
463            self.dot(x as usize, y as usize);
464        }
465    }
466
467    /// Get the pixel width of the canvas.
468    pub fn width(&self) -> usize {
469        self.px_w
470    }
471
472    /// Get the pixel height of the canvas.
473    pub fn height(&self) -> usize {
474        self.px_h
475    }
476
477    /// Set a single pixel at `(x, y)`.
478    pub fn dot(&mut self, x: usize, y: usize) {
479        self.dot_with_color(x, y, self.current_color);
480    }
481
482    /// Draw a line from `(x0, y0)` to `(x1, y1)` using Bresenham's algorithm.
483    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
484        let (mut x, mut y) = (x0 as isize, y0 as isize);
485        let (x1, y1) = (x1 as isize, y1 as isize);
486        let dx = (x1 - x).abs();
487        let dy = -(y1 - y).abs();
488        let sx = if x < x1 { 1 } else { -1 };
489        let sy = if y < y1 { 1 } else { -1 };
490        let mut err = dx + dy;
491
492        loop {
493            self.dot_isize(x, y);
494            if x == x1 && y == y1 {
495                break;
496            }
497            let e2 = 2 * err;
498            if e2 >= dy {
499                err += dy;
500                x += sx;
501            }
502            if e2 <= dx {
503                err += dx;
504                y += sy;
505            }
506        }
507    }
508
509    /// Draw a rectangle outline from `(x, y)` with `w` width and `h` height.
510    pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
511        if w == 0 || h == 0 {
512            return;
513        }
514
515        self.line(x, y, x + w.saturating_sub(1), y);
516        self.line(
517            x + w.saturating_sub(1),
518            y,
519            x + w.saturating_sub(1),
520            y + h.saturating_sub(1),
521        );
522        self.line(
523            x + w.saturating_sub(1),
524            y + h.saturating_sub(1),
525            x,
526            y + h.saturating_sub(1),
527        );
528        self.line(x, y + h.saturating_sub(1), x, y);
529    }
530
531    /// Draw a circle outline centered at `(cx, cy)` with radius `r`.
532    pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
533        let mut x = r as isize;
534        let mut y: isize = 0;
535        let mut err: isize = 1 - x;
536        let (cx, cy) = (cx as isize, cy as isize);
537
538        while x >= y {
539            for &(dx, dy) in &[
540                (x, y),
541                (y, x),
542                (-x, y),
543                (-y, x),
544                (x, -y),
545                (y, -x),
546                (-x, -y),
547                (-y, -x),
548            ] {
549                let px = cx + dx;
550                let py = cy + dy;
551                self.dot_isize(px, py);
552            }
553
554            y += 1;
555            if err < 0 {
556                err += 2 * y + 1;
557            } else {
558                x -= 1;
559                err += 2 * (y - x) + 1;
560            }
561        }
562    }
563
564    /// Set the drawing color for subsequent shapes.
565    pub fn set_color(&mut self, color: Color) {
566        self.current_color = color;
567    }
568
569    /// Get the current drawing color.
570    pub fn color(&self) -> Color {
571        self.current_color
572    }
573
574    /// Draw a filled rectangle.
575    pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
576        if w == 0 || h == 0 {
577            return;
578        }
579
580        let x_end = x.saturating_add(w).min(self.px_w);
581        let y_end = y.saturating_add(h).min(self.px_h);
582        if x >= x_end || y >= y_end {
583            return;
584        }
585
586        for yy in y..y_end {
587            self.line(x, yy, x_end.saturating_sub(1), yy);
588        }
589    }
590
591    /// Draw a filled circle.
592    pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
593        let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
594        for y in (cy - r)..=(cy + r) {
595            let dy = y - cy;
596            let span_sq = (r * r - dy * dy).max(0);
597            let dx = (span_sq as f64).sqrt() as isize;
598            for x in (cx - dx)..=(cx + dx) {
599                self.dot_isize(x, y);
600            }
601        }
602    }
603
604    /// Draw a triangle outline.
605    pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
606        self.line(x0, y0, x1, y1);
607        self.line(x1, y1, x2, y2);
608        self.line(x2, y2, x0, y0);
609    }
610
611    /// Draw a filled triangle.
612    pub fn filled_triangle(
613        &mut self,
614        x0: usize,
615        y0: usize,
616        x1: usize,
617        y1: usize,
618        x2: usize,
619        y2: usize,
620    ) {
621        let vertices = [
622            (x0 as isize, y0 as isize),
623            (x1 as isize, y1 as isize),
624            (x2 as isize, y2 as isize),
625        ];
626        let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
627        let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
628
629        for y in min_y..=max_y {
630            let mut intersections: Vec<f64> = Vec::new();
631
632            for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
633                let (x_a, y_a) = vertices[edge.0];
634                let (x_b, y_b) = vertices[edge.1];
635                if y_a == y_b {
636                    continue;
637                }
638
639                let (x_start, y_start, x_end, y_end) = if y_a < y_b {
640                    (x_a, y_a, x_b, y_b)
641                } else {
642                    (x_b, y_b, x_a, y_a)
643                };
644
645                if y < y_start || y >= y_end {
646                    continue;
647                }
648
649                let t = (y - y_start) as f64 / (y_end - y_start) as f64;
650                intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
651            }
652
653            intersections.sort_by(|a, b| a.total_cmp(b));
654            let mut i = 0usize;
655            while i + 1 < intersections.len() {
656                let x_start = intersections[i].ceil() as isize;
657                let x_end = intersections[i + 1].floor() as isize;
658                for x in x_start..=x_end {
659                    self.dot_isize(x, y);
660                }
661                i += 2;
662            }
663        }
664
665        self.triangle(x0, y0, x1, y1, x2, y2);
666    }
667
668    /// Draw multiple points at once.
669    pub fn points(&mut self, pts: &[(usize, usize)]) {
670        for &(x, y) in pts {
671            self.dot(x, y);
672        }
673    }
674
675    /// Draw a polyline connecting the given points in order.
676    pub fn polyline(&mut self, pts: &[(usize, usize)]) {
677        for window in pts.windows(2) {
678            if let [(x0, y0), (x1, y1)] = window {
679                self.line(*x0, *y0, *x1, *y1);
680            }
681        }
682    }
683
684    /// Place a text label at pixel position `(x, y)`.
685    /// Text is rendered in regular characters overlaying the braille grid.
686    pub fn print(&mut self, x: usize, y: usize, text: &str) {
687        if text.is_empty() {
688            return;
689        }
690
691        let color = self.current_color;
692        if let Some(layer) = self.current_layer_mut() {
693            layer.labels.push(CanvasLabel {
694                x,
695                y,
696                text: text.to_string(),
697                color,
698            });
699        }
700    }
701
702    /// Start a new drawing layer. Shapes on later layers overlay earlier ones.
703    pub fn layer(&mut self) {
704        self.layers.push(Self::new_layer(self.cols, self.rows));
705    }
706
707    pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
708        let mut final_grid = vec![
709            vec![
710                CanvasPixel {
711                    bits: 0,
712                    color: Color::Reset,
713                };
714                self.cols
715            ];
716            self.rows
717        ];
718        let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
719            vec![vec![None; self.cols]; self.rows];
720
721        for layer in &self.layers {
722            for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
723                for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
724                    let src = layer.grid[row][col];
725                    if src.bits == 0 {
726                        continue;
727                    }
728
729                    let merged = dst.bits | src.bits;
730                    if merged != dst.bits {
731                        dst.bits = merged;
732                        dst.color = src.color;
733                    }
734                }
735            }
736
737            for label in &layer.labels {
738                let row = label.y / 4;
739                if row >= self.rows {
740                    continue;
741                }
742                let start_col = label.x / 2;
743                for (offset, ch) in label.text.chars().enumerate() {
744                    let col = start_col + offset;
745                    if col >= self.cols {
746                        break;
747                    }
748                    labels_overlay[row][col] = Some((ch, label.color));
749                }
750            }
751        }
752
753        let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
754        for row in 0..self.rows {
755            let mut segments: Vec<(String, Color)> = Vec::new();
756            let mut current_color: Option<Color> = None;
757            let mut current_text = String::new();
758
759            for col in 0..self.cols {
760                let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
761                    (label_ch, label_color)
762                } else {
763                    let bits = final_grid[row][col].bits;
764                    let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
765                    (ch, final_grid[row][col].color)
766                };
767
768                match current_color {
769                    Some(c) if c == color => {
770                        current_text.push(ch);
771                    }
772                    Some(c) => {
773                        segments.push((std::mem::take(&mut current_text), c));
774                        current_text.push(ch);
775                        current_color = Some(color);
776                    }
777                    None => {
778                        current_text.push(ch);
779                        current_color = Some(color);
780                    }
781                }
782            }
783
784            if let Some(color) = current_color {
785                segments.push((current_text, color));
786            }
787            lines.push(segments);
788        }
789
790        lines
791    }
792}
793
794impl<'a> ContainerBuilder<'a> {
795    // ── border ───────────────────────────────────────────────────────
796
797    /// Apply a reusable [`ContainerStyle`] recipe. Only set fields override
798    /// the builder's current values. Chain multiple `.apply()` calls to compose.
799    pub fn apply(mut self, style: &ContainerStyle) -> Self {
800        if let Some(v) = style.border {
801            self.border = Some(v);
802        }
803        if let Some(v) = style.border_sides {
804            self.border_sides = v;
805        }
806        if let Some(v) = style.border_style {
807            self.border_style = v;
808        }
809        if let Some(v) = style.bg {
810            self.bg = Some(v);
811        }
812        if let Some(v) = style.dark_bg {
813            self.dark_bg = Some(v);
814        }
815        if let Some(v) = style.dark_border_style {
816            self.dark_border_style = Some(v);
817        }
818        if let Some(v) = style.padding {
819            self.padding = v;
820        }
821        if let Some(v) = style.margin {
822            self.margin = v;
823        }
824        if let Some(v) = style.gap {
825            self.gap = v;
826        }
827        if let Some(v) = style.grow {
828            self.grow = v;
829        }
830        if let Some(v) = style.align {
831            self.align = v;
832        }
833        if let Some(v) = style.justify {
834            self.justify = v;
835        }
836        if let Some(w) = style.w {
837            self.constraints.min_width = Some(w);
838            self.constraints.max_width = Some(w);
839        }
840        if let Some(h) = style.h {
841            self.constraints.min_height = Some(h);
842            self.constraints.max_height = Some(h);
843        }
844        if let Some(v) = style.min_w {
845            self.constraints.min_width = Some(v);
846        }
847        if let Some(v) = style.max_w {
848            self.constraints.max_width = Some(v);
849        }
850        if let Some(v) = style.min_h {
851            self.constraints.min_height = Some(v);
852        }
853        if let Some(v) = style.max_h {
854            self.constraints.max_height = Some(v);
855        }
856        if let Some(v) = style.w_pct {
857            self.constraints.width_pct = Some(v);
858        }
859        if let Some(v) = style.h_pct {
860            self.constraints.height_pct = Some(v);
861        }
862        self
863    }
864
865    /// Set the border style.
866    pub fn border(mut self, border: Border) -> Self {
867        self.border = Some(border);
868        self
869    }
870
871    /// Show or hide the top border.
872    pub fn border_top(mut self, show: bool) -> Self {
873        self.border_sides.top = show;
874        self
875    }
876
877    /// Show or hide the right border.
878    pub fn border_right(mut self, show: bool) -> Self {
879        self.border_sides.right = show;
880        self
881    }
882
883    /// Show or hide the bottom border.
884    pub fn border_bottom(mut self, show: bool) -> Self {
885        self.border_sides.bottom = show;
886        self
887    }
888
889    /// Show or hide the left border.
890    pub fn border_left(mut self, show: bool) -> Self {
891        self.border_sides.left = show;
892        self
893    }
894
895    /// Set which border sides are visible.
896    pub fn border_sides(mut self, sides: BorderSides) -> Self {
897        self.border_sides = sides;
898        self
899    }
900
901    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
902    pub fn rounded(self) -> Self {
903        self.border(Border::Rounded)
904    }
905
906    /// Set the style applied to the border characters.
907    pub fn border_style(mut self, style: Style) -> Self {
908        self.border_style = style;
909        self
910    }
911
912    /// Border style used when dark mode is active.
913    pub fn dark_border_style(mut self, style: Style) -> Self {
914        self.dark_border_style = Some(style);
915        self
916    }
917
918    pub fn bg(mut self, color: Color) -> Self {
919        self.bg = Some(color);
920        self
921    }
922
923    /// Background color used when dark mode is active.
924    pub fn dark_bg(mut self, color: Color) -> Self {
925        self.dark_bg = Some(color);
926        self
927    }
928
929    /// Background color applied when the parent group is hovered.
930    pub fn group_hover_bg(mut self, color: Color) -> Self {
931        self.group_hover_bg = Some(color);
932        self
933    }
934
935    /// Border style applied when the parent group is hovered.
936    pub fn group_hover_border_style(mut self, style: Style) -> Self {
937        self.group_hover_border_style = Some(style);
938        self
939    }
940
941    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
942
943    /// Set uniform padding on all sides. Alias for [`pad`](Self::pad).
944    pub fn p(self, value: u32) -> Self {
945        self.pad(value)
946    }
947
948    /// Set uniform padding on all sides.
949    pub fn pad(mut self, value: u32) -> Self {
950        self.padding = Padding::all(value);
951        self
952    }
953
954    /// Set horizontal padding (left and right).
955    pub fn px(mut self, value: u32) -> Self {
956        self.padding.left = value;
957        self.padding.right = value;
958        self
959    }
960
961    /// Set vertical padding (top and bottom).
962    pub fn py(mut self, value: u32) -> Self {
963        self.padding.top = value;
964        self.padding.bottom = value;
965        self
966    }
967
968    /// Set top padding.
969    pub fn pt(mut self, value: u32) -> Self {
970        self.padding.top = value;
971        self
972    }
973
974    /// Set right padding.
975    pub fn pr(mut self, value: u32) -> Self {
976        self.padding.right = value;
977        self
978    }
979
980    /// Set bottom padding.
981    pub fn pb(mut self, value: u32) -> Self {
982        self.padding.bottom = value;
983        self
984    }
985
986    /// Set left padding.
987    pub fn pl(mut self, value: u32) -> Self {
988        self.padding.left = value;
989        self
990    }
991
992    /// Set per-side padding using a [`Padding`] value.
993    pub fn padding(mut self, padding: Padding) -> Self {
994        self.padding = padding;
995        self
996    }
997
998    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
999
1000    /// Set uniform margin on all sides.
1001    pub fn m(mut self, value: u32) -> Self {
1002        self.margin = Margin::all(value);
1003        self
1004    }
1005
1006    /// Set horizontal margin (left and right).
1007    pub fn mx(mut self, value: u32) -> Self {
1008        self.margin.left = value;
1009        self.margin.right = value;
1010        self
1011    }
1012
1013    /// Set vertical margin (top and bottom).
1014    pub fn my(mut self, value: u32) -> Self {
1015        self.margin.top = value;
1016        self.margin.bottom = value;
1017        self
1018    }
1019
1020    /// Set top margin.
1021    pub fn mt(mut self, value: u32) -> Self {
1022        self.margin.top = value;
1023        self
1024    }
1025
1026    /// Set right margin.
1027    pub fn mr(mut self, value: u32) -> Self {
1028        self.margin.right = value;
1029        self
1030    }
1031
1032    /// Set bottom margin.
1033    pub fn mb(mut self, value: u32) -> Self {
1034        self.margin.bottom = value;
1035        self
1036    }
1037
1038    /// Set left margin.
1039    pub fn ml(mut self, value: u32) -> Self {
1040        self.margin.left = value;
1041        self
1042    }
1043
1044    /// Set per-side margin using a [`Margin`] value.
1045    pub fn margin(mut self, margin: Margin) -> Self {
1046        self.margin = margin;
1047        self
1048    }
1049
1050    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
1051
1052    /// Set a fixed width (sets both min and max width).
1053    pub fn w(mut self, value: u32) -> Self {
1054        self.constraints.min_width = Some(value);
1055        self.constraints.max_width = Some(value);
1056        self
1057    }
1058
1059    /// Width applied only at Xs breakpoint (< 40 cols).
1060    ///
1061    /// # Example
1062    /// ```ignore
1063    /// ui.container().w(20).md_w(40).lg_w(60).col(|ui| { ... });
1064    /// ```
1065    pub fn xs_w(self, value: u32) -> Self {
1066        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1067        if is_xs {
1068            self.w(value)
1069        } else {
1070            self
1071        }
1072    }
1073
1074    /// Width applied only at Sm breakpoint (40-79 cols).
1075    pub fn sm_w(self, value: u32) -> Self {
1076        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1077        if is_sm {
1078            self.w(value)
1079        } else {
1080            self
1081        }
1082    }
1083
1084    /// Width applied only at Md breakpoint (80-119 cols).
1085    pub fn md_w(self, value: u32) -> Self {
1086        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1087        if is_md {
1088            self.w(value)
1089        } else {
1090            self
1091        }
1092    }
1093
1094    /// Width applied only at Lg breakpoint (120-159 cols).
1095    pub fn lg_w(self, value: u32) -> Self {
1096        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1097        if is_lg {
1098            self.w(value)
1099        } else {
1100            self
1101        }
1102    }
1103
1104    /// Width applied only at Xl breakpoint (>= 160 cols).
1105    pub fn xl_w(self, value: u32) -> Self {
1106        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1107        if is_xl {
1108            self.w(value)
1109        } else {
1110            self
1111        }
1112    }
1113    pub fn w_at(self, bp: Breakpoint, value: u32) -> Self {
1114        if self.ctx.breakpoint() == bp {
1115            self.w(value)
1116        } else {
1117            self
1118        }
1119    }
1120
1121    /// Set a fixed height (sets both min and max height).
1122    pub fn h(mut self, value: u32) -> Self {
1123        self.constraints.min_height = Some(value);
1124        self.constraints.max_height = Some(value);
1125        self
1126    }
1127
1128    /// Height applied only at Xs breakpoint (< 40 cols).
1129    pub fn xs_h(self, value: u32) -> Self {
1130        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1131        if is_xs {
1132            self.h(value)
1133        } else {
1134            self
1135        }
1136    }
1137
1138    /// Height applied only at Sm breakpoint (40-79 cols).
1139    pub fn sm_h(self, value: u32) -> Self {
1140        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1141        if is_sm {
1142            self.h(value)
1143        } else {
1144            self
1145        }
1146    }
1147
1148    /// Height applied only at Md breakpoint (80-119 cols).
1149    pub fn md_h(self, value: u32) -> Self {
1150        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1151        if is_md {
1152            self.h(value)
1153        } else {
1154            self
1155        }
1156    }
1157
1158    /// Height applied only at Lg breakpoint (120-159 cols).
1159    pub fn lg_h(self, value: u32) -> Self {
1160        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1161        if is_lg {
1162            self.h(value)
1163        } else {
1164            self
1165        }
1166    }
1167
1168    /// Height applied only at Xl breakpoint (>= 160 cols).
1169    pub fn xl_h(self, value: u32) -> Self {
1170        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1171        if is_xl {
1172            self.h(value)
1173        } else {
1174            self
1175        }
1176    }
1177    pub fn h_at(self, bp: Breakpoint, value: u32) -> Self {
1178        if self.ctx.breakpoint() == bp {
1179            self.h(value)
1180        } else {
1181            self
1182        }
1183    }
1184
1185    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
1186    pub fn min_w(mut self, value: u32) -> Self {
1187        self.constraints.min_width = Some(value);
1188        self
1189    }
1190
1191    /// Minimum width applied only at Xs breakpoint (< 40 cols).
1192    pub fn xs_min_w(self, value: u32) -> Self {
1193        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1194        if is_xs {
1195            self.min_w(value)
1196        } else {
1197            self
1198        }
1199    }
1200
1201    /// Minimum width applied only at Sm breakpoint (40-79 cols).
1202    pub fn sm_min_w(self, value: u32) -> Self {
1203        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1204        if is_sm {
1205            self.min_w(value)
1206        } else {
1207            self
1208        }
1209    }
1210
1211    /// Minimum width applied only at Md breakpoint (80-119 cols).
1212    pub fn md_min_w(self, value: u32) -> Self {
1213        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1214        if is_md {
1215            self.min_w(value)
1216        } else {
1217            self
1218        }
1219    }
1220
1221    /// Minimum width applied only at Lg breakpoint (120-159 cols).
1222    pub fn lg_min_w(self, value: u32) -> Self {
1223        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1224        if is_lg {
1225            self.min_w(value)
1226        } else {
1227            self
1228        }
1229    }
1230
1231    /// Minimum width applied only at Xl breakpoint (>= 160 cols).
1232    pub fn xl_min_w(self, value: u32) -> Self {
1233        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1234        if is_xl {
1235            self.min_w(value)
1236        } else {
1237            self
1238        }
1239    }
1240    pub fn min_w_at(self, bp: Breakpoint, value: u32) -> Self {
1241        if self.ctx.breakpoint() == bp {
1242            self.min_w(value)
1243        } else {
1244            self
1245        }
1246    }
1247
1248    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
1249    pub fn max_w(mut self, value: u32) -> Self {
1250        self.constraints.max_width = Some(value);
1251        self
1252    }
1253
1254    /// Maximum width applied only at Xs breakpoint (< 40 cols).
1255    pub fn xs_max_w(self, value: u32) -> Self {
1256        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1257        if is_xs {
1258            self.max_w(value)
1259        } else {
1260            self
1261        }
1262    }
1263
1264    /// Maximum width applied only at Sm breakpoint (40-79 cols).
1265    pub fn sm_max_w(self, value: u32) -> Self {
1266        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1267        if is_sm {
1268            self.max_w(value)
1269        } else {
1270            self
1271        }
1272    }
1273
1274    /// Maximum width applied only at Md breakpoint (80-119 cols).
1275    pub fn md_max_w(self, value: u32) -> Self {
1276        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1277        if is_md {
1278            self.max_w(value)
1279        } else {
1280            self
1281        }
1282    }
1283
1284    /// Maximum width applied only at Lg breakpoint (120-159 cols).
1285    pub fn lg_max_w(self, value: u32) -> Self {
1286        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1287        if is_lg {
1288            self.max_w(value)
1289        } else {
1290            self
1291        }
1292    }
1293
1294    /// Maximum width applied only at Xl breakpoint (>= 160 cols).
1295    pub fn xl_max_w(self, value: u32) -> Self {
1296        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1297        if is_xl {
1298            self.max_w(value)
1299        } else {
1300            self
1301        }
1302    }
1303    pub fn max_w_at(self, bp: Breakpoint, value: u32) -> Self {
1304        if self.ctx.breakpoint() == bp {
1305            self.max_w(value)
1306        } else {
1307            self
1308        }
1309    }
1310
1311    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
1312    pub fn min_h(mut self, value: u32) -> Self {
1313        self.constraints.min_height = Some(value);
1314        self
1315    }
1316
1317    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
1318    pub fn max_h(mut self, value: u32) -> Self {
1319        self.constraints.max_height = Some(value);
1320        self
1321    }
1322
1323    /// Set the minimum width constraint in cells.
1324    pub fn min_width(mut self, value: u32) -> Self {
1325        self.constraints.min_width = Some(value);
1326        self
1327    }
1328
1329    /// Set the maximum width constraint in cells.
1330    pub fn max_width(mut self, value: u32) -> Self {
1331        self.constraints.max_width = Some(value);
1332        self
1333    }
1334
1335    /// Set the minimum height constraint in rows.
1336    pub fn min_height(mut self, value: u32) -> Self {
1337        self.constraints.min_height = Some(value);
1338        self
1339    }
1340
1341    /// Set the maximum height constraint in rows.
1342    pub fn max_height(mut self, value: u32) -> Self {
1343        self.constraints.max_height = Some(value);
1344        self
1345    }
1346
1347    /// Set width as a percentage (1-100) of the parent container.
1348    pub fn w_pct(mut self, pct: u8) -> Self {
1349        self.constraints.width_pct = Some(pct.min(100));
1350        self
1351    }
1352
1353    /// Set height as a percentage (1-100) of the parent container.
1354    pub fn h_pct(mut self, pct: u8) -> Self {
1355        self.constraints.height_pct = Some(pct.min(100));
1356        self
1357    }
1358
1359    /// Set all size constraints at once using a [`Constraints`] value.
1360    pub fn constraints(mut self, constraints: Constraints) -> Self {
1361        self.constraints = constraints;
1362        self
1363    }
1364
1365    // ── flex ─────────────────────────────────────────────────────────
1366
1367    /// Set the gap (in cells) between child elements.
1368    pub fn gap(mut self, gap: u32) -> Self {
1369        self.gap = gap;
1370        self
1371    }
1372
1373    /// Gap applied only at Xs breakpoint (< 40 cols).
1374    pub fn xs_gap(self, value: u32) -> Self {
1375        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1376        if is_xs {
1377            self.gap(value)
1378        } else {
1379            self
1380        }
1381    }
1382
1383    /// Gap applied only at Sm breakpoint (40-79 cols).
1384    pub fn sm_gap(self, value: u32) -> Self {
1385        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1386        if is_sm {
1387            self.gap(value)
1388        } else {
1389            self
1390        }
1391    }
1392
1393    /// Gap applied only at Md breakpoint (80-119 cols).
1394    ///
1395    /// # Example
1396    /// ```ignore
1397    /// ui.container().gap(0).md_gap(2).col(|ui| { ... });
1398    /// ```
1399    pub fn md_gap(self, value: u32) -> Self {
1400        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1401        if is_md {
1402            self.gap(value)
1403        } else {
1404            self
1405        }
1406    }
1407
1408    /// Gap applied only at Lg breakpoint (120-159 cols).
1409    pub fn lg_gap(self, value: u32) -> Self {
1410        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1411        if is_lg {
1412            self.gap(value)
1413        } else {
1414            self
1415        }
1416    }
1417
1418    /// Gap applied only at Xl breakpoint (>= 160 cols).
1419    pub fn xl_gap(self, value: u32) -> Self {
1420        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1421        if is_xl {
1422            self.gap(value)
1423        } else {
1424            self
1425        }
1426    }
1427
1428    pub fn gap_at(self, bp: Breakpoint, value: u32) -> Self {
1429        if self.ctx.breakpoint() == bp {
1430            self.gap(value)
1431        } else {
1432            self
1433        }
1434    }
1435
1436    /// Set the flex-grow factor. `1` means the container expands to fill available space.
1437    pub fn grow(mut self, grow: u16) -> Self {
1438        self.grow = grow;
1439        self
1440    }
1441
1442    /// Grow factor applied only at Xs breakpoint (< 40 cols).
1443    pub fn xs_grow(self, value: u16) -> Self {
1444        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1445        if is_xs {
1446            self.grow(value)
1447        } else {
1448            self
1449        }
1450    }
1451
1452    /// Grow factor applied only at Sm breakpoint (40-79 cols).
1453    pub fn sm_grow(self, value: u16) -> Self {
1454        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1455        if is_sm {
1456            self.grow(value)
1457        } else {
1458            self
1459        }
1460    }
1461
1462    /// Grow factor applied only at Md breakpoint (80-119 cols).
1463    pub fn md_grow(self, value: u16) -> Self {
1464        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1465        if is_md {
1466            self.grow(value)
1467        } else {
1468            self
1469        }
1470    }
1471
1472    /// Grow factor applied only at Lg breakpoint (120-159 cols).
1473    pub fn lg_grow(self, value: u16) -> Self {
1474        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1475        if is_lg {
1476            self.grow(value)
1477        } else {
1478            self
1479        }
1480    }
1481
1482    /// Grow factor applied only at Xl breakpoint (>= 160 cols).
1483    pub fn xl_grow(self, value: u16) -> Self {
1484        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1485        if is_xl {
1486            self.grow(value)
1487        } else {
1488            self
1489        }
1490    }
1491    pub fn grow_at(self, bp: Breakpoint, value: u16) -> Self {
1492        if self.ctx.breakpoint() == bp {
1493            self.grow(value)
1494        } else {
1495            self
1496        }
1497    }
1498
1499    /// Uniform padding applied only at Xs breakpoint (< 40 cols).
1500    pub fn xs_p(self, value: u32) -> Self {
1501        let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1502        if is_xs {
1503            self.p(value)
1504        } else {
1505            self
1506        }
1507    }
1508
1509    /// Uniform padding applied only at Sm breakpoint (40-79 cols).
1510    pub fn sm_p(self, value: u32) -> Self {
1511        let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1512        if is_sm {
1513            self.p(value)
1514        } else {
1515            self
1516        }
1517    }
1518
1519    /// Uniform padding applied only at Md breakpoint (80-119 cols).
1520    pub fn md_p(self, value: u32) -> Self {
1521        let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1522        if is_md {
1523            self.p(value)
1524        } else {
1525            self
1526        }
1527    }
1528
1529    /// Uniform padding applied only at Lg breakpoint (120-159 cols).
1530    pub fn lg_p(self, value: u32) -> Self {
1531        let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1532        if is_lg {
1533            self.p(value)
1534        } else {
1535            self
1536        }
1537    }
1538
1539    /// Uniform padding applied only at Xl breakpoint (>= 160 cols).
1540    pub fn xl_p(self, value: u32) -> Self {
1541        let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1542        if is_xl {
1543            self.p(value)
1544        } else {
1545            self
1546        }
1547    }
1548    pub fn p_at(self, bp: Breakpoint, value: u32) -> Self {
1549        if self.ctx.breakpoint() == bp {
1550            self.p(value)
1551        } else {
1552            self
1553        }
1554    }
1555
1556    // ── alignment ───────────────────────────────────────────────────
1557
1558    /// Set the cross-axis alignment of child elements.
1559    pub fn align(mut self, align: Align) -> Self {
1560        self.align = align;
1561        self
1562    }
1563
1564    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
1565    pub fn center(self) -> Self {
1566        self.align(Align::Center)
1567    }
1568
1569    /// Set the main-axis content distribution mode.
1570    pub fn justify(mut self, justify: Justify) -> Self {
1571        self.justify = justify;
1572        self
1573    }
1574
1575    /// Distribute children with equal space between; first at start, last at end.
1576    pub fn space_between(self) -> Self {
1577        self.justify(Justify::SpaceBetween)
1578    }
1579
1580    /// Distribute children with equal space around each child.
1581    pub fn space_around(self) -> Self {
1582        self.justify(Justify::SpaceAround)
1583    }
1584
1585    /// Distribute children with equal space between all children and edges.
1586    pub fn space_evenly(self) -> Self {
1587        self.justify(Justify::SpaceEvenly)
1588    }
1589
1590    // ── title ────────────────────────────────────────────────────────
1591
1592    /// Set a plain-text title rendered in the top border.
1593    pub fn title(self, title: impl Into<String>) -> Self {
1594        self.title_styled(title, Style::new())
1595    }
1596
1597    /// Set a styled title rendered in the top border.
1598    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1599        self.title = Some((title.into(), style));
1600        self
1601    }
1602
1603    // ── internal ─────────────────────────────────────────────────────
1604
1605    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
1606    pub fn scroll_offset(mut self, offset: u32) -> Self {
1607        self.scroll_offset = Some(offset);
1608        self
1609    }
1610
1611    fn group_name(mut self, name: String) -> Self {
1612        self.group_name = Some(name);
1613        self
1614    }
1615
1616    /// Finalize the builder as a vertical (column) container.
1617    ///
1618    /// The closure receives a `&mut Context` for rendering children.
1619    /// Returns a [`Response`] with click/hover state for this container.
1620    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1621        self.finish(Direction::Column, f)
1622    }
1623
1624    /// Finalize the builder as a horizontal (row) container.
1625    ///
1626    /// The closure receives a `&mut Context` for rendering children.
1627    /// Returns a [`Response`] with click/hover state for this container.
1628    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1629        self.finish(Direction::Row, f)
1630    }
1631
1632    /// Finalize the builder as an inline text line.
1633    ///
1634    /// Like [`row`](ContainerBuilder::row) but gap is forced to zero
1635    /// for seamless inline rendering of mixed-style text.
1636    pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1637        self.gap = 0;
1638        self.finish(Direction::Row, f)
1639    }
1640
1641    /// Finalize the builder as a raw-draw region with direct buffer access.
1642    ///
1643    /// The closure receives `(&mut Buffer, Rect)` after layout is computed.
1644    /// Use `buf.set_char()`, `buf.set_string()`, `buf.get_mut()` to write
1645    /// directly into the terminal buffer. Writes outside `rect` are clipped.
1646    ///
1647    /// The closure must be `'static` because it is deferred until after layout.
1648    /// To capture local data, clone or move it into the closure:
1649    /// ```ignore
1650    /// let data = my_vec.clone();
1651    /// ui.container().w(40).h(20).draw(move |buf, rect| {
1652    ///     // use `data` here
1653    /// });
1654    /// ```
1655    pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1656        let draw_id = self.ctx.deferred_draws.len();
1657        self.ctx.deferred_draws.push(Some(Box::new(f)));
1658        self.ctx.interaction_count += 1;
1659        self.ctx.commands.push(Command::RawDraw {
1660            draw_id,
1661            constraints: self.constraints,
1662            grow: self.grow,
1663            margin: self.margin,
1664        });
1665    }
1666
1667    fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1668        let interaction_id = self.ctx.interaction_count;
1669        self.ctx.interaction_count += 1;
1670
1671        let in_hovered_group = self
1672            .group_name
1673            .as_ref()
1674            .map(|name| self.ctx.is_group_hovered(name))
1675            .unwrap_or(false)
1676            || self
1677                .ctx
1678                .group_stack
1679                .last()
1680                .map(|name| self.ctx.is_group_hovered(name))
1681                .unwrap_or(false);
1682        let in_focused_group = self
1683            .group_name
1684            .as_ref()
1685            .map(|name| self.ctx.is_group_focused(name))
1686            .unwrap_or(false)
1687            || self
1688                .ctx
1689                .group_stack
1690                .last()
1691                .map(|name| self.ctx.is_group_focused(name))
1692                .unwrap_or(false);
1693
1694        let resolved_bg = if self.ctx.dark_mode {
1695            self.dark_bg.or(self.bg)
1696        } else {
1697            self.bg
1698        };
1699        let resolved_border_style = if self.ctx.dark_mode {
1700            self.dark_border_style.unwrap_or(self.border_style)
1701        } else {
1702            self.border_style
1703        };
1704        let bg_color = if in_hovered_group || in_focused_group {
1705            self.group_hover_bg.or(resolved_bg)
1706        } else {
1707            resolved_bg
1708        };
1709        let border_style = if in_hovered_group || in_focused_group {
1710            self.group_hover_border_style
1711                .unwrap_or(resolved_border_style)
1712        } else {
1713            resolved_border_style
1714        };
1715        let group_name = self.group_name.take();
1716        let is_group_container = group_name.is_some();
1717
1718        if let Some(scroll_offset) = self.scroll_offset {
1719            self.ctx.commands.push(Command::BeginScrollable {
1720                grow: self.grow,
1721                border: self.border,
1722                border_sides: self.border_sides,
1723                border_style,
1724                padding: self.padding,
1725                margin: self.margin,
1726                constraints: self.constraints,
1727                title: self.title,
1728                scroll_offset,
1729            });
1730        } else {
1731            self.ctx.commands.push(Command::BeginContainer {
1732                direction,
1733                gap: self.gap,
1734                align: self.align,
1735                justify: self.justify,
1736                border: self.border,
1737                border_sides: self.border_sides,
1738                border_style,
1739                bg_color,
1740                padding: self.padding,
1741                margin: self.margin,
1742                constraints: self.constraints,
1743                title: self.title,
1744                grow: self.grow,
1745                group_name,
1746            });
1747        }
1748        f(self.ctx);
1749        self.ctx.commands.push(Command::EndContainer);
1750        self.ctx.last_text_idx = None;
1751
1752        if is_group_container {
1753            self.ctx.group_stack.pop();
1754            self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1755        }
1756
1757        self.ctx.response_for(interaction_id)
1758    }
1759}
1760
1761impl Context {
1762    pub(crate) fn new(
1763        events: Vec<Event>,
1764        width: u32,
1765        height: u32,
1766        state: &mut FrameState,
1767        theme: Theme,
1768    ) -> Self {
1769        let consumed = vec![false; events.len()];
1770
1771        let mut mouse_pos = state.last_mouse_pos;
1772        let mut click_pos = None;
1773        for event in &events {
1774            if let Event::Mouse(mouse) = event {
1775                mouse_pos = Some((mouse.x, mouse.y));
1776                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1777                    click_pos = Some((mouse.x, mouse.y));
1778                }
1779            }
1780        }
1781
1782        let mut focus_index = state.focus_index;
1783        if let Some((mx, my)) = click_pos {
1784            let mut best: Option<(usize, u64)> = None;
1785            for &(fid, rect) in &state.prev_focus_rects {
1786                if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1787                    let area = rect.width as u64 * rect.height as u64;
1788                    if best.map_or(true, |(_, ba)| area < ba) {
1789                        best = Some((fid, area));
1790                    }
1791                }
1792            }
1793            if let Some((fid, _)) = best {
1794                focus_index = fid;
1795            }
1796        }
1797
1798        Self {
1799            commands: Vec::new(),
1800            events,
1801            consumed,
1802            should_quit: false,
1803            area_width: width,
1804            area_height: height,
1805            tick: state.tick,
1806            focus_index,
1807            focus_count: 0,
1808            hook_states: std::mem::take(&mut state.hook_states),
1809            hook_cursor: 0,
1810            prev_focus_count: state.prev_focus_count,
1811            scroll_count: 0,
1812            prev_scroll_infos: std::mem::take(&mut state.prev_scroll_infos),
1813            prev_scroll_rects: std::mem::take(&mut state.prev_scroll_rects),
1814            interaction_count: 0,
1815            prev_hit_map: std::mem::take(&mut state.prev_hit_map),
1816            group_stack: Vec::new(),
1817            prev_group_rects: std::mem::take(&mut state.prev_group_rects),
1818            group_count: 0,
1819            prev_focus_groups: std::mem::take(&mut state.prev_focus_groups),
1820            _prev_focus_rects: std::mem::take(&mut state.prev_focus_rects),
1821            mouse_pos,
1822            click_pos,
1823            last_text_idx: None,
1824            overlay_depth: 0,
1825            modal_active: false,
1826            prev_modal_active: state.prev_modal_active,
1827            clipboard_text: None,
1828            debug: state.debug_mode,
1829            theme,
1830            dark_mode: theme.is_dark,
1831            is_real_terminal: false,
1832            deferred_draws: Vec::new(),
1833        }
1834    }
1835
1836    pub(crate) fn process_focus_keys(&mut self) {
1837        for (i, event) in self.events.iter().enumerate() {
1838            if let Event::Key(key) = event {
1839                if key.kind != KeyEventKind::Press {
1840                    continue;
1841                }
1842                if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1843                    if self.prev_focus_count > 0 {
1844                        self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1845                    }
1846                    self.consumed[i] = true;
1847                } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1848                    || key.code == KeyCode::BackTab
1849                {
1850                    if self.prev_focus_count > 0 {
1851                        self.focus_index = if self.focus_index == 0 {
1852                            self.prev_focus_count - 1
1853                        } else {
1854                            self.focus_index - 1
1855                        };
1856                    }
1857                    self.consumed[i] = true;
1858                }
1859            }
1860        }
1861    }
1862
1863    /// Render a custom [`Widget`].
1864    ///
1865    /// Calls [`Widget::ui`] with this context and returns the widget's response.
1866    pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1867        w.ui(self)
1868    }
1869
1870    /// Wrap child widgets in a panic boundary.
1871    ///
1872    /// If the closure panics, the panic is caught and an error message is
1873    /// rendered in place of the children. The app continues running.
1874    ///
1875    /// # Example
1876    ///
1877    /// ```no_run
1878    /// # slt::run(|ui: &mut slt::Context| {
1879    /// ui.error_boundary(|ui| {
1880    ///     ui.text("risky widget");
1881    /// });
1882    /// # });
1883    /// ```
1884    pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1885        self.error_boundary_with(f, |ui, msg| {
1886            ui.styled(
1887                format!("⚠ Error: {msg}"),
1888                Style::new().fg(ui.theme.error).bold(),
1889            );
1890        });
1891    }
1892
1893    /// Like [`error_boundary`](Self::error_boundary), but renders a custom
1894    /// fallback instead of the default error message.
1895    ///
1896    /// The fallback closure receives the panic message as a [`String`].
1897    ///
1898    /// # Example
1899    ///
1900    /// ```no_run
1901    /// # slt::run(|ui: &mut slt::Context| {
1902    /// ui.error_boundary_with(
1903    ///     |ui| {
1904    ///         ui.text("risky widget");
1905    ///     },
1906    ///     |ui, msg| {
1907    ///         ui.text(format!("Recovered from panic: {msg}"));
1908    ///     },
1909    /// );
1910    /// # });
1911    /// ```
1912    pub fn error_boundary_with(
1913        &mut self,
1914        f: impl FnOnce(&mut Context),
1915        fallback: impl FnOnce(&mut Context, String),
1916    ) {
1917        let snapshot = ContextSnapshot::capture(self);
1918
1919        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1920            f(self);
1921        }));
1922
1923        match result {
1924            Ok(()) => {}
1925            Err(panic_info) => {
1926                if self.is_real_terminal {
1927                    let _ = crossterm::terminal::enable_raw_mode();
1928                    let _ = crossterm::execute!(
1929                        std::io::stdout(),
1930                        crossterm::terminal::EnterAlternateScreen
1931                    );
1932                }
1933
1934                snapshot.restore(self);
1935
1936                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1937                    (*s).to_string()
1938                } else if let Some(s) = panic_info.downcast_ref::<String>() {
1939                    s.clone()
1940                } else {
1941                    "widget panicked".to_string()
1942                };
1943
1944                fallback(self, msg);
1945            }
1946        }
1947    }
1948
1949    /// Allocate a click/hover interaction slot and return the [`Response`].
1950    ///
1951    /// Use this in custom widgets to detect mouse clicks and hovers without
1952    /// wrapping content in a container. Each call reserves one slot in the
1953    /// hit-test map, so the call order must be stable across frames.
1954    pub fn interaction(&mut self) -> Response {
1955        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1956            return Response::default();
1957        }
1958        let id = self.interaction_count;
1959        self.interaction_count += 1;
1960        self.response_for(id)
1961    }
1962
1963    /// Register a widget as focusable and return whether it currently has focus.
1964    ///
1965    /// Call this in custom widgets that need keyboard focus. Each call increments
1966    /// the internal focus counter, so the call order must be stable across frames.
1967    pub fn register_focusable(&mut self) -> bool {
1968        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1969            return false;
1970        }
1971        let id = self.focus_count;
1972        self.focus_count += 1;
1973        self.commands.push(Command::FocusMarker(id));
1974        if self.prev_focus_count == 0 {
1975            return true;
1976        }
1977        self.focus_index % self.prev_focus_count == id
1978    }
1979
1980    /// Create persistent state that survives across frames.
1981    ///
1982    /// Returns a `State<T>` handle. Access with `state.get(ui)` / `state.get_mut(ui)`.
1983    ///
1984    /// # Rules
1985    /// - Must be called in the same order every frame (like React hooks)
1986    /// - Do NOT call inside if/else that changes between frames
1987    ///
1988    /// # Example
1989    /// ```ignore
1990    /// let count = ui.use_state(|| 0i32);
1991    /// let val = count.get(ui);
1992    /// ui.text(format!("Count: {val}"));
1993    /// if ui.button("+1") {
1994    ///     *count.get_mut(ui) += 1;
1995    /// }
1996    /// ```
1997    pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
1998        let idx = self.hook_cursor;
1999        self.hook_cursor += 1;
2000
2001        if idx >= self.hook_states.len() {
2002            self.hook_states.push(Box::new(init()));
2003        }
2004
2005        State {
2006            idx,
2007            _marker: std::marker::PhantomData,
2008        }
2009    }
2010
2011    /// Memoize a computed value. Recomputes only when `deps` changes.
2012    ///
2013    /// # Example
2014    /// ```ignore
2015    /// let doubled = ui.use_memo(&count, |c| c * 2);
2016    /// ui.text(format!("Doubled: {doubled}"));
2017    /// ```
2018    pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
2019        &mut self,
2020        deps: &D,
2021        compute: impl FnOnce(&D) -> T,
2022    ) -> &T {
2023        let idx = self.hook_cursor;
2024        self.hook_cursor += 1;
2025
2026        let should_recompute = if idx >= self.hook_states.len() {
2027            true
2028        } else {
2029            let (stored_deps, _) = self.hook_states[idx]
2030                .downcast_ref::<(D, T)>()
2031                .expect("use_memo type mismatch");
2032            stored_deps != deps
2033        };
2034
2035        if should_recompute {
2036            let value = compute(deps);
2037            let slot = Box::new((deps.clone(), value));
2038            if idx < self.hook_states.len() {
2039                self.hook_states[idx] = slot;
2040            } else {
2041                self.hook_states.push(slot);
2042            }
2043        }
2044
2045        let (_, value) = self.hook_states[idx]
2046            .downcast_ref::<(D, T)>()
2047            .expect("use_memo type mismatch");
2048        value
2049    }
2050}
2051
2052mod widgets_display;
2053mod widgets_input;
2054mod widgets_interactive;
2055mod widgets_viz;
2056
2057#[inline]
2058fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2059    if char_index == 0 {
2060        return 0;
2061    }
2062    value
2063        .char_indices()
2064        .nth(char_index)
2065        .map_or(value.len(), |(idx, _)| idx)
2066}
2067
2068fn format_token_count(count: usize) -> String {
2069    if count >= 1_000_000 {
2070        format!("{:.1}M", count as f64 / 1_000_000.0)
2071    } else if count >= 1_000 {
2072        format!("{:.1}k", count as f64 / 1_000.0)
2073    } else {
2074        format!("{count}")
2075    }
2076}
2077
2078fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2079    let mut parts: Vec<String> = Vec::new();
2080    for (i, width) in widths.iter().enumerate() {
2081        let cell = cells.get(i).map(String::as_str).unwrap_or("");
2082        let cell_width = UnicodeWidthStr::width(cell) as u32;
2083        let padding = (*width).saturating_sub(cell_width) as usize;
2084        parts.push(format!("{cell}{}", " ".repeat(padding)));
2085    }
2086    parts.join(separator)
2087}
2088
2089fn format_compact_number(value: f64) -> String {
2090    if value.fract().abs() < f64::EPSILON {
2091        return format!("{value:.0}");
2092    }
2093
2094    let mut s = format!("{value:.2}");
2095    while s.contains('.') && s.ends_with('0') {
2096        s.pop();
2097    }
2098    if s.ends_with('.') {
2099        s.pop();
2100    }
2101    s
2102}
2103
2104fn center_text(text: &str, width: usize) -> String {
2105    let text_width = UnicodeWidthStr::width(text);
2106    if text_width >= width {
2107        return text.to_string();
2108    }
2109
2110    let total = width - text_width;
2111    let left = total / 2;
2112    let right = total - left;
2113    format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2114}
2115
2116struct TextareaVLine {
2117    logical_row: usize,
2118    char_start: usize,
2119    char_count: usize,
2120}
2121
2122fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
2123    let mut out = Vec::new();
2124    for (row, line) in lines.iter().enumerate() {
2125        if line.is_empty() || wrap_width == u32::MAX {
2126            out.push(TextareaVLine {
2127                logical_row: row,
2128                char_start: 0,
2129                char_count: line.chars().count(),
2130            });
2131            continue;
2132        }
2133        let mut seg_start = 0usize;
2134        let mut seg_chars = 0usize;
2135        let mut seg_width = 0u32;
2136        for (idx, ch) in line.chars().enumerate() {
2137            let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
2138            if seg_width + cw > wrap_width && seg_chars > 0 {
2139                out.push(TextareaVLine {
2140                    logical_row: row,
2141                    char_start: seg_start,
2142                    char_count: seg_chars,
2143                });
2144                seg_start = idx;
2145                seg_chars = 0;
2146                seg_width = 0;
2147            }
2148            seg_chars += 1;
2149            seg_width += cw;
2150        }
2151        out.push(TextareaVLine {
2152            logical_row: row,
2153            char_start: seg_start,
2154            char_count: seg_chars,
2155        });
2156    }
2157    out
2158}
2159
2160fn textarea_logical_to_visual(
2161    vlines: &[TextareaVLine],
2162    logical_row: usize,
2163    logical_col: usize,
2164) -> (usize, usize) {
2165    for (i, vl) in vlines.iter().enumerate() {
2166        if vl.logical_row != logical_row {
2167            continue;
2168        }
2169        let seg_end = vl.char_start + vl.char_count;
2170        if logical_col >= vl.char_start && logical_col < seg_end {
2171            return (i, logical_col - vl.char_start);
2172        }
2173        if logical_col == seg_end {
2174            let is_last_seg = vlines
2175                .get(i + 1)
2176                .map_or(true, |next| next.logical_row != logical_row);
2177            if is_last_seg {
2178                return (i, logical_col - vl.char_start);
2179            }
2180        }
2181    }
2182    (vlines.len().saturating_sub(1), 0)
2183}
2184
2185fn textarea_visual_to_logical(
2186    vlines: &[TextareaVLine],
2187    visual_row: usize,
2188    visual_col: usize,
2189) -> (usize, usize) {
2190    if let Some(vl) = vlines.get(visual_row) {
2191        let logical_col = vl.char_start + visual_col.min(vl.char_count);
2192        (vl.logical_row, logical_col)
2193    } else {
2194        (0, 0)
2195    }
2196}
2197
2198fn open_url(url: &str) -> std::io::Result<()> {
2199    #[cfg(target_os = "macos")]
2200    {
2201        std::process::Command::new("open").arg(url).spawn()?;
2202    }
2203    #[cfg(target_os = "linux")]
2204    {
2205        std::process::Command::new("xdg-open").arg(url).spawn()?;
2206    }
2207    #[cfg(target_os = "windows")]
2208    {
2209        std::process::Command::new("cmd")
2210            .args(["/c", "start", "", url])
2211            .spawn()?;
2212    }
2213    Ok(())
2214}