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