use std::fs; use iced::{ Color, Element, Renderer, Subscription, Task, Theme, alignment::{Horizontal, Vertical}, color, event, keyboard::{self, Key, key}, theme, widget, }; mod data; macro_rules! data { () => { dirs::data_dir().unwrap().join("fridge.ron") }; } #[derive(Clone, Debug)] enum Message { AddCategory, RemoveCategory { i_category: usize, }, CategoryChanged { i_category: usize, new_value: String, }, AddElement { i_category: usize, }, RemoveElement { i_category: usize, i_element: usize, }, MoveElement { i_category: usize, i_element: usize, target_category: usize, }, ElementChanged { i_category: usize, i_element: usize, new_value: String, }, EditNotes(widget::text_editor::Action), Event(iced::Event), } #[derive(Clone, Debug, Default)] struct State { table: data::Table, content: widget::text_editor::Content, /// has the application initialized successfully? ok: bool, } impl State { fn new() -> Self { let mut state = Self::default(); if let Ok(str) = fs::read_to_string(data!()).inspect_err(|e| eprintln!("failed to open data file: {e}")) { let saved = ron::de::from_str::(&str).unwrap(); state.table = saved.table; state.content = widget::text_editor::Content::with_text(&saved.notes); } state.ok = true; state } fn view(&self) -> Element<'_, Message, Theme, Renderer> { widget::column([ widget::button("add category") .on_press(Message::AddCategory) .into(), widget::row(self.table.iter().enumerate().map(|(i_category, category)| { widget::column([ widget::row([ widget::text_input("category", &category.header) .on_input(move |new_value| Message::CategoryChanged { i_category, new_value, }) .size(30) .into(), widget::button("X") .style(widget::button::danger) .on_press(Message::RemoveCategory { i_category }) .height(50) .into(), ]) .align_y(Vertical::Center) .into(), widget::scrollable( widget::column(category.elements.iter().enumerate().map( |(i_element, element)| { widget::row([ widget::text_input("content", element) .on_input(move |new_value| Message::ElementChanged { i_category, i_element, new_value, }) .into(), widget::button("◄") .on_press_maybe(if i_category > 0 { Some(Message::MoveElement { i_category, i_element, target_category: i_category - 1, }) } else { None }) .into(), widget::button("►") .on_press_maybe(if i_category < self.table.len() - 1 { Some(Message::MoveElement { i_category, i_element, target_category: i_category + 1, }) } else { None }) .into(), widget::button("X") .style(widget::button::warning) .on_press(Message::RemoveElement { i_category, i_element, }) .into(), ]) .into() }, )) .align_x(Horizontal::Center) .push( widget::button("add element") .on_press(Message::AddElement { i_category }), ), ) .into(), ]) .align_x(Horizontal::Center) .into() })) .spacing(15) .into(), ]) .push(widget::text_editor(&self.content).on_action(Message::EditNotes)) .align_x(Horizontal::Center) .spacing(15) .padding(15) .into() } fn update(&mut self, message: Message) -> Task { match message { Message::AddCategory => { self.table.push(Default::default()); } Message::RemoveCategory { i_category } => { self.table.remove(i_category); } Message::CategoryChanged { i_category, new_value, } => { self.table[i_category].header = new_value; } Message::AddElement { i_category } => { self.table[i_category].elements.push(Default::default()); } Message::RemoveElement { i_category, i_element, } => { self.table[i_category].elements.remove(i_element); } Message::MoveElement { i_category, i_element, target_category, } => { let moved_element = self.table[i_category].elements.remove(i_element); self.table[target_category].elements.reverse(); self.table[target_category].elements.push(moved_element); self.table[target_category].elements.reverse(); } Message::ElementChanged { i_category, i_element, new_value, } => { self.table[i_category].elements[i_element] = new_value; } Message::EditNotes(action) => { self.content.perform(action); } Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. })) => match key.as_ref() { // input field cycling Key::Named(key::Named::Tab) => { if modifiers.shift() { return widget::operation::focus_previous(); } else { return widget::operation::focus_next(); } } // ignore unused keys _ => {} }, // ignore unused events Message::Event(_) => {} } Task::none() } fn subscription(&self) -> Subscription { event::listen().map(Message::Event) } } impl Drop for State { fn drop(&mut self) { if !self.ok { eprintln!("app failed to start, not saving data"); return; } ron::Options::default() .to_io_writer( fs::File::create(data!()).unwrap(), &data::Saved { table: self.table.clone(), notes: self.content.text(), }, ) .unwrap(); eprintln!("successfully saved data"); } } fn main() -> Result<(), iced::Error> { iced::application(State::new, State::update, State::view) .subscription(State::subscription) .theme(Theme::custom( "custom", theme::Palette { background: color!(0x080808), text: Color::WHITE, primary: color!(0xff00ff), success: color!(0x00ff00), warning: color!(0x880000), danger: color!(0xff0000), }, )) .run() }