use crate::{app::App, entities::Account, enums::*};
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style, palette::tailwind::SLATE},
symbols::Marker,
text::{Line, Span, Text},
widgets::{
Axis, Block, Borders, Chart, Clear, Dataset, GraphType, HighlightSpacing, List, ListItem,
Paragraph, Row, StatefulWidget, Table, Wrap,
},
};
const TEXT_FG_COLOR: Color = SLATE.c200;
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
pub fn ui(frame: &mut Frame, app: &mut App) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
.split(frame.area());
let right_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Percentage(10), Constraint::Percentage(90)])
.split(layout[1]);
let mut ac_block = Block::default()
.title("Accounts")
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::DarkGray));
let info_block = Block::default()
.title("Account info")
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::DarkGray));
let mut trx_block = Block::default()
.title(build_tab_title(app.selected_tab, app.tabs.clone()))
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::DarkGray));
let active_style = Style::default();
match app.current_widget {
CurrentWidget::AccountList => ac_block = ac_block.border_style(active_style),
CurrentWidget::TrxList => trx_block = trx_block.border_style(active_style),
};
let ac_items: Vec<ListItem> = app
.get_list_accounts()
.iter()
.enumerate()
.map(|(_, acc)| ListItem::from(acc))
.collect();
let list = List::new(ac_items)
.block(ac_block)
.highlight_style(SELECTED_STYLE)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always);
StatefulWidget::render(list, layout[0], frame.buffer_mut(), &mut app.acc_list.state);
let info = if let Some(i) = app.acc_list.state.selected() {
format!(
"Total: {}",
app.get_list_accounts()[i]
.get_total(&app.connection)
.unwrap_or_default()
.to_string()
)
} else {
"No account selected...".to_string()
};
frame.render_widget(Paragraph::new(info).block(info_block), right_layout[0]);
if app.selected_tab == 0 {
let trx_header = Row::new(["Date", "Type", "Amount", "Description"]);
let trx_rows = app.get_list_trx().into_iter().map(|tr| {
Row::new([
tr.get_date().format("%Y-%m-%d").to_string(),
tr.get_type().get_description(),
tr.get_amount().to_string(),
tr.get_desc(),
])
.style(Style::new().fg(if tr.get_amount() > 0.0 {
Color::Green
} else {
Color::Red
}))
});
let widths = [
Constraint::Percentage(30),
Constraint::Percentage(20),
Constraint::Percentage(20),
Constraint::Percentage(50),
];
let trx_table = Table::new(trx_rows, widths)
.block(trx_block)
.header(trx_header)
.row_highlight_style(SELECTED_STYLE)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always)
.column_spacing(1);
StatefulWidget::render(
trx_table,
right_layout[1],
frame.buffer_mut(),
&mut app.trx_table.state,
);
} else if app.selected_tab == 1 {
let slice: &[(f64, f64)] = &app.get_trtype_data();
let dataset = Dataset::default()
.marker(Marker::Braille)
.graph_type(GraphType::Line)
.style(Color::Blue)
.data(slice);
let min_x = slice
.into_iter()
.map(|&(x, _)| x)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less))
.unwrap_or_default()
.round();
let max_x = slice
.into_iter()
.map(|&(x, _)| x)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater))
.unwrap_or_default()
.round()
+ 1.0;
let x_axis = Axis::default()
.title("Temps")
.bounds([min_x, max_x])
.labels([min_x.to_string(), max_x.to_string()]);
let min_y = slice
.into_iter()
.map(|&(_, y)| y)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less))
.unwrap_or_default()
.round()
- 1.0;
let max_y = slice
.into_iter()
.map(|&(_, y)| y)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater))
.unwrap_or_default()
.round()
+ 1.0;
let y_axis = Axis::default()
.title("Coƻts")
.bounds([min_y, max_y])
.labels([min_y.to_string(), max_y.to_string()]);
let chart = Chart::new(vec![dataset])
.block(trx_block.clone())
.x_axis(x_axis)
.y_axis(y_axis);
frame.render_widget(chart, right_layout[1]);
let p = Paragraph::new(app.selected_tr_type_chart_desc.clone())
.block(trx_block)
.style(Style::default())
.alignment(ratatui::layout::HorizontalAlignment::Center);
frame.render_widget(p, right_layout[1]);
let helper = Block::default()
.title("Helper")
.borders(Borders::all())
.style(Style::default());
let helper_string: String = app
.tr_types
.iter()
.map(|tr_type| tr_type.get_id().to_string() + " " + &tr_type.get_description() + "\n")
.collect();
let helper_text = Text::styled(helper_string, Style::default());
let helper_paragraph = Paragraph::new(helper_text)
.block(helper)
.wrap(Wrap { trim: false });
let area = helper_rect(frame.area());
frame.render_widget(Clear, area);
frame.render_widget(helper_paragraph, area);
} else if app.selected_tab == 2 {
let slice: &[(f64, f64)] = &app.get_cumulative_total();
let dataset = Dataset::default()
.marker(Marker::Braille)
.graph_type(GraphType::Line)
.style(Color::Blue)
.data(slice);
let min_x = slice
.into_iter()
.map(|&(x, _)| x)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less))
.unwrap_or_default()
.round();
let max_x = slice
.into_iter()
.map(|&(x, _)| x)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater))
.unwrap_or_default()
.round()
+ 1.0;
let x_axis = Axis::default()
.title("Time")
.bounds([min_x, max_x])
.labels([min_x.to_string(), max_x.to_string()]);
let min_y = slice
.into_iter()
.map(|&(_, y)| y)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less))
.unwrap_or_default()
.round()
- 1.0;
let max_y = slice
.into_iter()
.map(|&(_, y)| y)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater))
.unwrap_or_default()
.round()
+ 1.0;
let y_axis = Axis::default()
.title("Total")
.bounds([min_y, max_y])
.labels([min_y.to_string(), max_y.to_string()]);
let chart = Chart::new(vec![dataset])
.block(trx_block.clone())
.x_axis(x_axis)
.y_axis(y_axis);
frame.render_widget(chart, right_layout[1]);
} else {
let p = Paragraph::new("Tab not implemented")
.block(trx_block)
.style(Style::default());
frame.render_widget(p, right_layout[1]);
}
if let CurrentScreen::NewAccount = app.current_screen {
let popup = Block::default()
.title("New account")
.borders(Borders::all())
.style(Style::default());
let area = centered_rect(50, 40, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(popup, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.split(area);
let ac_asset = Paragraph::new(ac_input_str(AccountInput::Asset, app))
.style(ac_input_style(AccountInput::Asset, app))
.block(
Block::default()
.borders(Borders::BOTTOM)
.title("Asset Price"),
);
frame.render_widget(ac_asset, chunks[0]);
let ac_name = Paragraph::new(ac_input_str(AccountInput::Name, app))
.style(ac_input_style(AccountInput::Name, app))
.block(Block::default().borders(Borders::BOTTOM).title("Name"));
frame.render_widget(ac_name, chunks[1]);
let ac_type = Paragraph::new(ac_input_str(AccountInput::Type, app))
.style(ac_input_style(AccountInput::Type, app))
.block(Block::default().borders(Borders::BOTTOM).title("Type"));
frame.render_widget(ac_type, chunks[2]);
}
if let CurrentScreen::NewTransaction = app.current_screen {
let popup = Block::default()
.title("New Transaction")
.borders(Borders::all())
.style(Style::default());
let area = centered_rect(50, 40, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(popup, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.split(area);
let tr_type = Paragraph::new(tr_input_str(TransactionInput::Type, app))
.style(tr_input_style(TransactionInput::Type, app))
.block(Block::default().borders(Borders::BOTTOM).title("Type"));
frame.render_widget(tr_type, chunks[0]);
let amnt = Paragraph::new(tr_input_str(TransactionInput::Amount, app))
.style(tr_input_style(TransactionInput::Amount, app))
.block(Block::default().borders(Borders::BOTTOM).title("Amount"));
frame.render_widget(amnt, chunks[1]);
let date = Paragraph::new(tr_input_str(TransactionInput::Date, app))
.style(tr_input_style(TransactionInput::Date, app))
.block(Block::default().borders(Borders::BOTTOM).title("Date"));
frame.render_widget(date, chunks[2]);
let desc = Paragraph::new(tr_input_str(TransactionInput::Description, app))
.style(tr_input_style(TransactionInput::Description, app))
.block(
Block::default()
.borders(Borders::BOTTOM)
.title("Description"),
);
frame.render_widget(desc, chunks[3]);
let asset_amnt = Paragraph::new(tr_input_str(TransactionInput::Asset, app))
.style(tr_input_style(TransactionInput::Asset, app))
.block(
Block::default()
.borders(Borders::BOTTOM)
.title("Asset amount"),
);
frame.render_widget(asset_amnt, chunks[4]);
if let TransactionInput::Type = app.selected_transaction_input {
let helper = Block::default()
.title("Helper")
.borders(Borders::all())
.style(Style::default());
let helper_string: String = app
.tr_types
.iter()
.map(|tr_type| {
tr_type.get_id().to_string() + " " + &tr_type.get_description() + "\n"
})
.collect();
let helper_text = Text::styled(helper_string, Style::default());
let helper_paragraph = Paragraph::new(helper_text)
.block(helper)
.wrap(Wrap { trim: false });
let area = helper_rect(frame.area());
frame.render_widget(Clear, area);
frame.render_widget(helper_paragraph, area);
}
}
if let CurrentScreen::Exiting = app.current_screen {
let popup = Block::default()
.title("Exiting program")
.borders(Borders::all())
.style(Style::default());
let exit_text = Text::styled(
"Are you sure you want to close the program? (y/n)",
Style::default().fg(Color::Red),
);
let exit_paragraph = Paragraph::new(exit_text)
.block(popup)
.wrap(Wrap { trim: false });
let area = centered_rect(50, 20, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(exit_paragraph, area);
}
if !app.error.is_empty() {
app.error_showing = true;
let popup = Block::default()
.title("Error")
.borders(Borders::all())
.style(Style::default());
let error_text = Text::styled(&app.error, Style::default().fg(Color::Red));
let error_paragraph = Paragraph::new(error_text)
.block(popup)
.wrap(Wrap { trim: false });
let area = centered_rect(50, 20, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(error_paragraph, area);
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
return Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1];
}
fn helper_rect(r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
.split(r);
return Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(20), Constraint::Percentage(70)])
.split(popup_layout[1])[0];
}
fn tr_input_style(input: TransactionInput, app: &mut App) -> Style {
if app.selected_transaction_input == input {
return SELECTED_STYLE;
}
return Style::default();
}
fn tr_input_str(input: TransactionInput, app: &mut App) -> String {
if app.selected_transaction_input == input {
return app.current_input.clone();
}
match input {
TransactionInput::Type => app.new_transaction.get_type().get_id().to_string(),
TransactionInput::Amount => app.new_transaction.get_amount().to_string(),
TransactionInput::Date => app
.new_transaction
.get_date()
.format("%Y-%m-%d")
.to_string(),
TransactionInput::Description => app.new_transaction.get_desc(),
TransactionInput::Asset => app.new_transaction.get_asset_amnt().to_string(),
}
}
fn ac_input_style(input: AccountInput, app: &mut App) -> Style {
if app.selected_account_input == input {
return SELECTED_STYLE;
}
return Style::default();
}
fn ac_input_str(input: AccountInput, app: &mut App) -> String {
if app.selected_account_input == input {
return app.current_input.clone();
}
match input {
AccountInput::Name => app.new_account.get_name(),
AccountInput::Type => {
let ac_type = app.new_account.get_ac_type() as usize;
return ac_type.to_string();
}
AccountInput::Asset => app.new_account.get_asset_price().to_string(),
}
}
fn build_tab_title(selected: usize, labels: Vec<String>) -> Vec<Span<'static>> {
let mut spans = Vec::new();
for (i, label) in labels.iter().enumerate() {
let style = if i == selected {
Style::new().fg(Color::Magenta)
} else {
Style::new()
};
spans.push(Span::raw(label.clone()).style(style));
if i + 1 < labels.len() {
spans.push(Span::raw(" | ").style(Style::new().fg(Color::DarkGray)));
}
}
return spans;
}
impl From<&Account> for ListItem<'_> {
fn from(value: &Account) -> Self {
let line = Line::styled(
value.get_id().to_string() + " " + &value.get_name(),
TEXT_FG_COLOR,
);
ListItem::new(line)
}
}