Skip to main content

omp_tui/components/
progress.rs

1use std::fmt::Write as _;
2
3use crate::{
4	component::{Component, PaintCtx, Slot, next_slot},
5	context::UiContext,
6	frame::{Rect, Style},
7	props::{Prop, PropValue, Props},
8	rich::cell_width,
9};
10
11/// A determinate progress bar backing the `<progress>` markup tag.
12pub struct Progress {
13	props:   Props,
14	slot:    Slot,
15	scratch: String,
16}
17
18impl Progress {
19	/// Creates a progress bar at its default value.
20	pub fn new() -> Self {
21		Self { props: Props::new(), slot: next_slot(), scratch: String::new() }
22	}
23
24	/// Sets one progress-bar property.
25	pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
26		self.props.set(prop, value);
27		self
28	}
29
30	/// Sets one progress-bar property from a string.
31	pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
32		self.props.set(prop, value);
33		self
34	}
35
36	fn amount(&self) -> (u64, u64) {
37		let number = |prop| match self.props.get(prop) {
38			Some(PropValue::U16(value)) => Some(u64::from(*value)),
39			Some(PropValue::I64(value)) => Some((*value).max(0) as u64),
40			Some(PropValue::F32(value)) => Some(value.max(0.0) as u64),
41			Some(PropValue::Str(value)) => value.parse().ok(),
42			_ => None,
43		};
44		let maximum = number(Prop::Max).unwrap_or(100).max(1);
45		(number(Prop::Value).unwrap_or(0).min(maximum), maximum)
46	}
47}
48
49impl Default for Progress {
50	fn default() -> Self {
51		Self::new()
52	}
53}
54
55impl Component for Progress {
56	fn props(&self) -> &Props {
57		&self.props
58	}
59
60	fn props_mut(&mut self) -> &mut Props {
61		&mut self.props
62	}
63
64	fn slot(&self) -> Slot {
65		self.slot
66	}
67
68	fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
69		(16, 40)
70	}
71
72	fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
73		1
74	}
75
76	fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
77		if rect.y >= pc.clip {
78			return;
79		}
80		let (value, maximum) = self.amount();
81		let percent = value.saturating_mul(100) / maximum;
82		let mut x = rect.x;
83		if let Some(label) = self.props.str_of(Prop::Label) {
84			x = pc
85				.frame
86				.put(x, rect.y, label, Style::new().fg(pc.ctx.theme.fg));
87			x = pc
88				.frame
89				.put(x, rect.y, " ", Style::new().fg(pc.ctx.theme.fg));
90		}
91		self.scratch.clear();
92		let _ = write!(self.scratch, " {percent}%");
93		let bar_width = rect
94			.x
95			.saturating_add(rect.width)
96			.saturating_sub(x)
97			.saturating_sub(cell_width(&self.scratch))
98			.max(4);
99		let fill =
100			u16::try_from(u64::from(bar_width).saturating_mul(value) / maximum).unwrap_or(bar_width);
101		for index in 0..bar_width {
102			let (glyph, style) = if index < fill {
103				(pc.ctx.charset.progress().0, Style::new().fg(pc.ctx.theme.accent))
104			} else {
105				(pc.ctx.charset.progress().1, Style::new().fg(pc.ctx.theme.muted))
106			};
107			x = pc.frame.put(x, rect.y, glyph, style);
108		}
109		pc.frame
110			.put(x, rect.y, &self.scratch, Style::new().fg(pc.ctx.theme.muted));
111	}
112}
113
114#[cfg(test)]
115mod tests {
116	use super::*;
117	use crate::{
118		component::PaintCtx,
119		frame::{Frame, Size},
120		test_support::frame_row_text,
121	};
122
123	#[test]
124	fn fill_uses_value_over_maximum() {
125		let mut progress = Progress::new()
126			.with(Prop::Value, "3")
127			.with(Prop::Max, 4_u16);
128		let ctx = UiContext::default();
129		let mut frame = Frame::new(Size::new(12, 1));
130		let mut hits = Vec::new();
131		progress.paint(
132			&mut PaintCtx::new(&mut frame, &ctx, &mut hits, &mut Vec::new()),
133			Rect::new(0, 0, 12, 1),
134		);
135		let row = frame_row_text(&frame, 0);
136		assert!(row.starts_with("██████░░"), "{row:?}");
137		assert!(row.ends_with(" 75%"), "{row:?}");
138	}
139}