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 normal_style = Style::default().bg(background).fg(foreground);
248 let div = crate::utils::get_block(borders, title, active, inactive_style);
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 = TuiBarChart::default()
259 .style(normal_style)
260 .block(div)
261 .data(data_ref.as_slice());
262 if let Some(gap) = self
263 .props
264 .get(Attribute::Custom(BAR_CHART_BARS_GAP))
265 .map(|x| x.unwrap_size())
266 {
267 widget = widget.bar_gap(gap);
268 }
269 if let Some(width) = self.props.get(Attribute::Width).map(|x| x.unwrap_size()) {
270 widget = widget.bar_width(width);
271 }
272 if let Some(style) = self
273 .props
274 .get(Attribute::Custom(BAR_CHART_BARS_STYLE))
275 .map(|x| x.unwrap_style())
276 {
277 widget = widget.bar_style(style);
278 }
279 if let Some(style) = self
280 .props
281 .get(Attribute::Custom(BAR_CHART_LABEL_STYLE))
282 .map(|x| x.unwrap_style())
283 {
284 widget = widget.label_style(style);
285 }
286 if let Some(style) = self
287 .props
288 .get(Attribute::Custom(BAR_CHART_VALUES_STYLE))
289 .map(|x| x.unwrap_style())
290 {
291 widget = widget.value_style(style);
292 }
293 render.render_widget(widget, area);
295 }
296 }
297
298 fn query(&self, attr: Attribute) -> Option<AttrValue> {
299 self.props.get(attr)
300 }
301
302 fn attr(&mut self, attr: Attribute, value: AttrValue) {
303 self.props.set(attr, value);
304 }
305
306 fn perform(&mut self, cmd: Cmd) -> CmdResult {
307 if !self.is_disabled() {
308 match cmd {
309 Cmd::Move(Direction::Left) => {
310 self.states.move_cursor_left();
311 }
312 Cmd::Move(Direction::Right) => {
313 self.states.move_cursor_right(self.data_len());
314 }
315 Cmd::GoTo(Position::Begin) => {
316 self.states.reset_cursor();
317 }
318 Cmd::GoTo(Position::End) => {
319 self.states.cursor_at_end(self.data_len());
320 }
321 _ => {}
322 }
323 }
324 CmdResult::None
325 }
326
327 fn state(&self) -> State {
328 State::None
329 }
330}
331
332#[cfg(test)]
333mod test {
334
335 use super::*;
336
337 use pretty_assertions::assert_eq;
338
339 #[test]
340 fn test_components_bar_chart_states() {
341 let mut states: BarChartStates = BarChartStates::default();
342 assert_eq!(states.cursor, 0);
343 states.move_cursor_right(2);
345 assert_eq!(states.cursor, 1);
346 states.move_cursor_right(2);
348 assert_eq!(states.cursor, 1);
349 states.move_cursor_left();
351 assert_eq!(states.cursor, 0);
352 states.move_cursor_left();
354 assert_eq!(states.cursor, 0);
355 states.cursor_at_end(3);
357 assert_eq!(states.cursor, 2);
358 states.reset_cursor();
359 assert_eq!(states.cursor, 0);
360 }
361
362 #[test]
363 fn test_components_bar_chart() {
364 let mut component: BarChart = BarChart::default()
365 .disabled(false)
366 .title("my incomes", Alignment::Center)
367 .label_style(Style::default().fg(Color::Yellow))
368 .bar_style(Style::default().fg(Color::LightYellow))
369 .bar_gap(2)
370 .width(4)
371 .borders(Borders::default())
372 .max_bars(6)
373 .value_style(Style::default().fg(Color::LightBlue))
374 .data(&[
375 ("january", 250),
376 ("february", 300),
377 ("march", 275),
378 ("april", 312),
379 ("may", 420),
380 ("june", 170),
381 ("july", 220),
382 ("august", 160),
383 ("september", 180),
384 ("october", 470),
385 ("november", 380),
386 ("december", 820),
387 ]);
388 assert_eq!(component.state(), State::None);
390 assert_eq!(
392 component.perform(Cmd::Move(Direction::Right)),
393 CmdResult::None
394 );
395 assert_eq!(component.states.cursor, 1);
396 assert_eq!(
398 component.perform(Cmd::Move(Direction::Left)),
399 CmdResult::None
400 );
401 assert_eq!(component.states.cursor, 0);
402 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
404 assert_eq!(component.states.cursor, 11);
405 assert_eq!(
407 component.perform(Cmd::GoTo(Position::Begin)),
408 CmdResult::None
409 );
410 assert_eq!(component.states.cursor, 0);
411 }
412}