Skip to main content

tui_realm_stdlib/components/
line_gauge.rs

1use tuirealm::command::{Cmd, CmdResult};
2use tuirealm::component::Component;
3use tuirealm::props::{
4    AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, SpanStatic,
5    Style, TextModifiers, Title,
6};
7use tuirealm::ratatui::Frame;
8use tuirealm::ratatui::layout::Rect;
9use tuirealm::ratatui::text::Span;
10use tuirealm::ratatui::widgets::LineGauge as TuiLineGauge;
11use tuirealm::state::State;
12
13use crate::prop_ext::CommonProps;
14
15// -- Component
16
17/// `LineGauge`, also known as progress bars, provides a component which shows a line which is some percent filled.
18///
19/// It is possible to set the style for the progress bar and the text shown above it.
20///
21/// Read more in [`LineGauge`](TuiLineGauge).
22///
23/// If a multi-line Guage is necessary, use [`Gauge`](crate::components::Gauge) instead.
24#[derive(Default)]
25#[must_use]
26pub struct LineGauge {
27    common: CommonProps,
28    props: Props,
29}
30
31impl LineGauge {
32    /// Set the main foreground color. This may get overwritten by individual text styles.
33    pub fn foreground(mut self, fg: Color) -> Self {
34        self.attr(Attribute::Foreground, AttrValue::Color(fg));
35        self
36    }
37
38    /// Set the main background color. This may get overwritten by individual text styles.
39    pub fn background(mut self, bg: Color) -> Self {
40        self.attr(Attribute::Background, AttrValue::Color(bg));
41        self
42    }
43
44    /// Set the main text modifiers. This may get overwritten by individual text styles.
45    pub fn modifiers(mut self, m: TextModifiers) -> Self {
46        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
47        self
48    }
49
50    /// Set the main style. This may get overwritten by individual text styles.
51    ///
52    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
53    pub fn style(mut self, style: Style) -> Self {
54        self.attr(Attribute::Style, AttrValue::Style(style));
55        self
56    }
57
58    /// Set a custom style for the border when the component is unfocused.
59    pub fn inactive(mut self, s: Style) -> Self {
60        self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
61        self
62    }
63
64    /// Add a border to the component.
65    pub fn borders(mut self, b: Borders) -> Self {
66        self.attr(Attribute::Borders, AttrValue::Borders(b));
67        self
68    }
69
70    /// Add a title to the component.
71    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
72        self.attr(Attribute::Title, AttrValue::Title(title.into()));
73        self
74    }
75
76    /// Set a label text for the Gauge.
77    pub fn label<S: Into<String>>(mut self, s: S) -> Self {
78        // TODO: we should consider using Span or Line
79        self.attr(Attribute::Text, AttrValue::String(s.into()));
80        self
81    }
82
83    /// Set the initial progress.
84    pub fn progress(mut self, p: f64) -> Self {
85        Self::assert_progress(p);
86        self.attr(
87            Attribute::Value,
88            AttrValue::Payload(PropPayload::Single(PropValue::F64(p))),
89        );
90        self
91    }
92
93    /// Set custom Style & Symbols for the filled & unfilled styles.
94    ///
95    /// By default ratatui uses [`HORIZONTAL`](tuirealm::ratatui::symbols::line::HORIZONTAL) for *both*.
96    pub fn line_style<F: Into<SpanStatic>, U: Into<SpanStatic>>(
97        mut self,
98        filled: F,
99        unfilled: U,
100    ) -> Self {
101        self.attr(
102            Attribute::HighlightedStr,
103            AttrValue::Payload(PropPayload::Pair((
104                PropValue::TextSpan(filled.into()),
105                PropValue::TextSpan(unfilled.into()),
106            ))),
107        );
108
109        self
110    }
111
112    fn get_line_style(&self) -> Option<(&Span<'_>, &Span<'_>)> {
113        self.props
114            .get(Attribute::HighlightedStr)
115            .and_then(AttrValue::as_payload)
116            .and_then(PropPayload::as_pair)
117            .and_then(|pair| Some((pair.0.as_textspan()?, pair.1.as_textspan()?)))
118    }
119
120    fn assert_progress(p: f64) {
121        assert!(
122            (0.0..=1.0).contains(&p),
123            "Progress value must be in range [0.0, 1.0]"
124        );
125    }
126}
127
128impl Component for LineGauge {
129    fn view(&mut self, render: &mut Frame, area: Rect) {
130        if !self.common.display {
131            return;
132        }
133
134        // Text
135        let label = self
136            .props
137            .get(Attribute::Text)
138            .and_then(AttrValue::as_string)
139            .map(String::as_str)
140            .unwrap_or_default();
141        // Get percentage
142        let percentage = self
143            .props
144            .get(Attribute::Value)
145            .and_then(AttrValue::as_payload)
146            .and_then(PropPayload::as_single)
147            .and_then(PropValue::as_f64)
148            .unwrap_or_default();
149
150        let mut widget = TuiLineGauge::default()
151            .style(self.common.style)
152            .filled_style(self.common.style)
153            .label(label)
154            .ratio(percentage);
155
156        if let Some(block) = self.common.get_block() {
157            widget = widget.block(block);
158        }
159
160        if let Some(line_style) = self.get_line_style() {
161            widget = widget
162                .filled_symbol(&line_style.0.content)
163                .filled_style(line_style.0.style)
164                .unfilled_symbol(&line_style.1.content)
165                .unfilled_style(line_style.1.style);
166        }
167
168        // Make progress bar
169        render.render_widget(widget, area);
170    }
171
172    fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
173        if let Some(value) = self.common.get_for_query(attr) {
174            return Some(value);
175        }
176
177        self.props.get_for_query(attr)
178    }
179
180    fn attr(&mut self, attr: Attribute, value: AttrValue) {
181        if let Some(value) = self.common.set(attr, value) {
182            if let Attribute::Value = attr
183                && let AttrValue::Payload(p) = value.clone()
184            {
185                Self::assert_progress(p.unwrap_single().unwrap_f64());
186            }
187            self.props.set(attr, value);
188        }
189    }
190
191    fn state(&self) -> State {
192        State::None
193    }
194
195    fn perform(&mut self, cmd: Cmd) -> CmdResult {
196        CmdResult::Invalid(cmd)
197    }
198}
199
200#[cfg(test)]
201mod test {
202
203    use pretty_assertions::assert_eq;
204    use tuirealm::props::{BorderType, HorizontalAlignment};
205    use tuirealm::ratatui::symbols::line::{DOUBLE_HORIZONTAL, HORIZONTAL};
206
207    use super::*;
208
209    #[test]
210    fn test_components_progress_bar() {
211        let component = LineGauge::default()
212            .background(Color::Red)
213            .foreground(Color::White)
214            .progress(0.60)
215            .title(Title::from("Downloading file...").alignment(HorizontalAlignment::Center))
216            .label("60% - ETA 00:20")
217            .line_style(DOUBLE_HORIZONTAL, HORIZONTAL)
218            .borders(Borders::default());
219        // Get value
220        assert_eq!(component.state(), State::None);
221    }
222
223    #[test]
224    #[should_panic = "Progress value must be in range [0.0, 1.0]"]
225    fn line_gauge_bad_prog() {
226        let _ = LineGauge::default()
227            .background(Color::Red)
228            .foreground(Color::White)
229            .progress(6.0)
230            .title(Title::from("Downloading file...").alignment(HorizontalAlignment::Center))
231            .label("60% - ETA 00:20")
232            .borders(Borders::default());
233    }
234
235    #[test]
236    fn should_allow_styling_line() {
237        let _ = LineGauge::default()
238            .borders(
239                Borders::default()
240                    .color(Color::Blue)
241                    .modifiers(BorderType::Rounded),
242            )
243            .foreground(Color::Blue)
244            .label("0%")
245            .title(Title::from("Loading...").alignment(HorizontalAlignment::Center))
246            .line_style(
247                Span::styled(HORIZONTAL, Style::new().fg(Color::Red)),
248                Span::styled(HORIZONTAL, Style::new().fg(Color::Gray)),
249            )
250            .progress(0.0);
251    }
252}