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