rat_widget/
statusline.rs

1//!
2//! Statusbar with multiple sections.
3//!
4//! ```
5//!
6//! use ratatui::buffer::Buffer;
7//! use ratatui::layout::{Constraint, Rect};
8//! use ratatui::style::{Style, Stylize};
9//! use ratatui::widgets::StatefulWidget;
10//! use rat_widget::statusline::{StatusLine, StatusLineState};
11//!
12//! let mut status_line_state = StatusLineState::new();
13//! status_line_state.status(0, "Everything's fine.");
14//! status_line_state.status(1, "50%");
15//! status_line_state.status(2, "72%");
16//!
17//!
18//! # let area = Rect::new(0,24,80,1);
19//! # let mut buf = Buffer::empty(area);
20//! # let buf = &mut buf;
21//!
22//! StatusLine::new()
23//!     .layout([
24//!         Constraint::Fill(1),
25//!         Constraint::Length(8),
26//!         Constraint::Length(8)
27//!     ])
28//!     .styles([
29//!         Style::new().white().on_dark_gray(),
30//!         Style::new().white().on_cyan(),
31//!         Style::new().white().on_blue()
32//!     ])
33//!     .render(area, buf, &mut status_line_state);
34//!
35//! ```
36
37use crate::_private::NonExhaustive;
38use crate::text::HasScreenCursor;
39use rat_event::{HandleEvent, MouseOnly, Outcome, Regular};
40use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
41use rat_reloc::{RelocatableState, relocate_area, relocate_areas};
42use ratatui::buffer::Buffer;
43use ratatui::layout::{Constraint, Layout, Rect};
44use ratatui::style::Style;
45use ratatui::text::{Line, Span};
46use ratatui::widgets::{StatefulWidget, Widget};
47use std::borrow::Cow;
48use std::fmt::Debug;
49
50/// Statusbar with multiple sections.
51#[derive(Debug, Default, Clone)]
52pub struct StatusLine {
53    sep: Option<Cow<'static, str>>,
54    style: Vec<Style>,
55    widths: Vec<Constraint>,
56}
57
58/// Combined style.
59#[derive(Debug, Clone)]
60pub struct StatusLineStyle {
61    // separator
62    pub sep: Option<Cow<'static, str>>,
63    // styles
64    pub styles: Vec<Style>,
65    pub non_exhaustive: NonExhaustive,
66}
67
68/// State & event handling.
69#[derive(Debug, Clone)]
70pub struct StatusLineState {
71    /// Total area
72    /// __readonly__. renewed for each render.
73    pub area: Rect,
74    /// Areas for each section.
75    /// __readonly__. renewed for each render.
76    pub areas: Vec<Rect>,
77
78    /// Statustext for each section.
79    /// __read+write__
80    pub status: Vec<String>,
81
82    pub non_exhaustive: NonExhaustive,
83}
84
85impl Default for StatusLineStyle {
86    fn default() -> Self {
87        Self {
88            sep: Default::default(),
89            styles: Default::default(),
90            non_exhaustive: NonExhaustive,
91        }
92    }
93}
94
95impl StatusLine {
96    /// New widget.
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Layout for the sections.
102    ///
103    /// This layout determines the number of sections.
104    /// If the styles or the status text vec differ, defaults are used.
105    pub fn layout<It, Item>(mut self, widths: It) -> Self
106    where
107        It: IntoIterator<Item = Item>,
108        Item: Into<Constraint>,
109    {
110        self.widths = widths.into_iter().map(|v| v.into()).collect();
111        self
112    }
113
114    /// Styles for each section.
115    pub fn styles(mut self, style: impl IntoIterator<Item = impl Into<Style>>) -> Self {
116        self.style = style.into_iter().map(|v| v.into()).collect();
117        self
118    }
119
120    /// Set all styles.
121    pub fn styles_ext(mut self, styles: StatusLineStyle) -> Self {
122        self.sep = styles.sep;
123        self.style = styles.styles;
124        self
125    }
126}
127
128impl Default for StatusLineState {
129    fn default() -> Self {
130        Self {
131            area: Default::default(),
132            areas: Default::default(),
133            status: Default::default(),
134            non_exhaustive: NonExhaustive,
135        }
136    }
137}
138
139impl HasFocus for StatusLineState {
140    fn build(&self, _builder: &mut FocusBuilder) {
141        // none
142    }
143
144    fn focus(&self) -> FocusFlag {
145        unimplemented!("not available")
146    }
147
148    fn area(&self) -> Rect {
149        unimplemented!("not available")
150    }
151}
152
153impl HasScreenCursor for StatusLineState {
154    fn screen_cursor(&self) -> Option<(u16, u16)> {
155        None
156    }
157}
158
159impl RelocatableState for StatusLineState {
160    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
161        self.area = relocate_area(self.area, shift, clip);
162        relocate_areas(self.areas.as_mut(), shift, clip);
163    }
164}
165
166impl StatusLineState {
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// New named widget.
172    pub fn named(_name: &str) -> Self {
173        Self::default()
174    }
175
176    /// Clear all status text.
177    pub fn clear_status(&mut self) {
178        self.status.clear();
179    }
180
181    /// Set the specific status section.
182    pub fn status<S: Into<String>>(&mut self, idx: usize, msg: S) {
183        while self.status.len() <= idx {
184            self.status.push("".to_string());
185        }
186        self.status[idx] = msg.into();
187    }
188}
189
190impl StatefulWidget for &StatusLine {
191    type State = StatusLineState;
192
193    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
194        render_ref(self, area, buf, state);
195    }
196}
197
198impl StatefulWidget for StatusLine {
199    type State = StatusLineState;
200
201    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
202        render_ref(&self, area, buf, state);
203    }
204}
205
206fn render_ref(widget: &StatusLine, area: Rect, buf: &mut Buffer, state: &mut StatusLineState) {
207    state.area = area;
208
209    let layout = Layout::horizontal(widget.widths.iter()).split(state.area);
210
211    for (i, rect) in layout.iter().enumerate() {
212        let style = widget.style.get(i).copied().unwrap_or_default();
213        let txt = state.status.get(i).map(|v| v.as_str()).unwrap_or("");
214
215        let sep = if i > 0 {
216            if let Some(sep) = widget.sep.as_ref().map(|v| v.as_ref()) {
217                Span::from(sep)
218            } else {
219                Span::default()
220            }
221        } else {
222            Span::default()
223        };
224
225        Line::from_iter([
226            sep, //
227            Span::from(txt),
228        ])
229        .render(*rect, buf);
230
231        buf.set_style(*rect, style);
232    }
233}
234
235impl HandleEvent<crossterm::event::Event, Regular, Outcome> for StatusLineState {
236    fn handle(&mut self, _event: &crossterm::event::Event, _qualifier: Regular) -> Outcome {
237        Outcome::Continue
238    }
239}
240
241impl HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for StatusLineState {
242    fn handle(&mut self, _event: &crossterm::event::Event, _qualifier: MouseOnly) -> Outcome {
243        Outcome::Continue
244    }
245}