diff --git a/Cargo.lock b/Cargo.lock index 5d6db91..cd3c194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,6 +449,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "equivalent" version = "1.0.1" @@ -674,6 +680,8 @@ name = "glaurung" version = "0.1.0" dependencies = [ "iced", + "iced_aw", + "itertools", ] [[package]] @@ -803,6 +811,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -829,6 +843,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "iced_aw" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c51860ce7be5be6f6104c4e13b14e56662ebbd7c96c50e10069d59f8c3d892" +dependencies = [ + "iced_widget", +] + [[package]] name = "iced_core" version = "0.10.0" @@ -957,6 +980,7 @@ dependencies = [ "iced_runtime", "iced_style", "num-traits", + "ouroboros", "thiserror", "unicode-segmentation", ] @@ -1022,6 +1046,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -1493,6 +1526,30 @@ dependencies = [ "libredox", ] +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "owned_ttf_parser" version = "0.20.0" @@ -1668,6 +1725,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 067f8d5..8b705c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,9 @@ authors = ["traxys "] edition = "2021" [dependencies] -iced = "0.10.0" +iced = { version = "0.10.0", features = ["lazy"] } +iced_aw = { version = "0.7.0", default-features = false, features = [ + "modal", + "card", +] } +itertools = "0.11.0" diff --git a/flake.nix b/flake.nix index e41f9b1..a935c3a 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,7 @@ libPath = with pkgs; lib.makeLibraryPath [libxkbcommon wayland]; in { devShell = pkgs.mkShell { - nativeBuildInputs = [rust]; + nativeBuildInputs = [rust pkgs.cargo-watch]; RUST_PATH = "${rust}"; RUST_DOC_PATH = "${rust}/share/doc/rust/html/std/index.html"; LD_LIBRARY_PATH = libPath; diff --git a/src/main.rs b/src/main.rs index 9af61e4..83545e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,277 @@ -use iced::{widget::column, Sandbox, Settings}; +use std::collections::BTreeMap; -#[derive(Clone, Copy, Debug)] -enum Message {} +use iced::{ + font, + widget::{button, column, component, horizontal_rule, row, text, text_input}, + Application, Command, Renderer, Settings, Theme, +}; +use iced_aw::{card, modal}; +use itertools::Itertools; -struct Glaurung {} +type Element<'a> = iced::Element<'a, Message>; -impl Sandbox for Glaurung { +#[derive(Clone, Debug)] +enum Message { + AddRecurring(String, f64), + FontLoaded(Result<(), font::Error>), +} + +struct Glaurung { + recurring: BTreeMap, +} + +struct AddRecurring { + on_submit: F, +} + +#[derive(Default)] +struct AddRecurringState { + value: String, + item: String, +} + +#[derive(Debug, Clone)] +enum AddRecurringEvent { + SetItem(String), + SetValue(String), + SubmitValue, +} + +impl AddRecurring { + fn new(on_submit: F) -> Self { + Self { on_submit } + } +} + +impl iced::widget::Component for AddRecurring +where + F: FnMut(String, f64) -> M, +{ + type State = AddRecurringState; + type Event = AddRecurringEvent; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + AddRecurringEvent::SetItem(i) => state.item = i, + AddRecurringEvent::SetValue(v) => state.value = v, + AddRecurringEvent::SubmitValue => { + if let Ok(v) = state.value.parse() { + state.value.clear(); + return Some((self.on_submit)(std::mem::take(&mut state.item), v)); + } + } + } + + None + } + + fn view(&self, state: &Self::State) -> iced::Element<'_, Self::Event, Renderer> { + column![ + text_input("item", &state.item).on_input(AddRecurringEvent::SetItem), + text_input("value", &state.value) + .on_input(AddRecurringEvent::SetValue) + .on_submit(AddRecurringEvent::SubmitValue) + ] + .into() + } +} + +struct EditRecurring<'a, F> { + value: f64, + name: &'a str, + on_submit: F, +} + +#[derive(Default)] +struct EditRecurringState { + edit: String, + modal: bool, +} + +#[derive(Clone, Debug)] +enum EditRecurringEvent { + Edit(String), + Open, + Close, + Submit, +} + +impl iced::widget::Component for EditRecurring<'_, F> +where + F: FnMut(f64) -> M, +{ + type State = EditRecurringState; + type Event = EditRecurringEvent; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + EditRecurringEvent::Edit(_) => todo!(), + EditRecurringEvent::Submit => { + if let Ok(v) = state.edit.parse() { + state.edit.clear(); + state.modal = false; + return Some((self.on_submit)(v)); + } + } + EditRecurringEvent::Open => { + state.modal = true; + } + EditRecurringEvent::Close => { + state.modal = false; + state.edit.clear(); + } + } + + None + } + + fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> { + let underlay = button(text("Edit")).on_press(EditRecurringEvent::Open); + let overlay = match state.modal { + true => Some( + card(text(&format!("Edit {}", self.name)), text("todo")) + .max_width(300.0) + .on_close(EditRecurringEvent::Close), + ), + false => None, + }; + + modal(underlay, overlay) + .backdrop(EditRecurringEvent::Close) + .on_esc(EditRecurringEvent::Close) + .into() + } +} + +#[derive(Clone, Debug)] +enum RecurringMessage { + CloseAdd, + Add, + DoAdd(String, f64), +} + +#[derive(Default)] +struct RecurringState { + add_recurring: bool, +} + +struct Recurring<'a, F> { + items: &'a BTreeMap, + on_add: F, +} + +impl iced::widget::Component for Recurring<'_, F> +where + F: FnMut(String, f64) -> M, +{ + type State = RecurringState; + type Event = RecurringMessage; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + RecurringMessage::Add => state.add_recurring = true, + RecurringMessage::CloseAdd => { + state.add_recurring = false; + } + RecurringMessage::DoAdd(item, value) => { + state.add_recurring = false; + return Some((self.on_add)(item, value)); + } + } + + None + } + + fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> { + let underlay = column![ + text("Recurring").size(20), + button(text("Add")).on_press(RecurringMessage::Add), + horizontal_rule(5), + column( + self.items + .iter() + .map(|(name, &value)| row![ + text(name).size(17), + text(&format!("{value} €")), + component(EditRecurring { + value, + name, + on_submit: |v| RecurringMessage::DoAdd(name.to_string(), v) + }) + ] + .spacing(5) + .align_items(iced::Alignment::Center) + .into()) + .intersperse_with(|| horizontal_rule(5).into()) + .collect() + ), + ]; + + let overlay = match state.add_recurring { + true => Some( + card( + text("Add a recurring spending"), + component(AddRecurring::new(RecurringMessage::DoAdd)), + ) + .on_close(RecurringMessage::CloseAdd) + .max_width(300.0), + ), + false => None, + }; + + modal(underlay, overlay) + .backdrop(RecurringMessage::CloseAdd) + .on_esc(RecurringMessage::CloseAdd) + .into() + } +} + +impl Application for Glaurung { type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; + type Flags = (); - fn new() -> Self { - Self {} + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + Self { + recurring: BTreeMap::new(), + }, + Command::batch(vec![ + font::load(iced_aw::graphics::icons::ICON_FONT_BYTES).map(Message::FontLoaded) + ]), + ) } fn title(&self) -> String { "Glaurung - Account Manager".into() } - fn update(&mut self, message: Self::Message) { - match message {} + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::AddRecurring(name, value) => { + self.recurring.insert(name, value); + } + Message::FontLoaded(r) => r.expect("could not load font"), + } + + Command::none() } - fn view(&self) -> iced::Element<'_, Self::Message> { - column![].into() + fn view(&self) -> Element { + column![ + text("Spendings").size(30), + component(Recurring { + items: &self.recurring, + on_add: Message::AddRecurring, + }) + ] + .padding(5) + .into() + } + + fn theme(&self) -> iced::Theme { + iced::Theme::Dark } }