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