Skip to main content

slt/
context.rs

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