Skip to main content

wisp/components/
thought_message.rs

1use tui::{FitOptions, Frame, Line, Style, Theme, ViewContext};
2
3pub struct ThoughtMessage<'a> {
4    pub text: &'a str,
5}
6
7impl ThoughtMessage<'_> {
8    fn format_line(text: &str, theme: &Theme) -> Line {
9        Line::with_style(text, Style::fg(theme.muted()).italic())
10    }
11
12    fn format_lines(text: &str, theme: &Theme) -> Vec<Line> {
13        text.lines().map(|line| Self::format_line(line, theme)).collect()
14    }
15}
16
17impl ThoughtMessage<'_> {
18    pub fn render(&self, context: &ViewContext) -> Frame {
19        if self.text.is_empty() {
20            return Frame::empty();
21        }
22
23        let lines = Self::format_lines(self.text, &context.theme);
24        Frame::new(lines).fit(context.size.width, FitOptions::wrap())
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn renders_italic_muted_thought_line() {
34        let component = ThoughtMessage { text: "check plan" };
35        let context = ViewContext::new((80, 24));
36        let frame = component.render(&context);
37        let lines = frame.lines();
38        assert_eq!(lines.len(), 1);
39        assert_eq!(lines[0].plain_text(), "check plan");
40        let style = lines[0].spans()[0].style();
41        assert_eq!(style.fg, Some(context.theme.muted()));
42        assert!(style.italic);
43    }
44
45    #[test]
46    fn renders_all_lines_as_italic_muted() {
47        let component = ThoughtMessage { text: "line one\nline two" };
48        let context = ViewContext::new((80, 24));
49        let frame = component.render(&context);
50        let lines = frame.lines();
51        assert_eq!(lines.len(), 2);
52        assert_eq!(lines[0].plain_text(), "line one");
53        assert_eq!(lines[1].plain_text(), "line two");
54        for line in lines {
55            let style = line.spans()[0].style();
56            assert_eq!(style.fg, Some(context.theme.muted()));
57            assert!(style.italic);
58        }
59    }
60
61    #[test]
62    fn wrapped_continuation_rows_remain_italic_muted() {
63        let component = ThoughtMessage { text: "abcdefghijklmnopqrstuvwxyz" };
64        let context = ViewContext::new((80, 24));
65        let frame = component.render(&context);
66        let wrapped = frame.lines()[0].soft_wrap(12);
67        assert!(wrapped.len() > 1);
68
69        for row in wrapped.iter().skip(1) {
70            assert!(!row.spans().is_empty());
71            assert!(
72                row.spans()
73                    .iter()
74                    .all(|span| { span.style().fg == Some(context.theme.muted()) && span.style().italic })
75            );
76        }
77    }
78}