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