1use ratatui::{
2 buffer::Buffer,
3 layout::{Constraint, Flex, Layout, Rect},
4 style::{Color, Style, Stylize},
5 text::{Line, Span, Text},
6 widgets::{Block, StatefulWidget, Widget},
7};
8
9use crate::{config::Theme, note_editor::state::Mode};
10
11fn luminance(color: Color) -> Option<f32> {
14 match color {
15 Color::Rgb(r, g, b) => Some(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32),
16 _ => None,
17 }
18}
19
20fn legible_over(fill: Color, theme: &Theme) -> Color {
24 match (
25 luminance(fill),
26 luminance(theme.background),
27 luminance(theme.text),
28 ) {
29 (Some(fill), Some(background), Some(text)) => {
30 if (fill - text).abs() > (fill - background).abs() {
31 theme.text
32 } else {
33 theme.background
34 }
35 }
36 _ => Color::Black,
37 }
38}
39
40#[derive(Default, Clone, PartialEq)]
41pub struct StatusBarState<'a> {
42 active_component_name: &'a str,
43 mode: Mode,
44 word_count: usize,
45 char_count: usize,
46}
47
48impl<'a> StatusBarState<'a> {
49 pub fn new(
50 active_component_name: &'a str,
51 mode: Mode,
52 word_count: usize,
53 char_count: usize,
54 ) -> Self {
55 Self {
56 active_component_name,
57 mode,
58 word_count,
59 char_count,
60 }
61 }
62}
63
64pub struct StatusBar<'a> {
65 theme: &'a Theme,
66}
67
68impl<'a> StatusBar<'a> {
69 pub(crate) fn new(theme: &'a Theme) -> Self {
70 Self { theme }
71 }
72}
73
74impl<'a> StatefulWidget for StatusBar<'a> {
75 type State = StatusBarState<'a>;
76
77 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
78 let bar = self.theme.status_bar;
79 Block::new()
80 .style(Style::new().bg(bar.background))
81 .render(area, buf);
82
83 let [left, right] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(28)])
84 .flex(Flex::SpaceBetween)
85 .areas(area);
86
87 let mode = state.mode;
89 let mode_color = mode.color(self.theme);
90 let status = Line::from(vec![
91 Span::from(format!(" {} ", mode.label()))
92 .fg(legible_over(mode_color, self.theme))
93 .bg(mode_color)
94 .bold(),
95 Span::from(format!(" {}", state.active_component_name))
96 .fg(bar.foreground)
97 .bold(),
98 ]);
99 Text::from(status).render(left, buf);
100
101 let [word_count, char_count] =
102 Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)])
103 .flex(Flex::End)
104 .areas(right);
105
106 Text::from(
107 format!(
108 "{} word{}",
109 state.word_count,
110 if state.word_count == 1 { "" } else { "s" }
111 )
112 .fg(bar.foreground),
113 )
114 .right_aligned()
115 .render(word_count, buf);
116
117 Text::from(
118 format!(
119 "{} char{}",
120 state.char_count,
121 if state.char_count == 1 { "" } else { "s" }
122 )
123 .fg(bar.foreground),
124 )
125 .right_aligned()
126 .render(char_count, buf);
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn legible_over_picks_the_contrasting_neutral() {
136 let theme = Theme {
137 background: Color::Rgb(20, 20, 20),
138 text: Color::Rgb(230, 230, 230),
139 ..Theme::default()
140 };
141 assert_eq!(
143 legible_over(Color::Rgb(220, 210, 120), &theme),
144 theme.background
145 );
146 assert_eq!(legible_over(Color::Rgb(60, 40, 40), &theme), theme.text);
147 }
148
149 #[test]
150 fn legible_over_falls_back_to_dark_for_ansi_colours() {
151 assert_eq!(legible_over(Color::Red, &Theme::default()), Color::Black);
154 }
155}