Skip to main content

urushi/style/
layout.rs

1//! Alignment, sizing, and box-side values used by logical styles.
2
3use std::borrow::Cow;
4use std::fmt;
5use std::num::NonZeroU16;
6
7/// A box dimension: an absolute size, or a share of the remaining area.
8///
9/// Every length measures the box the terminal shows — content plus padding
10/// plus enabled border edges — with margin outside it. `u16` converts into
11/// [`Length::Cells`], so `width(20)` stays concise.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Length {
14    /// An absolute number of terminal cells.
15    Cells(u16),
16    /// A weighted share of the area remaining to the box's siblings.
17    Fill(NonZeroU16),
18}
19
20impl Length {
21    /// Creates a weighted share of the area remaining to the box's siblings.
22    ///
23    /// Use [`Length::try_fill`] when `weight` comes from input that may be
24    /// zero.
25    ///
26    /// # Panics
27    ///
28    /// Panics if `weight` is zero.
29    pub const fn fill(weight: u16) -> Self {
30        match Self::try_fill(weight) {
31            Ok(length) => length,
32            Err(_) => panic!("fill weight must be greater than zero"),
33        }
34    }
35
36    /// Tries to create a weighted share of the remaining area.
37    pub const fn try_fill(weight: u16) -> Result<Self, InvalidFillWeight> {
38        match NonZeroU16::new(weight) {
39            Some(weight) => Ok(Self::Fill(weight)),
40            None => Err(InvalidFillWeight),
41        }
42    }
43}
44
45/// A fill weight was zero.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct InvalidFillWeight;
48
49impl fmt::Display for InvalidFillWeight {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        formatter.write_str("fill weight must be greater than zero")
52    }
53}
54
55impl std::error::Error for InvalidFillWeight {}
56
57impl From<u16> for Length {
58    fn from(value: u16) -> Self {
59        Self::Cells(value)
60    }
61}
62
63#[cfg(test)]
64mod length_tests {
65    use super::{InvalidFillWeight, Length};
66
67    #[test]
68    fn fill_and_try_fill_construct_the_same_positive_weight() {
69        assert_eq!(Length::fill(1), Length::try_fill(1).unwrap());
70        assert_eq!(Length::fill(2), Length::try_fill(2).unwrap());
71    }
72
73    #[test]
74    fn try_fill_rejects_zero() {
75        assert_eq!(Length::try_fill(0), Err(InvalidFillWeight));
76    }
77
78    #[test]
79    #[should_panic(expected = "fill weight must be greater than zero")]
80    fn fill_panics_on_zero() {
81        Length::fill(0);
82    }
83}
84
85/// How content that does not fit its box is absorbed.
86///
87/// The frame always closes at the resolved size; overflow is absorbed by the
88/// content. This governs the width axis. Height always clips inside the frame.
89///
90/// Clipping carries the marker that stands for what was cut, because which
91/// glyph a terminal can show is the application's knowledge, not the
92/// library's: `…` on a capable terminal, `...` where [`Border::ASCII`] would
93/// be chosen for the same reason, and `""` for a silent cut.
94///
95/// ```
96/// use urushi::Overflow;
97///
98/// let quiet = Overflow::clip();
99/// let marked = Overflow::ellipsis();
100/// let ascii = Overflow::clip_with("...");
101/// ```
102///
103/// [`Border::ASCII`]: crate::Border::ASCII
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
105pub enum Overflow {
106    /// Reflow the content to the content width.
107    #[default]
108    Wrap,
109    /// Cut inside the frame, ending the cut line with this marker. The frame
110    /// stays closed; an empty marker cuts silently.
111    Clip(Cow<'static, str>),
112}
113
114impl Overflow {
115    /// Cuts without marking the cut.
116    pub const fn clip() -> Self {
117        Self::Clip(Cow::Borrowed(""))
118    }
119
120    /// Cuts, ending the line with a horizontal ellipsis.
121    pub const fn ellipsis() -> Self {
122        Self::Clip(Cow::Borrowed("…"))
123    }
124
125    /// Cuts, ending the line with `marker`.
126    ///
127    /// The marker occupies cells of its own: the content keeps the box's width
128    /// less the marker's display width. A marker that cannot fit the box is
129    /// dropped, leaving a silent cut rather than an open frame.
130    pub fn clip_with(marker: impl Into<Cow<'static, str>>) -> Self {
131        Self::Clip(marker.into())
132    }
133
134    /// Returns the marker this policy ends a cut line with, if it cuts at all.
135    pub fn clip_marker(&self) -> Option<&str> {
136        match self {
137            Self::Wrap => None,
138            Self::Clip(marker) => Some(marker),
139        }
140    }
141}
142
143/// Horizontal alignment of content within a styled block.
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145pub enum Align {
146    #[default]
147    Left,
148    Center,
149    Right,
150}
151
152/// Vertical alignment of content within a styled block or of blocks joined
153/// side by side.
154#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
155pub enum VerticalAlign {
156    #[default]
157    Top,
158    Center,
159    Bottom,
160}
161
162/// Spacing values for the four sides of a box.
163#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
164pub struct Sides {
165    pub top: u16,
166    pub right: u16,
167    pub bottom: u16,
168    pub left: u16,
169}
170
171impl From<u16> for Sides {
172    fn from(value: u16) -> Self {
173        Self {
174            top: value,
175            right: value,
176            bottom: value,
177            left: value,
178        }
179    }
180}
181
182impl From<(u16, u16)> for Sides {
183    fn from((vertical, horizontal): (u16, u16)) -> Self {
184        Self {
185            top: vertical,
186            right: horizontal,
187            bottom: vertical,
188            left: horizontal,
189        }
190    }
191}
192
193impl From<(u16, u16, u16, u16)> for Sides {
194    fn from((top, right, bottom, left): (u16, u16, u16, u16)) -> Self {
195        Self {
196            top,
197            right,
198            bottom,
199            left,
200        }
201    }
202}