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