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