tui_realm_stdlib/components/
bar_chart.rs1use std::collections::LinkedList;
6use tuirealm::command::{Cmd, CmdResult, Direction, Position};
7use tuirealm::props::{
8 Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
9};
10use tuirealm::ratatui::{layout::Rect, widgets::BarChart as TuiBarChart};
11use tuirealm::{Frame, MockComponent, State};
12
13use super::props::{
16 BAR_CHART_BARS_GAP, BAR_CHART_BARS_STYLE, BAR_CHART_LABEL_STYLE, BAR_CHART_MAX_BARS,
17 BAR_CHART_VALUES_STYLE,
18};
19
20#[derive(Default)]
26pub struct BarChartStates {
27 pub cursor: usize,
28}
29
30impl BarChartStates {
31 pub fn move_cursor_left(&mut self) {
35 if self.cursor > 0 {
36 self.cursor -= 1;
37 }
38 }
39
40 pub fn move_cursor_right(&mut self, data_len: usize) {
44 if data_len > 0 && self.cursor + 1 < data_len {
45 self.cursor += 1;
46 }
47 }
48
49 pub fn reset_cursor(&mut self) {
53 self.cursor = 0;
54 }
55
56 pub fn cursor_at_end(&mut self, data_len: usize) {
60 if data_len > 0 {
61 self.cursor = data_len - 1;
62 } else {
63 self.cursor = 0;
64 }
65 }
66}
67
68#[derive(Default)]
84#[must_use]
85pub struct BarChart {
86 props: Props,
87 pub states: BarChartStates,
88}
89
90impl BarChart {
91 pub fn foreground(mut self, fg: Color) -> Self {
92 self.attr(Attribute::Foreground, AttrValue::Color(fg));
93 self
94 }
95
96 pub fn background(mut self, bg: Color) -> Self {
97 self.attr(Attribute::Background, AttrValue::Color(bg));
98 self
99 }
100
101 pub fn borders(mut self, b: Borders) -> Self {
102 self.attr(Attribute::Borders, AttrValue::Borders(b));
103 self
104 }
105
106 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
107 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
108 self
109 }
110
111 pub fn disabled(mut self, disabled: bool) -> Self {
112 self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
113 self
114 }
115
116 pub fn inactive(mut self, s: Style) -> Self {
117 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
118 self
119 }
120
121 pub fn data(mut self, data: &[(&str, u64)]) -> Self {
122 let mut list: LinkedList<PropPayload> = LinkedList::new();
123 for (a, b) in data {
124 list.push_back(PropPayload::Tup2((
125 PropValue::Str((*a).to_string()),
126 PropValue::U64(*b),
127 )));
128 }
129 self.attr(
130 Attribute::Dataset,
131 AttrValue::Payload(PropPayload::Linked(list)),
132 );
133 self
134 }
135
136 pub fn bar_gap(mut self, gap: u16) -> Self {
137 self.attr(Attribute::Custom(BAR_CHART_BARS_GAP), AttrValue::Size(gap));
138 self
139 }
140
141 pub fn bar_style(mut self, s: Style) -> Self {
142 self.attr(Attribute::Custom(BAR_CHART_BARS_STYLE), AttrValue::Style(s));
143 self
144 }
145
146 pub fn label_style(mut self, s: Style) -> Self {
147 self.attr(
148 Attribute::Custom(BAR_CHART_LABEL_STYLE),
149 AttrValue::Style(s),
150 );
151 self
152 }
153
154 pub fn max_bars(mut self, l: usize) -> Self {
155 self.attr(Attribute::Custom(BAR_CHART_MAX_BARS), AttrValue::Length(l));
156 self
157 }
158
159 pub fn value_style(mut self, s: Style) -> Self {
160 self.attr(
161 Attribute::Custom(BAR_CHART_VALUES_STYLE),
162 AttrValue::Style(s),
163 );
164 self
165 }
166
167 pub fn width(mut self, w: u16) -> Self {
168 self.attr(Attribute::Width, AttrValue::Size(w));
169 self
170 }
171
172 fn is_disabled(&self) -> bool {
173 self.props
174 .get_or(Attribute::Disabled, AttrValue::Flag(false))
175 .unwrap_flag()
176 }
177
178 fn data_len(&self) -> usize {
182 self.props
183 .get(Attribute::Dataset)
184 .map_or(0, |x| x.unwrap_payload().unwrap_linked().len())
185 }
186
187 fn get_data(&self, start: usize, len: usize) -> Vec<(String, u64)> {
188 if let Some(PropPayload::Linked(list)) = self
189 .props
190 .get(Attribute::Dataset)
191 .map(|x| x.unwrap_payload())
192 {
193 let len: usize = std::cmp::min(len, self.data_len() - start);
195 let mut data: Vec<(String, u64)> = Vec::with_capacity(len);
197 for (cursor, item) in list.iter().enumerate() {
198 if cursor < start {
200 continue;
201 }
202 if let PropPayload::Tup2((PropValue::Str(label), PropValue::U64(value))) = item {
204 data.push((label.clone(), *value));
205 }
206 if data.len() >= len {
208 break;
209 }
210 }
211
212 data
213 } else {
214 Vec::new()
215 }
216 }
217}
218
219impl MockComponent for BarChart {
220 fn view(&mut self, render: &mut Frame, area: Rect) {
221 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
222 let foreground = self
223 .props
224 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
225 .unwrap_color();
226 let background = self
227 .props
228 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
229 .unwrap_color();
230 let borders = self
231 .props
232 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
233 .unwrap_borders();
234 let title = self
235 .props
236 .get_ref(Attribute::Title)
237 .and_then(|x| x.as_title());
238 let focus = self
239 .props
240 .get_or(Attribute::Focus, AttrValue::Flag(false))
241 .unwrap_flag();
242 let inactive_style = self
243 .props
244 .get(Attribute::FocusStyle)
245 .map(|x| x.unwrap_style());
246 let active: bool = if self.is_disabled() { true } else { focus };
247 let mut div = crate::utils::get_block(borders, title, active, inactive_style);
248 div = div.style(Style::default().bg(background).fg(foreground));
249 let data_max_len = self
251 .props
252 .get(Attribute::Custom(BAR_CHART_MAX_BARS))
253 .map_or(self.data_len(), |x| x.unwrap_length());
254 let data = self.get_data(self.states.cursor, data_max_len);
256 let data_ref: Vec<(&str, u64)> = data.iter().map(|x| (x.0.as_str(), x.1)).collect();
257 let mut widget: TuiBarChart =
259 TuiBarChart::default().block(div).data(data_ref.as_slice());
260 if let Some(gap) = self
261 .props
262 .get(Attribute::Custom(BAR_CHART_BARS_GAP))
263 .map(|x| x.unwrap_size())
264 {
265 widget = widget.bar_gap(gap);
266 }
267 if let Some(width) = self.props.get(Attribute::Width).map(|x| x.unwrap_size()) {
268 widget = widget.bar_width(width);
269 }
270 if let Some(style) = self
271 .props
272 .get(Attribute::Custom(BAR_CHART_BARS_STYLE))
273 .map(|x| x.unwrap_style())
274 {
275 widget = widget.bar_style(style);
276 }
277 if let Some(style) = self
278 .props
279 .get(Attribute::Custom(BAR_CHART_LABEL_STYLE))
280 .map(|x| x.unwrap_style())
281 {
282 widget = widget.label_style(style);
283 }
284 if let Some(style) = self
285 .props
286 .get(Attribute::Custom(BAR_CHART_VALUES_STYLE))
287 .map(|x| x.unwrap_style())
288 {
289 widget = widget.value_style(style);
290 }
291 render.render_widget(widget, area);
293 }
294 }
295
296 fn query(&self, attr: Attribute) -> Option<AttrValue> {
297 self.props.get(attr)
298 }
299
300 fn attr(&mut self, attr: Attribute, value: AttrValue) {
301 self.props.set(attr, value);
302 }
303
304 fn perform(&mut self, cmd: Cmd) -> CmdResult {
305 if !self.is_disabled() {
306 match cmd {
307 Cmd::Move(Direction::Left) => {
308 self.states.move_cursor_left();
309 }
310 Cmd::Move(Direction::Right) => {
311 self.states.move_cursor_right(self.data_len());
312 }
313 Cmd::GoTo(Position::Begin) => {
314 self.states.reset_cursor();
315 }
316 Cmd::GoTo(Position::End) => {
317 self.states.cursor_at_end(self.data_len());
318 }
319 _ => {}
320 }
321 }
322 CmdResult::None
323 }
324
325 fn state(&self) -> State {
326 State::None
327 }
328}
329
330#[cfg(test)]
331mod test {
332
333 use super::*;
334
335 use pretty_assertions::assert_eq;
336
337 #[test]
338 fn test_components_bar_chart_states() {
339 let mut states: BarChartStates = BarChartStates::default();
340 assert_eq!(states.cursor, 0);
341 states.move_cursor_right(2);
343 assert_eq!(states.cursor, 1);
344 states.move_cursor_right(2);
346 assert_eq!(states.cursor, 1);
347 states.move_cursor_left();
349 assert_eq!(states.cursor, 0);
350 states.move_cursor_left();
352 assert_eq!(states.cursor, 0);
353 states.cursor_at_end(3);
355 assert_eq!(states.cursor, 2);
356 states.reset_cursor();
357 assert_eq!(states.cursor, 0);
358 }
359
360 #[test]
361 fn test_components_bar_chart() {
362 let mut component: BarChart = BarChart::default()
363 .disabled(false)
364 .title("my incomes", Alignment::Center)
365 .label_style(Style::default().fg(Color::Yellow))
366 .bar_style(Style::default().fg(Color::LightYellow))
367 .bar_gap(2)
368 .width(4)
369 .borders(Borders::default())
370 .max_bars(6)
371 .value_style(Style::default().fg(Color::LightBlue))
372 .data(&[
373 ("january", 250),
374 ("february", 300),
375 ("march", 275),
376 ("april", 312),
377 ("may", 420),
378 ("june", 170),
379 ("july", 220),
380 ("august", 160),
381 ("september", 180),
382 ("october", 470),
383 ("november", 380),
384 ("december", 820),
385 ]);
386 assert_eq!(component.state(), State::None);
388 assert_eq!(
390 component.perform(Cmd::Move(Direction::Right)),
391 CmdResult::None
392 );
393 assert_eq!(component.states.cursor, 1);
394 assert_eq!(
396 component.perform(Cmd::Move(Direction::Left)),
397 CmdResult::None
398 );
399 assert_eq!(component.states.cursor, 0);
400 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
402 assert_eq!(component.states.cursor, 11);
403 assert_eq!(
405 component.perform(Cmd::GoTo(Position::Begin)),
406 CmdResult::None
407 );
408 assert_eq!(component.states.cursor, 0);
409 }
410}