Handle variable costs

This commit is contained in:
Quentin Boyer 2023-11-12 15:19:12 +01:00
parent 3824700332
commit 9e849349d4

View file

@ -1,5 +1,5 @@
use std::{
collections::BTreeMap,
collections::{BTreeMap, HashMap},
fs::{File, OpenOptions},
io::BufReader,
path::PathBuf,
@ -8,8 +8,10 @@ use std::{
use anyhow::anyhow;
use directories::ProjectDirs;
use iced::{
font, subscription,
widget::{button, column, component, horizontal_rule, horizontal_space, row, text, text_input},
font, subscription, theme,
widget::{
button, column, component, horizontal_rule, horizontal_space, row, rule, text, text_input,
},
window, Application, Command, Event, Length, Renderer, Settings, Theme,
};
use iced_aw::{card, modal};
@ -26,6 +28,7 @@ const TEXT_EMPH1: u16 = 17;
const TEXT_EMPH2: u16 = 20;
const LIST_RULE: u16 = 5;
const SECTION_RULE: u16 = 15;
struct AddRecurring<F> {
on_submit: F,
@ -233,8 +236,7 @@ where
),
horizontal_rule(LIST_RULE),
text(&format!("Total: {}", self.items.values().sum::<f64>())).size(TEXT_EMPH2)
]
.max_width(500.0);
];
let overlay = match state.add_recurring {
true => Some(
@ -255,21 +257,190 @@ where
}
}
struct ExprInput<'a, F> {
expr: &'a str,
on_submit: F,
}
#[derive(Default)]
struct ExprInputState {
state: Option<String>,
}
#[derive(Clone, Debug)]
enum ExprInputMessage {
Edit(String),
Submit,
}
impl<M, F> iced::widget::Component<M, Renderer> for ExprInput<'_, F>
where
F: FnMut(String) -> M,
{
type State = ExprInputState;
type Event = ExprInputMessage;
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
match event {
ExprInputMessage::Edit(v) => {
state.state = Some(v);
}
ExprInputMessage::Submit => {
return Some((self.on_submit)(
state.state.clone().unwrap_or_else(|| self.expr.to_string()),
))
}
}
None
}
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
text_input("expr", state.state.as_deref().unwrap_or(self.expr))
.on_input(ExprInputMessage::Edit)
.on_submit(ExprInputMessage::Submit)
.into()
}
}
#[derive(Clone, Debug)]
enum VariableMessage {
CloseAdd,
Add,
EditAdd(String),
ExprEdit(String, String),
DoAdd,
}
#[derive(Default)]
struct VariableState {
modal: bool,
add_value: String,
}
struct VariableSpendings<'a, F, G> {
items: &'a BTreeMap<String, (String, Option<f64>)>,
on_add: F,
on_expr_edit: G,
}
impl<M, F, G> iced::widget::Component<M, Renderer> for VariableSpendings<'_, F, G>
where
F: FnMut(String) -> M,
G: FnMut(String, String) -> M,
{
type State = VariableState;
type Event = VariableMessage;
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
match event {
VariableMessage::CloseAdd => {
state.add_value.clear();
state.modal = false;
}
VariableMessage::Add => {
state.modal = true;
}
VariableMessage::EditAdd(v) => {
state.add_value = v;
}
VariableMessage::DoAdd => {
state.modal = false;
return Some((self.on_add)(std::mem::take(&mut state.add_value)));
}
VariableMessage::ExprEdit(name, value) => {
return Some((self.on_expr_edit)(name, value));
}
}
None
}
#[allow(unstable_name_collisions)]
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
let underlay = column![
text("Variable").size(TEXT_H2),
button(text("Add")).on_press(VariableMessage::Add),
horizontal_rule(LIST_RULE),
column(
self.items
.iter()
.map(|(name, (expr, value))| {
let row = row![
text(name).size(TEXT_EMPH1),
component(ExprInput {
expr,
on_submit: |expr| VariableMessage::ExprEdit(name.clone(), expr)
}),
]
.spacing(5)
.align_items(iced::Alignment::Center);
let mut col = vec![row.into()];
if let Some(value) = value {
col.push(
row![
horizontal_space(Length::Fill),
text(&format!(" = {value}")),
]
.into(),
);
}
column(col).into()
})
.intersperse_with(|| horizontal_rule(LIST_RULE).into())
.collect()
),
horizontal_rule(LIST_RULE),
text(&format!(
"Total: {} €",
self.items.values().flat_map(|(_, v)| v).sum::<f64>()
))
.size(TEXT_EMPH2)
];
let overlay = match state.modal {
true => Some(
card(
text("Add variable spending"),
text_input("name", &state.add_value)
.on_input(VariableMessage::EditAdd)
.on_submit(VariableMessage::DoAdd),
)
.max_width(300.0),
),
false => None,
};
modal(underlay, overlay)
.backdrop(VariableMessage::CloseAdd)
.on_esc(VariableMessage::CloseAdd)
.into()
}
}
#[derive(Clone, Debug)]
enum Message {
Event(Event),
AddRecurring(String, f64),
FontLoaded(Result<(), font::Error>),
AddVariable(String),
EditVariable(String, String),
}
struct Glaurung {
recurring: BTreeMap<String, f64>,
variable: BTreeMap<String, (String, Option<f64>)>,
save_file: PathBuf,
}
#[derive(Serialize, Deserialize, Default)]
struct SaveFile {
#[serde(default)]
recurring: BTreeMap<String, f64>,
#[serde(default)]
variable: HashMap<String, String>,
}
#[derive(Default)]
@ -288,6 +459,15 @@ impl Application for Glaurung {
(
Self {
recurring: config.save.recurring,
variable: config
.save
.variable
.into_iter()
.map(|(k, e)| {
let f = calc::calc_parser::calc(&e);
(k, (e, f.ok()))
})
.collect(),
save_file: config.save_file,
},
Command::batch(vec![
@ -322,6 +502,10 @@ impl Application for Glaurung {
save_file,
&SaveFile {
recurring: std::mem::take(&mut self.recurring),
variable: std::mem::take(&mut self.variable)
.into_iter()
.map(|(k, (e, _))| (k, e))
.collect(),
},
)
.expect("could not write save file");
@ -329,6 +513,15 @@ impl Application for Glaurung {
return window::close();
}
}
Message::AddVariable(name) => {
self.variable.insert(name, ("0".into(), Some(0.)));
}
Message::EditVariable(v, expr) => {
if let Some(entry) = self.variable.get_mut(&v) {
entry.1 = calc::calc_parser::calc(&expr).ok();
entry.0 = expr;
}
}
}
Command::none()
@ -340,8 +533,18 @@ impl Application for Glaurung {
component(Recurring {
items: &self.recurring,
on_add: Message::AddRecurring,
}),
horizontal_rule(SECTION_RULE).style(|theme: &Theme| {
let def = rule::StyleSheet::appearance(theme, &theme::Rule::Default);
rule::Appearance { width: 3, ..def }
}),
component(VariableSpendings {
items: &self.variable,
on_add: Message::AddVariable,
on_expr_edit: Message::EditVariable,
})
]
.max_width(500)
.padding(5)
.into()
}