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