Skip to main content

slt/
context.rs

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