Skip to main content

datui_lib/widgets/ui/
working.rs

1//! Work in progress said where its result will appear: the spinner and one plain
2//! status, styled as the loading screen's phase line.
3
4use crate::render::context::RenderContext;
5use ratatui::buffer::Buffer;
6use ratatui::layout::Rect;
7use ratatui::style::{Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::{Clear, Paragraph, Widget};
10
11#[derive(Clone, Copy)]
12pub struct Working<'a> {
13    pub text: &'a str,
14    /// The throbber frame, so this spinner turns with the footer's.
15    pub frame: usize,
16}
17
18impl Working<'_> {
19    pub fn line(&self, ctx: &RenderContext) -> Line<'static> {
20        let g = crate::glyphs::get();
21        Line::from(vec![
22            Span::styled(
23                format!("{}  ", g.spinner[self.frame % g.spinner.len()]),
24                Style::default().fg(ctx.throbber),
25            ),
26            Span::styled(
27                self.text.to_string(),
28                Style::default()
29                    .fg(ctx.text_primary)
30                    .add_modifier(Modifier::BOLD),
31            ),
32        ])
33    }
34
35    /// In place of what `area` held, centered a little above the middle, where the
36    /// loading screen puts its phase.
37    pub fn render_centered(&self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
38        Clear.render(area, buf);
39        if area.height == 0 {
40            return;
41        }
42        let y = area.y + (area.height / 2).saturating_sub(1);
43        Paragraph::new(self.line(ctx)).centered().render(
44            Rect {
45                y,
46                height: 1,
47                ..area
48            },
49            buf,
50        );
51    }
52
53    /// Over the top right of what `area` already shows, which stays on screen until
54    /// the work is done.
55    pub fn render_corner(&self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
56        let line = self.line(ctx);
57        // A cell of air either side, so the label never touches a mark.
58        let width = (line.width() as u16 + 2).min(area.width);
59        if area.height == 0 || width == 0 {
60            return;
61        }
62        let spot = Rect {
63            x: area.right() - width,
64            y: area.y,
65            width,
66            height: 1,
67        };
68        Clear.render(spot, buf);
69        Paragraph::new(line).centered().render(spot, buf);
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn centered_replaces_what_was_there() {
79        let ctx = RenderContext::for_test();
80        let area = Rect::new(0, 0, 30, 5);
81        let mut buf = Buffer::empty(area);
82        Paragraph::new(vec![Line::from("old"); 5]).render(area, &mut buf);
83        Working {
84            text: "Applying SQL query...",
85            frame: 0,
86        }
87        .render_centered(area, &mut buf, &ctx);
88        let rows = crate::tests::buffer_lines(&buf);
89        assert!(rows[1].contains("Applying SQL query..."), "{rows:#?}");
90        assert!(rows.iter().all(|row| !row.contains("old")), "{rows:#?}");
91    }
92
93    #[test]
94    fn corner_keeps_what_is_under_it() {
95        let ctx = RenderContext::for_test();
96        let area = Rect::new(0, 0, 40, 3);
97        let mut buf = Buffer::empty(area);
98        Paragraph::new(vec![Line::from("chart"); 3]).render(area, &mut buf);
99        Working {
100            text: "Computing...",
101            frame: 0,
102        }
103        .render_corner(area, &mut buf, &ctx);
104        let rows = crate::tests::buffer_lines(&buf);
105        assert!(rows[0].trim_end().ends_with("Computing..."), "{rows:#?}");
106        assert!(rows.iter().all(|row| row.starts_with("chart")), "{rows:#?}");
107    }
108}