diff --git a/Cargo.lock b/Cargo.lock index edec213..47e7c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -714,6 +714,7 @@ dependencies = [ "peg", "serde", "serde_json", + "toml", ] [[package]] @@ -1804,7 +1805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -2047,6 +2048,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2310,11 +2320,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2327,6 +2352,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "ttf-parser" version = "0.19.2" diff --git a/Cargo.toml b/Cargo.toml index 7b9c4be..67c508c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ itertools = "0.11.0" peg = "0.8.2" serde = { version = "1.0.192", features = ["derive"] } serde_json = "1.0.108" +toml = "0.8.8" diff --git a/src/main.rs b/src/main.rs index 6dd472b..9fef740 100644 --- a/src/main.rs +++ b/src/main.rs @@ -420,6 +420,105 @@ where } } +struct ErrorEdit { + v: String, + parse: F, + on_submit: G, +} + +#[derive(Default)] +struct ErrorEditState { + error: bool, + value: Option, +} + +#[derive(Clone, Debug)] +enum ErrorEditMsg { + Edit(String), + Submit, +} + +struct ErrorEditStyleSheet { + error: bool, +} + +impl text_input::StyleSheet for ErrorEditStyleSheet { + type Style = iced::Theme; + + fn active(&self, style: &Self::Style) -> text_input::Appearance { + style.active(&theme::TextInput::Default) + } + + fn focused(&self, style: &Self::Style) -> text_input::Appearance { + let def = style.focused(&theme::TextInput::Default); + + text_input::Appearance { + border_color: match self.error { + true => iced::Color::from_rgb8(240, 14, 27), + false => def.border_color, + }, + ..def + } + } + + fn placeholder_color(&self, style: &Self::Style) -> iced::Color { + style.placeholder_color(&theme::TextInput::Default) + } + + fn value_color(&self, style: &Self::Style) -> iced::Color { + style.value_color(&theme::TextInput::Default) + } + + fn disabled_color(&self, style: &Self::Style) -> iced::Color { + style.disabled_color(&theme::TextInput::Default) + } + + fn selection_color(&self, style: &Self::Style) -> iced::Color { + style.selection_color(&theme::TextInput::Default) + } + + fn disabled(&self, style: &Self::Style) -> text_input::Appearance { + style.disabled(&theme::TextInput::Default) + } +} + +impl iced::widget::Component for ErrorEdit +where + F: FnMut(&str) -> Result, + G: FnMut(T) -> M, +{ + type State = ErrorEditState; + type Event = ErrorEditMsg; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + ErrorEditMsg::Edit(e) => { + state.error = (self.parse)(&e).is_err(); + state.value = Some(e); + } + ErrorEditMsg::Submit => { + if let Some(v) = &state.value { + if let Ok(v) = (self.parse)(v) { + return Some((self.on_submit)(v)); + } + } + } + } + + None + } + + fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> { + text_input("value", state.value.as_ref().unwrap_or(&self.v)) + .on_input(ErrorEditMsg::Edit) + .on_submit(ErrorEditMsg::Submit) + .style(theme::TextInput::Custom(Box::new(ErrorEditStyleSheet { + error: state.error, + }))) + .into() + } +} + #[derive(Clone, Debug)] enum Message { Event(Event), @@ -427,12 +526,17 @@ enum Message { FontLoaded(Result<(), font::Error>), AddVariable(String), EditVariable(String, String), + EditEarings1(f64), + EditEarings2(f64), } struct Glaurung { + config: Config, recurring: BTreeMap, variable: BTreeMap)>, save_file: PathBuf, + earnings_1: f64, + earnings_2: f64, } #[derive(Serialize, Deserialize, Default)] @@ -441,12 +545,17 @@ struct SaveFile { recurring: BTreeMap, #[serde(default)] variable: HashMap, + #[serde(default)] + earnings_1: f64, + #[serde(default)] + earnings_2: f64, } #[derive(Default)] struct AppConfig { save: SaveFile, save_file: PathBuf, + config: Config, } impl Application for Glaurung { @@ -458,6 +567,7 @@ impl Application for Glaurung { fn new(config: Self::Flags) -> (Self, Command) { ( Self { + config: config.config, recurring: config.save.recurring, variable: config .save @@ -468,6 +578,8 @@ impl Application for Glaurung { (k, (e, f.ok())) }) .collect(), + earnings_1: config.save.earnings_1, + earnings_2: config.save.earnings_2, save_file: config.save_file, }, Command::batch(vec![ @@ -506,6 +618,8 @@ impl Application for Glaurung { .into_iter() .map(|(k, (e, _))| (k, e)) .collect(), + earnings_1: self.earnings_1, + earnings_2: self.earnings_2, }, ) .expect("could not write save file"); @@ -522,6 +636,12 @@ impl Application for Glaurung { entry.0 = expr; } } + Message::EditEarings1(v) => { + self.earnings_1 = v; + } + Message::EditEarings2(v) => { + self.earnings_2 = v; + } } Command::none() @@ -555,6 +675,30 @@ impl Application for Glaurung { .sum::() )) .size(TEXT_EMPH2), + text("Earnings").size(TEXT_H1), + row![ + text(&self.config.person_1).size(TEXT_EMPH1), + component(ErrorEdit { + v: self.earnings_1.to_string(), + parse: |s: &str| -> Result { s.parse() }, + on_submit: Message::EditEarings1, + }) + ] + .align_items(iced::Alignment::Center), + row![ + text(&self.config.person_2).size(TEXT_EMPH1), + component(ErrorEdit { + v: self.earnings_2.to_string(), + parse: |s: &str| -> Result { s.parse() }, + on_submit: Message::EditEarings2, + }) + ] + .align_items(iced::Alignment::Center), + text(&format!( + "Total earnings: {} €", + self.earnings_1 + self.earnings_2, + )) + .size(TEXT_EMPH2), ] .max_width(500) .padding(5) @@ -566,6 +710,32 @@ impl Application for Glaurung { } } +fn person_1() -> String { + "Person 1".into() +} + +fn person_2() -> String { + "Person 2".into() +} + +#[derive(serde::Deserialize)] +struct Config { + #[serde(default = "person_1")] + person_1: String, + + #[serde(default = "person_2")] + person_2: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + person_1: person_1(), + person_2: person_2(), + } + } +} + fn main() -> anyhow::Result<()> { let project_dir = ProjectDirs::from("net", "traxys", "glaurung").ok_or(anyhow!(""))?; let state_dir = project_dir @@ -580,7 +750,20 @@ fn main() -> anyhow::Result<()> { true => serde_json::from_reader(BufReader::new(File::open(&save_file)?))?, }; - let mut settings = Settings::with_flags(AppConfig { save, save_file }); + let config_file = project_dir.config_dir().join("config.toml"); + let config: Config = match config_file.exists() { + true => { + let config = std::fs::read_to_string(config_file)?; + toml::from_str(&config)? + } + false => Config::default(), + }; + + let mut settings = Settings::with_flags(AppConfig { + save, + save_file, + config, + }); settings.exit_on_close_request = false; Glaurung::run(settings)?;