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    ///
98    /// The actual number of items is set by [layout].
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    /// Layout for the sections.
104    ///
105    /// This layout determines the number of sections.
106    /// If the styles or the status text vec differ, defaults are used.
107    pub fn layout<It, Item>(mut self, widths: It) -> Self
108    where
109        It: IntoIterator<Item = Item>,
110        Item: Into<Constraint>,
111    {
112        self.widths = widths.into_iter().map(|v| v.into()).collect();
113        self
114    }
115
116    /// Styles for each section.
117    pub fn styles(mut self, style: impl IntoIterator<Item = impl Into<Style>>) -> Self {
118        self.style = style.into_iter().map(|v| v.into()).collect();
119        self
120    }
121
122    /// Set all styles.
123    pub fn styles_ext(mut self, styles: StatusLineStyle) -> Self {
124        self.sep = styles.sep;
125        self.style = styles.styles;
126        self
127    }
128}
129
130impl Default for StatusLineState {
131    fn default() -> Self {
132        Self {
133            area: Default::default(),
134            areas: Default::default(),
135            status: Default::default(),
136            non_exhaustive: NonExhaustive,
137        }
138    }
139}
140
141impl HasFocus for StatusLineState {
142    fn build(&self, _builder: &mut FocusBuilder) {
143        // none
144    }
145
146    fn focus(&self) -> FocusFlag {
147        unimplemented!("not available")
148    }
149
150    fn area(&self) -> Rect {
151        unimplemented!("not available")
152    }
153}
154
155impl HasScreenCursor for StatusLineState {
156    fn screen_cursor(&self) -> Option<(u16, u16)> {
157        None
158    }
159}
160
161impl RelocatableState for StatusLineState {
162    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
163        self.area = relocate_area(self.area, shift, clip);
164        relocate_areas(self.areas.as_mut(), shift, clip);
165    }
166}
167
168impl StatusLineState {
169    pub fn new() -> Self {
170        Self::default()
171    }
172
173    /// New named widget.
174    pub fn named(_name: &str) -> Self {
175        Self::default()
176    }
177
178    /// Clear all status text.
179    pub fn clear_status(&mut self) {
180        self.status.clear();
181    }
182
183    /// Set the specific status section.
184    pub fn status<S: Into<String>>(&mut self, idx: usize, msg: S) {
185        while self.status.len() <= idx {
186            self.status.push("".to_string());
187        }
188        self.status[idx] = msg.into();
189    }
190}
191
192impl StatefulWidget for &StatusLine {
193    type State = StatusLineState;
194
195    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
196        render_ref(self, area, buf, state);
197    }
198}
199
200impl StatefulWidget for StatusLine {
201    type State = StatusLineState;
202
203    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
204        render_ref(&self, area, buf, state);
205    }
206}
207
208fn render_ref(widget: &StatusLine, area: Rect, buf: &mut Buffer, state: &mut StatusLineState) {
209    state.area = area;
210
211    let layout = Layout::horizontal(widget.widths.iter()).split(state.area);
212
213    for (i, rect) in layout.iter().enumerate() {
214        let style = widget.style.get(i).copied().unwrap_or_default();
215        let txt = state.status.get(i).map(|v| v.as_str()).unwrap_or("");
216
217        let sep = if i > 0 {
218            if let Some(sep) = widget.sep.as_ref().map(|v| v.as_ref()) {
219                Span::from(sep)
220            } else {
221                Span::default()
222            }
223        } else {
224            Span::default()
225        };
226
227        Line::from_iter([
228            sep, //
229            Span::from(txt),
230        ])
231        .render(*rect, buf);
232
233        buf.set_style(*rect, style);
234    }
235}
236
237impl HandleEvent<crossterm::event::Event, Regular, Outcome> for StatusLineState {
238    fn handle(&mut self, _event: &crossterm::event::Event, _qualifier: Regular) -> Outcome {
239        Outcome::Continue
240    }
241}
242
243impl HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for StatusLineState {
244    fn handle(&mut self, _event: &crossterm::event::Event, _qualifier: MouseOnly) -> Outcome {
245        Outcome::Continue
246    }
247}