Skip to main content

slt/context/
container.rs

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