rat_widget/
statusline.rs

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