Skip to main content

slt/
style.rs

1//! Visual styling primitives.
2//!
3//! Colors, themes, borders, padding, margin, constraints, alignment, and
4//! text modifiers. Every widget inherits these through [`Theme`] automatically.
5
6mod color;
7mod theme;
8pub use color::{Color, ColorDepth};
9pub use theme::{Theme, ThemeBuilder};
10
11/// Terminal size breakpoint for responsive layouts.
12///
13/// Based on the current terminal width. Use [`Context::breakpoint`] to
14/// get the active breakpoint.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Breakpoint {
17    /// Width < 40 columns (phone-sized)
18    Xs,
19    /// Width 40-79 columns (small terminal)
20    Sm,
21    /// Width 80-119 columns (standard terminal)
22    Md,
23    /// Width 120-159 columns (wide terminal)
24    Lg,
25    /// Width >= 160 columns (ultra-wide)
26    Xl,
27}
28
29/// Border style for containers.
30///
31/// Pass to `Context::bordered()` to draw a box around a container.
32/// Each variant uses a different set of Unicode box-drawing characters.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub enum Border {
36    /// Single-line box: `┌─┐│└─┘`
37    Single,
38    /// Double-line box: `╔═╗║╚═╝`
39    Double,
40    /// Rounded corners: `╭─╮│╰─╯`
41    Rounded,
42    /// Thick single-line box: `┏━┓┃┗━┛`
43    Thick,
44    /// Dashed border using light dash characters: ┄╌┄╌
45    Dashed,
46    /// Heavy dashed border: ┅╍┅╍
47    DashedThick,
48}
49
50/// Character set for a specific border style.
51///
52/// Returned by [`Border::chars`]. Contains the six box-drawing characters
53/// needed to render a complete border: four corners and two line segments.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct BorderChars {
56    /// Top-left corner character.
57    pub tl: char,
58    /// Top-right corner character.
59    pub tr: char,
60    /// Bottom-left corner character.
61    pub bl: char,
62    /// Bottom-right corner character.
63    pub br: char,
64    /// Horizontal line character.
65    pub h: char,
66    /// Vertical line character.
67    pub v: char,
68}
69
70/// Controls which sides of a border are visible.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub struct BorderSides {
74    pub top: bool,
75    pub right: bool,
76    pub bottom: bool,
77    pub left: bool,
78}
79
80impl BorderSides {
81    pub const fn all() -> Self {
82        Self {
83            top: true,
84            right: true,
85            bottom: true,
86            left: true,
87        }
88    }
89
90    pub const fn none() -> Self {
91        Self {
92            top: false,
93            right: false,
94            bottom: false,
95            left: false,
96        }
97    }
98
99    pub const fn horizontal() -> Self {
100        Self {
101            top: true,
102            right: false,
103            bottom: true,
104            left: false,
105        }
106    }
107
108    pub const fn vertical() -> Self {
109        Self {
110            top: false,
111            right: true,
112            bottom: false,
113            left: true,
114        }
115    }
116
117    pub fn has_horizontal(&self) -> bool {
118        self.top || self.bottom
119    }
120
121    pub fn has_vertical(&self) -> bool {
122        self.left || self.right
123    }
124}
125
126impl Default for BorderSides {
127    fn default() -> Self {
128        Self::all()
129    }
130}
131
132impl Border {
133    /// Return the [`BorderChars`] for this border style.
134    pub const fn chars(self) -> BorderChars {
135        match self {
136            Self::Single => BorderChars {
137                tl: '┌',
138                tr: '┐',
139                bl: '└',
140                br: '┘',
141                h: '─',
142                v: '│',
143            },
144            Self::Double => BorderChars {
145                tl: '╔',
146                tr: '╗',
147                bl: '╚',
148                br: '╝',
149                h: '═',
150                v: '║',
151            },
152            Self::Rounded => BorderChars {
153                tl: '╭',
154                tr: '╮',
155                bl: '╰',
156                br: '╯',
157                h: '─',
158                v: '│',
159            },
160            Self::Thick => BorderChars {
161                tl: '┏',
162                tr: '┓',
163                bl: '┗',
164                br: '┛',
165                h: '━',
166                v: '┃',
167            },
168            Self::Dashed => BorderChars {
169                tl: '┌',
170                tr: '┐',
171                bl: '└',
172                br: '┘',
173                h: '┄',
174                v: '┆',
175            },
176            Self::DashedThick => BorderChars {
177                tl: '┏',
178                tr: '┓',
179                bl: '┗',
180                br: '┛',
181                h: '┅',
182                v: '┇',
183            },
184        }
185    }
186}
187
188/// Padding inside a container border.
189///
190/// Shrinks the content area inward from each edge. All values are in terminal
191/// columns/rows.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194pub struct Padding {
195    /// Padding on the top edge.
196    pub top: u32,
197    /// Padding on the right edge.
198    pub right: u32,
199    /// Padding on the bottom edge.
200    pub bottom: u32,
201    /// Padding on the left edge.
202    pub left: u32,
203}
204
205impl Padding {
206    /// Create uniform padding on all four sides.
207    pub const fn all(v: u32) -> Self {
208        Self::new(v, v, v, v)
209    }
210
211    /// Create padding with `x` on left/right and `y` on top/bottom.
212    pub const fn xy(x: u32, y: u32) -> Self {
213        Self::new(y, x, y, x)
214    }
215
216    /// Create padding with explicit values for each side.
217    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
218        Self {
219            top,
220            right,
221            bottom,
222            left,
223        }
224    }
225
226    /// Total horizontal padding (`left + right`).
227    pub const fn horizontal(self) -> u32 {
228        self.left + self.right
229    }
230
231    /// Total vertical padding (`top + bottom`).
232    pub const fn vertical(self) -> u32 {
233        self.top + self.bottom
234    }
235}
236
237/// Margin outside a container.
238///
239/// Adds space around the outside of a container's border. All values are in
240/// terminal columns/rows.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
243pub struct Margin {
244    /// Margin on the top edge.
245    pub top: u32,
246    /// Margin on the right edge.
247    pub right: u32,
248    /// Margin on the bottom edge.
249    pub bottom: u32,
250    /// Margin on the left edge.
251    pub left: u32,
252}
253
254impl Margin {
255    /// Create uniform margin on all four sides.
256    pub const fn all(v: u32) -> Self {
257        Self::new(v, v, v, v)
258    }
259
260    /// Create margin with `x` on left/right and `y` on top/bottom.
261    pub const fn xy(x: u32, y: u32) -> Self {
262        Self::new(y, x, y, x)
263    }
264
265    /// Create margin with explicit values for each side.
266    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
267        Self {
268            top,
269            right,
270            bottom,
271            left,
272        }
273    }
274
275    /// Total horizontal margin (`left + right`).
276    pub const fn horizontal(self) -> u32 {
277        self.left + self.right
278    }
279
280    /// Total vertical margin (`top + bottom`).
281    pub const fn vertical(self) -> u32 {
282        self.top + self.bottom
283    }
284}
285
286/// Size constraints for layout computation.
287///
288/// All fields are optional. Unset constraints are unconstrained. Use the
289/// builder methods to set individual bounds in a fluent style.
290///
291/// # Example
292///
293/// ```
294/// use slt::Constraints;
295///
296/// let c = Constraints::default().min_w(10).max_w(40);
297/// ```
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
299#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
300#[must_use = "configure constraints using the returned value"]
301pub struct Constraints {
302    /// Minimum width in terminal columns, if any.
303    pub min_width: Option<u32>,
304    /// Maximum width in terminal columns, if any.
305    pub max_width: Option<u32>,
306    /// Minimum height in terminal rows, if any.
307    pub min_height: Option<u32>,
308    /// Maximum height in terminal rows, if any.
309    pub max_height: Option<u32>,
310    /// Width as a percentage (1-100) of the parent container.
311    pub width_pct: Option<u8>,
312    /// Height as a percentage (1-100) of the parent container.
313    pub height_pct: Option<u8>,
314}
315
316impl Constraints {
317    /// Set the minimum width constraint.
318    pub const fn min_w(mut self, min_width: u32) -> Self {
319        self.min_width = Some(min_width);
320        self
321    }
322
323    /// Set the maximum width constraint.
324    pub const fn max_w(mut self, max_width: u32) -> Self {
325        self.max_width = Some(max_width);
326        self
327    }
328
329    /// Set the minimum height constraint.
330    pub const fn min_h(mut self, min_height: u32) -> Self {
331        self.min_height = Some(min_height);
332        self
333    }
334
335    /// Set the maximum height constraint.
336    pub const fn max_h(mut self, max_height: u32) -> Self {
337        self.max_height = Some(max_height);
338        self
339    }
340
341    /// Set width as a percentage (1-100) of the parent container.
342    pub const fn w_pct(mut self, pct: u8) -> Self {
343        self.width_pct = Some(pct);
344        self
345    }
346
347    /// Set height as a percentage (1-100) of the parent container.
348    pub const fn h_pct(mut self, pct: u8) -> Self {
349        self.height_pct = Some(pct);
350        self
351    }
352}
353
354/// Cross-axis alignment within a container.
355///
356/// Controls how children are positioned along the axis perpendicular to the
357/// container's main axis. For a `row()`, this is vertical alignment; for a
358/// `col()`, this is horizontal alignment.
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
360#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
361pub enum Align {
362    /// Align children to the start of the cross axis (default).
363    #[default]
364    Start,
365    /// Center children on the cross axis.
366    Center,
367    /// Align children to the end of the cross axis.
368    End,
369}
370
371/// Main-axis content distribution within a container.
372///
373/// Controls how children are distributed along the main axis. For a `row()`,
374/// this is horizontal distribution; for a `col()`, this is vertical.
375///
376/// When children have `grow > 0`, they consume remaining space before justify
377/// distribution applies. Justify modes only affect the leftover space after
378/// flex-grow allocation.
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
380#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
381pub enum Justify {
382    /// Pack children at the start (default). Uses `gap` for spacing.
383    #[default]
384    Start,
385    /// Center children along the main axis with `gap` spacing.
386    Center,
387    /// Pack children at the end with `gap` spacing.
388    End,
389    /// First child at start, last at end, equal space between.
390    SpaceBetween,
391    /// Equal space around each child (half-size space at edges).
392    SpaceAround,
393    /// Equal space between all children and at both edges.
394    SpaceEvenly,
395}
396
397/// Text modifier bitflags stored as a `u8`.
398///
399/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
400/// [`Modifiers::contains`].
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
402#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
403#[cfg_attr(feature = "serde", serde(transparent))]
404pub struct Modifiers(pub u8);
405
406impl Modifiers {
407    /// No modifiers set.
408    pub const NONE: Self = Self(0);
409    /// Bold text.
410    pub const BOLD: Self = Self(1 << 0);
411    /// Dimmed/faint text.
412    pub const DIM: Self = Self(1 << 1);
413    /// Italic text.
414    pub const ITALIC: Self = Self(1 << 2);
415    /// Underlined text.
416    pub const UNDERLINE: Self = Self(1 << 3);
417    /// Reversed foreground/background colors.
418    pub const REVERSED: Self = Self(1 << 4);
419    /// Strikethrough text.
420    pub const STRIKETHROUGH: Self = Self(1 << 5);
421
422    /// Returns `true` if all bits in `other` are set in `self`.
423    #[inline]
424    pub fn contains(self, other: Self) -> bool {
425        (self.0 & other.0) == other.0
426    }
427
428    /// Set all bits from `other` into `self`.
429    #[inline]
430    pub fn insert(&mut self, other: Self) {
431        self.0 |= other.0;
432    }
433
434    /// Returns `true` if no modifiers are set.
435    #[inline]
436    pub fn is_empty(self) -> bool {
437        self.0 == 0
438    }
439}
440
441impl std::ops::BitOr for Modifiers {
442    type Output = Self;
443    #[inline]
444    fn bitor(self, rhs: Self) -> Self {
445        Self(self.0 | rhs.0)
446    }
447}
448
449impl std::ops::BitOrAssign for Modifiers {
450    #[inline]
451    fn bitor_assign(&mut self, rhs: Self) {
452        self.0 |= rhs.0;
453    }
454}
455
456/// Visual style for a terminal cell (foreground, background, modifiers).
457///
458/// Styles are applied to text via the builder methods on `Context` widget
459/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
460/// `None` means "inherit from the terminal default."
461///
462/// # Example
463///
464/// ```
465/// use slt::{Style, Color};
466///
467/// let style = Style::new().fg(Color::Cyan).bold();
468/// ```
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
470#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
471#[must_use = "build and pass the returned Style value"]
472pub struct Style {
473    /// Foreground color, or `None` to use the terminal default.
474    pub fg: Option<Color>,
475    /// Background color, or `None` to use the terminal default.
476    pub bg: Option<Color>,
477    /// Text modifiers (bold, italic, underline, etc.).
478    pub modifiers: Modifiers,
479}
480
481impl Style {
482    /// Create a new style with no color or modifiers set.
483    pub const fn new() -> Self {
484        Self {
485            fg: None,
486            bg: None,
487            modifiers: Modifiers::NONE,
488        }
489    }
490
491    /// Set the foreground color.
492    pub const fn fg(mut self, color: Color) -> Self {
493        self.fg = Some(color);
494        self
495    }
496
497    /// Set the background color.
498    pub const fn bg(mut self, color: Color) -> Self {
499        self.bg = Some(color);
500        self
501    }
502
503    /// Add the bold modifier.
504    pub fn bold(mut self) -> Self {
505        self.modifiers |= Modifiers::BOLD;
506        self
507    }
508
509    /// Add the dim modifier.
510    pub fn dim(mut self) -> Self {
511        self.modifiers |= Modifiers::DIM;
512        self
513    }
514
515    /// Add the italic modifier.
516    pub fn italic(mut self) -> Self {
517        self.modifiers |= Modifiers::ITALIC;
518        self
519    }
520
521    /// Add the underline modifier.
522    pub fn underline(mut self) -> Self {
523        self.modifiers |= Modifiers::UNDERLINE;
524        self
525    }
526
527    /// Add the reversed (inverted colors) modifier.
528    pub fn reversed(mut self) -> Self {
529        self.modifiers |= Modifiers::REVERSED;
530        self
531    }
532
533    /// Add the strikethrough modifier.
534    pub fn strikethrough(mut self) -> Self {
535        self.modifiers |= Modifiers::STRIKETHROUGH;
536        self
537    }
538}
539
540/// Reusable container style recipe.
541///
542/// Define once, apply anywhere with [`ContainerBuilder::apply`]. All fields
543/// are optional — only set fields override the builder's current values.
544/// Styles compose: apply multiple recipes in sequence, last write wins.
545///
546/// # Example
547///
548/// ```ignore
549/// use slt::{ContainerStyle, Border, Color};
550///
551/// const CARD: ContainerStyle = ContainerStyle::new()
552///     .border(Border::Rounded)
553///     .p(1)
554///     .bg(Color::Indexed(236));
555///
556/// const DANGER: ContainerStyle = ContainerStyle::new()
557///     .bg(Color::Red);
558///
559/// // Apply one or compose multiple:
560/// ui.container().apply(&CARD).col(|ui| { ... });
561/// ui.container().apply(&CARD).apply(&DANGER).col(|ui| { ... });
562/// ```
563#[derive(Debug, Clone, Copy, Default)]
564pub struct ContainerStyle {
565    pub border: Option<Border>,
566    pub border_sides: Option<BorderSides>,
567    pub border_style: Option<Style>,
568    pub bg: Option<Color>,
569    pub dark_bg: Option<Color>,
570    pub dark_border_style: Option<Style>,
571    pub padding: Option<Padding>,
572    pub margin: Option<Margin>,
573    pub gap: Option<u32>,
574    pub grow: Option<u16>,
575    pub align: Option<Align>,
576    pub justify: Option<Justify>,
577    pub w: Option<u32>,
578    pub h: Option<u32>,
579    pub min_w: Option<u32>,
580    pub max_w: Option<u32>,
581    pub min_h: Option<u32>,
582    pub max_h: Option<u32>,
583    pub w_pct: Option<u8>,
584    pub h_pct: Option<u8>,
585}
586
587impl ContainerStyle {
588    /// Create an empty container style with no overrides.
589    pub const fn new() -> Self {
590        Self {
591            border: None,
592            border_sides: None,
593            border_style: None,
594            bg: None,
595            dark_bg: None,
596            dark_border_style: None,
597            padding: None,
598            margin: None,
599            gap: None,
600            grow: None,
601            align: None,
602            justify: None,
603            w: None,
604            h: None,
605            min_w: None,
606            max_w: None,
607            min_h: None,
608            max_h: None,
609            w_pct: None,
610            h_pct: None,
611        }
612    }
613
614    /// Set the border style.
615    pub const fn border(mut self, border: Border) -> Self {
616        self.border = Some(border);
617        self
618    }
619
620    /// Set which border sides to render.
621    pub const fn border_sides(mut self, sides: BorderSides) -> Self {
622        self.border_sides = Some(sides);
623        self
624    }
625
626    /// Set the background color.
627    pub const fn bg(mut self, color: Color) -> Self {
628        self.bg = Some(color);
629        self
630    }
631
632    /// Set the dark-mode background color.
633    pub const fn dark_bg(mut self, color: Color) -> Self {
634        self.dark_bg = Some(color);
635        self
636    }
637
638    /// Set uniform padding on all sides.
639    pub const fn p(mut self, value: u32) -> Self {
640        self.padding = Some(Padding {
641            top: value,
642            bottom: value,
643            left: value,
644            right: value,
645        });
646        self
647    }
648
649    /// Set horizontal padding.
650    pub const fn px(mut self, value: u32) -> Self {
651        let p = match self.padding {
652            Some(p) => Padding {
653                left: value,
654                right: value,
655                ..p
656            },
657            None => Padding {
658                top: 0,
659                bottom: 0,
660                left: value,
661                right: value,
662            },
663        };
664        self.padding = Some(p);
665        self
666    }
667
668    /// Set vertical padding.
669    pub const fn py(mut self, value: u32) -> Self {
670        let p = match self.padding {
671            Some(p) => Padding {
672                top: value,
673                bottom: value,
674                ..p
675            },
676            None => Padding {
677                top: value,
678                bottom: value,
679                left: 0,
680                right: 0,
681            },
682        };
683        self.padding = Some(p);
684        self
685    }
686
687    /// Set uniform margin on all sides.
688    pub const fn m(mut self, value: u32) -> Self {
689        self.margin = Some(Margin {
690            top: value,
691            bottom: value,
692            left: value,
693            right: value,
694        });
695        self
696    }
697
698    /// Set the gap between children.
699    pub const fn gap(mut self, value: u32) -> Self {
700        self.gap = Some(value);
701        self
702    }
703
704    /// Set the flex-grow factor.
705    pub const fn grow(mut self, value: u16) -> Self {
706        self.grow = Some(value);
707        self
708    }
709
710    /// Set fixed width.
711    pub const fn w(mut self, value: u32) -> Self {
712        self.w = Some(value);
713        self
714    }
715
716    /// Set fixed height.
717    pub const fn h(mut self, value: u32) -> Self {
718        self.h = Some(value);
719        self
720    }
721
722    /// Set minimum width.
723    pub const fn min_w(mut self, value: u32) -> Self {
724        self.min_w = Some(value);
725        self
726    }
727
728    /// Set maximum width.
729    pub const fn max_w(mut self, value: u32) -> Self {
730        self.max_w = Some(value);
731        self
732    }
733
734    /// Set cross-axis alignment.
735    pub const fn align(mut self, value: Align) -> Self {
736        self.align = Some(value);
737        self
738    }
739
740    /// Set main-axis justification.
741    pub const fn justify(mut self, value: Justify) -> Self {
742        self.justify = Some(value);
743        self
744    }
745
746    /// Set minimum height.
747    pub const fn min_h(mut self, value: u32) -> Self {
748        self.min_h = Some(value);
749        self
750    }
751
752    /// Set maximum height.
753    pub const fn max_h(mut self, value: u32) -> Self {
754        self.max_h = Some(value);
755        self
756    }
757
758    /// Set width as percentage of parent (1-100).
759    pub const fn w_pct(mut self, value: u8) -> Self {
760        self.w_pct = Some(value);
761        self
762    }
763
764    /// Set height as percentage of parent (1-100).
765    pub const fn h_pct(mut self, value: u8) -> Self {
766        self.h_pct = Some(value);
767        self
768    }
769}