Skip to main content

slt/context/
container.rs

1use super::*;
2
3/// Fluent builder for configuring containers before calling `.col()` or `.row()`.
4///
5/// Obtain one via [`Context::container`] or [`Context::bordered`]. Chain the
6/// configuration methods you need, then finalize with `.col(|ui| { ... })` or
7/// `.row(|ui| { ... })`.
8///
9/// # Example
10///
11/// ```no_run
12/// # slt::run(|ui: &mut slt::Context| {
13/// use slt::{Border, Color};
14/// ui.container()
15///     .border(Border::Rounded)
16///     .pad(1)
17///     .grow(1)
18///     .col(|ui| {
19///         ui.text("inside a bordered, padded, growing column");
20///     });
21/// # });
22/// ```
23#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
24pub struct ContainerBuilder<'a> {
25    pub(crate) ctx: &'a mut Context,
26    pub(crate) gap: u32,
27    pub(crate) row_gap: Option<u32>,
28    pub(crate) col_gap: Option<u32>,
29    pub(crate) align: Align,
30    pub(crate) align_self_value: Option<Align>,
31    pub(crate) justify: Justify,
32    pub(crate) border: Option<Border>,
33    pub(crate) border_sides: BorderSides,
34    pub(crate) border_style: Style,
35    pub(crate) bg: Option<Color>,
36    pub(crate) text_color: Option<Color>,
37    pub(crate) dark_bg: Option<Color>,
38    pub(crate) dark_border_style: Option<Style>,
39    pub(crate) group_hover_bg: Option<Color>,
40    pub(crate) group_hover_border_style: Option<Style>,
41    pub(crate) group_name: Option<std::sync::Arc<str>>,
42    pub(crate) padding: Padding,
43    pub(crate) margin: Margin,
44    pub(crate) constraints: Constraints,
45    pub(crate) title: Option<(String, Style)>,
46    pub(crate) grow: u16,
47    pub(crate) scroll_offset: Option<u32>,
48}
49
50/// Drawing context for the [`Context::canvas`] widget.
51///
52/// Provides pixel-level drawing on a braille character grid. Each terminal
53/// cell maps to a 2x4 dot matrix, so a canvas of `width` columns x `height`
54/// rows gives `width*2` x `height*4` pixel resolution.
55/// A colored pixel in the canvas grid.
56#[derive(Debug, Clone, Copy)]
57struct CanvasPixel {
58    bits: u32,
59    color: Color,
60}
61
62/// Text label placed on the canvas.
63#[derive(Debug, Clone)]
64struct CanvasLabel {
65    x: usize,
66    y: usize,
67    text: String,
68    color: Color,
69}
70
71/// A layer in the canvas, supporting z-ordering.
72#[derive(Debug, Clone)]
73struct CanvasLayer {
74    grid: Vec<Vec<CanvasPixel>>,
75    labels: Vec<CanvasLabel>,
76}
77
78/// Drawing context for the canvas widget.
79pub struct CanvasContext {
80    layers: Vec<CanvasLayer>,
81    cols: usize,
82    rows: usize,
83    px_w: usize,
84    px_h: usize,
85    current_color: Color,
86    /// Flat scratch buffer for `render()` pixel composition.
87    /// Capacity = `cols * rows`; flat index = `row * cols + col`.
88    scratch_pixels: Vec<CanvasPixel>,
89    /// Flat scratch buffer for `render()` label overlay.
90    /// Capacity = `cols * rows`; flat index = `row * cols + col`.
91    scratch_labels: Vec<Option<(char, Color)>>,
92}
93
94/// Integer square root for non-negative `i64` values, returning `isize`.
95///
96/// Uses an `f64` seed plus a bounded correction step to absorb rounding at
97/// integer boundaries. Avoids the unconditional `f64` round-trip used in
98/// hot canvas paths (e.g. `filled_circle`). Replace with `u64::isqrt()`
99/// once the project MSRV reaches 1.84.
100#[inline]
101fn isqrt_i64(n: i64) -> isize {
102    if n <= 0 {
103        return 0;
104    }
105    let mut x = (n as f64).sqrt() as i64;
106    // Single correction step handles f64 rounding at integer boundaries.
107    while x > 0 && x.saturating_mul(x) > n {
108        x -= 1;
109    }
110    while (x + 1).saturating_mul(x + 1) <= n {
111        x += 1;
112    }
113    x as isize
114}
115
116impl CanvasContext {
117    pub(crate) fn new(cols: usize, rows: usize) -> Self {
118        let cell_count = cols.saturating_mul(rows);
119        Self {
120            layers: vec![Self::new_layer(cols, rows)],
121            cols,
122            rows,
123            px_w: cols * 2,
124            px_h: rows * 4,
125            current_color: Color::Reset,
126            scratch_pixels: vec![
127                CanvasPixel {
128                    bits: 0,
129                    color: Color::Reset,
130                };
131                cell_count
132            ],
133            scratch_labels: vec![None; cell_count],
134        }
135    }
136
137    fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
138        CanvasLayer {
139            grid: vec![
140                vec![
141                    CanvasPixel {
142                        bits: 0,
143                        color: Color::Reset,
144                    };
145                    cols
146                ];
147                rows
148            ],
149            labels: Vec::new(),
150        }
151    }
152
153    fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
154        self.layers.last_mut()
155    }
156
157    fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
158        if x >= self.px_w || y >= self.px_h {
159            return;
160        }
161
162        let char_col = x / 2;
163        let char_row = y / 4;
164        let sub_col = x % 2;
165        let sub_row = y % 4;
166        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
167        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
168
169        let bit = if sub_col == 0 {
170            LEFT_BITS[sub_row]
171        } else {
172            RIGHT_BITS[sub_row]
173        };
174
175        if let Some(layer) = self.current_layer_mut() {
176            let cell = &mut layer.grid[char_row][char_col];
177            let new_bits = cell.bits | bit;
178            if new_bits != cell.bits {
179                cell.bits = new_bits;
180                cell.color = color;
181            }
182        }
183    }
184
185    fn dot_isize(&mut self, x: isize, y: isize) {
186        if x >= 0 && y >= 0 {
187            self.dot(x as usize, y as usize);
188        }
189    }
190
191    /// Get the pixel width of the canvas.
192    pub fn width(&self) -> usize {
193        self.px_w
194    }
195
196    /// Get the pixel height of the canvas.
197    pub fn height(&self) -> usize {
198        self.px_h
199    }
200
201    /// Set a single pixel at `(x, y)`.
202    pub fn dot(&mut self, x: usize, y: usize) {
203        self.dot_with_color(x, y, self.current_color);
204    }
205
206    /// Draw a line from `(x0, y0)` to `(x1, y1)` using Bresenham's algorithm.
207    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
208        let (mut x, mut y) = (x0 as isize, y0 as isize);
209        let (x1, y1) = (x1 as isize, y1 as isize);
210        let dx = (x1 - x).abs();
211        let dy = -(y1 - y).abs();
212        let sx = if x < x1 { 1 } else { -1 };
213        let sy = if y < y1 { 1 } else { -1 };
214        let mut err = dx + dy;
215
216        loop {
217            self.dot_isize(x, y);
218            if x == x1 && y == y1 {
219                break;
220            }
221            let e2 = 2 * err;
222            if e2 >= dy {
223                err += dy;
224                x += sx;
225            }
226            if e2 <= dx {
227                err += dx;
228                y += sy;
229            }
230        }
231    }
232
233    /// Draw a rectangle outline from `(x, y)` with `w` width and `h` height.
234    pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
235        if w == 0 || h == 0 {
236            return;
237        }
238
239        self.line(x, y, x + w.saturating_sub(1), y);
240        self.line(
241            x + w.saturating_sub(1),
242            y,
243            x + w.saturating_sub(1),
244            y + h.saturating_sub(1),
245        );
246        self.line(
247            x + w.saturating_sub(1),
248            y + h.saturating_sub(1),
249            x,
250            y + h.saturating_sub(1),
251        );
252        self.line(x, y + h.saturating_sub(1), x, y);
253    }
254
255    /// Draw a circle outline centered at `(cx, cy)` with radius `r`.
256    pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
257        let mut x = r as isize;
258        let mut y: isize = 0;
259        let mut err: isize = 1 - x;
260        let (cx, cy) = (cx as isize, cy as isize);
261
262        while x >= y {
263            for &(dx, dy) in &[
264                (x, y),
265                (y, x),
266                (-x, y),
267                (-y, x),
268                (x, -y),
269                (y, -x),
270                (-x, -y),
271                (-y, -x),
272            ] {
273                let px = cx + dx;
274                let py = cy + dy;
275                self.dot_isize(px, py);
276            }
277
278            y += 1;
279            if err < 0 {
280                err += 2 * y + 1;
281            } else {
282                x -= 1;
283                err += 2 * (y - x) + 1;
284            }
285        }
286    }
287
288    /// Set the drawing color for subsequent shapes.
289    pub fn set_color(&mut self, color: Color) {
290        self.current_color = color;
291    }
292
293    /// Get the current drawing color.
294    pub fn color(&self) -> Color {
295        self.current_color
296    }
297
298    /// Draw a filled rectangle.
299    pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
300        if w == 0 || h == 0 {
301            return;
302        }
303
304        let x_end = x.saturating_add(w).min(self.px_w);
305        let y_end = y.saturating_add(h).min(self.px_h);
306        if x >= x_end || y >= y_end {
307            return;
308        }
309
310        for yy in y..y_end {
311            self.line(x, yy, x_end.saturating_sub(1), yy);
312        }
313    }
314
315    /// Draw a filled circle.
316    pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
317        let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
318        for y in (cy - r)..=(cy + r) {
319            let dy = y - cy;
320            let span_sq = (r * r - dy * dy).max(0);
321            // TODO(msrv): switch to u64::isqrt() when MSRV >= 1.84
322            let dx = isqrt_i64(span_sq as i64);
323            for x in (cx - dx)..=(cx + dx) {
324                self.dot_isize(x, y);
325            }
326        }
327    }
328
329    /// Draw a triangle outline.
330    pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
331        self.line(x0, y0, x1, y1);
332        self.line(x1, y1, x2, y2);
333        self.line(x2, y2, x0, y0);
334    }
335
336    /// Draw a filled triangle.
337    pub fn filled_triangle(
338        &mut self,
339        x0: usize,
340        y0: usize,
341        x1: usize,
342        y1: usize,
343        x2: usize,
344        y2: usize,
345    ) {
346        let vertices = [
347            (x0 as isize, y0 as isize),
348            (x1 as isize, y1 as isize),
349            (x2 as isize, y2 as isize),
350        ];
351        let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
352        let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
353
354        for y in min_y..=max_y {
355            // A triangle has exactly 3 edges -> at most 3 intersections per
356            // scanline. A 4-element stack array avoids per-scanline heap
357            // allocations from the previous Vec<f64>.
358            let mut intersections = [0.0f64; 4];
359            let mut isect_count = 0usize;
360
361            for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
362                let (x_a, y_a) = vertices[edge.0];
363                let (x_b, y_b) = vertices[edge.1];
364                if y_a == y_b {
365                    continue;
366                }
367
368                let (x_start, y_start, x_end, y_end) = if y_a < y_b {
369                    (x_a, y_a, x_b, y_b)
370                } else {
371                    (x_b, y_b, x_a, y_a)
372                };
373
374                if y < y_start || y >= y_end {
375                    continue;
376                }
377
378                let t = (y - y_start) as f64 / (y_end - y_start) as f64;
379                if isect_count < intersections.len() {
380                    intersections[isect_count] = x_start as f64 + t * (x_end - x_start) as f64;
381                    isect_count += 1;
382                }
383            }
384
385            intersections[..isect_count].sort_by(|a, b| a.total_cmp(b));
386            let mut i = 0usize;
387            while i + 1 < isect_count {
388                let x_start = intersections[i].ceil() as isize;
389                let x_end = intersections[i + 1].floor() as isize;
390                for x in x_start..=x_end {
391                    self.dot_isize(x, y);
392                }
393                i += 2;
394            }
395        }
396
397        self.triangle(x0, y0, x1, y1, x2, y2);
398    }
399
400    /// Draw multiple points at once.
401    pub fn points(&mut self, pts: &[(usize, usize)]) {
402        for &(x, y) in pts {
403            self.dot(x, y);
404        }
405    }
406
407    /// Draw a polyline connecting the given points in order.
408    pub fn polyline(&mut self, pts: &[(usize, usize)]) {
409        for window in pts.windows(2) {
410            if let [(x0, y0), (x1, y1)] = window {
411                self.line(*x0, *y0, *x1, *y1);
412            }
413        }
414    }
415
416    /// Place a text label at pixel position `(x, y)`.
417    /// Text is rendered in regular characters overlaying the braille grid.
418    pub fn print(&mut self, x: usize, y: usize, text: &str) {
419        if text.is_empty() {
420            return;
421        }
422
423        let color = self.current_color;
424        if let Some(layer) = self.current_layer_mut() {
425            layer.labels.push(CanvasLabel {
426                x,
427                y,
428                text: text.to_string(),
429                color,
430            });
431        }
432    }
433
434    /// Start a new drawing layer. Shapes on later layers overlay earlier ones.
435    pub fn layer(&mut self) {
436        self.layers.push(Self::new_layer(self.cols, self.rows));
437    }
438
439    pub(crate) fn render(&mut self) -> Vec<Vec<(String, Color)>> {
440        let cell_count = self.cols.saturating_mul(self.rows);
441
442        // Reset reusable scratch buffers, growing them only if `cols`/`rows`
443        // changed since construction. `fill` keeps the existing allocation.
444        if self.scratch_pixels.len() < cell_count {
445            self.scratch_pixels.resize(
446                cell_count,
447                CanvasPixel {
448                    bits: 0,
449                    color: Color::Reset,
450                },
451            );
452        }
453        if self.scratch_labels.len() < cell_count {
454            self.scratch_labels.resize(cell_count, None);
455        }
456        for px in &mut self.scratch_pixels[..cell_count] {
457            *px = CanvasPixel {
458                bits: 0,
459                color: Color::Reset,
460            };
461        }
462        for slot in &mut self.scratch_labels[..cell_count] {
463            *slot = None;
464        }
465
466        let cols = self.cols;
467        let rows = self.rows;
468
469        for layer in &self.layers {
470            for (row, src_row) in layer.grid.iter().enumerate().take(rows) {
471                let row_offset = row * cols;
472                for (col, src) in src_row.iter().enumerate().take(cols) {
473                    if src.bits == 0 {
474                        continue;
475                    }
476                    let dst = &mut self.scratch_pixels[row_offset + col];
477                    let merged = dst.bits | src.bits;
478                    if merged != dst.bits {
479                        dst.bits = merged;
480                        dst.color = src.color;
481                    }
482                }
483            }
484
485            for label in &layer.labels {
486                let row = label.y / 4;
487                if row >= rows {
488                    continue;
489                }
490                let start_col = label.x / 2;
491                let row_offset = row * cols;
492                for (offset, ch) in label.text.chars().enumerate() {
493                    let col = start_col + offset;
494                    if col >= cols {
495                        break;
496                    }
497                    self.scratch_labels[row_offset + col] = Some((ch, label.color));
498                }
499            }
500        }
501
502        let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(rows);
503        for row in 0..rows {
504            let row_offset = row * cols;
505            let mut segments: Vec<(String, Color)> = Vec::new();
506            let mut current_color: Option<Color> = None;
507            let mut current_text = String::new();
508
509            for col in 0..cols {
510                let idx = row_offset + col;
511                let (ch, color) = if let Some((label_ch, label_color)) = self.scratch_labels[idx] {
512                    (label_ch, label_color)
513                } else {
514                    let pixel = self.scratch_pixels[idx];
515                    let ch = char::from_u32(0x2800 + pixel.bits).unwrap_or(' ');
516                    (ch, pixel.color)
517                };
518
519                match current_color {
520                    Some(c) if c == color => {
521                        current_text.push(ch);
522                    }
523                    Some(c) => {
524                        segments.push((std::mem::take(&mut current_text), c));
525                        current_text.push(ch);
526                        current_color = Some(color);
527                    }
528                    None => {
529                        current_text.push(ch);
530                        current_color = Some(color);
531                    }
532                }
533            }
534
535            if let Some(color) = current_color {
536                segments.push((current_text, color));
537            }
538            lines.push(segments);
539        }
540
541        lines
542    }
543}
544
545macro_rules! define_breakpoint_methods {
546    (
547        base = $base:ident,
548        arg = $arg:ident : $arg_ty:ty,
549        xs = $xs_fn:ident => [$( $xs_doc:literal ),* $(,)?],
550        sm = $sm_fn:ident => [$( $sm_doc:literal ),* $(,)?],
551        md = $md_fn:ident => [$( $md_doc:literal ),* $(,)?],
552        lg = $lg_fn:ident => [$( $lg_doc:literal ),* $(,)?],
553        xl = $xl_fn:ident => [$( $xl_doc:literal ),* $(,)?],
554        at = $at_fn:ident => [$( $at_doc:literal ),* $(,)?]
555    ) => {
556        $(#[doc = $xs_doc])*
557        pub fn $xs_fn(self, $arg: $arg_ty) -> Self {
558            if self.ctx.breakpoint() == Breakpoint::Xs {
559                self.$base($arg)
560            } else {
561                self
562            }
563        }
564
565        $(#[doc = $sm_doc])*
566        pub fn $sm_fn(self, $arg: $arg_ty) -> Self {
567            if self.ctx.breakpoint() == Breakpoint::Sm {
568                self.$base($arg)
569            } else {
570                self
571            }
572        }
573
574        $(#[doc = $md_doc])*
575        pub fn $md_fn(self, $arg: $arg_ty) -> Self {
576            if self.ctx.breakpoint() == Breakpoint::Md {
577                self.$base($arg)
578            } else {
579                self
580            }
581        }
582
583        $(#[doc = $lg_doc])*
584        pub fn $lg_fn(self, $arg: $arg_ty) -> Self {
585            if self.ctx.breakpoint() == Breakpoint::Lg {
586                self.$base($arg)
587            } else {
588                self
589            }
590        }
591
592        $(#[doc = $xl_doc])*
593        pub fn $xl_fn(self, $arg: $arg_ty) -> Self {
594            if self.ctx.breakpoint() == Breakpoint::Xl {
595                self.$base($arg)
596            } else {
597                self
598            }
599        }
600
601        $(#[doc = $at_doc])*
602        pub fn $at_fn(self, bp: Breakpoint, $arg: $arg_ty) -> Self {
603            if self.ctx.breakpoint() == bp {
604                self.$base($arg)
605            } else {
606                self
607            }
608        }
609    };
610}
611
612impl<'a> ContainerBuilder<'a> {
613    // ── border ───────────────────────────────────────────────────────
614
615    /// Apply a reusable [`ContainerStyle`] recipe. Only set fields override
616    /// the builder's current values. Chain multiple `.apply()` calls to compose.
617    ///
618    /// If the style has an [`ContainerStyle::extends`] base, the base is applied
619    /// first, then the style's own fields override.
620    ///
621    /// [`ThemeColor`] fields (`theme_bg`, `theme_text_color`, `theme_border_fg`)
622    /// are resolved against the active theme at apply time.
623    pub fn apply(mut self, style: &ContainerStyle) -> Self {
624        // Apply base style first if this style extends another
625        if let Some(base) = style.extends {
626            self = self.apply(base);
627        }
628        if let Some(v) = style.border {
629            self.border = Some(v);
630        }
631        if let Some(v) = style.border_sides {
632            self.border_sides = v;
633        }
634        if let Some(v) = style.border_style {
635            self.border_style = v;
636        }
637        if let Some(v) = style.bg {
638            self.bg = Some(v);
639        }
640        if let Some(v) = style.dark_bg {
641            self.dark_bg = Some(v);
642        }
643        if let Some(v) = style.dark_border_style {
644            self.dark_border_style = Some(v);
645        }
646        if let Some(v) = style.padding {
647            self.padding = v;
648        }
649        if let Some(v) = style.margin {
650            self.margin = v;
651        }
652        if let Some(v) = style.gap {
653            self.gap = v;
654        }
655        if let Some(v) = style.row_gap {
656            self.row_gap = Some(v);
657        }
658        if let Some(v) = style.col_gap {
659            self.col_gap = Some(v);
660        }
661        if let Some(v) = style.grow {
662            self.grow = v;
663        }
664        if let Some(v) = style.align {
665            self.align = v;
666        }
667        if let Some(v) = style.align_self {
668            self.align_self_value = Some(v);
669        }
670        if let Some(v) = style.justify {
671            self.justify = v;
672        }
673        if let Some(v) = style.text_color {
674            self.text_color = Some(v);
675        }
676        if let Some(w) = style.w {
677            self.constraints.min_width = Some(w);
678            self.constraints.max_width = Some(w);
679        }
680        if let Some(h) = style.h {
681            self.constraints.min_height = Some(h);
682            self.constraints.max_height = Some(h);
683        }
684        if let Some(v) = style.min_w {
685            self.constraints.min_width = Some(v);
686        }
687        if let Some(v) = style.max_w {
688            self.constraints.max_width = Some(v);
689        }
690        if let Some(v) = style.min_h {
691            self.constraints.min_height = Some(v);
692        }
693        if let Some(v) = style.max_h {
694            self.constraints.max_height = Some(v);
695        }
696        if let Some(v) = style.w_pct {
697            self.constraints.width_pct = Some(v);
698        }
699        if let Some(v) = style.h_pct {
700            self.constraints.height_pct = Some(v);
701        }
702        // Resolve ThemeColor fields against the active theme (overrides literal colors)
703        if let Some(tc) = style.theme_bg {
704            self.bg = Some(self.ctx.theme.resolve(tc));
705        }
706        if let Some(tc) = style.theme_text_color {
707            self.text_color = Some(self.ctx.theme.resolve(tc));
708        }
709        if let Some(tc) = style.theme_border_fg {
710            let color = self.ctx.theme.resolve(tc);
711            self.border_style = Style::new().fg(color);
712        }
713        self
714    }
715
716    /// Set the border style.
717    pub fn border(mut self, border: Border) -> Self {
718        self.border = Some(border);
719        self
720    }
721
722    /// Show or hide the top border.
723    pub fn border_top(mut self, show: bool) -> Self {
724        self.border_sides.top = show;
725        self
726    }
727
728    /// Show or hide the right border.
729    pub fn border_right(mut self, show: bool) -> Self {
730        self.border_sides.right = show;
731        self
732    }
733
734    /// Show or hide the bottom border.
735    pub fn border_bottom(mut self, show: bool) -> Self {
736        self.border_sides.bottom = show;
737        self
738    }
739
740    /// Show or hide the left border.
741    pub fn border_left(mut self, show: bool) -> Self {
742        self.border_sides.left = show;
743        self
744    }
745
746    /// Set which border sides are visible.
747    pub fn border_sides(mut self, sides: BorderSides) -> Self {
748        self.border_sides = sides;
749        self
750    }
751
752    /// Show only left and right borders. Shorthand for horizontal border sides.
753    pub fn border_x(self) -> Self {
754        self.border_sides(BorderSides {
755            top: false,
756            right: true,
757            bottom: false,
758            left: true,
759        })
760    }
761
762    /// Show only top and bottom borders. Shorthand for vertical border sides.
763    pub fn border_y(self) -> Self {
764        self.border_sides(BorderSides {
765            top: true,
766            right: false,
767            bottom: true,
768            left: false,
769        })
770    }
771
772    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
773    pub fn rounded(self) -> Self {
774        self.border(Border::Rounded)
775    }
776
777    /// Set the style applied to the border characters.
778    pub fn border_style(mut self, style: Style) -> Self {
779        self.border_style = style;
780        self
781    }
782
783    /// Set the border foreground color.
784    pub fn border_fg(mut self, color: Color) -> Self {
785        self.border_style = self.border_style.fg(color);
786        self
787    }
788
789    /// Border style used when dark mode is active.
790    pub fn dark_border_style(mut self, style: Style) -> Self {
791        self.dark_border_style = Some(style);
792        self
793    }
794
795    /// Set the background color.
796    pub fn bg(mut self, color: Color) -> Self {
797        self.bg = Some(color);
798        self
799    }
800
801    /// Set the default text color for all child text elements in this container.
802    /// Individual `.fg()` calls on text elements will still override this.
803    pub fn text_color(mut self, color: Color) -> Self {
804        self.text_color = Some(color);
805        self
806    }
807
808    /// Background color used when dark mode is active.
809    pub fn dark_bg(mut self, color: Color) -> Self {
810        self.dark_bg = Some(color);
811        self
812    }
813
814    /// Background color applied when the parent group is hovered.
815    pub fn group_hover_bg(mut self, color: Color) -> Self {
816        self.group_hover_bg = Some(color);
817        self
818    }
819
820    /// Border style applied when the parent group is hovered.
821    pub fn group_hover_border_style(mut self, style: Style) -> Self {
822        self.group_hover_border_style = Some(style);
823        self
824    }
825
826    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
827
828    /// Set uniform padding on all sides. Alias for [`pad`](Self::pad).
829    pub fn p(self, value: u32) -> Self {
830        self.pad(value)
831    }
832
833    /// Set uniform padding on all sides.
834    pub fn pad(mut self, value: u32) -> Self {
835        self.padding = Padding::all(value);
836        self
837    }
838
839    /// Set horizontal padding (left and right).
840    pub fn px(mut self, value: u32) -> Self {
841        self.padding.left = value;
842        self.padding.right = value;
843        self
844    }
845
846    /// Set vertical padding (top and bottom).
847    pub fn py(mut self, value: u32) -> Self {
848        self.padding.top = value;
849        self.padding.bottom = value;
850        self
851    }
852
853    /// Set top padding.
854    pub fn pt(mut self, value: u32) -> Self {
855        self.padding.top = value;
856        self
857    }
858
859    /// Set right padding.
860    pub fn pr(mut self, value: u32) -> Self {
861        self.padding.right = value;
862        self
863    }
864
865    /// Set bottom padding.
866    pub fn pb(mut self, value: u32) -> Self {
867        self.padding.bottom = value;
868        self
869    }
870
871    /// Set left padding.
872    pub fn pl(mut self, value: u32) -> Self {
873        self.padding.left = value;
874        self
875    }
876
877    /// Set per-side padding using a [`Padding`] value.
878    pub fn padding(mut self, padding: Padding) -> Self {
879        self.padding = padding;
880        self
881    }
882
883    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
884
885    /// Set uniform margin on all sides.
886    pub fn m(mut self, value: u32) -> Self {
887        self.margin = Margin::all(value);
888        self
889    }
890
891    /// Set horizontal margin (left and right).
892    pub fn mx(mut self, value: u32) -> Self {
893        self.margin.left = value;
894        self.margin.right = value;
895        self
896    }
897
898    /// Set vertical margin (top and bottom).
899    pub fn my(mut self, value: u32) -> Self {
900        self.margin.top = value;
901        self.margin.bottom = value;
902        self
903    }
904
905    /// Set top margin.
906    pub fn mt(mut self, value: u32) -> Self {
907        self.margin.top = value;
908        self
909    }
910
911    /// Set right margin.
912    pub fn mr(mut self, value: u32) -> Self {
913        self.margin.right = value;
914        self
915    }
916
917    /// Set bottom margin.
918    pub fn mb(mut self, value: u32) -> Self {
919        self.margin.bottom = value;
920        self
921    }
922
923    /// Set left margin.
924    pub fn ml(mut self, value: u32) -> Self {
925        self.margin.left = value;
926        self
927    }
928
929    /// Set per-side margin using a [`Margin`] value.
930    pub fn margin(mut self, margin: Margin) -> Self {
931        self.margin = margin;
932        self
933    }
934
935    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
936
937    /// Set a fixed width (sets both min and max width).
938    pub fn w(mut self, value: u32) -> Self {
939        self.constraints.min_width = Some(value);
940        self.constraints.max_width = Some(value);
941        self
942    }
943
944    define_breakpoint_methods!(
945        base = w,
946        arg = value: u32,
947        xs = xs_w => [
948            "Width applied only at Xs breakpoint (< 40 cols).",
949            "",
950            "# Example",
951            "```ignore",
952            "ui.container().w(20).md_w(40).lg_w(60).col(|ui| { ... });",
953            "```"
954        ],
955        sm = sm_w => ["Width applied only at Sm breakpoint (40-79 cols)."],
956        md = md_w => ["Width applied only at Md breakpoint (80-119 cols)."],
957        lg = lg_w => ["Width applied only at Lg breakpoint (120-159 cols)."],
958        xl = xl_w => ["Width applied only at Xl breakpoint (>= 160 cols)."],
959        at = w_at => ["Width applied only at the given breakpoint."]
960    );
961
962    /// Set a fixed height (sets both min and max height).
963    pub fn h(mut self, value: u32) -> Self {
964        self.constraints.min_height = Some(value);
965        self.constraints.max_height = Some(value);
966        self
967    }
968
969    define_breakpoint_methods!(
970        base = h,
971        arg = value: u32,
972        xs = xs_h => ["Height applied only at Xs breakpoint (< 40 cols)."],
973        sm = sm_h => ["Height applied only at Sm breakpoint (40-79 cols)."],
974        md = md_h => ["Height applied only at Md breakpoint (80-119 cols)."],
975        lg = lg_h => ["Height applied only at Lg breakpoint (120-159 cols)."],
976        xl = xl_h => ["Height applied only at Xl breakpoint (>= 160 cols)."],
977        at = h_at => ["Height applied only at the given breakpoint."]
978    );
979
980    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
981    pub fn min_w(mut self, value: u32) -> Self {
982        self.constraints.min_width = Some(value);
983        self
984    }
985
986    define_breakpoint_methods!(
987        base = min_w,
988        arg = value: u32,
989        xs = xs_min_w => ["Minimum width applied only at Xs breakpoint (< 40 cols)."],
990        sm = sm_min_w => ["Minimum width applied only at Sm breakpoint (40-79 cols)."],
991        md = md_min_w => ["Minimum width applied only at Md breakpoint (80-119 cols)."],
992        lg = lg_min_w => ["Minimum width applied only at Lg breakpoint (120-159 cols)."],
993        xl = xl_min_w => ["Minimum width applied only at Xl breakpoint (>= 160 cols)."],
994        at = min_w_at => ["Minimum width applied only at the given breakpoint."]
995    );
996
997    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
998    pub fn max_w(mut self, value: u32) -> Self {
999        self.constraints.max_width = Some(value);
1000        self
1001    }
1002
1003    define_breakpoint_methods!(
1004        base = max_w,
1005        arg = value: u32,
1006        xs = xs_max_w => ["Maximum width applied only at Xs breakpoint (< 40 cols)."],
1007        sm = sm_max_w => ["Maximum width applied only at Sm breakpoint (40-79 cols)."],
1008        md = md_max_w => ["Maximum width applied only at Md breakpoint (80-119 cols)."],
1009        lg = lg_max_w => ["Maximum width applied only at Lg breakpoint (120-159 cols)."],
1010        xl = xl_max_w => ["Maximum width applied only at Xl breakpoint (>= 160 cols)."],
1011        at = max_w_at => ["Maximum width applied only at the given breakpoint."]
1012    );
1013
1014    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
1015    pub fn min_h(mut self, value: u32) -> Self {
1016        self.constraints.min_height = Some(value);
1017        self
1018    }
1019
1020    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
1021    pub fn max_h(mut self, value: u32) -> Self {
1022        self.constraints.max_height = Some(value);
1023        self
1024    }
1025
1026    /// Set the minimum width constraint in cells.
1027    pub fn min_width(mut self, value: u32) -> Self {
1028        self.constraints.min_width = Some(value);
1029        self
1030    }
1031
1032    /// Set the maximum width constraint in cells.
1033    pub fn max_width(mut self, value: u32) -> Self {
1034        self.constraints.max_width = Some(value);
1035        self
1036    }
1037
1038    /// Set the minimum height constraint in rows.
1039    pub fn min_height(mut self, value: u32) -> Self {
1040        self.constraints.min_height = Some(value);
1041        self
1042    }
1043
1044    /// Set the maximum height constraint in rows.
1045    pub fn max_height(mut self, value: u32) -> Self {
1046        self.constraints.max_height = Some(value);
1047        self
1048    }
1049
1050    /// Set width as a percentage (1-100) of the parent container.
1051    pub fn w_pct(mut self, pct: u8) -> Self {
1052        self.constraints.width_pct = Some(pct.min(100));
1053        self
1054    }
1055
1056    /// Set height as a percentage (1-100) of the parent container.
1057    pub fn h_pct(mut self, pct: u8) -> Self {
1058        self.constraints.height_pct = Some(pct.min(100));
1059        self
1060    }
1061
1062    /// Set all size constraints at once using a [`Constraints`] value.
1063    pub fn constraints(mut self, constraints: Constraints) -> Self {
1064        self.constraints = constraints;
1065        self
1066    }
1067
1068    // ── flex ─────────────────────────────────────────────────────────
1069
1070    /// Set the gap (in cells) between child elements.
1071    pub fn gap(mut self, gap: u32) -> Self {
1072        self.gap = gap;
1073        self
1074    }
1075
1076    /// Set the gap between children for column layouts (vertical spacing).
1077    /// Overrides `.gap()` when finalized with `.col()`.
1078    pub fn row_gap(mut self, value: u32) -> Self {
1079        self.row_gap = Some(value);
1080        self
1081    }
1082
1083    /// Set the gap between children for row layouts (horizontal spacing).
1084    /// Overrides `.gap()` when finalized with `.row()`.
1085    pub fn col_gap(mut self, value: u32) -> Self {
1086        self.col_gap = Some(value);
1087        self
1088    }
1089
1090    define_breakpoint_methods!(
1091        base = gap,
1092        arg = value: u32,
1093        xs = xs_gap => ["Gap applied only at Xs breakpoint (< 40 cols)."],
1094        sm = sm_gap => ["Gap applied only at Sm breakpoint (40-79 cols)."],
1095        md = md_gap => [
1096            "Gap applied only at Md breakpoint (80-119 cols).",
1097            "",
1098            "# Example",
1099            "```ignore",
1100            "ui.container().gap(0).md_gap(2).col(|ui| { ... });",
1101            "```"
1102        ],
1103        lg = lg_gap => ["Gap applied only at Lg breakpoint (120-159 cols)."],
1104        xl = xl_gap => ["Gap applied only at Xl breakpoint (>= 160 cols)."],
1105        at = gap_at => ["Gap applied only at the given breakpoint."]
1106    );
1107
1108    /// Set the flex-grow factor. `1` means the container expands to fill available space.
1109    pub fn grow(mut self, grow: u16) -> Self {
1110        self.grow = grow;
1111        self
1112    }
1113
1114    define_breakpoint_methods!(
1115        base = grow,
1116        arg = value: u16,
1117        xs = xs_grow => ["Grow factor applied only at Xs breakpoint (< 40 cols)."],
1118        sm = sm_grow => ["Grow factor applied only at Sm breakpoint (40-79 cols)."],
1119        md = md_grow => ["Grow factor applied only at Md breakpoint (80-119 cols)."],
1120        lg = lg_grow => ["Grow factor applied only at Lg breakpoint (120-159 cols)."],
1121        xl = xl_grow => ["Grow factor applied only at Xl breakpoint (>= 160 cols)."],
1122        at = grow_at => ["Grow factor applied only at the given breakpoint."]
1123    );
1124
1125    define_breakpoint_methods!(
1126        base = p,
1127        arg = value: u32,
1128        xs = xs_p => ["Uniform padding applied only at Xs breakpoint (< 40 cols)."],
1129        sm = sm_p => ["Uniform padding applied only at Sm breakpoint (40-79 cols)."],
1130        md = md_p => ["Uniform padding applied only at Md breakpoint (80-119 cols)."],
1131        lg = lg_p => ["Uniform padding applied only at Lg breakpoint (120-159 cols)."],
1132        xl = xl_p => ["Uniform padding applied only at Xl breakpoint (>= 160 cols)."],
1133        at = p_at => ["Padding applied only at the given breakpoint."]
1134    );
1135
1136    // ── alignment ───────────────────────────────────────────────────
1137
1138    /// Set the cross-axis alignment of child elements.
1139    pub fn align(mut self, align: Align) -> Self {
1140        self.align = align;
1141        self
1142    }
1143
1144    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
1145    pub fn center(self) -> Self {
1146        self.align(Align::Center)
1147    }
1148
1149    /// Set the main-axis content distribution mode.
1150    pub fn justify(mut self, justify: Justify) -> Self {
1151        self.justify = justify;
1152        self
1153    }
1154
1155    /// Distribute children with equal space between; first at start, last at end.
1156    pub fn space_between(self) -> Self {
1157        self.justify(Justify::SpaceBetween)
1158    }
1159
1160    /// Distribute children with equal space around each child.
1161    pub fn space_around(self) -> Self {
1162        self.justify(Justify::SpaceAround)
1163    }
1164
1165    /// Distribute children with equal space between all children and edges.
1166    pub fn space_evenly(self) -> Self {
1167        self.justify(Justify::SpaceEvenly)
1168    }
1169
1170    /// Center children on both axes. Shorthand for `.justify(Justify::Center).align(Align::Center)`.
1171    pub fn flex_center(self) -> Self {
1172        self.justify(Justify::Center).align(Align::Center)
1173    }
1174
1175    /// Override the parent's cross-axis alignment for this container only.
1176    /// Like CSS `align-self`.
1177    pub fn align_self(mut self, align: Align) -> Self {
1178        self.align_self_value = Some(align);
1179        self
1180    }
1181
1182    // ── title ────────────────────────────────────────────────────────
1183
1184    /// Set a plain-text title rendered in the top border.
1185    pub fn title(self, title: impl Into<String>) -> Self {
1186        self.title_styled(title, Style::new())
1187    }
1188
1189    /// Set a styled title rendered in the top border.
1190    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1191        self.title = Some((title.into(), style));
1192        self
1193    }
1194
1195    // ── conditional / grouped builder helpers ───────────────────────
1196
1197    /// Apply `f` only if `cond` is true. Returns the builder for chaining.
1198    ///
1199    /// Use this to attach a block of builder modifiers without breaking the
1200    /// fluent chain. The closure takes the builder by value and must return
1201    /// it (matching the rest of `ContainerBuilder`'s by-value API), so any
1202    /// builder method (`.border()`, `.title()`, `.bg()`, etc.) can be chained
1203    /// inside.
1204    ///
1205    /// Zero allocation: the closure is inlined and skipped entirely when
1206    /// `cond` is `false`.
1207    ///
1208    /// # Example
1209    ///
1210    /// ```no_run
1211    /// # slt::run(|ui: &mut slt::Context| {
1212    /// use slt::Border;
1213    /// let highlighted = true;
1214    /// ui.container()
1215    ///     .pad(1)
1216    ///     .with_if(highlighted, |c| c.border(Border::Single).title("Active"))
1217    ///     .col(|ui| {
1218    ///         ui.text("body");
1219    ///     });
1220    /// # });
1221    /// ```
1222    pub fn with_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
1223        if cond {
1224            f(self)
1225        } else {
1226            self
1227        }
1228    }
1229
1230    /// Apply `f` unconditionally. Useful for factoring out a block of builder
1231    /// modifier calls while keeping the fluent chain intact.
1232    ///
1233    /// The closure takes the builder by value and must return it.
1234    ///
1235    /// # Example
1236    ///
1237    /// ```no_run
1238    /// # slt::run(|ui: &mut slt::Context| {
1239    /// use slt::Border;
1240    /// ui.container()
1241    ///     .with(|c| c.border(Border::Rounded).pad(1))
1242    ///     .col(|ui| {
1243    ///         ui.text("body");
1244    ///     });
1245    /// # });
1246    /// ```
1247    pub fn with(self, f: impl FnOnce(Self) -> Self) -> Self {
1248        f(self)
1249    }
1250
1251    // ── internal ─────────────────────────────────────────────────────
1252
1253    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
1254    ///
1255    /// This is a crate-internal helper; external callers should use
1256    /// [`Context::scrollable`] together with a [`ScrollState`].
1257    ///
1258    /// [`ScrollState`]: crate::widgets::ScrollState
1259    pub fn scroll_offset(mut self, offset: u32) -> Self {
1260        self.scroll_offset = Some(offset);
1261        self
1262    }
1263
1264    /// Internal entry point that takes an already-shared `Arc<str>`.
1265    ///
1266    /// Used by `Context::group()` so the name allocated in the public path
1267    /// is pushed onto `group_stack` and threaded into `BeginContainerArgs`
1268    /// through a single `Arc::clone` instead of two `String` allocations.
1269    /// Closes #145 (double `to_string`) and completes the `Arc<str>`
1270    /// migration in #139.
1271    pub(crate) fn group_name_arc(mut self, name: std::sync::Arc<str>) -> Self {
1272        self.group_name = Some(name);
1273        self
1274    }
1275
1276    /// Finalize the builder as a vertical (column) container.
1277    ///
1278    /// The closure receives a `&mut Context` for rendering children.
1279    /// Returns a [`Response`] with click/hover state for this container.
1280    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1281        self.finish(Direction::Column, f)
1282    }
1283
1284    /// Finalize the builder as a horizontal (row) container.
1285    ///
1286    /// The closure receives a `&mut Context` for rendering children.
1287    /// Returns a [`Response`] with click/hover state for this container.
1288    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1289        self.finish(Direction::Row, f)
1290    }
1291
1292    /// Finalize the builder as an inline text line.
1293    ///
1294    /// Like [`row`](ContainerBuilder::row) but gap is forced to zero
1295    /// for seamless inline rendering of mixed-style text.
1296    pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1297        self.gap = 0;
1298        self.finish(Direction::Row, f)
1299    }
1300
1301    /// Finalize the builder as a raw-draw region with direct buffer access.
1302    ///
1303    /// The closure receives `(&mut Buffer, Rect)` after layout is computed.
1304    /// Use `buf.set_char()`, `buf.set_string()`, `buf.get_mut()` to write
1305    /// directly into the terminal buffer. Writes outside `rect` are clipped.
1306    ///
1307    /// The closure must be `'static` because it is deferred until after layout.
1308    /// To capture local data, clone or move it into the closure:
1309    /// ```ignore
1310    /// let data = my_vec.clone();
1311    /// ui.container().w(40).h(20).draw(move |buf, rect| {
1312    ///     // use `data` here
1313    /// });
1314    /// ```
1315    pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1316        let draw_id = self.ctx.deferred_draws.len();
1317        self.ctx.deferred_draws.push(Some(Box::new(f)));
1318        self.ctx.skip_interaction_slot();
1319        self.ctx.commands.push(Command::RawDraw {
1320            draw_id,
1321            constraints: self.constraints,
1322            grow: self.grow,
1323            margin: self.margin,
1324        });
1325    }
1326
1327    /// Like [`draw`](Self::draw), but carries owned per-frame `data` through
1328    /// to the deferred closure as a borrow.
1329    ///
1330    /// Raw-draw closures must be `'static` because they run after layout is
1331    /// computed — which normally forces callers to snapshot any borrowed
1332    /// state into an owned value before passing it in. `draw_with` makes
1333    /// that explicit: hand the snapshot over, borrow it inside the closure.
1334    ///
1335    /// # Example
1336    ///
1337    /// ```no_run
1338    /// # use slt::{Buffer, Rect, Style};
1339    /// # slt::run(|ui: &mut slt::Context| {
1340    /// let points: Vec<(u32, u32)> = (0..20).map(|i| (i, i * 2)).collect();
1341    /// ui.container().w(40).h(20).draw_with(points, |buf, rect, points| {
1342    ///     for (x, y) in points {
1343    ///         if rect.contains(*x, *y) {
1344    ///             buf.set_char(*x, *y, '●', Style::new());
1345    ///         }
1346    ///     }
1347    /// });
1348    /// # });
1349    /// ```
1350    pub fn draw_with<D: 'static>(
1351        self,
1352        data: D,
1353        f: impl FnOnce(&mut crate::buffer::Buffer, Rect, &D) + 'static,
1354    ) {
1355        let draw_id = self.ctx.deferred_draws.len();
1356        self.ctx
1357            .deferred_draws
1358            .push(Some(Box::new(move |buf, rect| f(buf, rect, &data))));
1359        self.ctx.skip_interaction_slot();
1360        self.ctx.commands.push(Command::RawDraw {
1361            draw_id,
1362            constraints: self.constraints,
1363            grow: self.grow,
1364            margin: self.margin,
1365        });
1366    }
1367
1368    /// Custom drawing with click and hover detection.
1369    ///
1370    /// Like [`draw`](Self::draw), but the returned [`Response`] reports
1371    /// `clicked` and `hovered` based on the laid-out region — exactly like
1372    /// `.col()` or `.row()`.
1373    ///
1374    /// # Example
1375    ///
1376    /// ```no_run
1377    /// # slt::run(|ui: &mut slt::Context| {
1378    /// let resp = ui.container()
1379    ///     .w(40).h(10)
1380    ///     .draw_interactive(|buf, rect| {
1381    ///         buf.set_string(rect.x, rect.y, "Click me!", slt::Style::new());
1382    ///     });
1383    /// if resp.clicked {
1384    ///     // handle click
1385    /// }
1386    /// # });
1387    /// ```
1388    pub fn draw_interactive(
1389        self,
1390        f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static,
1391    ) -> Response {
1392        let draw_id = self.ctx.deferred_draws.len();
1393        self.ctx.deferred_draws.push(Some(Box::new(f)));
1394        let interaction_id = self.ctx.next_interaction_id();
1395        self.ctx.commands.push(Command::RawDraw {
1396            draw_id,
1397            constraints: self.constraints,
1398            grow: self.grow,
1399            margin: self.margin,
1400        });
1401        self.ctx.response_for(interaction_id)
1402    }
1403
1404    fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1405        let interaction_id = self.ctx.next_interaction_id();
1406        let resolved_gap = match direction {
1407            Direction::Column => self.row_gap.unwrap_or(self.gap),
1408            Direction::Row => self.col_gap.unwrap_or(self.gap),
1409        };
1410
1411        let in_hovered_group = self
1412            .group_name
1413            .as_ref()
1414            .map(|name| self.ctx.is_group_hovered(name))
1415            .unwrap_or(false)
1416            || self
1417                .ctx
1418                .rollback
1419                .group_stack
1420                .last()
1421                .map(|name| self.ctx.is_group_hovered(name))
1422                .unwrap_or(false);
1423        let in_focused_group = self
1424            .group_name
1425            .as_ref()
1426            .map(|name| self.ctx.is_group_focused(name))
1427            .unwrap_or(false)
1428            || self
1429                .ctx
1430                .rollback
1431                .group_stack
1432                .last()
1433                .map(|name| self.ctx.is_group_focused(name))
1434                .unwrap_or(false);
1435
1436        let resolved_bg = if self.ctx.rollback.dark_mode {
1437            self.dark_bg.or(self.bg)
1438        } else {
1439            self.bg
1440        };
1441        let resolved_border_style = if self.ctx.rollback.dark_mode {
1442            self.dark_border_style.unwrap_or(self.border_style)
1443        } else {
1444            self.border_style
1445        };
1446        let bg_color = if in_hovered_group || in_focused_group {
1447            self.group_hover_bg.or(resolved_bg)
1448        } else {
1449            resolved_bg
1450        };
1451        let border_style = if in_hovered_group || in_focused_group {
1452            self.group_hover_border_style
1453                .unwrap_or(resolved_border_style)
1454        } else {
1455            resolved_border_style
1456        };
1457        let group_name = self.group_name.take();
1458        let is_group_container = group_name.is_some();
1459
1460        if let Some(scroll_offset) = self.scroll_offset {
1461            self.ctx
1462                .commands
1463                .push(Command::BeginScrollable(Box::new(BeginScrollableArgs {
1464                    grow: self.grow,
1465                    border: self.border,
1466                    border_sides: self.border_sides,
1467                    border_style,
1468                    bg_color,
1469                    align: self.align,
1470                    align_self: self.align_self_value,
1471                    justify: self.justify,
1472                    gap: resolved_gap,
1473                    padding: self.padding,
1474                    margin: self.margin,
1475                    constraints: self.constraints,
1476                    title: self.title,
1477                    scroll_offset,
1478                    group_name,
1479                })));
1480        } else {
1481            self.ctx
1482                .commands
1483                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
1484                    direction,
1485                    gap: resolved_gap,
1486                    align: self.align,
1487                    align_self: self.align_self_value,
1488                    justify: self.justify,
1489                    border: self.border,
1490                    border_sides: self.border_sides,
1491                    border_style,
1492                    bg_color,
1493                    padding: self.padding,
1494                    margin: self.margin,
1495                    constraints: self.constraints,
1496                    title: self.title,
1497                    grow: self.grow,
1498                    group_name,
1499                })));
1500        }
1501        self.ctx.rollback.text_color_stack.push(self.text_color);
1502        f(self.ctx);
1503        self.ctx.rollback.text_color_stack.pop();
1504        self.ctx.commands.push(Command::EndContainer);
1505        self.ctx.rollback.last_text_idx = None;
1506
1507        if is_group_container {
1508            self.ctx.rollback.group_stack.pop();
1509            self.ctx.rollback.group_count = self.ctx.rollback.group_count.saturating_sub(1);
1510        }
1511
1512        self.ctx.response_for(interaction_id)
1513    }
1514}
1515
1516#[cfg(test)]
1517mod hotfix_tests {
1518    //! Regression tests for v0.19.1 A3 hotfixes (issues #143, #144, #146, #149).
1519
1520    use super::*;
1521
1522    // -- #143: filled_triangle stack-array intersections ----------------
1523
1524    /// Filling a triangle must paint the same pixel set whether the
1525    /// previous Vec<f64> path or the new inline-array path is used.
1526    #[test]
1527    fn filled_triangle_paints_expected_interior() {
1528        let mut canvas = CanvasContext::new(20, 20);
1529        canvas.filled_triangle(2, 2, 18, 4, 6, 18);
1530
1531        // Sample a point that must be filled (lies clearly inside the
1532        // triangle) and a point that must remain empty.
1533        let lines = canvas.render();
1534        // Pixel (8, 8) -> char cell (4, 2). Pull bits via re-render fallback.
1535        let inside_row = 8 / 4;
1536        let outside_row = 0;
1537        // Each row must be present in the rendered output.
1538        assert!(lines.len() > inside_row);
1539        assert!(lines.len() > outside_row);
1540
1541        // Inside row must contain at least one non-blank braille glyph.
1542        let inside: String = lines[inside_row].iter().map(|(s, _)| s.as_str()).collect();
1543        assert!(
1544            inside.chars().any(|c| c != '\u{2800}' && c != ' '),
1545            "expected filled glyphs inside triangle, got: {inside:?}"
1546        );
1547    }
1548
1549    /// Tall triangles previously allocated O(H) Vecs; the new path must
1550    /// still produce filled output for many scanlines without panicking.
1551    #[test]
1552    fn filled_triangle_handles_tall_triangle_without_panic() {
1553        let mut canvas = CanvasContext::new(8, 50);
1554        canvas.filled_triangle(0, 0, 15, 0, 8, 199);
1555        let lines = canvas.render();
1556        assert_eq!(lines.len(), 50);
1557    }
1558
1559    /// Degenerate horizontal triangle (all three vertices on the same row)
1560    /// must not panic and must produce no fill (only the outline edges).
1561    #[test]
1562    fn filled_triangle_degenerate_horizontal_is_safe() {
1563        let mut canvas = CanvasContext::new(20, 20);
1564        canvas.filled_triangle(0, 0, 10, 0, 19, 0);
1565        let _ = canvas.render();
1566    }
1567
1568    // -- #146: integer isqrt for filled_circle -------------------------
1569
1570    #[test]
1571    fn isqrt_i64_matches_floor_sqrt_for_small_values() {
1572        for n in 0i64..=10_000 {
1573            let expected = (n as f64).sqrt().floor() as isize;
1574            assert_eq!(isqrt_i64(n), expected, "mismatch at n={n}");
1575        }
1576    }
1577
1578    #[test]
1579    fn isqrt_i64_handles_perfect_squares_and_boundaries() {
1580        for k in 0i64..=4096 {
1581            assert_eq!(isqrt_i64(k * k), k as isize);
1582            if k > 0 {
1583                assert_eq!(isqrt_i64(k * k - 1), (k - 1) as isize);
1584            }
1585        }
1586    }
1587
1588    #[test]
1589    fn isqrt_i64_clamps_non_positive_to_zero() {
1590        assert_eq!(isqrt_i64(0), 0);
1591        assert_eq!(isqrt_i64(-1), 0);
1592        assert_eq!(isqrt_i64(i64::MIN), 0);
1593    }
1594
1595    /// `filled_circle` should produce a symmetric span around its center
1596    /// after switching from f64 sqrt to integer isqrt.
1597    #[test]
1598    fn filled_circle_renders_without_panic_and_is_non_empty() {
1599        let mut canvas = CanvasContext::new(20, 20);
1600        canvas.filled_circle(10, 10, 6);
1601        let lines = canvas.render();
1602        let any_filled = lines
1603            .iter()
1604            .flatten()
1605            .any(|(s, _)| s.chars().any(|c| c != '\u{2800}' && c != ' '));
1606        assert!(any_filled, "filled_circle produced empty output");
1607    }
1608
1609    // -- #149: scroll_offset visibility (compile-time check) -----------
1610
1611    /// The crate-internal `scroll_offset` helper must remain callable
1612    /// from inside the crate. Resolving the function path under `pub(crate)`
1613    /// is a compile-time guarantee — this test compiles only when the path
1614    /// is reachable.
1615    #[test]
1616    fn scroll_offset_is_crate_internal_api() {
1617        let _ = ContainerBuilder::scroll_offset;
1618    }
1619}