Skip to main content

slt/context/
container.rs

1use super::*;
2
3#[inline]
4fn saturating_gap(value: u32) -> i32 {
5    value.min(i32::MAX as u32) as i32
6}
7
8#[inline]
9fn saturating_overlap(overlap: u32) -> i32 {
10    -saturating_gap(overlap)
11}
12
13/// Options for [`Context::modal_with`].
14///
15/// Controls focus behavior when a modal overlay is active.
16///
17/// # Example
18///
19/// ```no_run
20/// # let mut show = true;
21/// # slt::run(|ui: &mut slt::Context| {
22/// if show {
23///     ui.modal_with(slt::context::ModalOptions { tab_trap: true }, |ui| {
24///         ui.text("Are you sure?");
25///         if ui.button("OK").clicked { show = false; }
26///     });
27/// }
28/// # });
29/// ```
30#[derive(Debug, Clone, Copy)]
31pub struct ModalOptions {
32    /// When `true`, Tab/Shift+Tab navigation cannot leave the modal's focus
33    /// range, even if [`Context::set_focus_index`] or a mouse click moved
34    /// focus outside.
35    ///
36    /// Default: `true` — aligned with WCAG 2.1 SC 2.4.3 (Focus Order),
37    /// which recommends trapping focus inside modal dialogs.
38    ///
39    /// Set to `false` to preserve the legacy behavior where focus could
40    /// escape via programmatic means.
41    pub tab_trap: bool,
42}
43
44impl Default for ModalOptions {
45    fn default() -> Self {
46        Self { tab_trap: true }
47    }
48}
49
50/// Fluent builder for configuring containers before calling `.col()` or `.row()`.
51///
52/// Obtain one via [`Context::container`] or [`Context::bordered`]. Chain the
53/// configuration methods you need, then finalize with `.col(|ui| { ... })` or
54/// `.row(|ui| { ... })`.
55///
56/// # Example
57///
58/// ```no_run
59/// # slt::run(|ui: &mut slt::Context| {
60/// use slt::{Border, Color};
61/// ui.container()
62///     .border(Border::Rounded)
63///     .p(1)
64///     .grow(1)
65///     .col(|ui| {
66///         ui.text("inside a bordered, padded, growing column");
67///     });
68/// # });
69/// ```
70#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
71pub struct ContainerBuilder<'a> {
72    pub(crate) ctx: &'a mut Context,
73    /// Resolved main-axis gap, in cells. Signed (#222): negative means
74    /// adjacent children overlap, set via [`ContainerBuilder::gap_overlap`].
75    /// The public [`ContainerBuilder::gap`] setter takes `u32` and is
76    /// source-compatible; only `gap_overlap` can store a negative value.
77    pub(crate) gap: i32,
78    pub(crate) row_gap: Option<u32>,
79    pub(crate) col_gap: Option<u32>,
80    pub(crate) align: Align,
81    pub(crate) align_self_value: Option<Align>,
82    pub(crate) justify: Justify,
83    pub(crate) border: Option<Border>,
84    pub(crate) border_sides: BorderSides,
85    pub(crate) border_style: Style,
86    pub(crate) bg: Option<Color>,
87    pub(crate) text_color: Option<Color>,
88    pub(crate) dark_bg: Option<Color>,
89    pub(crate) dark_border_style: Option<Style>,
90    pub(crate) group_hover_bg: Option<Color>,
91    pub(crate) group_hover_border_style: Option<Style>,
92    pub(crate) group_name: Option<std::sync::Arc<str>>,
93    pub(crate) padding: Padding,
94    pub(crate) margin: Margin,
95    pub(crate) constraints: Constraints,
96    pub(crate) title: Option<(String, Style)>,
97    pub(crate) grow: u16,
98    /// Opt-in flex-shrink flag. Set via [`ContainerBuilder::shrink`].
99    ///
100    /// When `true`, this container participates in proportional shrinking
101    /// if its parent row/column overflows. Default `false` keeps the
102    /// historic overflow-by-design behavior. Closes #161.
103    pub(crate) shrink_flag: bool,
104    /// Opt-in container-level flex-wrap flag. Set via
105    /// [`ContainerBuilder::wrap`].
106    ///
107    /// When `true` on a row, children that overflow the available width flow
108    /// onto subsequent lines instead of overflowing past the right edge.
109    /// Default `false` keeps the historic single-line behavior. No-op on a
110    /// column. Closes #258.
111    pub(crate) wrap_flag: bool,
112    /// Optional flex-basis (initial main-axis size, in cells). Set via
113    /// [`ContainerBuilder::basis`]. `None` (default) falls back to the
114    /// child's min size, preserving current behavior. Closes #258.
115    pub(crate) basis: Option<u32>,
116    pub(crate) scroll_offset: Option<u32>,
117    /// Horizontal scroll offset for a scrollable row (#247). Set internally by
118    /// [`crate::Context::scrollable`] from `ScrollState::offset_x`; carried into
119    /// `BeginScrollableArgs` and applied by the tree builder only when the
120    /// finalizing direction is `Direction::Row`.
121    pub(crate) scroll_offset_x: Option<u32>,
122    pub(crate) theme_override: Option<Theme>,
123}
124
125/// Drawing context for the [`Context::canvas`] widget.
126///
127/// Provides pixel-level drawing on a braille character grid. Each terminal
128/// cell maps to a 2x4 dot matrix, so a canvas of `width` columns x `height`
129/// rows gives `width*2` x `height*4` pixel resolution.
130/// A colored pixel in the canvas grid.
131#[derive(Debug, Clone, Copy)]
132struct CanvasPixel {
133    bits: u32,
134    color: Color,
135}
136
137/// Text label placed on the canvas.
138#[derive(Debug, Clone)]
139struct CanvasLabel {
140    x: usize,
141    y: usize,
142    text: String,
143    color: Color,
144}
145
146/// A layer in the canvas, supporting z-ordering.
147#[derive(Debug, Clone)]
148struct CanvasLayer {
149    grid: Vec<Vec<CanvasPixel>>,
150    labels: Vec<CanvasLabel>,
151}
152
153/// Drawing context for the canvas widget.
154pub struct CanvasContext {
155    layers: Vec<CanvasLayer>,
156    cols: usize,
157    rows: usize,
158    px_w: usize,
159    px_h: usize,
160    current_color: Color,
161    /// Flat scratch buffer for `render()` pixel composition.
162    /// Capacity = `cols * rows`; flat index = `row * cols + col`.
163    scratch_pixels: Vec<CanvasPixel>,
164    /// Flat scratch buffer for `render()` label overlay.
165    /// Capacity = `cols * rows`; flat index = `row * cols + col`.
166    scratch_labels: Vec<Option<(String, Color)>>,
167}
168
169/// Integer square root for non-negative `i64` values, returning `isize`.
170///
171/// Uses the standard integer square root available below the crate's MSRV.
172#[inline]
173fn isqrt_i64(n: i64) -> isize {
174    u64::try_from(n).map_or(0, |value| value.isqrt() as isize)
175}
176
177impl CanvasContext {
178    pub(crate) fn new(cols: usize, rows: usize) -> Self {
179        let cell_count = cols.saturating_mul(rows);
180        Self {
181            layers: vec![Self::new_layer(cols, rows)],
182            cols,
183            rows,
184            px_w: cols * 2,
185            px_h: rows * 4,
186            current_color: Color::Reset,
187            scratch_pixels: vec![
188                CanvasPixel {
189                    bits: 0,
190                    color: Color::Reset,
191                };
192                cell_count
193            ],
194            scratch_labels: vec![None; cell_count],
195        }
196    }
197
198    fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
199        CanvasLayer {
200            grid: vec![
201                vec![
202                    CanvasPixel {
203                        bits: 0,
204                        color: Color::Reset,
205                    };
206                    cols
207                ];
208                rows
209            ],
210            labels: Vec::new(),
211        }
212    }
213
214    fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
215        self.layers.last_mut()
216    }
217
218    fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
219        if x >= self.px_w || y >= self.px_h {
220            return;
221        }
222
223        let char_col = x / 2;
224        let char_row = y / 4;
225        let sub_col = x % 2;
226        let sub_row = y % 4;
227        const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
228        const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
229
230        let bit = if sub_col == 0 {
231            LEFT_BITS[sub_row]
232        } else {
233            RIGHT_BITS[sub_row]
234        };
235
236        if let Some(layer) = self.current_layer_mut() {
237            let cell = &mut layer.grid[char_row][char_col];
238            let new_bits = cell.bits | bit;
239            if new_bits != cell.bits {
240                cell.bits = new_bits;
241                cell.color = color;
242            }
243        }
244    }
245
246    fn dot_isize(&mut self, x: isize, y: isize) {
247        if x >= 0 && y >= 0 {
248            self.dot(x as usize, y as usize);
249        }
250    }
251
252    /// Get the pixel width of the canvas.
253    pub fn width(&self) -> usize {
254        self.px_w
255    }
256
257    /// Get the pixel height of the canvas.
258    pub fn height(&self) -> usize {
259        self.px_h
260    }
261
262    /// Set a single pixel at `(x, y)`.
263    pub fn dot(&mut self, x: usize, y: usize) {
264        self.dot_with_color(x, y, self.current_color);
265    }
266
267    /// Draw a line from `(x0, y0)` to `(x1, y1)` using Bresenham's algorithm.
268    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
269        let (mut x, mut y) = (x0 as isize, y0 as isize);
270        let (x1, y1) = (x1 as isize, y1 as isize);
271        let dx = (x1 - x).abs();
272        let dy = -(y1 - y).abs();
273        let sx = if x < x1 { 1 } else { -1 };
274        let sy = if y < y1 { 1 } else { -1 };
275        let mut err = dx + dy;
276
277        loop {
278            self.dot_isize(x, y);
279            if x == x1 && y == y1 {
280                break;
281            }
282            let e2 = 2 * err;
283            if e2 >= dy {
284                err += dy;
285                x += sx;
286            }
287            if e2 <= dx {
288                err += dx;
289                y += sy;
290            }
291        }
292    }
293
294    /// Draw a rectangle outline from `(x, y)` with `w` width and `h` height.
295    pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
296        if w == 0 || h == 0 {
297            return;
298        }
299
300        self.line(x, y, x + w.saturating_sub(1), y);
301        self.line(
302            x + w.saturating_sub(1),
303            y,
304            x + w.saturating_sub(1),
305            y + h.saturating_sub(1),
306        );
307        self.line(
308            x + w.saturating_sub(1),
309            y + h.saturating_sub(1),
310            x,
311            y + h.saturating_sub(1),
312        );
313        self.line(x, y + h.saturating_sub(1), x, y);
314    }
315
316    /// Draw a circle outline centered at `(cx, cy)` with radius `r`.
317    pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
318        let mut x = r as isize;
319        let mut y: isize = 0;
320        let mut err: isize = 1 - x;
321        let (cx, cy) = (cx as isize, cy as isize);
322
323        while x >= y {
324            for &(dx, dy) in &[
325                (x, y),
326                (y, x),
327                (-x, y),
328                (-y, x),
329                (x, -y),
330                (y, -x),
331                (-x, -y),
332                (-y, -x),
333            ] {
334                let px = cx + dx;
335                let py = cy + dy;
336                self.dot_isize(px, py);
337            }
338
339            y += 1;
340            if err < 0 {
341                err += 2 * y + 1;
342            } else {
343                x -= 1;
344                err += 2 * (y - x) + 1;
345            }
346        }
347    }
348
349    /// Set the drawing color for subsequent shapes.
350    pub fn set_color(&mut self, color: Color) {
351        self.current_color = color;
352    }
353
354    /// Get the current drawing color.
355    pub fn color(&self) -> Color {
356        self.current_color
357    }
358
359    /// Draw a filled rectangle.
360    pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
361        if w == 0 || h == 0 {
362            return;
363        }
364
365        let x_end = x.saturating_add(w).min(self.px_w);
366        let y_end = y.saturating_add(h).min(self.px_h);
367        if x >= x_end || y >= y_end {
368            return;
369        }
370
371        for yy in y..y_end {
372            self.line(x, yy, x_end.saturating_sub(1), yy);
373        }
374    }
375
376    /// Draw a filled circle.
377    pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
378        let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
379        for y in (cy - r)..=(cy + r) {
380            let dy = y - cy;
381            let span_sq = (r * r - dy * dy).max(0);
382            let dx = isqrt_i64(span_sq as i64);
383            for x in (cx - dx)..=(cx + dx) {
384                self.dot_isize(x, y);
385            }
386        }
387    }
388
389    /// Draw a triangle outline.
390    pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
391        self.line(x0, y0, x1, y1);
392        self.line(x1, y1, x2, y2);
393        self.line(x2, y2, x0, y0);
394    }
395
396    /// Draw a filled triangle.
397    pub fn filled_triangle(
398        &mut self,
399        x0: usize,
400        y0: usize,
401        x1: usize,
402        y1: usize,
403        x2: usize,
404        y2: usize,
405    ) {
406        let vertices = [
407            (x0 as isize, y0 as isize),
408            (x1 as isize, y1 as isize),
409            (x2 as isize, y2 as isize),
410        ];
411        let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
412        let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
413
414        for y in min_y..=max_y {
415            // A triangle has exactly 3 edges -> at most 3 intersections per
416            // scanline. A 4-element stack array avoids per-scanline heap
417            // allocations from the previous Vec<f64>.
418            let mut intersections = [0.0f64; 4];
419            let mut isect_count = 0usize;
420
421            for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
422                let (x_a, y_a) = vertices[edge.0];
423                let (x_b, y_b) = vertices[edge.1];
424                if y_a == y_b {
425                    continue;
426                }
427
428                let (x_start, y_start, x_end, y_end) = if y_a < y_b {
429                    (x_a, y_a, x_b, y_b)
430                } else {
431                    (x_b, y_b, x_a, y_a)
432                };
433
434                if y < y_start || y >= y_end {
435                    continue;
436                }
437
438                let t = (y - y_start) as f64 / (y_end - y_start) as f64;
439                if isect_count < intersections.len() {
440                    intersections[isect_count] = x_start as f64 + t * (x_end - x_start) as f64;
441                    isect_count += 1;
442                }
443            }
444
445            intersections[..isect_count].sort_by(|a, b| a.total_cmp(b));
446            let mut i = 0usize;
447            while i + 1 < isect_count {
448                let x_start = intersections[i].ceil() as isize;
449                let x_end = intersections[i + 1].floor() as isize;
450                for x in x_start..=x_end {
451                    self.dot_isize(x, y);
452                }
453                i += 2;
454            }
455        }
456
457        self.triangle(x0, y0, x1, y1, x2, y2);
458    }
459
460    /// Draw multiple points at once.
461    pub fn points(&mut self, pts: &[(usize, usize)]) {
462        for &(x, y) in pts {
463            self.dot(x, y);
464        }
465    }
466
467    /// Draw a polyline connecting the given points in order.
468    pub fn polyline(&mut self, pts: &[(usize, usize)]) {
469        for window in pts.windows(2) {
470            if let [(x0, y0), (x1, y1)] = window {
471                self.line(*x0, *y0, *x1, *y1);
472            }
473        }
474    }
475
476    /// Place a text label at pixel position `(x, y)`.
477    /// Text is rendered in regular characters overlaying the braille grid.
478    pub fn print(&mut self, x: usize, y: usize, text: &str) {
479        if text.is_empty() {
480            return;
481        }
482
483        let color = self.current_color;
484        if let Some(layer) = self.current_layer_mut() {
485            layer.labels.push(CanvasLabel {
486                x,
487                y,
488                text: text.to_string(),
489                color,
490            });
491        }
492    }
493
494    /// Start a new drawing layer. Shapes on later layers overlay earlier ones.
495    pub fn layer(&mut self) {
496        self.layers.push(Self::new_layer(self.cols, self.rows));
497    }
498
499    pub(crate) fn render(&mut self) -> Vec<Vec<(String, Color)>> {
500        let cell_count = self.cols.saturating_mul(self.rows);
501
502        // Reset reusable scratch buffers, growing them only if `cols`/`rows`
503        // changed since construction. `fill` keeps the existing allocation.
504        if self.scratch_pixels.len() < cell_count {
505            self.scratch_pixels.resize(
506                cell_count,
507                CanvasPixel {
508                    bits: 0,
509                    color: Color::Reset,
510                },
511            );
512        }
513        if self.scratch_labels.len() < cell_count {
514            self.scratch_labels.resize(cell_count, None);
515        }
516        for px in &mut self.scratch_pixels[..cell_count] {
517            *px = CanvasPixel {
518                bits: 0,
519                color: Color::Reset,
520            };
521        }
522        for slot in &mut self.scratch_labels[..cell_count] {
523            *slot = None;
524        }
525
526        let cols = self.cols;
527        let rows = self.rows;
528
529        for layer in &self.layers {
530            for (row, src_row) in layer.grid.iter().enumerate().take(rows) {
531                let row_offset = row * cols;
532                for (col, src) in src_row.iter().enumerate().take(cols) {
533                    if src.bits == 0 {
534                        continue;
535                    }
536                    let dst = &mut self.scratch_pixels[row_offset + col];
537                    let merged = dst.bits | src.bits;
538                    if merged != dst.bits {
539                        dst.bits = merged;
540                        dst.color = src.color;
541                    }
542                }
543            }
544
545            for label in &layer.labels {
546                let row = label.y / 4;
547                if row >= rows {
548                    continue;
549                }
550                let mut col = label.x / 2;
551                let row_offset = row * cols;
552                for grapheme in label.text.graphemes(true) {
553                    if col >= cols {
554                        break;
555                    }
556                    let width = UnicodeWidthStr::width(grapheme).max(1);
557                    if width > cols - col {
558                        break;
559                    }
560                    self.scratch_labels[row_offset + col] =
561                        Some((grapheme.to_string(), label.color));
562                    for continuation in 1..width {
563                        self.scratch_labels[row_offset + col + continuation] =
564                            Some((String::new(), label.color));
565                    }
566                    col += width;
567                }
568            }
569        }
570
571        let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(rows);
572        for row in 0..rows {
573            let row_offset = row * cols;
574            let mut segments: Vec<(String, Color)> = Vec::new();
575            let mut current_color: Option<Color> = None;
576            let mut current_text = String::new();
577
578            for col in 0..cols {
579                let idx = row_offset + col;
580                let (label, pixel_ch, color) =
581                    if let Some((label, label_color)) = &self.scratch_labels[idx] {
582                        if label.is_empty() {
583                            continue;
584                        }
585                        (Some(label.as_str()), None, *label_color)
586                    } else {
587                        let pixel = self.scratch_pixels[idx];
588                        let ch = char::from_u32(0x2800 + pixel.bits).unwrap_or(' ');
589                        (None, Some(ch), pixel.color)
590                    };
591                let append_symbol = |text: &mut String| {
592                    if let Some(label) = label {
593                        text.push_str(label);
594                    } else if let Some(ch) = pixel_ch {
595                        text.push(ch);
596                    }
597                };
598
599                match current_color {
600                    Some(c) if c == color => {
601                        append_symbol(&mut current_text);
602                    }
603                    Some(c) => {
604                        segments.push((std::mem::take(&mut current_text), c));
605                        append_symbol(&mut current_text);
606                        current_color = Some(color);
607                    }
608                    None => {
609                        append_symbol(&mut current_text);
610                        current_color = Some(color);
611                    }
612                }
613            }
614
615            if let Some(color) = current_color {
616                segments.push((current_text, color));
617            }
618            lines.push(segments);
619        }
620
621        lines
622    }
623}
624
625macro_rules! define_breakpoint_methods {
626    (
627        base = $base:ident,
628        arg = $arg:ident : $arg_ty:ty,
629        xs = $xs_fn:ident => [$( $xs_doc:literal ),* $(,)?],
630        sm = $sm_fn:ident => [$( $sm_doc:literal ),* $(,)?],
631        md = $md_fn:ident => [$( $md_doc:literal ),* $(,)?],
632        lg = $lg_fn:ident => [$( $lg_doc:literal ),* $(,)?],
633        xl = $xl_fn:ident => [$( $xl_doc:literal ),* $(,)?],
634        at = $at_fn:ident => [$( $at_doc:literal ),* $(,)?]
635    ) => {
636        $(#[doc = $xs_doc])*
637        pub fn $xs_fn(self, $arg: $arg_ty) -> Self {
638            if self.ctx.breakpoint() == Breakpoint::Xs {
639                self.$base($arg)
640            } else {
641                self
642            }
643        }
644
645        $(#[doc = $sm_doc])*
646        pub fn $sm_fn(self, $arg: $arg_ty) -> Self {
647            if self.ctx.breakpoint() == Breakpoint::Sm {
648                self.$base($arg)
649            } else {
650                self
651            }
652        }
653
654        $(#[doc = $md_doc])*
655        pub fn $md_fn(self, $arg: $arg_ty) -> Self {
656            if self.ctx.breakpoint() == Breakpoint::Md {
657                self.$base($arg)
658            } else {
659                self
660            }
661        }
662
663        $(#[doc = $lg_doc])*
664        pub fn $lg_fn(self, $arg: $arg_ty) -> Self {
665            if self.ctx.breakpoint() == Breakpoint::Lg {
666                self.$base($arg)
667            } else {
668                self
669            }
670        }
671
672        $(#[doc = $xl_doc])*
673        pub fn $xl_fn(self, $arg: $arg_ty) -> Self {
674            if self.ctx.breakpoint() == Breakpoint::Xl {
675                self.$base($arg)
676            } else {
677                self
678            }
679        }
680
681        $(#[doc = $at_doc])*
682        pub fn $at_fn(self, bp: Breakpoint, $arg: $arg_ty) -> Self {
683            if self.ctx.breakpoint() == bp {
684                self.$base($arg)
685            } else {
686                self
687            }
688        }
689    };
690}
691
692impl<'a> ContainerBuilder<'a> {
693    // ── border ───────────────────────────────────────────────────────
694
695    /// Apply a reusable [`ContainerStyle`] recipe. Only set fields override
696    /// the builder's current values. Chain multiple `.apply()` calls to compose.
697    ///
698    /// If the style has an [`ContainerStyle::extends`] base, the base is applied
699    /// first, then the style's own fields override.
700    ///
701    /// [`ThemeColor`] fields (`theme_bg`, `theme_text_color`, `theme_border_fg`)
702    /// are resolved against the active theme at apply time.
703    pub fn apply(mut self, style: &ContainerStyle) -> Self {
704        // Apply base style first if this style extends another
705        if let Some(base) = style.extends {
706            self = self.apply(base);
707        }
708        if let Some(v) = style.border {
709            self.border = Some(v);
710        }
711        if let Some(v) = style.border_sides {
712            self.border_sides = v;
713        }
714        if let Some(v) = style.border_style {
715            self.border_style = v;
716        }
717        if let Some(v) = style.bg {
718            self.bg = Some(v);
719        }
720        if let Some(v) = style.dark_bg {
721            self.dark_bg = Some(v);
722        }
723        if let Some(v) = style.dark_border_style {
724            self.dark_border_style = Some(v);
725        }
726        if let Some(v) = style.padding {
727            self.padding = v;
728        }
729        if let Some(v) = style.margin {
730            self.margin = v;
731        }
732        if let Some(v) = style.gap {
733            // `ContainerStyle::gap` stays `Option<u32>` (positive only); only
734            // `gap_overlap` produces a negative builder gap (#222).
735            self.gap = saturating_gap(v);
736        }
737        if let Some(v) = style.row_gap {
738            self.row_gap = Some(v);
739        }
740        if let Some(v) = style.col_gap {
741            self.col_gap = Some(v);
742        }
743        if let Some(v) = style.grow {
744            self.grow = v;
745        }
746        if let Some(v) = style.align {
747            self.align = v;
748        }
749        if let Some(v) = style.align_self {
750            self.align_self_value = Some(v);
751        }
752        if let Some(v) = style.justify {
753            self.justify = v;
754        }
755        if let Some(v) = style.text_color {
756            self.text_color = Some(v);
757        }
758        if let Some(w) = style.w {
759            self.constraints = self.constraints.w(w);
760        }
761        if let Some(h) = style.h {
762            self.constraints = self.constraints.h(h);
763        }
764        if let Some(v) = style.min_w {
765            self.constraints.set_min_width(Some(v));
766        }
767        if let Some(v) = style.max_w {
768            self.constraints.set_max_width(Some(v));
769        }
770        if let Some(v) = style.min_h {
771            self.constraints.set_min_height(Some(v));
772        }
773        if let Some(v) = style.max_h {
774            self.constraints.set_max_height(Some(v));
775        }
776        if let Some(v) = style.w_pct {
777            self.constraints.set_width_pct(Some(v));
778        }
779        if let Some(v) = style.h_pct {
780            self.constraints.set_height_pct(Some(v));
781        }
782        // Resolve ThemeColor fields against the active theme (overrides literal colors)
783        if let Some(tc) = style.theme_bg {
784            self.bg = Some(self.ctx.theme.resolve(tc));
785        }
786        if let Some(tc) = style.theme_text_color {
787            self.text_color = Some(self.ctx.theme.resolve(tc));
788        }
789        if let Some(tc) = style.theme_border_fg {
790            let color = self.ctx.theme.resolve(tc);
791            self.border_style = Style::new().fg(color);
792        }
793        self
794    }
795
796    /// Set the border style.
797    pub fn border(mut self, border: Border) -> Self {
798        self.border = Some(border);
799        self
800    }
801
802    /// Show or hide the top border.
803    pub fn border_top(mut self, show: bool) -> Self {
804        self.border_sides.top = show;
805        self
806    }
807
808    /// Show or hide the right border.
809    pub fn border_right(mut self, show: bool) -> Self {
810        self.border_sides.right = show;
811        self
812    }
813
814    /// Show or hide the bottom border.
815    pub fn border_bottom(mut self, show: bool) -> Self {
816        self.border_sides.bottom = show;
817        self
818    }
819
820    /// Show or hide the left border.
821    pub fn border_left(mut self, show: bool) -> Self {
822        self.border_sides.left = show;
823        self
824    }
825
826    /// Set which border sides are visible.
827    pub fn border_sides(mut self, sides: BorderSides) -> Self {
828        self.border_sides = sides;
829        self
830    }
831
832    /// Show only left and right borders. Shorthand for horizontal border sides.
833    pub fn border_x(self) -> Self {
834        self.border_sides(BorderSides {
835            top: false,
836            right: true,
837            bottom: false,
838            left: true,
839        })
840    }
841
842    /// Show only top and bottom borders. Shorthand for vertical border sides.
843    pub fn border_y(self) -> Self {
844        self.border_sides(BorderSides {
845            top: true,
846            right: false,
847            bottom: true,
848            left: false,
849        })
850    }
851
852    /// Set rounded border style. Shorthand for `.border(Border::Rounded)`.
853    pub fn rounded(self) -> Self {
854        self.border(Border::Rounded)
855    }
856
857    /// Set the style applied to the border characters.
858    pub fn border_style(mut self, style: Style) -> Self {
859        self.border_style = style;
860        self
861    }
862
863    /// Set the border foreground color.
864    pub fn border_fg(mut self, color: Color) -> Self {
865        self.border_style = self.border_style.fg(color);
866        self
867    }
868
869    /// Border style used when dark mode is active.
870    pub fn dark_border_style(mut self, style: Style) -> Self {
871        self.dark_border_style = Some(style);
872        self
873    }
874
875    /// Set the background color.
876    pub fn bg(mut self, color: Color) -> Self {
877        self.bg = Some(color);
878        self
879    }
880
881    /// Set the default text color for all child text elements in this container.
882    /// Individual `.fg()` calls on text elements will still override this.
883    pub fn text_color(mut self, color: Color) -> Self {
884        self.text_color = Some(color);
885        self
886    }
887
888    /// Background color used when dark mode is active.
889    pub fn dark_bg(mut self, color: Color) -> Self {
890        self.dark_bg = Some(color);
891        self
892    }
893
894    /// Background color applied when the parent group is hovered.
895    pub fn group_hover_bg(mut self, color: Color) -> Self {
896        self.group_hover_bg = Some(color);
897        self
898    }
899
900    /// Border style applied when the parent group is hovered.
901    pub fn group_hover_border_style(mut self, style: Style) -> Self {
902        self.group_hover_border_style = Some(style);
903        self
904    }
905
906    // ── padding (Tailwind: p, px, py, pt, pr, pb, pl) ───────────────
907
908    /// Set uniform padding on all sides.
909    pub fn p(mut self, value: u32) -> Self {
910        self.padding = Padding::all(value);
911        self
912    }
913
914    /// Set uniform padding on all sides. Deprecated alias for [`p`](Self::p).
915    #[deprecated(since = "0.20.0", note = "Use `p()` instead")]
916    pub fn pad(self, value: u32) -> Self {
917        self.p(value)
918    }
919
920    /// Set horizontal padding (left and right).
921    pub fn px(mut self, value: u32) -> Self {
922        self.padding.left = value;
923        self.padding.right = value;
924        self
925    }
926
927    /// Set vertical padding (top and bottom).
928    pub fn py(mut self, value: u32) -> Self {
929        self.padding.top = value;
930        self.padding.bottom = value;
931        self
932    }
933
934    /// Set top padding.
935    pub fn pt(mut self, value: u32) -> Self {
936        self.padding.top = value;
937        self
938    }
939
940    /// Set right padding.
941    pub fn pr(mut self, value: u32) -> Self {
942        self.padding.right = value;
943        self
944    }
945
946    /// Set bottom padding.
947    pub fn pb(mut self, value: u32) -> Self {
948        self.padding.bottom = value;
949        self
950    }
951
952    /// Set left padding.
953    pub fn pl(mut self, value: u32) -> Self {
954        self.padding.left = value;
955        self
956    }
957
958    /// Set per-side padding using a [`Padding`] value.
959    pub fn padding(mut self, padding: Padding) -> Self {
960        self.padding = padding;
961        self
962    }
963
964    // ── margin (Tailwind: m, mx, my, mt, mr, mb, ml) ────────────────
965
966    /// Set uniform margin on all sides.
967    pub fn m(mut self, value: u32) -> Self {
968        self.margin = Margin::all(value);
969        self
970    }
971
972    /// Set horizontal margin (left and right).
973    pub fn mx(mut self, value: u32) -> Self {
974        self.margin.left = value;
975        self.margin.right = value;
976        self
977    }
978
979    /// Set vertical margin (top and bottom).
980    pub fn my(mut self, value: u32) -> Self {
981        self.margin.top = value;
982        self.margin.bottom = value;
983        self
984    }
985
986    /// Set top margin.
987    pub fn mt(mut self, value: u32) -> Self {
988        self.margin.top = value;
989        self
990    }
991
992    /// Set right margin.
993    pub fn mr(mut self, value: u32) -> Self {
994        self.margin.right = value;
995        self
996    }
997
998    /// Set bottom margin.
999    pub fn mb(mut self, value: u32) -> Self {
1000        self.margin.bottom = value;
1001        self
1002    }
1003
1004    /// Set left margin.
1005    pub fn ml(mut self, value: u32) -> Self {
1006        self.margin.left = value;
1007        self
1008    }
1009
1010    /// Set per-side margin using a [`Margin`] value.
1011    pub fn margin(mut self, margin: Margin) -> Self {
1012        self.margin = margin;
1013        self
1014    }
1015
1016    // ── sizing (Tailwind: w, h, min-w, max-w, min-h, max-h) ────────
1017
1018    /// Set a fixed width (sets both min and max width).
1019    pub fn w(mut self, value: u32) -> Self {
1020        self.constraints = self.constraints.w(value);
1021        self
1022    }
1023
1024    define_breakpoint_methods!(
1025        base = w,
1026        arg = value: u32,
1027        xs = xs_w => [
1028            "Width applied only at Xs breakpoint (< 40 cols).",
1029            "",
1030            "# Example",
1031            "```ignore",
1032            "ui.container().w(20).md_w(40).lg_w(60).col(|ui| { ... });",
1033            "```"
1034        ],
1035        sm = sm_w => ["Width applied only at Sm breakpoint (40-79 cols)."],
1036        md = md_w => ["Width applied only at Md breakpoint (80-119 cols)."],
1037        lg = lg_w => ["Width applied only at Lg breakpoint (120-159 cols)."],
1038        xl = xl_w => ["Width applied only at Xl breakpoint (>= 160 cols)."],
1039        at = w_at => ["Width applied only at the given breakpoint."]
1040    );
1041
1042    /// Set a fixed height (sets both min and max height).
1043    pub fn h(mut self, value: u32) -> Self {
1044        self.constraints = self.constraints.h(value);
1045        self
1046    }
1047
1048    define_breakpoint_methods!(
1049        base = h,
1050        arg = value: u32,
1051        xs = xs_h => ["Height applied only at Xs breakpoint (< 40 cols)."],
1052        sm = sm_h => ["Height applied only at Sm breakpoint (40-79 cols)."],
1053        md = md_h => ["Height applied only at Md breakpoint (80-119 cols)."],
1054        lg = lg_h => ["Height applied only at Lg breakpoint (120-159 cols)."],
1055        xl = xl_h => ["Height applied only at Xl breakpoint (>= 160 cols)."],
1056        at = h_at => ["Height applied only at the given breakpoint."]
1057    );
1058
1059    /// Set the minimum width constraint. Shorthand for [`min_width`](Self::min_width).
1060    pub fn min_w(mut self, value: u32) -> Self {
1061        self.constraints.set_min_width(Some(value));
1062        self
1063    }
1064
1065    define_breakpoint_methods!(
1066        base = min_w,
1067        arg = value: u32,
1068        xs = xs_min_w => ["Minimum width applied only at Xs breakpoint (< 40 cols)."],
1069        sm = sm_min_w => ["Minimum width applied only at Sm breakpoint (40-79 cols)."],
1070        md = md_min_w => ["Minimum width applied only at Md breakpoint (80-119 cols)."],
1071        lg = lg_min_w => ["Minimum width applied only at Lg breakpoint (120-159 cols)."],
1072        xl = xl_min_w => ["Minimum width applied only at Xl breakpoint (>= 160 cols)."],
1073        at = min_w_at => ["Minimum width applied only at the given breakpoint."]
1074    );
1075
1076    /// Set the maximum width constraint. Shorthand for [`max_width`](Self::max_width).
1077    pub fn max_w(mut self, value: u32) -> Self {
1078        self.constraints.set_max_width(Some(value));
1079        self
1080    }
1081
1082    define_breakpoint_methods!(
1083        base = max_w,
1084        arg = value: u32,
1085        xs = xs_max_w => ["Maximum width applied only at Xs breakpoint (< 40 cols)."],
1086        sm = sm_max_w => ["Maximum width applied only at Sm breakpoint (40-79 cols)."],
1087        md = md_max_w => ["Maximum width applied only at Md breakpoint (80-119 cols)."],
1088        lg = lg_max_w => ["Maximum width applied only at Lg breakpoint (120-159 cols)."],
1089        xl = xl_max_w => ["Maximum width applied only at Xl breakpoint (>= 160 cols)."],
1090        at = max_w_at => ["Maximum width applied only at the given breakpoint."]
1091    );
1092
1093    /// Set the minimum height constraint. Shorthand for [`min_height`](Self::min_height).
1094    pub fn min_h(mut self, value: u32) -> Self {
1095        self.constraints.set_min_height(Some(value));
1096        self
1097    }
1098
1099    define_breakpoint_methods!(
1100        base = min_h,
1101        arg = value: u32,
1102        xs = xs_min_h => ["Minimum height applied only at Xs breakpoint (< 40 cols)."],
1103        sm = sm_min_h => ["Minimum height applied only at Sm breakpoint (40-79 cols)."],
1104        md = md_min_h => ["Minimum height applied only at Md breakpoint (80-119 cols)."],
1105        lg = lg_min_h => ["Minimum height applied only at Lg breakpoint (120-159 cols)."],
1106        xl = xl_min_h => ["Minimum height applied only at Xl breakpoint (>= 160 cols)."],
1107        at = min_h_at => ["Minimum height applied only at the given breakpoint."]
1108    );
1109
1110    /// Set the maximum height constraint. Shorthand for [`max_height`](Self::max_height).
1111    pub fn max_h(mut self, value: u32) -> Self {
1112        self.constraints.set_max_height(Some(value));
1113        self
1114    }
1115
1116    define_breakpoint_methods!(
1117        base = max_h,
1118        arg = value: u32,
1119        xs = xs_max_h => ["Maximum height applied only at Xs breakpoint (< 40 cols)."],
1120        sm = sm_max_h => ["Maximum height applied only at Sm breakpoint (40-79 cols)."],
1121        md = md_max_h => ["Maximum height applied only at Md breakpoint (80-119 cols)."],
1122        lg = lg_max_h => ["Maximum height applied only at Lg breakpoint (120-159 cols)."],
1123        xl = xl_max_h => ["Maximum height applied only at Xl breakpoint (>= 160 cols)."],
1124        at = max_h_at => ["Maximum height applied only at the given breakpoint."]
1125    );
1126
1127    /// Set the minimum width constraint in cells. Deprecated alias for [`min_w`](Self::min_w).
1128    #[deprecated(since = "0.20.0", note = "Use `min_w()` instead")]
1129    pub fn min_width(self, value: u32) -> Self {
1130        self.min_w(value)
1131    }
1132
1133    /// Set the maximum width constraint in cells. Deprecated alias for [`max_w`](Self::max_w).
1134    #[deprecated(since = "0.20.0", note = "Use `max_w()` instead")]
1135    pub fn max_width(self, value: u32) -> Self {
1136        self.max_w(value)
1137    }
1138
1139    /// Set the minimum height constraint in rows. Deprecated alias for [`min_h`](Self::min_h).
1140    #[deprecated(since = "0.20.0", note = "Use `min_h()` instead")]
1141    pub fn min_height(self, value: u32) -> Self {
1142        self.min_h(value)
1143    }
1144
1145    /// Set the maximum height constraint in rows. Deprecated alias for [`max_h`](Self::max_h).
1146    #[deprecated(since = "0.20.0", note = "Use `max_h()` instead")]
1147    pub fn max_height(self, value: u32) -> Self {
1148        self.max_h(value)
1149    }
1150
1151    /// Set width as a percentage (1-100) of the parent container.
1152    pub fn w_pct(mut self, pct: u8) -> Self {
1153        self.constraints.set_width_pct(Some(pct.min(100)));
1154        self
1155    }
1156
1157    /// Set height as a percentage (1-100) of the parent container.
1158    pub fn h_pct(mut self, pct: u8) -> Self {
1159        self.constraints.set_height_pct(Some(pct.min(100)));
1160        self
1161    }
1162
1163    /// Set all size constraints at once using a [`Constraints`] value.
1164    pub fn constraints(mut self, constraints: Constraints) -> Self {
1165        self.constraints = constraints;
1166        self
1167    }
1168
1169    // ── flex ─────────────────────────────────────────────────────────
1170
1171    /// Set the gap (in cells) between child elements.
1172    pub fn gap(mut self, gap: u32) -> Self {
1173        self.gap = saturating_gap(gap);
1174        self
1175    }
1176
1177    /// Set a *negative* gap, causing adjacent children to overlap by `overlap`
1178    /// cells on the main axis.
1179    ///
1180    /// This is SLT's analogue of ratatui's `Layout::spacing(-1)`. The common
1181    /// use is collapsing the duplicate border between two adjacent bordered
1182    /// panels: with `gap_overlap(1)` each panel's shared edge lands in the
1183    /// same column (row layout) or row (column layout), so the doubled border
1184    ///
1185    /// ```text
1186    /// ┌────┐┌────┐
1187    /// │    ││    │
1188    /// └────┘└────┘
1189    /// ```
1190    ///
1191    /// collapses to a single shared edge.
1192    ///
1193    /// `gap_overlap(0)` is identical to `gap(0)` (no overlap). It composes with
1194    /// the existing `gap` family: the last call wins, so call exactly one of
1195    /// `gap` / `gap_overlap` per builder.
1196    ///
1197    /// # Rendering note
1198    ///
1199    /// SLT does not (yet) merge the shared cells into junction glyphs (`┬`,
1200    /// `┼`, `┴`). When two bordered panels overlap, both write the shared
1201    /// column/row and the later panel's border character wins by buffer-diff
1202    /// order. To get a clean seam, give the panels compatible border styles or
1203    /// drop one panel's shared side (e.g. `border_sides` without the left edge).
1204    ///
1205    /// Large overlaps saturate gracefully — `gap_overlap(N)` past a child's
1206    /// extent never panics or wraps; positions clamp at 0.
1207    ///
1208    /// # Example
1209    ///
1210    /// ```no_run
1211    /// # slt::run(|ui: &mut slt::Context| {
1212    /// use slt::Border;
1213    /// // Two bordered panels sharing one border column.
1214    /// ui.container().gap_overlap(1).row(|ui| {
1215    ///     ui.bordered(Border::Single).w(10).col(|ui| {
1216    ///         ui.text("left");
1217    ///     });
1218    ///     ui.bordered(Border::Single).w(10).col(|ui| {
1219    ///         ui.text("right");
1220    ///     });
1221    /// });
1222    /// # });
1223    /// ```
1224    pub fn gap_overlap(mut self, overlap: u32) -> Self {
1225        self.gap = saturating_overlap(overlap);
1226        self
1227    }
1228
1229    /// Set the gap between children for column layouts (vertical spacing).
1230    /// Overrides `.gap()` when finalized with `.col()`.
1231    pub fn row_gap(mut self, value: u32) -> Self {
1232        self.row_gap = Some(value);
1233        self
1234    }
1235
1236    /// Set the gap between children for row layouts (horizontal spacing).
1237    /// Overrides `.gap()` when finalized with `.row()`.
1238    pub fn col_gap(mut self, value: u32) -> Self {
1239        self.col_gap = Some(value);
1240        self
1241    }
1242
1243    define_breakpoint_methods!(
1244        base = gap,
1245        arg = value: u32,
1246        xs = xs_gap => ["Gap applied only at Xs breakpoint (< 40 cols)."],
1247        sm = sm_gap => ["Gap applied only at Sm breakpoint (40-79 cols)."],
1248        md = md_gap => [
1249            "Gap applied only at Md breakpoint (80-119 cols).",
1250            "",
1251            "# Example",
1252            "```ignore",
1253            "ui.container().gap(0).md_gap(2).col(|ui| { ... });",
1254            "```"
1255        ],
1256        lg = lg_gap => ["Gap applied only at Lg breakpoint (120-159 cols)."],
1257        xl = xl_gap => ["Gap applied only at Xl breakpoint (>= 160 cols)."],
1258        at = gap_at => ["Gap applied only at the given breakpoint."]
1259    );
1260
1261    /// Set the flex-grow factor. `1` means the container expands to fill available space.
1262    pub fn grow(mut self, grow: u16) -> Self {
1263        self.grow = grow;
1264        self
1265    }
1266
1267    /// Expand to fill remaining space on the main axis. Shorthand for
1268    /// [`grow(1)`](Self::grow).
1269    ///
1270    /// Equivalent to CSS `flex: 1` and ratatui's `Constraint::Fill(1)`.
1271    /// This is the most common case in flex layouts and reads more
1272    /// naturally than `grow(1)` for new readers — the abstract "grow
1273    /// factor" terminology is replaced by a self-documenting verb.
1274    ///
1275    /// ```ignore
1276    /// ui.container().fill().col(|ui| { ... });
1277    /// // identical to:
1278    /// ui.container().grow(1).col(|ui| { ... });
1279    /// ```
1280    ///
1281    /// For other weights (e.g. a 2:1 split between two siblings), use
1282    /// `grow(N)` directly.
1283    pub fn fill(self) -> Self {
1284        self.grow(1)
1285    }
1286
1287    /// Opt this container into proportional flex-shrink.
1288    ///
1289    /// Marks this container as a shrink participant. When the parent
1290    /// row / column overflows (its children's combined width or height
1291    /// exceeds available space), shrink-flagged children scale their
1292    /// fixed sizes by `available / fixed_total` (CSS `flex-shrink`-style).
1293    /// Children without `.shrink()` keep their historic
1294    /// overflow-by-design size and clip naturally.
1295    ///
1296    /// Default for every container is `false` — opt in per child.
1297    /// Equivalent to CSS `flex-shrink: 1` (vs the SLT default of `0`).
1298    /// Closes #161.
1299    ///
1300    /// # Example
1301    ///
1302    /// Two siblings with combined fixed width `60` placed inside a
1303    /// `40`-cell row. Without `.shrink()`, the row overflows; with
1304    /// `.shrink()` on both, each scales to `40 * 30/60 = 20`:
1305    ///
1306    /// ```no_run
1307    /// # slt::run(|ui: &mut slt::Context| {
1308    /// // Without shrink — overflows the parent.
1309    /// ui.row(|ui| {
1310    ///     ui.container().w(30).col(|ui| { ui.text("left"); });
1311    ///     ui.container().w(30).col(|ui| { ui.text("right"); });
1312    /// });
1313    ///
1314    /// // With shrink on both — proportional fit, no clipping.
1315    /// ui.row(|ui| {
1316    ///     ui.container().w(30).shrink().col(|ui| { ui.text("left"); });
1317    ///     ui.container().w(30).shrink().col(|ui| { ui.text("right"); });
1318    /// });
1319    /// # });
1320    /// ```
1321    ///
1322    /// # Layout
1323    ///
1324    /// Only fixed-width children with `grow == 0` participate. Grow
1325    /// children already absorb leftover space and ignore the shrink
1326    /// flag. Mixing shrink and non-shrink siblings is supported — only
1327    /// the flagged ones contribute to the shrink budget.
1328    pub fn shrink(mut self) -> Self {
1329        self.shrink_flag = true;
1330        self
1331    }
1332
1333    /// Allow row children to wrap onto subsequent lines on main-axis overflow.
1334    ///
1335    /// When a `.row()` finalized with `wrap()` has children whose combined
1336    /// width exceeds the available width, the overflowing children flow onto
1337    /// the next line, and lines stack on the cross axis. This is the
1338    /// immediate-mode primitive for tag clouds, chip lists, wrapping toolbars,
1339    /// and responsive card grids that reflow as the terminal resizes — without
1340    /// per-frame breakpoint math. Equivalent to CSS `flex-wrap: wrap`.
1341    ///
1342    /// Spacing: within-line (main-axis) spacing uses `gap` / `col_gap` as
1343    /// usual; between-line (cross-axis) spacing uses `row_gap` when set, else
1344    /// `gap`. A child wider than the full available width occupies its own
1345    /// line (clipped, as a single-line row would clip) rather than producing
1346    /// an empty line.
1347    ///
1348    /// Row only. On `col()` this is a documented no-op (vertical-axis wrap is
1349    /// out of scope). Default: no wrap (single-line, current
1350    /// overflow-by-design behavior). Closes #258.
1351    ///
1352    /// # Example
1353    ///
1354    /// ```no_run
1355    /// # slt::run(|ui: &mut slt::Context| {
1356    /// // A chip list that reflows onto as many lines as the width needs.
1357    /// ui.container().wrap().gap(1).row(|ui| {
1358    ///     for tag in ["rust", "tui", "flexbox", "wrap", "immediate-mode"] {
1359    ///         ui.container().p(1).col(|ui| { ui.text(tag); });
1360    ///     }
1361    /// });
1362    /// # });
1363    /// ```
1364    #[doc(alias = "flex-wrap")]
1365    pub fn wrap(mut self) -> Self {
1366        self.wrap_flag = true;
1367        self
1368    }
1369
1370    /// Set the flex-basis: the initial main-axis size (in cells) that `grow`
1371    /// grows from and `shrink` (#161) shrinks from.
1372    ///
1373    /// CSS resolves flex sizing as `basis` (initial) → distribute free space
1374    /// by `grow` → distribute the deficit by `shrink`. By default SLT uses a
1375    /// child's min size as that base; `basis(n)` overrides it so a child can
1376    /// say "start at `n` cells, then grow / shrink from there". `None`
1377    /// (default, i.e. not calling this) falls back to the min size, preserving
1378    /// current behavior. Equivalent to CSS `flex-basis: <n>`. Closes #258.
1379    ///
1380    /// # Example
1381    ///
1382    /// ```no_run
1383    /// # slt::run(|ui: &mut slt::Context| {
1384    /// // Two cards that each start at 10 cells, then split the leftover.
1385    /// ui.row(|ui| {
1386    ///     ui.container().basis(10).grow(1).col(|ui| { ui.text("a"); });
1387    ///     ui.container().basis(10).grow(1).col(|ui| { ui.text("b"); });
1388    /// });
1389    /// # });
1390    /// ```
1391    #[doc(alias = "flex-basis")]
1392    pub fn basis(mut self, cells: u32) -> Self {
1393        self.basis = Some(cells);
1394        self
1395    }
1396
1397    define_breakpoint_methods!(
1398        base = grow,
1399        arg = value: u16,
1400        xs = xs_grow => ["Grow factor applied only at Xs breakpoint (< 40 cols)."],
1401        sm = sm_grow => ["Grow factor applied only at Sm breakpoint (40-79 cols)."],
1402        md = md_grow => ["Grow factor applied only at Md breakpoint (80-119 cols)."],
1403        lg = lg_grow => ["Grow factor applied only at Lg breakpoint (120-159 cols)."],
1404        xl = xl_grow => ["Grow factor applied only at Xl breakpoint (>= 160 cols)."],
1405        at = grow_at => ["Grow factor applied only at the given breakpoint."]
1406    );
1407
1408    define_breakpoint_methods!(
1409        base = p,
1410        arg = value: u32,
1411        xs = xs_p => ["Uniform padding applied only at Xs breakpoint (< 40 cols)."],
1412        sm = sm_p => ["Uniform padding applied only at Sm breakpoint (40-79 cols)."],
1413        md = md_p => ["Uniform padding applied only at Md breakpoint (80-119 cols)."],
1414        lg = lg_p => ["Uniform padding applied only at Lg breakpoint (120-159 cols)."],
1415        xl = xl_p => ["Uniform padding applied only at Xl breakpoint (>= 160 cols)."],
1416        at = p_at => ["Padding applied only at the given breakpoint."]
1417    );
1418
1419    // ── alignment ───────────────────────────────────────────────────
1420
1421    /// Set the cross-axis alignment of child elements.
1422    pub fn align(mut self, align: Align) -> Self {
1423        self.align = align;
1424        self
1425    }
1426
1427    /// Center children on the cross axis. Shorthand for `.align(Align::Center)`.
1428    pub fn center(self) -> Self {
1429        self.align(Align::Center)
1430    }
1431
1432    /// Set the main-axis content distribution mode.
1433    pub fn justify(mut self, justify: Justify) -> Self {
1434        self.justify = justify;
1435        self
1436    }
1437
1438    /// Distribute children with equal space between; first at start, last at end.
1439    pub fn space_between(self) -> Self {
1440        self.justify(Justify::SpaceBetween)
1441    }
1442
1443    /// Distribute children with equal space around each child.
1444    pub fn space_around(self) -> Self {
1445        self.justify(Justify::SpaceAround)
1446    }
1447
1448    /// Distribute children with equal space between all children and edges.
1449    pub fn space_evenly(self) -> Self {
1450        self.justify(Justify::SpaceEvenly)
1451    }
1452
1453    /// Center children on both axes. Shorthand for `.justify(Justify::Center).align(Align::Center)`.
1454    pub fn flex_center(self) -> Self {
1455        self.justify(Justify::Center).align(Align::Center)
1456    }
1457
1458    /// Override the parent's cross-axis alignment for this container only.
1459    /// Like CSS `align-self`.
1460    pub fn align_self(mut self, align: Align) -> Self {
1461        self.align_self_value = Some(align);
1462        self
1463    }
1464
1465    // ── title ────────────────────────────────────────────────────────
1466
1467    /// Set a plain-text title rendered in the top border.
1468    pub fn title(self, title: impl Into<String>) -> Self {
1469        self.title_styled(title, Style::new())
1470    }
1471
1472    /// Set a styled title rendered in the top border.
1473    pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1474        self.title = Some((title.into(), style));
1475        self
1476    }
1477
1478    // ── conditional / grouped builder helpers ───────────────────────
1479
1480    /// Apply `f` only if `cond` is true. Returns the builder for chaining.
1481    ///
1482    /// Use this to attach a block of builder modifiers without breaking the
1483    /// fluent chain. The closure takes the builder by value and must return
1484    /// it (matching the rest of `ContainerBuilder`'s by-value API), so any
1485    /// builder method (`.border()`, `.title()`, `.bg()`, etc.) can be chained
1486    /// inside.
1487    ///
1488    /// Zero allocation: the closure is inlined and skipped entirely when
1489    /// `cond` is `false`.
1490    ///
1491    /// # Example
1492    ///
1493    /// ```no_run
1494    /// # slt::run(|ui: &mut slt::Context| {
1495    /// use slt::Border;
1496    /// let highlighted = true;
1497    /// ui.container()
1498    ///     .p(1)
1499    ///     .with_if(highlighted, |c| c.border(Border::Single).title("Active"))
1500    ///     .col(|ui| {
1501    ///         ui.text("body");
1502    ///     });
1503    /// # });
1504    /// ```
1505    pub fn with_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
1506        if cond { f(self) } else { self }
1507    }
1508
1509    /// Override the active theme for all widgets rendered inside this container.
1510    ///
1511    /// The override is scoped to the container body (the closure passed to
1512    /// `.col()`, `.row()`, or `.line()`). The parent theme is restored when
1513    /// the container closes — including on panic.
1514    ///
1515    /// All built-in widgets read `ctx.theme` directly for color decisions,
1516    /// so this swap propagates through every nested widget without requiring
1517    /// them to opt in. Nested `.theme(...)` calls correctly nest: the
1518    /// innermost theme wins inside its own subtree, and the outer theme
1519    /// resumes once it closes.
1520    ///
1521    /// Independent of [`Context::provide`] / [`Context::use_context`] —
1522    /// this directly mutates the active theme used by SLT-owned widgets,
1523    /// while `provide`/`use_context` is the general-purpose context
1524    /// injection mechanism for user code.
1525    ///
1526    /// # Example
1527    ///
1528    /// ```no_run
1529    /// # slt::run(|ui: &mut slt::Context| {
1530    /// use slt::{Border, Theme};
1531    /// ui.container()
1532    ///     .theme(Theme::light())
1533    ///     .border(Border::Rounded)
1534    ///     .col(|ui| {
1535    ///         ui.text("This subtree renders with the light theme");
1536    ///         ui.button("Click me"); // also uses light theme colors
1537    ///     });
1538    /// # });
1539    /// ```
1540    pub fn theme(mut self, theme: Theme) -> Self {
1541        self.theme_override = Some(theme);
1542        self
1543    }
1544
1545    /// Apply `f` unconditionally. Useful for factoring out a block of builder
1546    /// modifier calls while keeping the fluent chain intact.
1547    ///
1548    /// The closure takes the builder by value and must return it.
1549    ///
1550    /// # Example
1551    ///
1552    /// ```no_run
1553    /// # slt::run(|ui: &mut slt::Context| {
1554    /// use slt::Border;
1555    /// ui.container()
1556    ///     .with(|c| c.border(Border::Rounded).p(1))
1557    ///     .col(|ui| {
1558    ///         ui.text("body");
1559    ///     });
1560    /// # });
1561    /// ```
1562    pub fn with(self, f: impl FnOnce(Self) -> Self) -> Self {
1563        f(self)
1564    }
1565
1566    // ── opt-in scoped cache (issue #273) ───────────────────────────────
1567
1568    /// Opt-in: declare a subtree **stable** when `version_key` is unchanged
1569    /// from the previous frame at this call site.
1570    ///
1571    /// This is an **author-controlled cache, not reactive binding**. Your
1572    /// closure is still the app ([Principle 2 — "Your Closure IS the App"]):
1573    /// `f` runs **every frame** exactly like `.col(f)`, so the rendered output
1574    /// is **byte-for-byte identical** to an uncached container — there is no
1575    /// retained widget identity, no message passing, no reactive subscription,
1576    /// and no behavior change whatsoever when you do not call `cached`.
1577    ///
1578    /// What `cached` adds is a single, principle-preserving signal: it records
1579    /// the `version_key` you supply (a value you already own — e.g. a hash of
1580    /// the non-streaming inputs, or `StreamingTextState::version` of the
1581    /// *other* panes) and compares it to the key this call site recorded last
1582    /// frame. A match is a *cache hit* (the subtree is declared unchanged); a
1583    /// change, a new call site, the first frame, or a terminal resize is a
1584    /// *miss*. The hit/miss tally is exposed via
1585    /// [`Context::region_cache_hits`](crate::Context::region_cache_hits) /
1586    /// [`Context::region_cache_misses`](crate::Context::region_cache_misses).
1587    ///
1588    /// # Why output is identical even on a hit (current implementation)
1589    ///
1590    /// Skipping `f` on a hit would require splicing the prior frame's recorded
1591    /// `Command`s, replaying its focus / hit-map / scroll / raw-draw feedback,
1592    /// and reusing its rendered cells — without that full replay the immediate-
1593    /// mode invariant breaks (focus and interaction would silently drop). That
1594    /// replay is deliberately **out of scope** here (it risks reintroducing a
1595    /// retained tree, the thing Principle 2 forbids). So `cached` keeps the
1596    /// invariant absolute — `f` always runs — and instead lands the *safe,
1597    /// reversible* half: a measured, author-keyed stability gate plus
1598    /// diagnostics. The streaming benchmark `bench_streaming_append_chat`
1599    /// (`benches/benchmarks.rs`) quantifies the upstream cost this gate is
1600    /// designed to eventually elide; see `docs/PERFORMANCE.md`.
1601    ///
1602    /// # Pattern: cache the chrome, not the stream
1603    ///
1604    /// During token streaming, wrap the *static* surroundings (chat history,
1605    /// sidebar, status bar) keyed off everything *except* the stream, and
1606    /// leave the stream itself uncached — it changes every token:
1607    ///
1608    /// ```no_run
1609    /// # slt::run(|ui: &mut slt::Context| {
1610    /// # let history_version = 3u64;
1611    /// # let mut stream = slt::StreamingTextState::new();
1612    /// ui.container().cached(history_version, |ui| {
1613    ///     ui.text("…long chat transcript…"); // unchanged this token
1614    /// });
1615    /// ui.streaming_text(&mut stream);         // changes every token
1616    /// # });
1617    /// ```
1618    ///
1619    /// [Principle 2 — "Your Closure IS the App"]: https://docs.rs/slt
1620    pub fn cached(self, version_key: u64, f: impl FnOnce(&mut Context)) -> Response {
1621        // Record the key / classify hit-vs-miss BEFORE running the body so the
1622        // declaration order (and thus the per-call-site slot index) matches
1623        // the order regions are authored, exactly like the hook cursor.
1624        let _hit = self.ctx.record_cached_region(version_key);
1625        // Always run the body: byte-identical output, immediate-mode invariant
1626        // preserved. `_hit` is the gate a future cell-level cache would use.
1627        self.col(f)
1628    }
1629
1630    // ── internal ─────────────────────────────────────────────────────
1631
1632    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
1633    ///
1634    /// This is a crate-internal helper; external callers should use
1635    /// [`Context::scrollable`] together with a [`ScrollState`].
1636    ///
1637    /// Hidden from rustdoc with `#[doc(hidden)]` so it does not appear in the
1638    /// public API surface, while remaining callable for backwards compatibility
1639    /// (cargo-semver-checks still tracks the symbol). Promote to `pub(crate)`
1640    /// at v1.0.
1641    ///
1642    /// [`ScrollState`]: crate::widgets::ScrollState
1643    #[doc(hidden)]
1644    pub fn scroll_offset(mut self, offset: u32) -> Self {
1645        self.scroll_offset = Some(offset);
1646        self
1647    }
1648
1649    /// Internal entry point that takes an already-shared `Arc<str>`.
1650    ///
1651    /// Used by `Context::group()` so the name allocated in the public path
1652    /// is pushed onto `group_stack` and threaded into `BeginContainerArgs`
1653    /// through a single `Arc::clone` instead of two `String` allocations.
1654    /// Closes #145 (double `to_string`) and completes the `Arc<str>`
1655    /// migration in #139.
1656    pub(crate) fn group_name_arc(mut self, name: std::sync::Arc<str>) -> Self {
1657        self.group_name = Some(name);
1658        self
1659    }
1660
1661    /// Finalize the builder as a vertical (column) container.
1662    ///
1663    /// The closure receives a `&mut Context` for rendering children.
1664    /// Returns a [`Response`] with click/hover state for this container.
1665    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1666        self.finish(Direction::Column, f)
1667    }
1668
1669    /// Finalize the builder as a horizontal (row) container.
1670    ///
1671    /// The closure receives a `&mut Context` for rendering children.
1672    /// Returns a [`Response`] with click/hover state for this container.
1673    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1674        self.finish(Direction::Row, f)
1675    }
1676
1677    /// Finalize the builder as an inline text line.
1678    ///
1679    /// Like [`row`](ContainerBuilder::row) but gap is forced to zero
1680    /// for seamless inline rendering of mixed-style text.
1681    pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1682        self.gap = 0;
1683        self.finish(Direction::Row, f)
1684    }
1685
1686    /// Finalize the builder as a raw-draw region with direct buffer access.
1687    ///
1688    /// The closure receives `(&mut Buffer, Rect)` after layout is computed.
1689    /// Use `buf.set_char()`, `buf.set_string()`, `buf.get_mut()` to write
1690    /// directly into the terminal buffer. Writes outside `rect` are clipped.
1691    ///
1692    /// The closure must be `'static` because it is deferred until after layout.
1693    /// To capture local data, clone or move it into the closure:
1694    /// ```ignore
1695    /// let data = my_vec.clone();
1696    /// ui.container().w(40).h(20).draw(move |buf, rect| {
1697    ///     // use `data` here
1698    /// });
1699    /// ```
1700    pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1701        let draw_id = self.ctx.deferred_draws.len();
1702        self.ctx.deferred_draws.push(Some(Box::new(f)));
1703        self.ctx.skip_interaction_slot();
1704        self.ctx.commands.push(Command::RawDraw {
1705            draw_id,
1706            constraints: self.constraints,
1707            grow: self.grow,
1708            margin: self.margin,
1709        });
1710    }
1711
1712    /// Like [`draw`](Self::draw), but carries owned per-frame `data` through
1713    /// to the deferred closure as a borrow.
1714    ///
1715    /// Raw-draw closures must be `'static` because they run after layout is
1716    /// computed — which normally forces callers to snapshot any borrowed
1717    /// state into an owned value before passing it in. `draw_with` makes
1718    /// that explicit: hand the snapshot over, borrow it inside the closure.
1719    ///
1720    /// # Example
1721    ///
1722    /// ```no_run
1723    /// # use slt::{Buffer, Rect, Style};
1724    /// # slt::run(|ui: &mut slt::Context| {
1725    /// let points: Vec<(u32, u32)> = (0..20).map(|i| (i, i * 2)).collect();
1726    /// ui.container().w(40).h(20).draw_with(points, |buf, rect, points| {
1727    ///     for (x, y) in points {
1728    ///         if rect.contains(*x, *y) {
1729    ///             buf.set_char(*x, *y, '●', Style::new());
1730    ///         }
1731    ///     }
1732    /// });
1733    /// # });
1734    /// ```
1735    pub fn draw_with<D: 'static>(
1736        self,
1737        data: D,
1738        f: impl FnOnce(&mut crate::buffer::Buffer, Rect, &D) + 'static,
1739    ) {
1740        let draw_id = self.ctx.deferred_draws.len();
1741        self.ctx
1742            .deferred_draws
1743            .push(Some(Box::new(move |buf, rect| f(buf, rect, &data))));
1744        self.ctx.skip_interaction_slot();
1745        self.ctx.commands.push(Command::RawDraw {
1746            draw_id,
1747            constraints: self.constraints,
1748            grow: self.grow,
1749            margin: self.margin,
1750        });
1751    }
1752
1753    /// Execute a borrowed-data draw closure immediately, then composite its
1754    /// owned cell snapshot after layout.
1755    ///
1756    /// Unlike [`draw`](Self::draw), `f` does not need to be `'static`: it runs
1757    /// before this method returns and may borrow local application state. The
1758    /// resulting source buffer is moved into the deferred layout callback.
1759    /// This is appropriate for cell-based custom drawing with known source
1760    /// dimensions; terminal protocol placements and raw escape sequences are
1761    /// intentionally not copied from the snapshot.
1762    ///
1763    /// # Errors
1764    ///
1765    /// Returns [`BufferError`](crate::buffer::BufferError) when the requested
1766    /// source geometry exceeds the configured cell/row budget.
1767    pub fn draw_precomputed(
1768        self,
1769        width: u32,
1770        height: u32,
1771        f: impl FnOnce(&mut crate::buffer::Buffer, Rect),
1772    ) -> Result<(), crate::buffer::BufferError> {
1773        let source_rect = Rect::new(0, 0, width, height);
1774        let mut source = crate::buffer::Buffer::try_empty(source_rect)?;
1775        f(&mut source, source_rect);
1776        self.draw(move |destination, rect| {
1777            let copy_width = source.area.width.min(rect.width);
1778            let copy_height = source.area.height.min(rect.height);
1779            for source_y in 0..copy_height {
1780                for source_x in 0..copy_width {
1781                    let Some(cell) = source.try_get(source_x, source_y) else {
1782                        continue;
1783                    };
1784                    if cell.is_continuation() {
1785                        continue;
1786                    }
1787                    let symbol = cell.normalized_symbol();
1788                    let x = rect.x.saturating_add(source_x);
1789                    let y = rect.y.saturating_add(source_y);
1790                    if let Some(url) = cell.hyperlink.as_deref() {
1791                        destination.set_string_linked(x, y, &symbol, cell.style, url);
1792                    } else {
1793                        destination.set_string(x, y, &symbol, cell.style);
1794                    }
1795                }
1796            }
1797        });
1798        Ok(())
1799    }
1800
1801    /// Finalize a raw-draw region with a render-stage panic fallback.
1802    ///
1803    /// Deferred draw callbacks execute after the normal Context tree has
1804    /// finished layout, so [`Context::error_boundary`] cannot safely rebuild
1805    /// its fallback at that stage. This method catches the draw panic inside
1806    /// the laid-out region and invokes `fallback` with the panic message while
1807    /// the same clip is active.
1808    pub fn draw_with_fallback(
1809        self,
1810        draw: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static,
1811        fallback: impl FnOnce(&mut crate::buffer::Buffer, Rect, &str) + 'static,
1812    ) {
1813        self.draw(move |buffer, rect| {
1814            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1815                draw(buffer, rect);
1816            }));
1817            if let Err(payload) = result {
1818                let message = if let Some(message) = payload.downcast_ref::<&str>() {
1819                    (*message).to_owned()
1820                } else if let Some(message) = payload.downcast_ref::<String>() {
1821                    message.clone()
1822                } else {
1823                    "draw callback panicked with a non-string payload".to_owned()
1824                };
1825                fallback(buffer, rect, &message);
1826            }
1827        });
1828    }
1829
1830    /// Custom drawing with click and hover detection.
1831    ///
1832    /// Like [`draw`](Self::draw), but the returned [`Response`] reports
1833    /// `clicked` and `hovered` based on the laid-out region — exactly like
1834    /// `.col()` or `.row()`.
1835    ///
1836    /// # Example
1837    ///
1838    /// ```no_run
1839    /// # slt::run(|ui: &mut slt::Context| {
1840    /// let resp = ui.container()
1841    ///     .w(40).h(10)
1842    ///     .draw_interactive(|buf, rect| {
1843    ///         buf.set_string(rect.x, rect.y, "Click me!", slt::Style::new());
1844    ///     });
1845    /// if resp.clicked {
1846    ///     // handle click
1847    /// }
1848    /// # });
1849    /// ```
1850    pub fn draw_interactive(
1851        self,
1852        f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static,
1853    ) -> Response {
1854        let draw_id = self.ctx.deferred_draws.len();
1855        self.ctx.deferred_draws.push(Some(Box::new(f)));
1856        let interaction_id = self.ctx.next_interaction_id();
1857        self.ctx.commands.push(Command::RawDraw {
1858            draw_id,
1859            constraints: self.constraints,
1860            grow: self.grow,
1861            margin: self.margin,
1862        });
1863        self.ctx.response_for(interaction_id)
1864    }
1865
1866    fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1867        let interaction_id = self.ctx.next_interaction_id();
1868        // `row_gap` / `col_gap` are `Option<u32>` (positive override); fall back
1869        // to the signed builder `gap`, which alone can carry an overlap (#222).
1870        let resolved_gap: i32 = match direction {
1871            Direction::Column => self.row_gap.map(saturating_gap).unwrap_or(self.gap),
1872            Direction::Row => self.col_gap.map(saturating_gap).unwrap_or(self.gap),
1873        };
1874        // Cross-axis (between-line) gap for a wrapping row (#258): `row_gap`
1875        // when set, else the builder `gap`. Only consulted by the layout pass
1876        // when this container is a wrapping `Direction::Row`.
1877        let resolved_cross_gap: i32 = self.row_gap.map(saturating_gap).unwrap_or(self.gap);
1878
1879        let in_hovered_group = self
1880            .group_name
1881            .as_ref()
1882            .map(|name| self.ctx.is_group_hovered(name))
1883            .unwrap_or(false)
1884            || self
1885                .ctx
1886                .rollback
1887                .group_stack
1888                .last()
1889                .map(|name| self.ctx.is_group_hovered(name))
1890                .unwrap_or(false);
1891        let in_focused_group = self
1892            .group_name
1893            .as_ref()
1894            .map(|name| self.ctx.is_group_focused(name))
1895            .unwrap_or(false)
1896            || self
1897                .ctx
1898                .rollback
1899                .group_stack
1900                .last()
1901                .map(|name| self.ctx.is_group_focused(name))
1902                .unwrap_or(false);
1903
1904        let resolved_bg = if self.ctx.rollback.dark_mode {
1905            self.dark_bg.or(self.bg)
1906        } else {
1907            self.bg
1908        };
1909        let resolved_border_style = if self.ctx.rollback.dark_mode {
1910            self.dark_border_style.unwrap_or(self.border_style)
1911        } else {
1912            self.border_style
1913        };
1914        let bg_color = if in_hovered_group || in_focused_group {
1915            self.group_hover_bg.or(resolved_bg)
1916        } else {
1917            resolved_bg
1918        };
1919        let border_style = if in_hovered_group || in_focused_group {
1920            self.group_hover_border_style
1921                .unwrap_or(resolved_border_style)
1922        } else {
1923            resolved_border_style
1924        };
1925        let group_name = self.group_name.take();
1926        let is_group_container = group_name.is_some();
1927
1928        // Opt-in flex-shrink (#161). Push a marker the layout pass picks up
1929        // and applies to the next `BeginContainer` / `BeginScrollable`,
1930        // mirroring the existing `FocusMarker` / `InteractionMarker` pattern.
1931        // This avoids touching every `BeginContainerArgs` construction site
1932        // across the widget modules — only `ContainerBuilder.shrink()`
1933        // emits the marker, and `LayoutNode::shrink` defaults to `false`.
1934        if self.shrink_flag {
1935            self.ctx.commands.push(Command::ShrinkMarker);
1936        }
1937
1938        // Opt-in flex-wrap / flex-basis (#258). Same marker pattern as shrink:
1939        // pushed just before the matching `Begin*`, picked up by the layout
1940        // pass and applied to the next node. Both default off / `None`, so
1941        // unflagged containers are byte-identical to pre-#258.
1942        if self.wrap_flag {
1943            self.ctx
1944                .commands
1945                .push(Command::WrapMarker(resolved_cross_gap));
1946        }
1947        if let Some(basis) = self.basis {
1948            self.ctx.commands.push(Command::BasisMarker(basis));
1949        }
1950
1951        if let Some(scroll_offset) = self.scroll_offset {
1952            // #247: carry the finalizing `.row()` / `.col()` direction and both
1953            // axis offsets. The tree builder applies the offset matching
1954            // `direction`; the cross-axis offset is `0` for a single-axis
1955            // scroller (the common case).
1956            self.ctx
1957                .commands
1958                .push(Command::BeginScrollable(Box::new(BeginScrollableArgs {
1959                    grow: self.grow,
1960                    direction,
1961                    border: self.border,
1962                    border_sides: self.border_sides,
1963                    border_style,
1964                    bg_color,
1965                    align: self.align,
1966                    align_self: self.align_self_value,
1967                    justify: self.justify,
1968                    gap: resolved_gap,
1969                    padding: self.padding,
1970                    margin: self.margin,
1971                    constraints: self.constraints,
1972                    title: self.title,
1973                    scroll_offset,
1974                    scroll_offset_x: self.scroll_offset_x.unwrap_or(0),
1975                    group_name,
1976                })));
1977        } else {
1978            self.ctx
1979                .commands
1980                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
1981                    direction,
1982                    gap: resolved_gap,
1983                    align: self.align,
1984                    align_self: self.align_self_value,
1985                    justify: self.justify,
1986                    border: self.border,
1987                    border_sides: self.border_sides,
1988                    border_style,
1989                    bg_color,
1990                    padding: self.padding,
1991                    margin: self.margin,
1992                    constraints: self.constraints,
1993                    title: self.title,
1994                    grow: self.grow,
1995                    group_name,
1996                })));
1997        }
1998        self.ctx.rollback.text_color_stack.push(self.text_color);
1999        // Swap active theme if a per-subtree override was requested.
2000        // The previous theme is restored after `f` returns — including on
2001        // panic, so no widget ever sees a leaked override theme.
2002        let theme_save = self.theme_override.map(|t| {
2003            let prev = self.ctx.theme;
2004            self.ctx.theme = t;
2005            // Also keep dark_mode flag in sync so `dark_*` style variants
2006            // resolve to the new theme's brightness, not the stale flag.
2007            self.ctx.rollback.dark_mode = t.is_dark;
2008            (prev, prev.is_dark)
2009        });
2010        // catch_unwind guards the restore path against panics inside `f`.
2011        // The overlay/group bookkeeping that follows assumes `theme` reflects
2012        // the parent scope, so we must restore before propagating the panic.
2013        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self.ctx)));
2014        if let Some((prev, prev_dark)) = theme_save {
2015            self.ctx.theme = prev;
2016            self.ctx.rollback.dark_mode = prev_dark;
2017        }
2018        self.ctx.rollback.text_color_stack.pop();
2019        self.ctx.commands.push(Command::EndContainer);
2020        self.ctx.rollback.last_text_idx = None;
2021        if let Err(panic) = result {
2022            std::panic::resume_unwind(panic);
2023        }
2024
2025        if is_group_container {
2026            self.ctx.rollback.group_stack.pop();
2027            self.ctx.rollback.group_count = self.ctx.rollback.group_count.saturating_sub(1);
2028        }
2029
2030        self.ctx.response_for(interaction_id)
2031    }
2032}
2033
2034#[cfg(test)]
2035mod hotfix_tests {
2036    //! Regression tests for v0.19.1 A3 hotfixes (issues #143, #144, #146, #149).
2037
2038    use super::*;
2039
2040    // -- #143: filled_triangle stack-array intersections ----------------
2041
2042    /// Filling a triangle must paint the same pixel set whether the
2043    /// previous Vec<f64> path or the new inline-array path is used.
2044    #[test]
2045    fn filled_triangle_paints_expected_interior() {
2046        let mut canvas = CanvasContext::new(20, 20);
2047        canvas.filled_triangle(2, 2, 18, 4, 6, 18);
2048
2049        // Sample a point that must be filled (lies clearly inside the
2050        // triangle) and a point that must remain empty.
2051        let lines = canvas.render();
2052        // Pixel (8, 8) -> char cell (4, 2). Pull bits via re-render fallback.
2053        let inside_row = 8 / 4;
2054        let outside_row = 0;
2055        // Each row must be present in the rendered output.
2056        assert!(lines.len() > inside_row);
2057        assert!(lines.len() > outside_row);
2058
2059        // Inside row must contain at least one non-blank braille glyph.
2060        let inside: String = lines[inside_row].iter().map(|(s, _)| s.as_str()).collect();
2061        assert!(
2062            inside.chars().any(|c| c != '\u{2800}' && c != ' '),
2063            "expected filled glyphs inside triangle, got: {inside:?}"
2064        );
2065    }
2066
2067    /// Tall triangles previously allocated O(H) Vecs; the new path must
2068    /// still produce filled output for many scanlines without panicking.
2069    #[test]
2070    fn filled_triangle_handles_tall_triangle_without_panic() {
2071        let mut canvas = CanvasContext::new(8, 50);
2072        canvas.filled_triangle(0, 0, 15, 0, 8, 199);
2073        let lines = canvas.render();
2074        assert_eq!(lines.len(), 50);
2075    }
2076
2077    /// Degenerate horizontal triangle (all three vertices on the same row)
2078    /// must not panic and must produce no fill (only the outline edges).
2079    #[test]
2080    fn filled_triangle_degenerate_horizontal_is_safe() {
2081        let mut canvas = CanvasContext::new(20, 20);
2082        canvas.filled_triangle(0, 0, 10, 0, 19, 0);
2083        let _ = canvas.render();
2084    }
2085
2086    // -- #146: integer isqrt for filled_circle -------------------------
2087
2088    #[test]
2089    fn isqrt_i64_matches_floor_sqrt_for_small_values() {
2090        for n in 0i64..=10_000 {
2091            let expected = (n as f64).sqrt().floor() as isize;
2092            assert_eq!(isqrt_i64(n), expected, "mismatch at n={n}");
2093        }
2094    }
2095
2096    #[test]
2097    fn isqrt_i64_handles_perfect_squares_and_boundaries() {
2098        for k in 0i64..=4096 {
2099            assert_eq!(isqrt_i64(k * k), k as isize);
2100            if k > 0 {
2101                assert_eq!(isqrt_i64(k * k - 1), (k - 1) as isize);
2102            }
2103        }
2104    }
2105
2106    #[test]
2107    fn isqrt_i64_clamps_non_positive_to_zero() {
2108        assert_eq!(isqrt_i64(0), 0);
2109        assert_eq!(isqrt_i64(-1), 0);
2110        assert_eq!(isqrt_i64(i64::MIN), 0);
2111    }
2112
2113    /// `filled_circle` should produce a symmetric span around its center
2114    /// after switching from f64 sqrt to integer isqrt.
2115    #[test]
2116    fn filled_circle_renders_without_panic_and_is_non_empty() {
2117        let mut canvas = CanvasContext::new(20, 20);
2118        canvas.filled_circle(10, 10, 6);
2119        let lines = canvas.render();
2120        let any_filled = lines
2121            .iter()
2122            .flatten()
2123            .any(|(s, _)| s.chars().any(|c| c != '\u{2800}' && c != ' '));
2124        assert!(any_filled, "filled_circle produced empty output");
2125    }
2126
2127    #[test]
2128    fn canvas_labels_respect_grapheme_cell_width() {
2129        let mut canvas = CanvasContext::new(4, 1);
2130        canvas.print(0, 0, "界A");
2131
2132        let lines = canvas.render();
2133        let rendered: String = lines[0].iter().map(|(text, _)| text.as_str()).collect();
2134        assert_eq!(rendered, "界A\u{2800}");
2135        assert_eq!(UnicodeWidthStr::width(rendered.as_str()), 4);
2136    }
2137
2138    #[test]
2139    fn canvas_labels_do_not_render_partial_wide_graphemes() {
2140        let mut canvas = CanvasContext::new(2, 1);
2141        canvas.print(2, 0, "A界");
2142
2143        let lines = canvas.render();
2144        let rendered: String = lines[0].iter().map(|(text, _)| text.as_str()).collect();
2145        assert_eq!(rendered, "\u{2800}A");
2146        assert_eq!(UnicodeWidthStr::width(rendered.as_str()), 2);
2147    }
2148
2149    // -- #149: scroll_offset visibility (compile-time check) -----------
2150
2151    /// The `scroll_offset` helper must remain callable from inside the crate.
2152    /// It is `#[doc(hidden)] pub` (Option B from the issue) so it is removed
2153    /// from rustdoc but still semver-tracked; this test compiles only when
2154    /// the path is reachable.
2155    #[test]
2156    fn scroll_offset_is_crate_internal_api() {
2157        let _ = ContainerBuilder::scroll_offset;
2158    }
2159
2160    #[test]
2161    fn draw_precomputed_accepts_borrowed_local_data() {
2162        let label = String::from("borrowed");
2163        let mut backend = crate::TestBackend::new(20, 3);
2164        backend.render(|ui| {
2165            ui.container()
2166                .w(8)
2167                .h(1)
2168                .draw_precomputed(8, 1, |buffer, rect| {
2169                    buffer.set_string(rect.x, rect.y, &label, crate::Style::new());
2170                })
2171                .expect("small snapshot geometry is valid");
2172        });
2173        backend.assert_contains("borrowed");
2174    }
2175
2176    #[test]
2177    fn draw_precomputed_rejects_pathological_source_geometry() {
2178        let mut state = crate::FrameState::default();
2179        let mut ui = crate::Context::new(Vec::new(), 20, 3, &mut state, crate::Theme::dark());
2180        let result = ui
2181            .container()
2182            .draw_precomputed(u32::MAX, u32::MAX, |_, _| {});
2183        assert!(result.is_err());
2184    }
2185
2186    #[test]
2187    fn draw_with_fallback_recovers_inside_the_raw_region() {
2188        let mut backend = crate::TestBackend::new(24, 3);
2189        backend.render(|ui| {
2190            ui.container().w(20).h(1).draw_with_fallback(
2191                |_, _| panic!("raw draw failed"),
2192                |buffer, rect, message| {
2193                    buffer.set_string(rect.x, rect.y, message, crate::Style::new());
2194                },
2195            );
2196        });
2197        backend.assert_contains("raw draw failed");
2198    }
2199}
2200
2201#[cfg(test)]
2202mod flex_wrap_tests {
2203    //! Render-level regression tests for flex-wrap / flex-basis (#258).
2204
2205    use crate::test_utils::TestBackend;
2206
2207    /// A wrapping row of labels wider than the backend must flow the
2208    /// overflowing label onto the second terminal row, not clip it off the
2209    /// right edge. Each label is a 1-cell-tall text node, so a line is one
2210    /// cell tall and a wrap is visible as text on row 1.
2211    #[test]
2212    fn wrap_row_flows_overflow_to_second_line() {
2213        // Backend is 12 wide. `col_gap(1)` sets within-line spacing only, so
2214        // the cross-axis (between-line) gap falls back to 0. "alpha"(5) + 1 +
2215        // "bravo"(5) = 11 fits line 0; "gamma" overflows (11 + 1 + 5 = 17 >
2216        // 12) to line 1, immediately below with no blank gap row.
2217        let mut tb = TestBackend::new(12, 4);
2218        tb.render(|ui| {
2219            let _ = ui.container().wrap().col_gap(1).row(|ui| {
2220                ui.text("alpha");
2221                ui.text("bravo");
2222                ui.text("gamma");
2223            });
2224        });
2225
2226        // Line 0 holds the first two labels; the third wrapped to line 1.
2227        tb.assert_line_contains(0, "alpha");
2228        tb.assert_line_contains(0, "bravo");
2229        tb.assert_line_contains(1, "gamma");
2230    }
2231
2232    /// `wrap()` is opt-in: without it the overflowing label clips off the
2233    /// right edge rather than wrapping, so nothing appears on row 1.
2234    #[test]
2235    fn no_wrap_row_keeps_single_line() {
2236        let mut tb = TestBackend::new(12, 4);
2237        tb.render(|ui| {
2238            let _ = ui.container().col_gap(1).row(|ui| {
2239                ui.text("alpha");
2240                ui.text("bravo");
2241                ui.text("gamma");
2242            });
2243        });
2244
2245        // Single line: first label on row 0, nothing wrapped to row 1.
2246        tb.assert_line_contains(0, "alpha");
2247        assert_eq!(tb.line(1), "");
2248    }
2249}
2250
2251#[cfg(test)]
2252mod cached_region_tests {
2253    //! Issue #273 — opt-in scoped cached region.
2254    //!
2255    //! The invariant under test: `cached(key, f)` is byte-identical to an
2256    //! uncached container in EVERY case (the body always runs), and it
2257    //! correctly classifies each call site as a hit (key unchanged) or miss
2258    //! (key changed / new / first frame / post-resize) so the hit/miss
2259    //! diagnostics — and a future cell-level cache — have a sound gate.
2260
2261    use crate::event::Event;
2262    use crate::test_utils::{EventBuilder, TestBackend};
2263    use std::cell::Cell;
2264
2265    /// First frame is always a miss, output identical to a plain container.
2266    #[test]
2267    fn cached_region_byte_identical_on_first_frame() {
2268        let mut cached = TestBackend::new(40, 6);
2269        cached.render(|ui| {
2270            let _ = ui.container().cached(7, |ui| {
2271                ui.text("static chrome line one");
2272                ui.text("static chrome line two");
2273            });
2274        });
2275
2276        let mut plain = TestBackend::new(40, 6);
2277        plain.render(|ui| {
2278            let _ = ui.container().col(|ui| {
2279                ui.text("static chrome line one");
2280                ui.text("static chrome line two");
2281            });
2282        });
2283
2284        assert_eq!(
2285            cached.buffer().snapshot_format(),
2286            plain.buffer().snapshot_format(),
2287            "cached region must render byte-identically to an uncached container"
2288        );
2289    }
2290
2291    /// An unchanged key is a hit on the second frame. The body still runs
2292    /// every frame (immediate-mode invariant), so the content stays visible
2293    /// and identical — `cached` only flips the hit classification.
2294    #[test]
2295    fn cached_region_hit_on_unchanged_key_body_still_runs() {
2296        let mut tb = TestBackend::new(40, 4);
2297        let runs = Cell::new(0u32);
2298        let hits = Cell::new(0u32);
2299        let misses = Cell::new(0u32);
2300
2301        let frame = |tb: &mut TestBackend| {
2302            tb.render(|ui| {
2303                let _ = ui.container().cached(99, |ui| {
2304                    runs.set(runs.get() + 1);
2305                    ui.text("stable");
2306                });
2307                hits.set(ui.region_cache_hits());
2308                misses.set(ui.region_cache_misses());
2309            });
2310        };
2311
2312        frame(&mut tb);
2313        assert_eq!(runs.get(), 1, "first frame runs the body");
2314        assert_eq!(misses.get(), 1, "first frame is a miss");
2315        assert_eq!(hits.get(), 0);
2316        tb.assert_contains("stable");
2317
2318        frame(&mut tb);
2319        // Body STILL runs (byte-identical guarantee) even though the key
2320        // matched — the only observable change is the hit classification.
2321        assert_eq!(runs.get(), 2, "body re-runs every frame regardless of hit");
2322        assert_eq!(hits.get(), 1, "unchanged key on the second frame is a hit");
2323        assert_eq!(misses.get(), 0);
2324        tb.assert_contains("stable");
2325    }
2326
2327    /// A changed key is a miss and the new content renders.
2328    #[test]
2329    fn cached_region_miss_on_key_change() {
2330        let mut tb = TestBackend::new(40, 4);
2331        let hits = Cell::new(0u32);
2332        let misses = Cell::new(0u32);
2333
2334        tb.render(|ui| {
2335            let _ = ui.container().cached(1, |ui| {
2336                ui.text("first");
2337            });
2338            hits.set(ui.region_cache_hits());
2339            misses.set(ui.region_cache_misses());
2340        });
2341        assert_eq!(misses.get(), 1);
2342        tb.assert_contains("first");
2343
2344        tb.render(|ui| {
2345            let _ = ui.container().cached(2, |ui| {
2346                ui.text("second");
2347            });
2348            hits.set(ui.region_cache_hits());
2349            misses.set(ui.region_cache_misses());
2350        });
2351        assert_eq!(hits.get(), 0, "changed key is not a hit");
2352        assert_eq!(misses.get(), 1, "changed key is a miss");
2353        tb.assert_contains("second");
2354    }
2355
2356    /// A resize clears the persisted keys, forcing the next frame to miss even
2357    /// when the author passes the same key.
2358    #[test]
2359    fn cached_region_invalidates_on_resize() {
2360        let mut tb = TestBackend::new(40, 4);
2361        let hits = Cell::new(0u32);
2362
2363        tb.render(|ui| {
2364            let _ = ui.container().cached(5, |ui| {
2365                ui.text("body");
2366            });
2367        });
2368        // Second frame, same key, no resize → hit.
2369        tb.render(|ui| {
2370            let _ = ui.container().cached(5, |ui| {
2371                ui.text("body");
2372            });
2373            hits.set(ui.region_cache_hits());
2374        });
2375        assert_eq!(hits.get(), 1, "same key without resize is a hit");
2376
2377        // Now resize: the persisted region keys are cleared, so the SAME key
2378        // is treated as a fresh slot (miss) on the post-resize frame.
2379        tb.render_with_events(vec![Event::Resize(60, 8)], 0, 0, |ui| {
2380            let _ = ui.container().cached(5, |ui| {
2381                ui.text("body");
2382            });
2383            hits.set(ui.region_cache_hits());
2384        });
2385        assert_eq!(hits.get(), 0, "resize forces a cache miss for all regions");
2386    }
2387
2388    /// Focus + hit-map continuity: a button inside a cached region keeps
2389    /// firing `clicked` across cached (hit) frames because the body always
2390    /// runs, so its focusable + hit-area are re-registered every frame.
2391    #[test]
2392    fn cached_region_preserves_focus_and_hit_map() {
2393        let mut tb = TestBackend::new(30, 5);
2394        let clicked = Cell::new(false);
2395
2396        // Frame 1: register the button so its hit-area lands in the feedback
2397        // map for the next frame's click resolution. Same key both frames.
2398        tb.render(|ui| {
2399            let _ = ui.container().cached(3, |ui| {
2400                let _ = ui.button("Go");
2401            });
2402        });
2403
2404        // Frame 2: click on the button's cell — even though the region is a
2405        // cache hit, the body re-ran and re-registered the hit-area, so the
2406        // click resolves.
2407        tb.render_with_events(EventBuilder::new().click(2, 0).build(), 0, 1, |ui| {
2408            let _ = ui.container().cached(3, |ui| {
2409                let resp = ui.button("Go");
2410                if resp.clicked {
2411                    clicked.set(true);
2412                }
2413            });
2414        });
2415        assert!(
2416            clicked.get(),
2417            "button inside a cached region must still receive clicks across hit frames"
2418        );
2419    }
2420
2421    /// Raw-draw inside a cached region: the deferred draw runs on every frame
2422    /// including cache-hit frames (deferred draws are one-shot per frame, and
2423    /// the body always runs, so they re-register).
2424    #[test]
2425    fn cached_region_raw_draw_replays() {
2426        let mut tb = TestBackend::new(20, 3);
2427
2428        let frame = |tb: &mut TestBackend| {
2429            tb.render(|ui| {
2430                let _ = ui.container().cached(8, |ui| {
2431                    ui.container().w(5).h(1).draw(|buf, rect| {
2432                        buf.set_string(rect.x, rect.y, "XXXXX", crate::style::Style::new());
2433                    });
2434                });
2435            });
2436        };
2437
2438        frame(&mut tb);
2439        tb.assert_contains("XXXXX");
2440
2441        // Second frame is a cache hit, but the raw draw must still paint.
2442        frame(&mut tb);
2443        tb.assert_contains("XXXXX");
2444    }
2445
2446    /// Two adjacent cached regions get independent per-call-site slots; one
2447    /// changing its key does not disturb the other's hit classification.
2448    #[test]
2449    fn cached_regions_do_not_collide_per_call_site() {
2450        let mut tb = TestBackend::new(40, 6);
2451        let hits = Cell::new(0u32);
2452        let misses = Cell::new(0u32);
2453
2454        // Frame 1: both new → 2 misses.
2455        tb.render(|ui| {
2456            let _ = ui.container().cached(10, |ui| {
2457                ui.text("region A");
2458            });
2459            let _ = ui.container().cached(20, |ui| {
2460                ui.text("region B");
2461            });
2462        });
2463
2464        // Frame 2: A unchanged (hit), B changed (miss).
2465        tb.render(|ui| {
2466            let _ = ui.container().cached(10, |ui| {
2467                ui.text("region A");
2468            });
2469            let _ = ui.container().cached(21, |ui| {
2470                ui.text("region B2");
2471            });
2472            hits.set(ui.region_cache_hits());
2473            misses.set(ui.region_cache_misses());
2474        });
2475        assert_eq!(hits.get(), 1, "region A unchanged → exactly one hit");
2476        assert_eq!(misses.get(), 1, "region B changed → exactly one miss");
2477        tb.assert_contains("region A");
2478        tb.assert_contains("region B2");
2479    }
2480}
2481
2482#[cfg(test)]
2483mod gap_saturation_tests {
2484    use super::*;
2485    use crate::test_utils::TestBackend;
2486    use proptest::prelude::*;
2487
2488    #[test]
2489    fn every_public_gap_boundary_saturates() {
2490        let mut state = FrameState::default();
2491        let mut ui = Context::new(Vec::new(), 20, 5, &mut state, Theme::dark());
2492
2493        assert_eq!(ui.container().gap(u32::MAX).gap, i32::MAX);
2494        assert_eq!(ui.container().gap_overlap(u32::MAX).gap, -i32::MAX);
2495        assert_eq!(ui.container().xs_gap(u32::MAX).gap, i32::MAX);
2496
2497        // Row/column overrides are converted when the container is finalized.
2498        let _ = ui.container().row_gap(u32::MAX).col(|_| {});
2499        let _ = ui.container().col_gap(u32::MAX).row(|_| {});
2500        let begin_gaps: Vec<i32> = ui
2501            .commands
2502            .iter()
2503            .filter_map(|command| match command {
2504                Command::BeginContainer(args) => Some(args.gap),
2505                _ => None,
2506            })
2507            .collect();
2508        assert_eq!(begin_gaps, vec![i32::MAX, i32::MAX]);
2509    }
2510
2511    proptest! {
2512        #[test]
2513        fn public_and_breakpoint_gaps_never_flip_sign(value in any::<u32>()) {
2514            let mut state = FrameState::default();
2515            let mut ui = Context::new(Vec::new(), 20, 5, &mut state, Theme::dark());
2516            let public = ui.container().gap(value).gap;
2517            let breakpoint = ui.container().xs_gap(value).gap;
2518
2519            prop_assert!(public >= 0);
2520            prop_assert_eq!(public, breakpoint);
2521            prop_assert_eq!(public, value.min(i32::MAX as u32) as i32);
2522            prop_assert_eq!(saturating_overlap(value), -public);
2523        }
2524
2525        #[test]
2526        fn arbitrary_large_overlaps_render_without_panicking(value in any::<u32>()) {
2527            let mut tb = TestBackend::new(8, 2);
2528            tb.render(|ui| {
2529                let _ = ui.container().gap_overlap(value).row(|ui| {
2530                    let _ = ui.container().w(4).col(|ui| { ui.text("left"); });
2531                    let _ = ui.container().w(4).col(|ui| { ui.text("right"); });
2532                });
2533            });
2534        }
2535    }
2536}