qframe/widgets/
divider.rs1use crate::geometry::{Rect, Size};
4use crate::text;
5use crate::widget::{MeasureCx, PaintCx, Widget};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Divider {
17 space: u16,
18 label: Option<String>,
19 band: bool,
20 vertical: bool,
21}
22
23impl Divider {
24 #[must_use]
26 pub fn new() -> Self {
27 Self { space: 1, label: None, band: false, vertical: false }
28 }
29
30 #[must_use]
32 pub fn space(mut self, cells: u16) -> Self {
33 self.space = cells;
34 self
35 }
36
37 #[must_use]
40 pub fn label(mut self, label: impl Into<String>) -> Self {
41 self.label = Some(label.into());
42 self
43 }
44
45 #[must_use]
47 pub fn band(mut self) -> Self {
48 self.band = true;
49 self
50 }
51
52 #[must_use]
55 pub fn vertical(mut self) -> Self {
56 self.vertical = true;
57 self
58 }
59
60 fn caption(&self) -> Option<&str> {
61 self.label.as_deref().filter(|_| !self.vertical)
62 }
63}
64
65impl Default for Divider {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl<Msg: 'static> Widget<Msg> for Divider {
72 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
73 if self.vertical {
74 return Size::new(self.space, 1).min(available);
76 }
77 let label_row = u16::from(self.caption().is_some());
78 Size::new(available.width, self.space.saturating_add(label_row)).min(available)
79 }
80
81 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
82 if self.band {
83 let style = cx.style("divider", None, &[]);
84 let band = style.color("band").unwrap_or_else(|| cx.color("raised"));
85 cx.clear(area, band);
86 }
87 if let Some(label) = self.caption()
88 && area.height > 0
89 {
90 let mut style = cx.style("divider-label", None, &[]).text();
91 style.bg = None;
92 let y = area.bottom() - 1;
93 let shown = text::truncate(label, area.width).into_owned();
94 cx.text(area.x, y, &shown, style, area.width);
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::runtime::{App, Command, Harness};
103 use crate::widget::View;
104 use crate::widgets::Text;
105
106 struct Demo(Divider);
107
108 impl App for Demo {
109 type Msg = ();
110 fn update(&mut self, _: ()) -> Command<()> {
111 Command::none()
112 }
113 fn view(&self, ui: &mut View<'_, ()>) {
114 ui.column(|ui| {
115 ui.add(Text::new("web"));
116 ui.add(self.0.clone());
117 ui.add(Text::new("db"));
118 })
119 .fill();
120 }
121 }
122
123 #[test]
124 fn plain_divider_is_space() {
125 let h = Harness::new(Demo(Divider::new()), 12, 4);
126 assert_eq!(h.screen(), "web\n\ndb\n\n");
127 }
128
129 #[test]
130 fn caption_sits_after_the_space_and_truncates() {
131 let h = Harness::new(Demo(Divider::new().label("STOPPED SERVICES")), 10, 5);
132 assert_eq!(h.screen(), "web\n\nSTOPPED S…\ndb\n\n");
133 assert_eq!(h.fg(0, 2), h.env().theme().color("muted"));
134 }
135
136 #[test]
137 fn band_paints_a_tone_without_characters() {
138 let h = Harness::new(Demo(Divider::new().band().space(2)), 8, 5);
139 assert_eq!(h.screen(), "web\n\n\ndb\n\n");
140 let theme = h.env().theme();
141 let band = theme.color("surface").expect("token").mix(theme.color("raised").expect("token"), 0.6);
142 assert_eq!(h.bg(3, 1), Some(band));
143 assert_eq!(h.bg(3, 2), Some(band));
144 }
145
146 struct Columns;
147
148 impl App for Columns {
149 type Msg = ();
150 fn update(&mut self, _: ()) -> Command<()> {
151 Command::none()
152 }
153 fn view(&self, ui: &mut View<'_, ()>) {
154 ui.row(|ui| {
155 ui.add(Text::new("cpu"));
156 ui.add(Divider::new().vertical().space(2).band().label("ignored")).fill_height();
157 ui.add(Text::new("mem"));
158 })
159 .fill();
160 }
161 }
162
163 #[test]
164 fn vertical_divider_is_a_column_gap() {
165 let h = Harness::new(Columns, 12, 2);
166 assert_eq!(h.screen(), "cpu mem\n\n");
167 assert_eq!(h.bg(3, 1), h.bg(4, 0));
168 }
169
170 #[test]
171 fn a_huge_space_with_a_caption_fills_the_area() {
172 let h = Harness::new(Demo(Divider::new().space(u16::MAX).label("STOPPED")), 10, 4);
173 assert_eq!(h.screen(), "web\n\n\nSTOPPED\n", "the caption keeps the last row");
174 }
175}