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 {
1492            f(self)
1493        } else {
1494            self
1495        }
1496    }
1497
1498    /// Override the active theme for all widgets rendered inside this container.
1499    ///
1500    /// The override is scoped to the container body (the closure passed to
1501    /// `.col()`, `.row()`, or `.line()`). The parent theme is restored when
1502    /// the container closes — including on panic.
1503    ///
1504    /// All built-in widgets read `ctx.theme` directly for color decisions,
1505    /// so this swap propagates through every nested widget without requiring
1506    /// them to opt in. Nested `.theme(...)` calls correctly nest: the
1507    /// innermost theme wins inside its own subtree, and the outer theme
1508    /// resumes once it closes.
1509    ///
1510    /// Independent of [`Context::provide`] / [`Context::use_context`] —
1511    /// this directly mutates the active theme used by SLT-owned widgets,
1512    /// while `provide`/`use_context` is the general-purpose context
1513    /// injection mechanism for user code.
1514    ///
1515    /// # Example
1516    ///
1517    /// ```no_run
1518    /// # slt::run(|ui: &mut slt::Context| {
1519    /// use slt::{Border, Theme};
1520    /// ui.container()
1521    ///     .theme(Theme::light())
1522    ///     .border(Border::Rounded)
1523    ///     .col(|ui| {
1524    ///         ui.text("This subtree renders with the light theme");
1525    ///         ui.button("Click me"); // also uses light theme colors
1526    ///     });
1527    /// # });
1528    /// ```
1529    pub fn theme(mut self, theme: Theme) -> Self {
1530        self.theme_override = Some(theme);
1531        self
1532    }
1533
1534    /// Apply `f` unconditionally. Useful for factoring out a block of builder
1535    /// modifier calls while keeping the fluent chain intact.
1536    ///
1537    /// The closure takes the builder by value and must return it.
1538    ///
1539    /// # Example
1540    ///
1541    /// ```no_run
1542    /// # slt::run(|ui: &mut slt::Context| {
1543    /// use slt::Border;
1544    /// ui.container()
1545    ///     .with(|c| c.border(Border::Rounded).p(1))
1546    ///     .col(|ui| {
1547    ///         ui.text("body");
1548    ///     });
1549    /// # });
1550    /// ```
1551    pub fn with(self, f: impl FnOnce(Self) -> Self) -> Self {
1552        f(self)
1553    }
1554
1555    // ── opt-in scoped cache (issue #273) ───────────────────────────────
1556
1557    /// Opt-in: declare a subtree **stable** when `version_key` is unchanged
1558    /// from the previous frame at this call site.
1559    ///
1560    /// This is an **author-controlled cache, not reactive binding**. Your
1561    /// closure is still the app ([Principle 2 — "Your Closure IS the App"]):
1562    /// `f` runs **every frame** exactly like `.col(f)`, so the rendered output
1563    /// is **byte-for-byte identical** to an uncached container — there is no
1564    /// retained widget identity, no message passing, no reactive subscription,
1565    /// and no behavior change whatsoever when you do not call `cached`.
1566    ///
1567    /// What `cached` adds is a single, principle-preserving signal: it records
1568    /// the `version_key` you supply (a value you already own — e.g. a hash of
1569    /// the non-streaming inputs, or `StreamingTextState::version` of the
1570    /// *other* panes) and compares it to the key this call site recorded last
1571    /// frame. A match is a *cache hit* (the subtree is declared unchanged); a
1572    /// change, a new call site, the first frame, or a terminal resize is a
1573    /// *miss*. The hit/miss tally is exposed via
1574    /// [`Context::region_cache_hits`](crate::Context::region_cache_hits) /
1575    /// [`Context::region_cache_misses`](crate::Context::region_cache_misses).
1576    ///
1577    /// # Why output is identical even on a hit (current implementation)
1578    ///
1579    /// Skipping `f` on a hit would require splicing the prior frame's recorded
1580    /// `Command`s, replaying its focus / hit-map / scroll / raw-draw feedback,
1581    /// and reusing its rendered cells — without that full replay the immediate-
1582    /// mode invariant breaks (focus and interaction would silently drop). That
1583    /// replay is deliberately **out of scope** here (it risks reintroducing a
1584    /// retained tree, the thing Principle 2 forbids). So `cached` keeps the
1585    /// invariant absolute — `f` always runs — and instead lands the *safe,
1586    /// reversible* half: a measured, author-keyed stability gate plus
1587    /// diagnostics. The streaming benchmark `bench_streaming_append_chat`
1588    /// (`benches/benchmarks.rs`) quantifies the upstream cost this gate is
1589    /// designed to eventually elide; see `docs/PERFORMANCE.md`.
1590    ///
1591    /// # Pattern: cache the chrome, not the stream
1592    ///
1593    /// During token streaming, wrap the *static* surroundings (chat history,
1594    /// sidebar, status bar) keyed off everything *except* the stream, and
1595    /// leave the stream itself uncached — it changes every token:
1596    ///
1597    /// ```no_run
1598    /// # slt::run(|ui: &mut slt::Context| {
1599    /// # let history_version = 3u64;
1600    /// # let mut stream = slt::StreamingTextState::new();
1601    /// ui.container().cached(history_version, |ui| {
1602    ///     ui.text("…long chat transcript…"); // unchanged this token
1603    /// });
1604    /// ui.streaming_text(&mut stream);         // changes every token
1605    /// # });
1606    /// ```
1607    ///
1608    /// [Principle 2 — "Your Closure IS the App"]: https://docs.rs/slt
1609    pub fn cached(self, version_key: u64, f: impl FnOnce(&mut Context)) -> Response {
1610        // Record the key / classify hit-vs-miss BEFORE running the body so the
1611        // declaration order (and thus the per-call-site slot index) matches
1612        // the order regions are authored, exactly like the hook cursor.
1613        let _hit = self.ctx.record_cached_region(version_key);
1614        // Always run the body: byte-identical output, immediate-mode invariant
1615        // preserved. `_hit` is the gate a future cell-level cache would use.
1616        self.col(f)
1617    }
1618
1619    // ── internal ─────────────────────────────────────────────────────
1620
1621    /// Set the vertical scroll offset in rows. Used internally by [`Context::scrollable`].
1622    ///
1623    /// This is a crate-internal helper; external callers should use
1624    /// [`Context::scrollable`] together with a [`ScrollState`].
1625    ///
1626    /// Hidden from rustdoc with `#[doc(hidden)]` so it does not appear in the
1627    /// public API surface, while remaining callable for backwards compatibility
1628    /// (cargo-semver-checks still tracks the symbol). Promote to `pub(crate)`
1629    /// at v1.0.
1630    ///
1631    /// [`ScrollState`]: crate::widgets::ScrollState
1632    #[doc(hidden)]
1633    pub fn scroll_offset(mut self, offset: u32) -> Self {
1634        self.scroll_offset = Some(offset);
1635        self
1636    }
1637
1638    /// Internal entry point that takes an already-shared `Arc<str>`.
1639    ///
1640    /// Used by `Context::group()` so the name allocated in the public path
1641    /// is pushed onto `group_stack` and threaded into `BeginContainerArgs`
1642    /// through a single `Arc::clone` instead of two `String` allocations.
1643    /// Closes #145 (double `to_string`) and completes the `Arc<str>`
1644    /// migration in #139.
1645    pub(crate) fn group_name_arc(mut self, name: std::sync::Arc<str>) -> Self {
1646        self.group_name = Some(name);
1647        self
1648    }
1649
1650    /// Finalize the builder as a vertical (column) container.
1651    ///
1652    /// The closure receives a `&mut Context` for rendering children.
1653    /// Returns a [`Response`] with click/hover state for this container.
1654    pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1655        self.finish(Direction::Column, f)
1656    }
1657
1658    /// Finalize the builder as a horizontal (row) container.
1659    ///
1660    /// The closure receives a `&mut Context` for rendering children.
1661    /// Returns a [`Response`] with click/hover state for this container.
1662    pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1663        self.finish(Direction::Row, f)
1664    }
1665
1666    /// Finalize the builder as an inline text line.
1667    ///
1668    /// Like [`row`](ContainerBuilder::row) but gap is forced to zero
1669    /// for seamless inline rendering of mixed-style text.
1670    pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1671        self.gap = 0;
1672        self.finish(Direction::Row, f)
1673    }
1674
1675    /// Finalize the builder as a raw-draw region with direct buffer access.
1676    ///
1677    /// The closure receives `(&mut Buffer, Rect)` after layout is computed.
1678    /// Use `buf.set_char()`, `buf.set_string()`, `buf.get_mut()` to write
1679    /// directly into the terminal buffer. Writes outside `rect` are clipped.
1680    ///
1681    /// The closure must be `'static` because it is deferred until after layout.
1682    /// To capture local data, clone or move it into the closure:
1683    /// ```ignore
1684    /// let data = my_vec.clone();
1685    /// ui.container().w(40).h(20).draw(move |buf, rect| {
1686    ///     // use `data` here
1687    /// });
1688    /// ```
1689    pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1690        let draw_id = self.ctx.deferred_draws.len();
1691        self.ctx.deferred_draws.push(Some(Box::new(f)));
1692        self.ctx.skip_interaction_slot();
1693        self.ctx.commands.push(Command::RawDraw {
1694            draw_id,
1695            constraints: self.constraints,
1696            grow: self.grow,
1697            margin: self.margin,
1698        });
1699    }
1700
1701    /// Like [`draw`](Self::draw), but carries owned per-frame `data` through
1702    /// to the deferred closure as a borrow.
1703    ///
1704    /// Raw-draw closures must be `'static` because they run after layout is
1705    /// computed — which normally forces callers to snapshot any borrowed
1706    /// state into an owned value before passing it in. `draw_with` makes
1707    /// that explicit: hand the snapshot over, borrow it inside the closure.
1708    ///
1709    /// # Example
1710    ///
1711    /// ```no_run
1712    /// # use slt::{Buffer, Rect, Style};
1713    /// # slt::run(|ui: &mut slt::Context| {
1714    /// let points: Vec<(u32, u32)> = (0..20).map(|i| (i, i * 2)).collect();
1715    /// ui.container().w(40).h(20).draw_with(points, |buf, rect, points| {
1716    ///     for (x, y) in points {
1717    ///         if rect.contains(*x, *y) {
1718    ///             buf.set_char(*x, *y, '●', Style::new());
1719    ///         }
1720    ///     }
1721    /// });
1722    /// # });
1723    /// ```
1724    pub fn draw_with<D: 'static>(
1725        self,
1726        data: D,
1727        f: impl FnOnce(&mut crate::buffer::Buffer, Rect, &D) + 'static,
1728    ) {
1729        let draw_id = self.ctx.deferred_draws.len();
1730        self.ctx
1731            .deferred_draws
1732            .push(Some(Box::new(move |buf, rect| f(buf, rect, &data))));
1733        self.ctx.skip_interaction_slot();
1734        self.ctx.commands.push(Command::RawDraw {
1735            draw_id,
1736            constraints: self.constraints,
1737            grow: self.grow,
1738            margin: self.margin,
1739        });
1740    }
1741
1742    /// Custom drawing with click and hover detection.
1743    ///
1744    /// Like [`draw`](Self::draw), but the returned [`Response`] reports
1745    /// `clicked` and `hovered` based on the laid-out region — exactly like
1746    /// `.col()` or `.row()`.
1747    ///
1748    /// # Example
1749    ///
1750    /// ```no_run
1751    /// # slt::run(|ui: &mut slt::Context| {
1752    /// let resp = ui.container()
1753    ///     .w(40).h(10)
1754    ///     .draw_interactive(|buf, rect| {
1755    ///         buf.set_string(rect.x, rect.y, "Click me!", slt::Style::new());
1756    ///     });
1757    /// if resp.clicked {
1758    ///     // handle click
1759    /// }
1760    /// # });
1761    /// ```
1762    pub fn draw_interactive(
1763        self,
1764        f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static,
1765    ) -> Response {
1766        let draw_id = self.ctx.deferred_draws.len();
1767        self.ctx.deferred_draws.push(Some(Box::new(f)));
1768        let interaction_id = self.ctx.next_interaction_id();
1769        self.ctx.commands.push(Command::RawDraw {
1770            draw_id,
1771            constraints: self.constraints,
1772            grow: self.grow,
1773            margin: self.margin,
1774        });
1775        self.ctx.response_for(interaction_id)
1776    }
1777
1778    fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1779        let interaction_id = self.ctx.next_interaction_id();
1780        // `row_gap` / `col_gap` are `Option<u32>` (positive override); fall back
1781        // to the signed builder `gap`, which alone can carry an overlap (#222).
1782        let resolved_gap: i32 = match direction {
1783            Direction::Column => self.row_gap.map(|g| g as i32).unwrap_or(self.gap),
1784            Direction::Row => self.col_gap.map(|g| g as i32).unwrap_or(self.gap),
1785        };
1786        // Cross-axis (between-line) gap for a wrapping row (#258): `row_gap`
1787        // when set, else the builder `gap`. Only consulted by the layout pass
1788        // when this container is a wrapping `Direction::Row`.
1789        let resolved_cross_gap: i32 = self.row_gap.map(|g| g as i32).unwrap_or(self.gap);
1790
1791        let in_hovered_group = self
1792            .group_name
1793            .as_ref()
1794            .map(|name| self.ctx.is_group_hovered(name))
1795            .unwrap_or(false)
1796            || self
1797                .ctx
1798                .rollback
1799                .group_stack
1800                .last()
1801                .map(|name| self.ctx.is_group_hovered(name))
1802                .unwrap_or(false);
1803        let in_focused_group = self
1804            .group_name
1805            .as_ref()
1806            .map(|name| self.ctx.is_group_focused(name))
1807            .unwrap_or(false)
1808            || self
1809                .ctx
1810                .rollback
1811                .group_stack
1812                .last()
1813                .map(|name| self.ctx.is_group_focused(name))
1814                .unwrap_or(false);
1815
1816        let resolved_bg = if self.ctx.rollback.dark_mode {
1817            self.dark_bg.or(self.bg)
1818        } else {
1819            self.bg
1820        };
1821        let resolved_border_style = if self.ctx.rollback.dark_mode {
1822            self.dark_border_style.unwrap_or(self.border_style)
1823        } else {
1824            self.border_style
1825        };
1826        let bg_color = if in_hovered_group || in_focused_group {
1827            self.group_hover_bg.or(resolved_bg)
1828        } else {
1829            resolved_bg
1830        };
1831        let border_style = if in_hovered_group || in_focused_group {
1832            self.group_hover_border_style
1833                .unwrap_or(resolved_border_style)
1834        } else {
1835            resolved_border_style
1836        };
1837        let group_name = self.group_name.take();
1838        let is_group_container = group_name.is_some();
1839
1840        // Opt-in flex-shrink (#161). Push a marker the layout pass picks up
1841        // and applies to the next `BeginContainer` / `BeginScrollable`,
1842        // mirroring the existing `FocusMarker` / `InteractionMarker` pattern.
1843        // This avoids touching every `BeginContainerArgs` construction site
1844        // across the widget modules — only `ContainerBuilder.shrink()`
1845        // emits the marker, and `LayoutNode::shrink` defaults to `false`.
1846        if self.shrink_flag {
1847            self.ctx.commands.push(Command::ShrinkMarker);
1848        }
1849
1850        // Opt-in flex-wrap / flex-basis (#258). Same marker pattern as shrink:
1851        // pushed just before the matching `Begin*`, picked up by the layout
1852        // pass and applied to the next node. Both default off / `None`, so
1853        // unflagged containers are byte-identical to pre-#258.
1854        if self.wrap_flag {
1855            self.ctx
1856                .commands
1857                .push(Command::WrapMarker(resolved_cross_gap));
1858        }
1859        if let Some(basis) = self.basis {
1860            self.ctx.commands.push(Command::BasisMarker(basis));
1861        }
1862
1863        if let Some(scroll_offset) = self.scroll_offset {
1864            // #247: carry the finalizing `.row()` / `.col()` direction and both
1865            // axis offsets. The tree builder applies the offset matching
1866            // `direction`; the cross-axis offset is `0` for a single-axis
1867            // scroller (the common case).
1868            self.ctx
1869                .commands
1870                .push(Command::BeginScrollable(Box::new(BeginScrollableArgs {
1871                    grow: self.grow,
1872                    direction,
1873                    border: self.border,
1874                    border_sides: self.border_sides,
1875                    border_style,
1876                    bg_color,
1877                    align: self.align,
1878                    align_self: self.align_self_value,
1879                    justify: self.justify,
1880                    gap: resolved_gap,
1881                    padding: self.padding,
1882                    margin: self.margin,
1883                    constraints: self.constraints,
1884                    title: self.title,
1885                    scroll_offset,
1886                    scroll_offset_x: self.scroll_offset_x.unwrap_or(0),
1887                    group_name,
1888                })));
1889        } else {
1890            self.ctx
1891                .commands
1892                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
1893                    direction,
1894                    gap: resolved_gap,
1895                    align: self.align,
1896                    align_self: self.align_self_value,
1897                    justify: self.justify,
1898                    border: self.border,
1899                    border_sides: self.border_sides,
1900                    border_style,
1901                    bg_color,
1902                    padding: self.padding,
1903                    margin: self.margin,
1904                    constraints: self.constraints,
1905                    title: self.title,
1906                    grow: self.grow,
1907                    group_name,
1908                })));
1909        }
1910        self.ctx.rollback.text_color_stack.push(self.text_color);
1911        // Swap active theme if a per-subtree override was requested.
1912        // The previous theme is restored after `f` returns — including on
1913        // panic, so no widget ever sees a leaked override theme.
1914        let theme_save = self.theme_override.map(|t| {
1915            let prev = self.ctx.theme;
1916            self.ctx.theme = t;
1917            // Also keep dark_mode flag in sync so `dark_*` style variants
1918            // resolve to the new theme's brightness, not the stale flag.
1919            self.ctx.rollback.dark_mode = t.is_dark;
1920            (prev, prev.is_dark)
1921        });
1922        // catch_unwind guards the restore path against panics inside `f`.
1923        // The overlay/group bookkeeping that follows assumes `theme` reflects
1924        // the parent scope, so we must restore before propagating the panic.
1925        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self.ctx)));
1926        if let Some((prev, prev_dark)) = theme_save {
1927            self.ctx.theme = prev;
1928            self.ctx.rollback.dark_mode = prev_dark;
1929        }
1930        self.ctx.rollback.text_color_stack.pop();
1931        self.ctx.commands.push(Command::EndContainer);
1932        self.ctx.rollback.last_text_idx = None;
1933        if let Err(panic) = result {
1934            std::panic::resume_unwind(panic);
1935        }
1936
1937        if is_group_container {
1938            self.ctx.rollback.group_stack.pop();
1939            self.ctx.rollback.group_count = self.ctx.rollback.group_count.saturating_sub(1);
1940        }
1941
1942        self.ctx.response_for(interaction_id)
1943    }
1944}
1945
1946#[cfg(test)]
1947mod hotfix_tests {
1948    //! Regression tests for v0.19.1 A3 hotfixes (issues #143, #144, #146, #149).
1949
1950    use super::*;
1951
1952    // -- #143: filled_triangle stack-array intersections ----------------
1953
1954    /// Filling a triangle must paint the same pixel set whether the
1955    /// previous Vec<f64> path or the new inline-array path is used.
1956    #[test]
1957    fn filled_triangle_paints_expected_interior() {
1958        let mut canvas = CanvasContext::new(20, 20);
1959        canvas.filled_triangle(2, 2, 18, 4, 6, 18);
1960
1961        // Sample a point that must be filled (lies clearly inside the
1962        // triangle) and a point that must remain empty.
1963        let lines = canvas.render();
1964        // Pixel (8, 8) -> char cell (4, 2). Pull bits via re-render fallback.
1965        let inside_row = 8 / 4;
1966        let outside_row = 0;
1967        // Each row must be present in the rendered output.
1968        assert!(lines.len() > inside_row);
1969        assert!(lines.len() > outside_row);
1970
1971        // Inside row must contain at least one non-blank braille glyph.
1972        let inside: String = lines[inside_row].iter().map(|(s, _)| s.as_str()).collect();
1973        assert!(
1974            inside.chars().any(|c| c != '\u{2800}' && c != ' '),
1975            "expected filled glyphs inside triangle, got: {inside:?}"
1976        );
1977    }
1978
1979    /// Tall triangles previously allocated O(H) Vecs; the new path must
1980    /// still produce filled output for many scanlines without panicking.
1981    #[test]
1982    fn filled_triangle_handles_tall_triangle_without_panic() {
1983        let mut canvas = CanvasContext::new(8, 50);
1984        canvas.filled_triangle(0, 0, 15, 0, 8, 199);
1985        let lines = canvas.render();
1986        assert_eq!(lines.len(), 50);
1987    }
1988
1989    /// Degenerate horizontal triangle (all three vertices on the same row)
1990    /// must not panic and must produce no fill (only the outline edges).
1991    #[test]
1992    fn filled_triangle_degenerate_horizontal_is_safe() {
1993        let mut canvas = CanvasContext::new(20, 20);
1994        canvas.filled_triangle(0, 0, 10, 0, 19, 0);
1995        let _ = canvas.render();
1996    }
1997
1998    // -- #146: integer isqrt for filled_circle -------------------------
1999
2000    #[test]
2001    fn isqrt_i64_matches_floor_sqrt_for_small_values() {
2002        for n in 0i64..=10_000 {
2003            let expected = (n as f64).sqrt().floor() as isize;
2004            assert_eq!(isqrt_i64(n), expected, "mismatch at n={n}");
2005        }
2006    }
2007
2008    #[test]
2009    fn isqrt_i64_handles_perfect_squares_and_boundaries() {
2010        for k in 0i64..=4096 {
2011            assert_eq!(isqrt_i64(k * k), k as isize);
2012            if k > 0 {
2013                assert_eq!(isqrt_i64(k * k - 1), (k - 1) as isize);
2014            }
2015        }
2016    }
2017
2018    #[test]
2019    fn isqrt_i64_clamps_non_positive_to_zero() {
2020        assert_eq!(isqrt_i64(0), 0);
2021        assert_eq!(isqrt_i64(-1), 0);
2022        assert_eq!(isqrt_i64(i64::MIN), 0);
2023    }
2024
2025    /// `filled_circle` should produce a symmetric span around its center
2026    /// after switching from f64 sqrt to integer isqrt.
2027    #[test]
2028    fn filled_circle_renders_without_panic_and_is_non_empty() {
2029        let mut canvas = CanvasContext::new(20, 20);
2030        canvas.filled_circle(10, 10, 6);
2031        let lines = canvas.render();
2032        let any_filled = lines
2033            .iter()
2034            .flatten()
2035            .any(|(s, _)| s.chars().any(|c| c != '\u{2800}' && c != ' '));
2036        assert!(any_filled, "filled_circle produced empty output");
2037    }
2038
2039    // -- #149: scroll_offset visibility (compile-time check) -----------
2040
2041    /// The `scroll_offset` helper must remain callable from inside the crate.
2042    /// It is `#[doc(hidden)] pub` (Option B from the issue) so it is removed
2043    /// from rustdoc but still semver-tracked; this test compiles only when
2044    /// the path is reachable.
2045    #[test]
2046    fn scroll_offset_is_crate_internal_api() {
2047        let _ = ContainerBuilder::scroll_offset;
2048    }
2049}
2050
2051#[cfg(test)]
2052mod flex_wrap_tests {
2053    //! Render-level regression tests for flex-wrap / flex-basis (#258).
2054
2055    use crate::test_utils::TestBackend;
2056
2057    /// A wrapping row of labels wider than the backend must flow the
2058    /// overflowing label onto the second terminal row, not clip it off the
2059    /// right edge. Each label is a 1-cell-tall text node, so a line is one
2060    /// cell tall and a wrap is visible as text on row 1.
2061    #[test]
2062    fn wrap_row_flows_overflow_to_second_line() {
2063        // Backend is 12 wide. `col_gap(1)` sets within-line spacing only, so
2064        // the cross-axis (between-line) gap falls back to 0. "alpha"(5) + 1 +
2065        // "bravo"(5) = 11 fits line 0; "gamma" overflows (11 + 1 + 5 = 17 >
2066        // 12) to line 1, immediately below with no blank gap row.
2067        let mut tb = TestBackend::new(12, 4);
2068        tb.render(|ui| {
2069            let _ = ui.container().wrap().col_gap(1).row(|ui| {
2070                ui.text("alpha");
2071                ui.text("bravo");
2072                ui.text("gamma");
2073            });
2074        });
2075
2076        // Line 0 holds the first two labels; the third wrapped to line 1.
2077        tb.assert_line_contains(0, "alpha");
2078        tb.assert_line_contains(0, "bravo");
2079        tb.assert_line_contains(1, "gamma");
2080    }
2081
2082    /// `wrap()` is opt-in: without it the overflowing label clips off the
2083    /// right edge rather than wrapping, so nothing appears on row 1.
2084    #[test]
2085    fn no_wrap_row_keeps_single_line() {
2086        let mut tb = TestBackend::new(12, 4);
2087        tb.render(|ui| {
2088            let _ = ui.container().col_gap(1).row(|ui| {
2089                ui.text("alpha");
2090                ui.text("bravo");
2091                ui.text("gamma");
2092            });
2093        });
2094
2095        // Single line: first label on row 0, nothing wrapped to row 1.
2096        tb.assert_line_contains(0, "alpha");
2097        assert_eq!(tb.line(1), "");
2098    }
2099}
2100
2101#[cfg(test)]
2102mod cached_region_tests {
2103    //! Issue #273 — opt-in scoped cached region.
2104    //!
2105    //! The invariant under test: `cached(key, f)` is byte-identical to an
2106    //! uncached container in EVERY case (the body always runs), and it
2107    //! correctly classifies each call site as a hit (key unchanged) or miss
2108    //! (key changed / new / first frame / post-resize) so the hit/miss
2109    //! diagnostics — and a future cell-level cache — have a sound gate.
2110
2111    use crate::event::Event;
2112    use crate::test_utils::{EventBuilder, TestBackend};
2113    use std::cell::Cell;
2114
2115    /// First frame is always a miss, output identical to a plain container.
2116    #[test]
2117    fn cached_region_byte_identical_on_first_frame() {
2118        let mut cached = TestBackend::new(40, 6);
2119        cached.render(|ui| {
2120            let _ = ui.container().cached(7, |ui| {
2121                ui.text("static chrome line one");
2122                ui.text("static chrome line two");
2123            });
2124        });
2125
2126        let mut plain = TestBackend::new(40, 6);
2127        plain.render(|ui| {
2128            let _ = ui.container().col(|ui| {
2129                ui.text("static chrome line one");
2130                ui.text("static chrome line two");
2131            });
2132        });
2133
2134        assert_eq!(
2135            cached.buffer().snapshot_format(),
2136            plain.buffer().snapshot_format(),
2137            "cached region must render byte-identically to an uncached container"
2138        );
2139    }
2140
2141    /// An unchanged key is a hit on the second frame. The body still runs
2142    /// every frame (immediate-mode invariant), so the content stays visible
2143    /// and identical — `cached` only flips the hit classification.
2144    #[test]
2145    fn cached_region_hit_on_unchanged_key_body_still_runs() {
2146        let mut tb = TestBackend::new(40, 4);
2147        let runs = Cell::new(0u32);
2148        let hits = Cell::new(0u32);
2149        let misses = Cell::new(0u32);
2150
2151        let frame = |tb: &mut TestBackend| {
2152            tb.render(|ui| {
2153                let _ = ui.container().cached(99, |ui| {
2154                    runs.set(runs.get() + 1);
2155                    ui.text("stable");
2156                });
2157                hits.set(ui.region_cache_hits());
2158                misses.set(ui.region_cache_misses());
2159            });
2160        };
2161
2162        frame(&mut tb);
2163        assert_eq!(runs.get(), 1, "first frame runs the body");
2164        assert_eq!(misses.get(), 1, "first frame is a miss");
2165        assert_eq!(hits.get(), 0);
2166        tb.assert_contains("stable");
2167
2168        frame(&mut tb);
2169        // Body STILL runs (byte-identical guarantee) even though the key
2170        // matched — the only observable change is the hit classification.
2171        assert_eq!(runs.get(), 2, "body re-runs every frame regardless of hit");
2172        assert_eq!(hits.get(), 1, "unchanged key on the second frame is a hit");
2173        assert_eq!(misses.get(), 0);
2174        tb.assert_contains("stable");
2175    }
2176
2177    /// A changed key is a miss and the new content renders.
2178    #[test]
2179    fn cached_region_miss_on_key_change() {
2180        let mut tb = TestBackend::new(40, 4);
2181        let hits = Cell::new(0u32);
2182        let misses = Cell::new(0u32);
2183
2184        tb.render(|ui| {
2185            let _ = ui.container().cached(1, |ui| {
2186                ui.text("first");
2187            });
2188            hits.set(ui.region_cache_hits());
2189            misses.set(ui.region_cache_misses());
2190        });
2191        assert_eq!(misses.get(), 1);
2192        tb.assert_contains("first");
2193
2194        tb.render(|ui| {
2195            let _ = ui.container().cached(2, |ui| {
2196                ui.text("second");
2197            });
2198            hits.set(ui.region_cache_hits());
2199            misses.set(ui.region_cache_misses());
2200        });
2201        assert_eq!(hits.get(), 0, "changed key is not a hit");
2202        assert_eq!(misses.get(), 1, "changed key is a miss");
2203        tb.assert_contains("second");
2204    }
2205
2206    /// A resize clears the persisted keys, forcing the next frame to miss even
2207    /// when the author passes the same key.
2208    #[test]
2209    fn cached_region_invalidates_on_resize() {
2210        let mut tb = TestBackend::new(40, 4);
2211        let hits = Cell::new(0u32);
2212
2213        tb.render(|ui| {
2214            let _ = ui.container().cached(5, |ui| {
2215                ui.text("body");
2216            });
2217        });
2218        // Second frame, same key, no resize → hit.
2219        tb.render(|ui| {
2220            let _ = ui.container().cached(5, |ui| {
2221                ui.text("body");
2222            });
2223            hits.set(ui.region_cache_hits());
2224        });
2225        assert_eq!(hits.get(), 1, "same key without resize is a hit");
2226
2227        // Now resize: the persisted region keys are cleared, so the SAME key
2228        // is treated as a fresh slot (miss) on the post-resize frame.
2229        tb.render_with_events(vec![Event::Resize(60, 8)], 0, 0, |ui| {
2230            let _ = ui.container().cached(5, |ui| {
2231                ui.text("body");
2232            });
2233            hits.set(ui.region_cache_hits());
2234        });
2235        assert_eq!(hits.get(), 0, "resize forces a cache miss for all regions");
2236    }
2237
2238    /// Focus + hit-map continuity: a button inside a cached region keeps
2239    /// firing `clicked` across cached (hit) frames because the body always
2240    /// runs, so its focusable + hit-area are re-registered every frame.
2241    #[test]
2242    fn cached_region_preserves_focus_and_hit_map() {
2243        let mut tb = TestBackend::new(30, 5);
2244        let clicked = Cell::new(false);
2245
2246        // Frame 1: register the button so its hit-area lands in the feedback
2247        // map for the next frame's click resolution. Same key both frames.
2248        tb.render(|ui| {
2249            let _ = ui.container().cached(3, |ui| {
2250                let _ = ui.button("Go");
2251            });
2252        });
2253
2254        // Frame 2: click on the button's cell — even though the region is a
2255        // cache hit, the body re-ran and re-registered the hit-area, so the
2256        // click resolves.
2257        tb.render_with_events(EventBuilder::new().click(2, 0).build(), 0, 1, |ui| {
2258            let _ = ui.container().cached(3, |ui| {
2259                let resp = ui.button("Go");
2260                if resp.clicked {
2261                    clicked.set(true);
2262                }
2263            });
2264        });
2265        assert!(
2266            clicked.get(),
2267            "button inside a cached region must still receive clicks across hit frames"
2268        );
2269    }
2270
2271    /// Raw-draw inside a cached region: the deferred draw runs on every frame
2272    /// including cache-hit frames (deferred draws are one-shot per frame, and
2273    /// the body always runs, so they re-register).
2274    #[test]
2275    fn cached_region_raw_draw_replays() {
2276        let mut tb = TestBackend::new(20, 3);
2277
2278        let frame = |tb: &mut TestBackend| {
2279            tb.render(|ui| {
2280                let _ = ui.container().cached(8, |ui| {
2281                    ui.container().w(5).h(1).draw(|buf, rect| {
2282                        buf.set_string(rect.x, rect.y, "XXXXX", crate::style::Style::new());
2283                    });
2284                });
2285            });
2286        };
2287
2288        frame(&mut tb);
2289        tb.assert_contains("XXXXX");
2290
2291        // Second frame is a cache hit, but the raw draw must still paint.
2292        frame(&mut tb);
2293        tb.assert_contains("XXXXX");
2294    }
2295
2296    /// Two adjacent cached regions get independent per-call-site slots; one
2297    /// changing its key does not disturb the other's hit classification.
2298    #[test]
2299    fn cached_regions_do_not_collide_per_call_site() {
2300        let mut tb = TestBackend::new(40, 6);
2301        let hits = Cell::new(0u32);
2302        let misses = Cell::new(0u32);
2303
2304        // Frame 1: both new → 2 misses.
2305        tb.render(|ui| {
2306            let _ = ui.container().cached(10, |ui| {
2307                ui.text("region A");
2308            });
2309            let _ = ui.container().cached(20, |ui| {
2310                ui.text("region B");
2311            });
2312        });
2313
2314        // Frame 2: A unchanged (hit), B changed (miss).
2315        tb.render(|ui| {
2316            let _ = ui.container().cached(10, |ui| {
2317                ui.text("region A");
2318            });
2319            let _ = ui.container().cached(21, |ui| {
2320                ui.text("region B2");
2321            });
2322            hits.set(ui.region_cache_hits());
2323            misses.set(ui.region_cache_misses());
2324        });
2325        assert_eq!(hits.get(), 1, "region A unchanged → exactly one hit");
2326        assert_eq!(misses.get(), 1, "region B changed → exactly one miss");
2327        tb.assert_contains("region A");
2328        tb.assert_contains("region B2");
2329    }
2330}