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