Skip to main content

retroglyph_core/layout/
align.rs

1//! Horizontal and vertical alignment within a bounded rectangle.
2//!
3//! [`HAlign`](crate::layout::HAlign) and [`VAlign`](crate::layout::VAlign) are plain data: no allocation, no text handling, nothing that
4//! depends on the `egc` feature. Private submodule of [`layout`](crate::layout), re-exported
5//! unconditionally from there regardless of `egc`; [`TextLayout`](crate::layout::TextLayout) (the
6//! `egc`-gated word-wrap builder) and
7//! [`Surface::print_aligned`](crate::surface::Surface::print_aligned) both use them.
8
9/// Horizontal alignment within a bounded rectangle.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11#[non_exhaustive]
12pub enum HAlign {
13    /// Align text to the left edge (default).
14    #[default]
15    Left,
16    /// Centre text horizontally.
17    Center,
18    /// Align text to the right edge.
19    Right,
20}
21
22impl HAlign {
23    /// The left offset, in columns, at which a `content_width`-column line should start within
24    /// an `area_width`-column area for this alignment.
25    ///
26    /// Saturates at `0` when the content is wider than the area, so the caller clips from the
27    /// left edge rather than underflowing.
28    #[must_use]
29    pub const fn offset(self, area_width: u16, content_width: u16) -> u16 {
30        let slack = area_width.saturating_sub(content_width);
31        match self {
32            Self::Left => 0,
33            Self::Center => slack / 2,
34            Self::Right => slack,
35        }
36    }
37}
38
39/// Vertical alignment within a bounded rectangle.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
41#[non_exhaustive]
42pub enum VAlign {
43    /// Align text to the top edge (default).
44    #[default]
45    Top,
46    /// Centre text vertically.
47    Middle,
48    /// Align text to the bottom edge.
49    Bottom,
50}
51
52impl VAlign {
53    /// The top offset, in rows, at which `content_height` rows of content should start within
54    /// an `area_height`-row area for this alignment.
55    ///
56    /// Saturates at `0` when the content is taller than the area, so the caller clips from the
57    /// top edge rather than underflowing.
58    #[must_use]
59    pub const fn offset(self, area_height: u16, content_height: u16) -> u16 {
60        let slack = area_height.saturating_sub(content_height);
61        match self {
62            Self::Top => 0,
63            Self::Middle => slack / 2,
64            Self::Bottom => slack,
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn h_align_offset_places_content_per_alignment() {
75        // 4-column word in a 10-column area: 6 columns of slack.
76        assert_eq!(HAlign::Left.offset(10, 4), 0);
77        assert_eq!(HAlign::Center.offset(10, 4), 3);
78        assert_eq!(HAlign::Right.offset(10, 4), 6);
79    }
80
81    #[test]
82    fn h_align_offset_wider_than_area_saturates_to_zero() {
83        assert_eq!(HAlign::Left.offset(3, 8), 0);
84        assert_eq!(HAlign::Center.offset(3, 8), 0);
85        assert_eq!(HAlign::Right.offset(3, 8), 0);
86    }
87
88    #[test]
89    fn v_align_offset_places_content_per_alignment() {
90        // 2-row block in a 10-row area: 8 rows of slack.
91        assert_eq!(VAlign::Top.offset(10, 2), 0);
92        assert_eq!(VAlign::Middle.offset(10, 2), 4);
93        assert_eq!(VAlign::Bottom.offset(10, 2), 8);
94    }
95
96    #[test]
97    fn v_align_offset_taller_than_area_saturates_to_zero() {
98        assert_eq!(VAlign::Top.offset(3, 8), 0);
99        assert_eq!(VAlign::Middle.offset(3, 8), 0);
100        assert_eq!(VAlign::Bottom.offset(3, 8), 0);
101    }
102}