use std::collections::LinkedList;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::component::Component;
use tuirealm::props::{
AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, Style,
TextModifiers, Title,
};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::widgets::BarChart as TuiBarChart;
use tuirealm::state::State;
use super::props::{
BAR_CHART_BARS_GAP, BAR_CHART_BARS_STYLE, BAR_CHART_LABEL_STYLE, BAR_CHART_MAX_BARS,
BAR_CHART_VALUES_STYLE,
};
use crate::prop_ext::CommonProps;
#[derive(Default)]
pub struct BarChartStates {
pub cursor: usize,
}
impl BarChartStates {
pub fn move_cursor_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn move_cursor_right(&mut self, data_len: usize) {
if data_len > 0 && self.cursor + 1 < data_len {
self.cursor += 1;
}
}
pub fn reset_cursor(&mut self) {
self.cursor = 0;
}
pub fn cursor_at_end(&mut self, data_len: usize) {
if data_len > 0 {
self.cursor = data_len - 1;
} else {
self.cursor = 0;
}
}
}
#[derive(Default)]
#[must_use]
pub struct BarChart {
common: CommonProps,
props: Props,
pub states: BarChartStates,
}
impl BarChart {
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
pub fn style(mut self, style: Style) -> Self {
self.attr(Attribute::Style, AttrValue::Style(style));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
self.attr(Attribute::Title, AttrValue::Title(title.into()));
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
self
}
pub fn inactive(mut self, s: Style) -> Self {
self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
self
}
pub fn data(mut self, data: &[(&str, u64)]) -> Self {
let mut list: LinkedList<PropPayload> = LinkedList::new();
for (a, b) in data {
list.push_back(PropPayload::Pair((
PropValue::Str((*a).to_string()),
PropValue::U64(*b),
)));
}
self.attr(
Attribute::Dataset,
AttrValue::Payload(PropPayload::Linked(list)),
);
self
}
pub fn bar_gap(mut self, gap: u16) -> Self {
self.attr(Attribute::Custom(BAR_CHART_BARS_GAP), AttrValue::Size(gap));
self
}
pub fn bar_style(mut self, s: Style) -> Self {
self.attr(Attribute::Custom(BAR_CHART_BARS_STYLE), AttrValue::Style(s));
self
}
pub fn label_style(mut self, s: Style) -> Self {
self.attr(
Attribute::Custom(BAR_CHART_LABEL_STYLE),
AttrValue::Style(s),
);
self
}
pub fn max_bars(mut self, l: usize) -> Self {
self.attr(Attribute::Custom(BAR_CHART_MAX_BARS), AttrValue::Length(l));
self
}
pub fn value_style(mut self, s: Style) -> Self {
self.attr(
Attribute::Custom(BAR_CHART_VALUES_STYLE),
AttrValue::Style(s),
);
self
}
pub fn width(mut self, w: u16) -> Self {
self.attr(Attribute::Width, AttrValue::Size(w));
self
}
fn is_disabled(&self) -> bool {
self.props
.get(Attribute::Disabled)
.and_then(AttrValue::as_flag)
.unwrap_or_default()
}
fn data_len(&self) -> usize {
self.props
.get(Attribute::Dataset)
.and_then(AttrValue::as_payload)
.and_then(PropPayload::as_linked)
.map_or(0, |x| x.len())
}
fn get_data(&self, start: usize, len: usize) -> Vec<(String, u64)> {
if let Some(PropPayload::Linked(list)) = self
.props
.get(Attribute::Dataset)
.and_then(AttrValue::as_payload)
{
let len: usize = std::cmp::min(len, self.data_len() - start);
let mut data: Vec<(String, u64)> = Vec::with_capacity(len);
for (cursor, item) in list.iter().enumerate() {
if cursor < start {
continue;
}
if let PropPayload::Pair((PropValue::Str(label), PropValue::U64(value))) = item {
data.push((label.clone(), *value));
}
if data.len() >= len {
break;
}
}
data
} else {
Vec::new()
}
}
}
impl Component for BarChart {
fn view(&mut self, render: &mut Frame, area: Rect) {
if !self.common.display {
return;
}
let data_max_len = self
.props
.get(Attribute::Custom(BAR_CHART_MAX_BARS))
.and_then(AttrValue::as_length)
.unwrap_or(self.data_len());
let data = self.get_data(self.states.cursor, data_max_len);
let data_ref: Vec<(&str, u64)> = data.iter().map(|x| (x.0.as_str(), x.1)).collect();
let mut widget: TuiBarChart = TuiBarChart::default()
.style(self.common.style)
.data(data_ref.as_slice());
if let Some(block) = self.common.get_block() {
widget = widget.block(block);
}
if let Some(gap) = self
.props
.get(Attribute::Custom(BAR_CHART_BARS_GAP))
.and_then(AttrValue::as_size)
{
widget = widget.bar_gap(gap);
}
if let Some(width) = self
.props
.get(Attribute::Width)
.and_then(AttrValue::as_size)
{
widget = widget.bar_width(width);
}
if let Some(style) = self
.props
.get(Attribute::Custom(BAR_CHART_BARS_STYLE))
.and_then(AttrValue::as_style)
{
widget = widget.bar_style(style);
}
if let Some(style) = self
.props
.get(Attribute::Custom(BAR_CHART_LABEL_STYLE))
.and_then(AttrValue::as_style)
{
widget = widget.label_style(style);
}
if let Some(style) = self
.props
.get(Attribute::Custom(BAR_CHART_VALUES_STYLE))
.and_then(AttrValue::as_style)
{
widget = widget.value_style(style);
}
render.render_widget(widget, area);
}
fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
if let Some(value) = self.common.get_for_query(attr) {
return Some(value);
}
self.props.get_for_query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
if let Some(value) = self.common.set(attr, value) {
self.props.set(attr, value);
}
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
if !self.is_disabled() {
match cmd {
Cmd::Move(Direction::Left) => {
self.states.move_cursor_left();
}
Cmd::Move(Direction::Right) => {
self.states.move_cursor_right(self.data_len());
}
Cmd::GoTo(Position::Begin) => {
self.states.reset_cursor();
}
Cmd::GoTo(Position::End) => {
self.states.cursor_at_end(self.data_len());
}
_ => return CmdResult::Invalid(cmd),
}
return CmdResult::Visual;
}
CmdResult::NoChange
}
fn state(&self) -> State {
State::None
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use tuirealm::props::HorizontalAlignment;
use super::*;
#[test]
fn test_components_bar_chart_states() {
let mut states: BarChartStates = BarChartStates::default();
assert_eq!(states.cursor, 0);
states.move_cursor_right(2);
assert_eq!(states.cursor, 1);
states.move_cursor_right(2);
assert_eq!(states.cursor, 1);
states.move_cursor_left();
assert_eq!(states.cursor, 0);
states.move_cursor_left();
assert_eq!(states.cursor, 0);
states.cursor_at_end(3);
assert_eq!(states.cursor, 2);
states.reset_cursor();
assert_eq!(states.cursor, 0);
}
#[test]
fn test_components_bar_chart() {
let mut component: BarChart = BarChart::default()
.disabled(false)
.title(Title::from("my incomes").alignment(HorizontalAlignment::Center))
.label_style(Style::default().fg(Color::Yellow))
.bar_style(Style::default().fg(Color::LightYellow))
.bar_gap(2)
.width(4)
.borders(Borders::default())
.max_bars(6)
.value_style(Style::default().fg(Color::LightBlue))
.data(&[
("january", 250),
("february", 300),
("march", 275),
("april", 312),
("may", 420),
("june", 170),
("july", 220),
("august", 160),
("september", 180),
("october", 470),
("november", 380),
("december", 820),
]);
assert_eq!(component.state(), State::None);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual
);
assert_eq!(component.states.cursor, 1);
assert_eq!(
component.perform(Cmd::Move(Direction::Left)),
CmdResult::Visual
);
assert_eq!(component.states.cursor, 0);
assert_eq!(
component.perform(Cmd::GoTo(Position::End)),
CmdResult::Visual
);
assert_eq!(component.states.cursor, 11);
assert_eq!(
component.perform(Cmd::GoTo(Position::Begin)),
CmdResult::Visual
);
assert_eq!(component.states.cursor, 0);
}
}