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