tui_realm_stdlib/components/
bar_chart.rs1use std::collections::LinkedList;
4
5use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6use tuirealm::component::Component;
7use tuirealm::props::{
8 AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, Style,
9 TextModifiers, Title,
10};
11use tuirealm::ratatui::Frame;
12use tuirealm::ratatui::layout::Rect;
13use tuirealm::ratatui::widgets::BarChart as TuiBarChart;
14use tuirealm::state::State;
15
16use super::props::{
17 BAR_CHART_BARS_GAP, BAR_CHART_BARS_STYLE, BAR_CHART_LABEL_STYLE, BAR_CHART_MAX_BARS,
18 BAR_CHART_VALUES_STYLE,
19};
20use crate::prop_ext::CommonProps;
22
23#[derive(Default)]
27pub struct BarChartStates {
28 pub cursor: usize,
29}
30
31impl BarChartStates {
32 pub fn move_cursor_left(&mut self) {
34 if self.cursor > 0 {
35 self.cursor -= 1;
36 }
37 }
38
39 pub fn move_cursor_right(&mut self, data_len: usize) {
41 if data_len > 0 && self.cursor + 1 < data_len {
42 self.cursor += 1;
43 }
44 }
45
46 pub fn reset_cursor(&mut self) {
48 self.cursor = 0;
49 }
50
51 pub fn cursor_at_end(&mut self, data_len: usize) {
53 if data_len > 0 {
54 self.cursor = data_len - 1;
55 } else {
56 self.cursor = 0;
57 }
58 }
59}
60
61#[derive(Default)]
75#[must_use]
76pub struct BarChart {
77 common: CommonProps,
78 props: Props,
79 pub states: BarChartStates,
80}
81
82impl BarChart {
83 pub fn foreground(mut self, fg: Color) -> Self {
85 self.attr(Attribute::Foreground, AttrValue::Color(fg));
86 self
87 }
88
89 pub fn background(mut self, bg: Color) -> Self {
91 self.attr(Attribute::Background, AttrValue::Color(bg));
92 self
93 }
94
95 pub fn modifiers(mut self, m: TextModifiers) -> Self {
97 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
98 self
99 }
100
101 pub fn style(mut self, style: Style) -> Self {
105 self.attr(Attribute::Style, AttrValue::Style(style));
106 self
107 }
108
109 pub fn borders(mut self, b: Borders) -> Self {
111 self.attr(Attribute::Borders, AttrValue::Borders(b));
112 self
113 }
114
115 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
117 self.attr(Attribute::Title, AttrValue::Title(title.into()));
118 self
119 }
120
121 pub fn disabled(mut self, disabled: bool) -> Self {
123 self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
124 self
125 }
126
127 pub fn inactive(mut self, s: Style) -> Self {
129 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
130 self
131 }
132
133 pub fn data(mut self, data: &[(&str, u64)]) -> Self {
135 let mut list: LinkedList<PropPayload> = LinkedList::new();
137 for (a, b) in data {
138 list.push_back(PropPayload::Pair((
139 PropValue::Str((*a).to_string()),
140 PropValue::U64(*b),
141 )));
142 }
143 self.attr(
144 Attribute::Dataset,
145 AttrValue::Payload(PropPayload::Linked(list)),
146 );
147 self
148 }
149
150 pub fn bar_gap(mut self, gap: u16) -> Self {
152 self.attr(Attribute::Custom(BAR_CHART_BARS_GAP), AttrValue::Size(gap));
153 self
154 }
155
156 pub fn bar_style(mut self, s: Style) -> Self {
158 self.attr(Attribute::Custom(BAR_CHART_BARS_STYLE), AttrValue::Style(s));
159 self
160 }
161
162 pub fn label_style(mut self, s: Style) -> Self {
164 self.attr(
165 Attribute::Custom(BAR_CHART_LABEL_STYLE),
166 AttrValue::Style(s),
167 );
168 self
169 }
170
171 pub fn max_bars(mut self, l: usize) -> Self {
175 self.attr(Attribute::Custom(BAR_CHART_MAX_BARS), AttrValue::Length(l));
176 self
177 }
178
179 pub fn value_style(mut self, s: Style) -> Self {
181 self.attr(
182 Attribute::Custom(BAR_CHART_VALUES_STYLE),
183 AttrValue::Style(s),
184 );
185 self
186 }
187
188 pub fn width(mut self, w: u16) -> Self {
190 self.attr(Attribute::Width, AttrValue::Size(w));
191 self
192 }
193
194 fn is_disabled(&self) -> bool {
195 self.props
196 .get(Attribute::Disabled)
197 .and_then(AttrValue::as_flag)
198 .unwrap_or_default()
199 }
200
201 fn data_len(&self) -> usize {
205 self.props
206 .get(Attribute::Dataset)
207 .and_then(AttrValue::as_payload)
208 .and_then(PropPayload::as_linked)
209 .map_or(0, |x| x.len())
210 }
211
212 fn get_data(&self, start: usize, len: usize) -> Vec<(String, u64)> {
213 if let Some(PropPayload::Linked(list)) = self
214 .props
215 .get(Attribute::Dataset)
216 .and_then(AttrValue::as_payload)
217 {
218 let len: usize = std::cmp::min(len, self.data_len() - start);
220 let mut data: Vec<(String, u64)> = Vec::with_capacity(len);
222 for (cursor, item) in list.iter().enumerate() {
223 if cursor < start {
225 continue;
226 }
227 if let PropPayload::Pair((PropValue::Str(label), PropValue::U64(value))) = item {
229 data.push((label.clone(), *value));
230 }
231 if data.len() >= len {
233 break;
234 }
235 }
236
237 data
238 } else {
239 Vec::new()
240 }
241 }
242}
243
244impl Component for BarChart {
245 fn view(&mut self, render: &mut Frame, area: Rect) {
246 if !self.common.display {
247 return;
248 }
249
250 let data_max_len = self
252 .props
253 .get(Attribute::Custom(BAR_CHART_MAX_BARS))
254 .and_then(AttrValue::as_length)
255 .unwrap_or(self.data_len());
256 let data = self.get_data(self.states.cursor, data_max_len);
258 let data_ref: Vec<(&str, u64)> = data.iter().map(|x| (x.0.as_str(), x.1)).collect();
259 let mut widget: TuiBarChart = TuiBarChart::default()
261 .style(self.common.style)
262 .data(data_ref.as_slice());
263
264 if let Some(block) = self.common.get_block() {
265 widget = widget.block(block);
266 }
267
268 if let Some(gap) = self
269 .props
270 .get(Attribute::Custom(BAR_CHART_BARS_GAP))
271 .and_then(AttrValue::as_size)
272 {
273 widget = widget.bar_gap(gap);
274 }
275 if let Some(width) = self
276 .props
277 .get(Attribute::Width)
278 .and_then(AttrValue::as_size)
279 {
280 widget = widget.bar_width(width);
281 }
282 if let Some(style) = self
283 .props
284 .get(Attribute::Custom(BAR_CHART_BARS_STYLE))
285 .and_then(AttrValue::as_style)
286 {
287 widget = widget.bar_style(style);
288 }
289 if let Some(style) = self
290 .props
291 .get(Attribute::Custom(BAR_CHART_LABEL_STYLE))
292 .and_then(AttrValue::as_style)
293 {
294 widget = widget.label_style(style);
295 }
296 if let Some(style) = self
297 .props
298 .get(Attribute::Custom(BAR_CHART_VALUES_STYLE))
299 .and_then(AttrValue::as_style)
300 {
301 widget = widget.value_style(style);
302 }
303
304 render.render_widget(widget, area);
306 }
307
308 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
309 if let Some(value) = self.common.get_for_query(attr) {
310 return Some(value);
311 }
312
313 self.props.get_for_query(attr)
314 }
315
316 fn attr(&mut self, attr: Attribute, value: AttrValue) {
317 if let Some(value) = self.common.set(attr, value) {
318 self.props.set(attr, value);
319 }
320 }
321
322 fn perform(&mut self, cmd: Cmd) -> CmdResult {
323 if !self.is_disabled() {
324 match cmd {
325 Cmd::Move(Direction::Left) => {
326 self.states.move_cursor_left();
327 }
328 Cmd::Move(Direction::Right) => {
329 self.states.move_cursor_right(self.data_len());
330 }
331 Cmd::GoTo(Position::Begin) => {
332 self.states.reset_cursor();
333 }
334 Cmd::GoTo(Position::End) => {
335 self.states.cursor_at_end(self.data_len());
336 }
337 _ => return CmdResult::Invalid(cmd),
338 }
339 return CmdResult::Visual;
340 }
341 CmdResult::NoChange
342 }
343
344 fn state(&self) -> State {
345 State::None
346 }
347}
348
349#[cfg(test)]
350mod test {
351
352 use pretty_assertions::assert_eq;
353 use tuirealm::props::HorizontalAlignment;
354
355 use super::*;
356
357 #[test]
358 fn test_components_bar_chart_states() {
359 let mut states: BarChartStates = BarChartStates::default();
360 assert_eq!(states.cursor, 0);
361 states.move_cursor_right(2);
363 assert_eq!(states.cursor, 1);
364 states.move_cursor_right(2);
366 assert_eq!(states.cursor, 1);
367 states.move_cursor_left();
369 assert_eq!(states.cursor, 0);
370 states.move_cursor_left();
372 assert_eq!(states.cursor, 0);
373 states.cursor_at_end(3);
375 assert_eq!(states.cursor, 2);
376 states.reset_cursor();
377 assert_eq!(states.cursor, 0);
378 }
379
380 #[test]
381 fn test_components_bar_chart() {
382 let mut component: BarChart = BarChart::default()
383 .disabled(false)
384 .title(Title::from("my incomes").alignment(HorizontalAlignment::Center))
385 .label_style(Style::default().fg(Color::Yellow))
386 .bar_style(Style::default().fg(Color::LightYellow))
387 .bar_gap(2)
388 .width(4)
389 .borders(Borders::default())
390 .max_bars(6)
391 .value_style(Style::default().fg(Color::LightBlue))
392 .data(&[
393 ("january", 250),
394 ("february", 300),
395 ("march", 275),
396 ("april", 312),
397 ("may", 420),
398 ("june", 170),
399 ("july", 220),
400 ("august", 160),
401 ("september", 180),
402 ("october", 470),
403 ("november", 380),
404 ("december", 820),
405 ]);
406 assert_eq!(component.state(), State::None);
408 assert_eq!(
410 component.perform(Cmd::Move(Direction::Right)),
411 CmdResult::Visual
412 );
413 assert_eq!(component.states.cursor, 1);
414 assert_eq!(
416 component.perform(Cmd::Move(Direction::Left)),
417 CmdResult::Visual
418 );
419 assert_eq!(component.states.cursor, 0);
420 assert_eq!(
422 component.perform(Cmd::GoTo(Position::End)),
423 CmdResult::Visual
424 );
425 assert_eq!(component.states.cursor, 11);
426 assert_eq!(
428 component.perform(Cmd::GoTo(Position::Begin)),
429 CmdResult::Visual
430 );
431 assert_eq!(component.states.cursor, 0);
432 }
433}