omp_tui/components/
progress.rs1use 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
11pub struct Progress {
13 props: Props,
14 slot: Slot,
15 scratch: String,
16}
17
18impl Progress {
19 pub fn new() -> Self {
21 Self { props: Props::new(), slot: next_slot(), scratch: String::new() }
22 }
23
24 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
26 self.props.set(prop, value);
27 self
28 }
29
30 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}