Skip to main content

omp_tui/components/
hr.rs

1use crate::{
2	component::{Component, PaintCtx, Slot, next_slot},
3	context::UiContext,
4	frame::Rect,
5	markup::Border,
6	props::{Prop, PropValue, Props},
7};
8
9/// A horizontal or vertical divider backing the `<hr>` markup tag.
10pub struct Hr {
11	props: Props,
12	slot:  Slot,
13	bar:   String,
14}
15
16impl Hr {
17	/// Creates a divider with default styling.
18	pub fn new() -> Self {
19		Self { props: Props::new(), slot: next_slot(), bar: String::new() }
20	}
21
22	/// Sets one divider property.
23	pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
24		self.props.set(prop, value);
25		self
26	}
27
28	/// Sets one divider property from a string.
29	pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
30		self.props.set(prop, value);
31		self
32	}
33}
34
35impl Default for Hr {
36	fn default() -> Self {
37		Self::new()
38	}
39}
40
41impl Component for Hr {
42	fn props(&self) -> &Props {
43		&self.props
44	}
45
46	fn props_mut(&mut self) -> &mut Props {
47		&mut self.props
48	}
49
50	fn slot(&self) -> Slot {
51		self.slot
52	}
53
54	fn paints_border(&self) -> bool {
55		false
56	}
57
58	fn stretch_in_row(&self) -> bool {
59		true
60	}
61
62	fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
63		if self.props.flag(Prop::Vertical) {
64			(1, 1)
65		} else {
66			(1, 4)
67		}
68	}
69
70	fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
71		1
72	}
73
74	fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
75		let border = self.props.border().unwrap_or(Border::Square);
76		let (.., horizontal, vertical) = pc.ctx.charset.border(border);
77		let style = self.props.style(&pc.ctx.theme);
78		// An unstyled rule takes the theme's border tone; `fg=`/`bc=` win.
79		let line = self.props.edge(&pc.ctx.theme).map_or_else(
80			|| {
81				if self.props.get(Prop::Fg).is_none() {
82					style.fg(pc.ctx.theme.border)
83				} else {
84					style.dim()
85				}
86			},
87			|color| style.fg(color),
88		);
89		if self.props.flag(Prop::Vertical) {
90			self.bar.clear();
91			repeated_char(&mut self.bar, vertical, 1);
92			for row in 0..rect.height {
93				let y = rect.y.saturating_add(row);
94				if y >= pc.clip {
95					break;
96				}
97				pc.frame.put(rect.x, y, &self.bar, line);
98			}
99			return;
100		}
101		if rect.y >= pc.clip {
102			return;
103		}
104		self.bar.clear();
105		repeated_char(&mut self.bar, horizontal, usize::from(rect.width));
106		pc.frame.put(rect.x, rect.y, &self.bar, line);
107		if let Some(title) = self.props.title() {
108			pc.frame.put(rect.x.saturating_add(2), rect.y, " ", style);
109			let end = pc
110				.frame
111				.put(rect.x.saturating_add(3), rect.y, title, style.bold());
112			pc.frame.put(end, rect.y, " ", style);
113		}
114	}
115}
116
117/// Flexible blank space backing the `<spacer>` markup tag.
118pub struct Spacer {
119	props: Props,
120	slot:  Slot,
121}
122
123impl Spacer {
124	/// Creates an empty spacer.
125	pub fn new() -> Self {
126		Self { props: Props::new(), slot: next_slot() }
127	}
128
129	/// Sets one spacer property.
130	pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
131		self.props.set(prop, value);
132		self
133	}
134
135	/// Sets one spacer property from a string.
136	pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
137		self.props.set(prop, value);
138		self
139	}
140}
141
142impl Default for Spacer {
143	fn default() -> Self {
144		Self::new()
145	}
146}
147
148impl Component for Spacer {
149	fn props(&self) -> &Props {
150		&self.props
151	}
152
153	fn props_mut(&mut self) -> &mut Props {
154		&mut self.props
155	}
156
157	fn slot(&self) -> Slot {
158		self.slot
159	}
160
161	fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
162		(1, 4)
163	}
164
165	fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
166		1
167	}
168
169	fn paint(&mut self, _pc: &mut PaintCtx<'_>, _rect: Rect) {}
170}
171
172fn repeated_char(output: &mut String, character: char, count: usize) {
173	output.reserve(count.saturating_mul(character.len_utf8()));
174	output.extend(std::iter::repeat_n(character, count));
175}
176
177#[cfg(test)]
178mod tests {
179	use super::Hr;
180	use crate::{
181		component::{Cached, Component, PaintCtx},
182		context::UiContext,
183		frame::{Frame, Rect, Size},
184		props::Prop,
185		test_support::frame_row_text,
186	};
187
188	#[test]
189	fn fills_horizontal_rule_with_charset_glyph() {
190		let ctx = UiContext::default();
191		let mut hr = Cached::new(Box::new(Hr::new()));
192		assert_eq!(hr.measure(&ctx), (1, 4));
193		hr.place(&ctx, Rect::new(0, 0, 6, 1));
194		let mut frame = Frame::new(Size::new(6, 1));
195		let mut hits = Vec::new();
196		hr.paint(&mut PaintCtx::new(&mut frame, &ctx, &mut hits, &mut Vec::new()));
197		assert_eq!(frame_row_text(&frame, 0), "──────");
198	}
199
200	#[test]
201	fn vertical_rule_uses_one_column_and_fills_height() {
202		let ctx = UiContext::default();
203		let mut hr = Hr::new().with(Prop::Vertical, true);
204		assert_eq!(hr.measure(&ctx), (1, 1));
205		let mut frame = Frame::new(Size::new(1, 3));
206		let mut hits = Vec::new();
207		hr.paint(
208			&mut PaintCtx::new(&mut frame, &ctx, &mut hits, &mut Vec::new()),
209			Rect::new(0, 0, 1, 3),
210		);
211		assert_eq!(frame_row_text(&frame, 0), "│");
212		assert_eq!(frame_row_text(&frame, 1), "│");
213		assert_eq!(frame_row_text(&frame, 2), "│");
214	}
215}