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