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