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}