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}