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 rat_reloc::{RelocatableState, relocate_area, relocate_areas};
39use ratatui::buffer::Buffer;
40use ratatui::layout::{Constraint, Layout, Rect};
41use ratatui::style::Style;
42use ratatui::text::{Line, Span};
43use ratatui::widgets::{StatefulWidget, Widget};
44use std::borrow::Cow;
45use std::fmt::Debug;
46
47/// Statusbar with multiple sections.
48#[derive(Debug, Default, Clone)]
49pub struct StatusLine {
50    sep: Option<Cow<'static, str>>,
51    style: Vec<Style>,
52    widths: Vec<Constraint>,
53}
54
55/// Combined style.
56#[derive(Debug, Clone)]
57pub struct StatusLineStyle {
58    // separator
59    pub sep: Option<Cow<'static, str>>,
60    // styles
61    pub styles: Vec<Style>,
62    pub non_exhaustive: NonExhaustive,
63}
64
65/// State & event handling.
66#[derive(Debug, Clone)]
67pub struct StatusLineState {
68    /// Total area
69    /// __readonly__. renewed for each render.
70    pub area: Rect,
71    /// Areas for each section.
72    /// __readonly__. renewed for each render.
73    pub areas: Vec<Rect>,
74
75    /// Statustext for each section.
76    /// __read+write__
77    pub status: Vec<String>,
78
79    pub non_exhaustive: NonExhaustive,
80}
81
82impl Default for StatusLineStyle {
83    fn default() -> Self {
84        Self {
85            sep: Default::default(),
86            styles: Default::default(),
87            non_exhaustive: NonExhaustive,
88        }
89    }
90}
91
92impl StatusLine {
93    /// New widget.
94    pub fn new() -> Self {
95        Self {
96            sep: Default::default(),
97            style: Default::default(),
98            widths: Default::default(),
99        }
100    }
101
102    /// Layout for the sections.
103    ///
104    /// This layout determines the number of sections.
105    /// If the styles or the status text vec differ, defaults are used.
106    pub fn layout<It, Item>(mut self, widths: It) -> Self
107    where
108        It: IntoIterator<Item = Item>,
109        Item: Into<Constraint>,
110    {
111        self.widths = widths.into_iter().map(|v| v.into()).collect();
112        self
113    }
114
115    /// Styles for each section.
116    pub fn styles(mut self, style: impl IntoIterator<Item = impl Into<Style>>) -> Self {
117        self.style = style.into_iter().map(|v| v.into()).collect();
118        self
119    }
120
121    /// Set all styles.
122    pub fn styles_ext(mut self, styles: StatusLineStyle) -> Self {
123        self.sep = styles.sep;
124        self.style = styles.styles;
125        self
126    }
127}
128
129impl Default for StatusLineState {
130    fn default() -> Self {
131        Self {
132            area: Default::default(),
133            areas: Default::default(),
134            status: Default::default(),
135            non_exhaustive: NonExhaustive,
136        }
137    }
138}
139
140impl RelocatableState for StatusLineState {
141    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
142        self.area = relocate_area(self.area, shift, clip);
143        relocate_areas(self.areas.as_mut(), shift, clip);
144    }
145}
146
147impl StatusLineState {
148    pub fn new() -> Self {
149        Self::default()
150    }
151
152    /// Clear all status text.
153    pub fn clear_status(&mut self) {
154        self.status.clear();
155    }
156
157    /// Set the specific status section.
158    pub fn status<S: Into<String>>(&mut self, idx: usize, msg: S) {
159        while self.status.len() <= idx {
160            self.status.push("".to_string());
161        }
162        self.status[idx] = msg.into();
163    }
164}
165
166impl StatefulWidget for &StatusLine {
167    type State = StatusLineState;
168
169    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
170        render_ref(self, area, buf, state);
171    }
172}
173
174impl StatefulWidget for StatusLine {
175    type State = StatusLineState;
176
177    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
178        render_ref(&self, area, buf, state);
179    }
180}
181
182fn render_ref(widget: &StatusLine, area: Rect, buf: &mut Buffer, state: &mut StatusLineState) {
183    state.area = area;
184
185    let layout = Layout::horizontal(widget.widths.iter()).split(state.area);
186
187    for (i, rect) in layout.iter().enumerate() {
188        let style = widget.style.get(i).copied().unwrap_or_default();
189        let txt = state.status.get(i).map(|v| v.as_str()).unwrap_or("");
190
191        let sep = if i > 0 {
192            if let Some(sep) = widget.sep.as_ref().map(|v| v.as_ref()) {
193                Span::from(sep)
194            } else {
195                Span::default()
196            }
197        } else {
198            Span::default()
199        };
200
201        Line::from_iter([
202            sep, //
203            Span::from(txt),
204        ])
205        .render(*rect, buf);
206
207        buf.set_style(*rect, style);
208    }
209}