ratatui_toolkit/statusline_stacked/
mod.rs1use ratatui::buffer::Buffer;
33use ratatui::layout::Rect;
34use ratatui::style::{Color, Style};
35use ratatui::text::{Line, Span};
36use ratatui::widgets::Widget;
37use std::marker::PhantomData;
38
39pub const SLANT_TL_BR: &str = "\u{e0b8}";
42
43pub const SLANT_BL_TR: &str = "\u{e0ba}";
46
47#[derive(Debug, Default, Clone)]
55pub struct StatusLineStacked<'a> {
56 style: Style,
57 left: Vec<(Line<'a>, Line<'a>)>,
58 center_margin: u16,
59 center: Line<'a>,
60 right: Vec<(Line<'a>, Line<'a>)>,
61 phantom: PhantomData<&'a ()>,
62}
63
64impl<'a> StatusLineStacked<'a> {
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn style(mut self, style: Style) -> Self {
71 self.style = style;
72 self
73 }
74
75 pub fn start(mut self, text: impl Into<Line<'a>>, gap: impl Into<Line<'a>>) -> Self {
82 self.left.push((text.into(), gap.into()));
83 self
84 }
85
86 pub fn start_bare(mut self, text: impl Into<Line<'a>>) -> Self {
89 self.left.push((text.into(), "".into()));
90 self
91 }
92
93 pub fn center_margin(mut self, margin: u16) -> Self {
95 self.center_margin = margin;
96 self
97 }
98
99 pub fn center(mut self, text: impl Into<Line<'a>>) -> Self {
101 self.center = text.into();
102 self
103 }
104
105 pub fn end(mut self, text: impl Into<Line<'a>>, gap: impl Into<Line<'a>>) -> Self {
112 self.right.push((text.into(), gap.into()));
113 self
114 }
115
116 pub fn end_bare(mut self, text: impl Into<Line<'a>>) -> Self {
119 self.right.push((text.into(), "".into()));
120 self
121 }
122}
123
124impl<'a> Widget for StatusLineStacked<'a> {
125 fn render(self, area: Rect, buf: &mut Buffer) {
126 let mut x_end = area.right();
128 for (status, gap) in self.right.iter() {
129 let width = status.width() as u16;
130 status.render(
131 Rect::new(x_end.saturating_sub(width), area.y, width, 1),
132 buf,
133 );
134 x_end = x_end.saturating_sub(width);
135
136 let width = gap.width() as u16;
137 gap.render(
138 Rect::new(x_end.saturating_sub(width), area.y, width, 1),
139 buf,
140 );
141 x_end = x_end.saturating_sub(width);
142 }
143
144 let mut x_start = area.x;
146 for (status, gap) in self.left.iter() {
147 let width = status.width() as u16;
148 status.render(Rect::new(x_start, area.y, width, 1), buf);
149 x_start += width;
150
151 let width = gap.width() as u16;
152 gap.render(Rect::new(x_start, area.y, width, 1), buf);
153 x_start += width;
154 }
155
156 buf.set_style(
158 Rect::new(x_start, area.y, x_end.saturating_sub(x_start), 1),
159 self.style,
160 );
161
162 let center_width = x_end
164 .saturating_sub(x_start)
165 .saturating_sub(self.center_margin * 2);
166
167 self.center.render(
168 Rect::new(x_start + self.center_margin, area.y, center_width, 1),
169 buf,
170 );
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
176pub enum OperationalMode {
177 #[default]
179 Operational,
180 Dire,
182 Evacuate,
184}
185
186pub struct StyledStatusLine<'a> {
190 mode: OperationalMode,
191 title: &'a str,
192 center_text: String,
193 render_count: usize,
194 event_count: usize,
195 render_time_us: u64,
196 event_time_us: u64,
197 message_count: u32,
198 use_slants: bool,
199}
200
201impl<'a> StyledStatusLine<'a> {
202 pub fn new() -> Self {
204 Self {
205 mode: OperationalMode::Operational,
206 title: " WESTINGHOUSE[STATUS]2 ",
207 center_text: String::new(),
208 render_count: 0,
209 event_count: 0,
210 render_time_us: 0,
211 event_time_us: 0,
212 message_count: 0,
213 use_slants: true,
214 }
215 }
216
217 pub fn mode(mut self, mode: OperationalMode) -> Self {
219 self.mode = mode;
220 self
221 }
222
223 pub fn title(mut self, title: &'a str) -> Self {
225 self.title = title;
226 self
227 }
228
229 pub fn center_text(mut self, text: impl Into<String>) -> Self {
231 self.center_text = text.into();
232 self
233 }
234
235 pub fn render_metrics(mut self, count: usize, time_us: u64) -> Self {
237 self.render_count = count;
238 self.render_time_us = time_us;
239 self
240 }
241
242 pub fn event_metrics(mut self, count: usize, time_us: u64) -> Self {
244 self.event_count = count;
245 self.event_time_us = time_us;
246 self
247 }
248
249 pub fn message_count(mut self, count: u32) -> Self {
251 self.message_count = count;
252 self
253 }
254
255 pub fn use_slants(mut self, use_slants: bool) -> Self {
257 self.use_slants = use_slants;
258 self
259 }
260
261 pub fn build(self) -> StatusLineStacked<'a> {
263 let color_title = Color::Rgb(70, 73, 77); let color_mode = match self.mode {
267 OperationalMode::Operational => Color::Rgb(42, 193, 138), OperationalMode::Dire => Color::Rgb(255, 210, 88), OperationalMode::Evacuate => Color::Rgb(246, 90, 90), };
271 let color_info = Color::Rgb(44, 163, 170); let color_dark = Color::Rgb(80, 202, 210); let text_black = Color::Rgb(16, 19, 23); let mode_str = match self.mode {
276 OperationalMode::Operational => " OPERATIONAL ",
277 OperationalMode::Dire => " DIRE ",
278 OperationalMode::Evacuate => " EVACUATE ",
279 };
280
281 if self.use_slants {
282 StatusLineStacked::new()
284 .style(Style::new().fg(Color::White).bg(color_dark))
285 .start(
286 Span::from(self.title).style(Style::new().fg(text_black).bg(color_title)),
287 Span::from(SLANT_TL_BR).style(Style::new().fg(color_title).bg(color_mode)),
288 )
289 .start(
290 Span::from(mode_str).style(Style::new().fg(text_black).bg(color_mode)),
291 Span::from(SLANT_TL_BR).style(Style::new().fg(color_mode)),
292 )
293 .center_margin(1)
294 .center(self.center_text)
295 .end(
296 Span::from(format!(
297 "R[{}][{}µs] ",
298 self.render_count, self.render_time_us
299 ))
300 .style(Style::new().fg(text_black).bg(color_info)),
301 Span::from(SLANT_BL_TR).style(Style::new().fg(color_info).bg(color_dark)),
302 )
303 .end(
304 "",
305 Span::from(SLANT_BL_TR).style(Style::new().fg(color_dark).bg(color_info)),
306 )
307 .end(
308 Span::from(format!(
309 "E[{}][{}µs] ",
310 self.event_count, self.event_time_us
311 ))
312 .style(Style::new().fg(text_black).bg(color_info)),
313 Span::from(SLANT_BL_TR).style(Style::new().fg(color_info).bg(color_dark)),
314 )
315 .end(
316 "",
317 Span::from(SLANT_BL_TR).style(Style::new().fg(color_dark).bg(color_info)),
318 )
319 .end(
320 Span::from(format!("MSG[{}] ", self.message_count))
321 .style(Style::new().fg(text_black).bg(color_info)),
322 Span::from(SLANT_BL_TR).style(Style::new().fg(color_info)),
323 )
324 } else {
325 StatusLineStacked::new()
327 .style(Style::new().fg(Color::White).bg(color_dark))
328 .start_bare(
329 Span::from(self.title).style(Style::new().fg(Color::White).bg(color_title)),
330 )
331 .start_bare(Span::from(mode_str).style(Style::new().fg(text_black).bg(color_mode)))
332 .center_margin(1)
333 .center(self.center_text)
334 .end_bare(
335 Span::from(format!(
336 "R[{}][{}µs] ",
337 self.render_count, self.render_time_us
338 ))
339 .style(Style::new().fg(text_black).bg(color_info)),
340 )
341 .end_bare(
342 Span::from(format!(
343 "E[{}][{}µs] ",
344 self.event_count, self.event_time_us
345 ))
346 .style(Style::new().fg(text_black).bg(color_info)),
347 )
348 .end_bare(
349 Span::from(format!(" MSG[{}] ", self.message_count))
350 .style(Style::new().fg(text_black).bg(color_info)),
351 )
352 }
353 }
354}
355
356impl<'a> Default for StyledStatusLine<'a> {
357 fn default() -> Self {
358 Self::new()
359 }
360}