Skip to main content

tui_lipan/widgets/
segment_cap.rs

1//! Composable segment caps used by widgets such as [`crate::widgets::Badge`].
2
3use crate::app::ContrastPolicy;
4use crate::core::element::Element;
5use crate::style::{Color, Length, Style};
6use crate::widgets::{HStack, Text};
7
8/// Visual treatment used for the two ends of a segmented element.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10pub enum CapStyle {
11    /// Keep the segment padded without drawing cap glyphs.
12    #[default]
13    Padded,
14    /// Use half-block caps; this is safe with ordinary terminal fonts.
15    Half,
16    /// Use rounded Powerline caps.
17    Round,
18    /// Use pointed Powerline caps.
19    Arrow,
20}
21
22impl CapStyle {
23    /// Return the `(left, right)` cap glyphs, or `None` for [`CapStyle::Padded`].
24    pub const fn glyphs(self) -> Option<(&'static str, &'static str)> {
25        match self {
26            Self::Padded => None,
27            // The left cap points right and the right cap points left, matching the workbar
28            // composition used by hyprmux.
29            Self::Half => Some(("\u{2590}", "\u{258c}")),
30            Self::Round => Some(("\u{e0b6}", "\u{e0b4}")),
31            Self::Arrow => Some(("\u{e0b2}", "\u{e0b0}")),
32        }
33    }
34
35    /// Return whether this style requires a Powerline/Nerd Font.
36    pub const fn requires_nerd_font(self) -> bool {
37        matches!(self, Self::Round | Self::Arrow)
38    }
39
40    /// Degrade a Nerd Font style to the undecorated padded style.
41    ///
42    /// [`CapStyle::Half`] remains unchanged when selected explicitly.
43    pub const fn font_safe(self) -> Self {
44        if self.requires_nerd_font() {
45            Self::Padded
46        } else {
47            self
48        }
49    }
50
51    /// Return the canonical appearance cycle.
52    pub const fn all() -> &'static [Self] {
53        &[Self::Padded, Self::Half, Self::Round, Self::Arrow]
54    }
55}
56
57/// Which sides of a segmented element receive caps.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
59pub enum CapSides {
60    /// Draw both caps.
61    #[default]
62    Both,
63    /// Draw only the left cap.
64    Left,
65    /// Draw only the right cap.
66    Right,
67    /// Draw neither cap.
68    None,
69}
70
71impl CapSides {
72    pub(crate) const fn has_left(self) -> bool {
73        matches!(self, Self::Both | Self::Left)
74    }
75
76    pub(crate) const fn has_right(self) -> bool {
77        matches!(self, Self::Both | Self::Right)
78    }
79}
80
81/// Compose a child with optional leading and trailing cap glyphs.
82pub(crate) fn segment_cap(
83    child: Element,
84    cap_style: CapStyle,
85    cap_sides: CapSides,
86    badge_bg: Color,
87    cap_behind: Color,
88    same_color_left: bool,
89) -> Element {
90    let glyphs = cap_style.glyphs();
91    if glyphs.is_none() && !(same_color_left && cap_sides.has_left()) {
92        return child;
93    }
94
95    let cap = |glyph: &'static str, same_color: bool| {
96        let contrast = if same_color {
97            ContrastPolicy::BlackOrWhite
98        } else {
99            ContrastPolicy::Off
100        };
101        Text::new(glyph)
102            .style(
103                Style::new()
104                    .fg(badge_bg)
105                    .bg(cap_behind)
106                    .contrast_policy(contrast),
107            )
108            .width(Length::Px(1))
109            .height(Length::Px(1))
110    };
111
112    let mut row = HStack::new().width(Length::Auto).height(Length::Auto);
113    if cap_sides.has_left() {
114        if same_color_left {
115            row = row.child(cap(same_color_separator(cap_style), true));
116        } else if let Some((left, _)) = glyphs {
117            row = row.child(cap(left, false));
118        }
119    }
120    row = row.child(child);
121    if cap_sides.has_right()
122        && let Some((_, right)) = glyphs
123    {
124        row = row.child(cap(right, false));
125    }
126    row.into()
127}
128
129/// Keep equal-color neighbors distinct without changing the seam width.
130const fn same_color_separator(cap_style: CapStyle) -> &'static str {
131    match cap_style {
132        CapStyle::Arrow => "\u{e0b3}",
133        CapStyle::Padded | CapStyle::Half | CapStyle::Round => "▏",
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn cap_style_glyphs_match_workbar_cap_sets() {
143        assert_eq!(CapStyle::Padded.glyphs(), None);
144        assert_eq!(CapStyle::Half.glyphs(), Some(("\u{2590}", "\u{258c}")));
145        assert_eq!(CapStyle::Round.glyphs(), Some(("\u{e0b6}", "\u{e0b4}")));
146        assert_eq!(CapStyle::Arrow.glyphs(), Some(("\u{e0b2}", "\u{e0b0}")));
147    }
148
149    #[test]
150    fn nerd_font_detection_and_safe_degradation_are_exact() {
151        assert!(!CapStyle::Padded.requires_nerd_font());
152        assert!(!CapStyle::Half.requires_nerd_font());
153        assert!(CapStyle::Round.requires_nerd_font());
154        assert!(CapStyle::Arrow.requires_nerd_font());
155        assert_eq!(CapStyle::Padded.font_safe(), CapStyle::Padded);
156        assert_eq!(CapStyle::Half.font_safe(), CapStyle::Half);
157        assert_eq!(CapStyle::Round.font_safe(), CapStyle::Padded);
158        assert_eq!(CapStyle::Arrow.font_safe(), CapStyle::Padded);
159    }
160
161    #[test]
162    fn all_returns_the_canonical_cap_style_cycle() {
163        assert_eq!(
164            CapStyle::all(),
165            &[
166                CapStyle::Padded,
167                CapStyle::Half,
168                CapStyle::Round,
169                CapStyle::Arrow
170            ]
171        );
172    }
173
174    #[test]
175    fn cap_sides_have_only_the_requested_edges() {
176        assert!(CapSides::Both.has_left());
177        assert!(CapSides::Both.has_right());
178        assert!(CapSides::Left.has_left());
179        assert!(!CapSides::Left.has_right());
180        assert!(!CapSides::Right.has_left());
181        assert!(CapSides::Right.has_right());
182        assert!(!CapSides::None.has_left());
183        assert!(!CapSides::None.has_right());
184    }
185
186    #[test]
187    fn same_color_separators_match_hyprmux_workbar_behavior() {
188        assert_eq!(same_color_separator(CapStyle::Arrow), "\u{e0b3}");
189        assert_eq!(same_color_separator(CapStyle::Round), "▏");
190        assert_eq!(same_color_separator(CapStyle::Half), "▏");
191        assert_eq!(same_color_separator(CapStyle::Padded), "▏");
192    }
193}