diff --git a/Cargo.toml b/Cargo.toml index 126d359..e904890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,12 @@ resolver = "2" publish = false [dependencies] -kas = { version = "0.12.0" } +kas = { version = "0.14.0-alpha" } chrono = "0.4" env_logger = "0.8" pest = "2.1" pest_derive = "2.1" + +[patch.crates-io.kas] +git = "https://github.com/kas-gui/kas.git" +rev = "90b19d6847ba1d4185de4605218b05db2b30024e" diff --git a/src/cells.pest b/src/cells.pest index 6113e6d..8095148 100644 --- a/src/cells.pest +++ b/src/cells.pest @@ -1,8 +1,10 @@ WHITESPACE = _{ " " | "\t" } number = @{ - ("." ~ ASCII_DIGIT+) - | (ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)?) - ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+)? + ("-" | "+")? ~ ( + ("." ~ ASCII_DIGIT+) + | + (ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)?) ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+)? + ) } reference = @{ ASCII_ALPHA ~ ASCII_DIGIT+ } value = { number | reference | ("(" ~ expression ~ ")") } @@ -11,6 +13,6 @@ product = { value ~ (product_op ~ value)* } sum_op = { "-" | "+" } summation = { sum_op? ~ product ~ (sum_op ~ product)* } expression = !{ summation } -formula = ${ SOI ~ "=" ~ WHITESPACE* ~ expression ~ WHITESPACE* } +formula = ${ SOI ~ "=" ~ WHITESPACE* ~ expression ~ WHITESPACE* ~ EOI } text = @{ !"=" ~ ANY* } cell = _{ formula | text } diff --git a/src/cells.rs b/src/cells.rs index aba5945..37b755a 100644 --- a/src/cells.rs +++ b/src/cells.rs @@ -5,16 +5,14 @@ //! Cells: a mini spreadsheet -use kas::event::Command; -use kas::model::{MatrixData, SharedData}; +use kas::event::{Command, FocusSource}; use kas::prelude::*; -use kas::view::{Driver, MatrixView, MaybeOwned}; +use kas::view::{DataKey, Driver, MatrixData, MatrixView, SharedData}; use kas::widgets::{EditBox, EditField, EditGuard, ScrollBars}; -use std::cell::RefCell; -use std::collections::hash_map::{Entry, HashMap}; +use std::collections::HashMap; use std::{fmt, iter, ops}; -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Hash)] pub struct ColKey(u8); type ColKeyIter = iter::Map, fn(u8) -> ColKey>; impl ColKey { @@ -43,12 +41,28 @@ impl fmt::Display for ColKey { const MAX_ROW: u8 = 99; -pub type Key = (ColKey, u8); +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct Key(ColKey, u8); +impl DataKey for Key { + fn make_id(&self, parent: &Id) -> Id { + assert_eq!(std::mem::size_of::(), 1); + let key = (((self.0).0 as usize) << 8) | (self.1 as usize); + parent.make_child(key) + } + + fn reconstruct_key(parent: &Id, child: &Id) -> Option { + child.next_key_after(parent).map(|key| { + let col = ColKey((key >> 8) as u8); + let row = key as u8; + Key(col, row) + }) + } +} fn make_key(k: &str) -> Key { let col = ColKey::from_u8(k.as_bytes()[0]); let row: u8 = k[1..].parse().unwrap(); - (col, row) + Key(col, row) } #[derive(Debug, PartialEq, Eq)] @@ -102,7 +116,8 @@ impl Formula { } mod parser { - use super::{ColKey, Formula}; + use super::{ColKey, Formula, Key}; + use pest::error::Error; use pest::iterators::Pairs; use pest::Parser; use pest_derive::Parser; @@ -125,7 +140,7 @@ mod parser { } let col = ColKey::from_u8(col); let row = s[1..].parse().unwrap(); - let key = (col, row); + let key = Key(col, row); Formula::Reference(key) } Rule::expression => parse_expression(pair.into_inner()), @@ -139,9 +154,11 @@ mod parser { for pair in pairs { match pair.as_rule() { Rule::product_op => { - if pair.as_span().as_str() == "/" { - div = true; - } + div = match pair.as_span().as_str() { + "*" => false, + "/" => true, + other => panic!("expected `*` or `/`, found `{other}`"), + }; } Rule::value => { let formula = parse_value(pair.into_inner()); @@ -167,9 +184,11 @@ mod parser { for pair in pairs { match pair.as_rule() { Rule::sum_op => { - if pair.as_span().as_str() == "-" { - sub = true; - } + sub = match pair.as_span().as_str() { + "+" => false, + "-" => true, + other => panic!("expected `+` or `-`, found `{other}`"), + }; } Rule::product => { let formula = parse_product(pair.into_inner()); @@ -190,7 +209,11 @@ mod parser { fn parse_expression(mut pairs: Pairs<'_, Rule>) -> Formula { let pair = pairs.next().unwrap(); - assert!(pairs.next().is_none()); + if let Some(pair) = pairs.next() { + if pair.as_rule() != Rule::EOI { + panic!("unexpected next pair: {pair:?}"); + } + } assert_eq!(pair.as_rule(), Rule::expression); let mut pairs = pair.into_inner(); @@ -200,21 +223,15 @@ mod parser { parse_summation(pair.into_inner()) } - pub fn parse(source: &str) -> Result, ()> { - match FormulaParser::parse(Rule::cell, source) { - Ok(mut pairs) => { - let pair = pairs.next().unwrap(); - Ok(match pair.as_rule() { - Rule::formula => Some(parse_expression(pair.into_inner())), - Rule::text => None, - _ => unreachable!(), - }) - } - Err(error) => { - println!("Error: {error}"); - Err(()) + pub fn parse(source: &str) -> Result, Error> { + FormulaParser::parse(Rule::cell, source).map(|mut pairs| { + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::formula => Some(parse_expression(pair.into_inner())), + Rule::text => None, + _ => unreachable!(), } - } + }) } } @@ -228,23 +245,24 @@ struct Cell { impl Cell { fn new(input: T) -> Self { - let input = input.to_string(); - let result = parser::parse(&input); - let parse_error = result.is_err(); - Cell { - input, - formula: result.ok().flatten(), - parse_error, - display: String::new(), - } + let mut cell = Cell::default(); + cell.update(input.to_string()); + cell } - fn update(&mut self, input: &str) { - let result = parser::parse(input); - self.input.clear(); - self.input.push_str(input); - self.parse_error = result.is_err(); - self.formula = result.ok().flatten(); + fn update(&mut self, input: String) { + match parser::parse(&input) { + Ok(opt_formula) => { + self.formula = opt_formula; + self.parse_error = false; + } + Err(error) => { + println!("Parse error: {error}"); + self.display = "BAD FORMULA".to_string(); + self.parse_error = true; + } + } + self.input = input; } /// Get display string @@ -257,7 +275,10 @@ impl Cell { } fn try_eval(&mut self, values: &HashMap) -> Result, EvalError> { - if let Some(ref f) = self.formula { + if self.parse_error { + // Display the error locally; propegate NaN + Ok(Some(f64::NAN)) + } else if let Some(ref f) = self.formula { let value = f.eval(values)?; self.display = value.to_string(); Ok(Some(value)) @@ -268,16 +289,14 @@ impl Cell { } #[derive(Debug)] -struct CellDataInner { - version: u64, +struct CellData { cells: HashMap, values: HashMap, } -impl CellDataInner { +impl CellData { fn new() -> Self { - CellDataInner { - version: 0, + CellData { cells: HashMap::new(), values: HashMap::new(), } @@ -326,41 +345,32 @@ impl CellDataInner { } } -#[derive(Debug)] -struct CellData { - inner: RefCell, -} - -impl CellData { - fn new() -> Self { - CellData { - inner: RefCell::new(CellDataInner::new()), - } - } +#[derive(Clone, Debug, Default)] +struct Item { + input: String, + display: String, + error: bool, } -/// Item is (input_string, display_string, error_state) -type ItemData = (String, String, bool); - impl SharedData for CellData { - type Key = (ColKey, u8); - type Item = ItemData; + type Key = Key; + type Item = Item; type ItemRef<'b> = Self::Item; - fn version(&self) -> u64 { - self.inner.borrow().version - } - fn contains_key(&self, _: &Self::Key) -> bool { // we know both sub-keys are valid and that the length is fixed true } fn borrow(&self, key: &Self::Key) -> Option { - let inner = self.inner.borrow(); - let cell = inner.cells.get(key); - cell.map(|cell| (cell.input.clone(), cell.display(), cell.parse_error)) - .or_else(|| Some(("".to_string(), "".to_string(), false))) + self.cells + .get(key) + .map(|cell| Item { + input: cell.input.clone(), + display: cell.display(), + error: cell.parse_error, + }) + .or_else(|| Some(Item::default())) } } @@ -377,19 +387,6 @@ impl MatrixData for CellData { (ColKey::LEN.cast(), 99) } - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - assert_eq!(std::mem::size_of::(), 1); - let key = (((key.0).0 as usize) << 8) | (key.1 as usize); - parent.make_child(key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - child.next_key_after(parent).map(|key| { - let col = ColKey((key >> 8) as u8); - let row = key as u8; - (col, row) - }) - } - fn col_iter_from(&self, start: usize, limit: usize) -> Self::ColKeyIter<'_> { ColKey::iter_keys().skip(start).take(limit) } @@ -400,98 +397,69 @@ impl MatrixData for CellData { (1..=MAX_ROW).skip(start).take(limit) } - fn make_key(col: &Self::ColKey, row: &Self::RowKey) -> Self::Key { - (*col, *row) + fn make_key(&self, col: &Self::ColKey, row: &Self::RowKey) -> Self::Key { + Key(*col, *row) } } #[derive(Debug)] -enum CellEvent { - Activate, - FocusLost, -} +struct UpdateInput(Key, String); #[derive(Clone, Default, Debug)] struct CellGuard { - input: String, + key: Key, + is_input: bool, } impl EditGuard for CellGuard { - fn activate(_: &mut EditField, mgr: &mut EventMgr) -> Response { - mgr.push_msg(CellEvent::Activate); - Response::Used + type Data = Item; + + fn update(edit: &mut EditField, cx: &mut ConfigCx, item: &Item) { + let mut action = edit.set_error_state(item.error); + if !edit.has_edit_focus() { + action |= edit.set_str(&item.display); + edit.guard.is_input = false; + } + cx.action(edit, action); } - fn focus_gained(edit: &mut EditField, mgr: &mut EventMgr) { - let mut s = String::default(); - std::mem::swap(&mut edit.guard.input, &mut s); - *mgr |= edit.set_string(s); + fn activate(edit: &mut EditField, cx: &mut EventCx, item: &Item) -> IsUsed { + Self::focus_lost(edit, cx, item); + IsUsed::Used } - fn focus_lost(_: &mut EditField, mgr: &mut EventMgr) { - mgr.push_msg(CellEvent::FocusLost); + fn focus_gained(edit: &mut EditField, cx: &mut EventCx, item: &Item) { + cx.action(edit.id(), edit.set_str(&item.input)); + edit.guard.is_input = true; + } + + fn focus_lost(edit: &mut EditField, cx: &mut EventCx, item: &Item) { + let s = edit.get_string(); + if edit.guard.is_input && s != item.input { + cx.push(UpdateInput(edit.guard.key, s)); + } } } #[derive(Debug)] struct CellDriver; -impl Driver for CellDriver { +impl Driver for CellDriver { // TODO: we should use EditField instead of EditBox but: // (a) there is currently no code to draw separators between cells // (b) EditField relies on a parent (EditBox) to draw background highlight on error state type Widget = EditBox; - fn make(&self) -> Self::Widget { - EditBox::new("".to_string()).with_guard(CellGuard::default()) - } - - fn set_mo( - &self, - edit: &mut Self::Widget, - _: &(ColKey, u8), - item: MaybeOwned<'_, ItemData>, - ) -> TkAction { - let item = item.into_owned(); - edit.guard.input = item.0; - edit.set_error_state(item.2); - if edit.has_key_focus() { - // assume that the contents of the EditBox are the latest - TkAction::empty() - } else { - edit.set_string(item.1) - } - } - - fn on_message( - &self, - mgr: &mut EventMgr, - widget: &mut Self::Widget, - data: &CellData, - key: &(ColKey, u8), - ) { - if mgr.try_observe_msg::().is_some() { - let mut inner = data.inner.borrow_mut(); - match inner.cells.entry(*key) { - Entry::Occupied(mut entry) => { - entry.get_mut().update(widget.get_str()); - } - Entry::Vacant(entry) => { - entry.insert(Cell::new(widget.get_string())); - } - } - - // TODO: we should not recompute everything here! - inner.update_values(); - inner.version += 1; - mgr.update_all(0); - } + fn make(&mut self, key: &Key) -> Self::Widget { + EditBox::new(CellGuard { + key: *key, + is_input: false, + }) } } -pub fn window() -> Box { +pub fn window() -> Window<()> { let mut data = CellData::new(); - let inner = data.inner.get_mut(); - let cells = &mut inner.cells; + let cells = &mut data.cells; cells.insert(make_key("A1"), Cell::new("Some values")); cells.insert(make_key("A2"), Cell::new("3")); cells.insert(make_key("A3"), Cell::new("4")); @@ -500,46 +468,51 @@ pub fn window() -> Box { cells.insert(make_key("B2"), Cell::new("= A2 + A3 + A4")); cells.insert(make_key("C1"), Cell::new("Prod")); cells.insert(make_key("C2"), Cell::new("= A2 * A3 * A4")); - inner.update_values(); + data.update_values(); - let cells = MatrixView::new_with_driver(CellDriver, data).with_num_visible(5, 20); + let cells = MatrixView::new(CellDriver).with_num_visible(5, 20); - Box::new(singleton! { - #[derive(Debug)] + let ui = impl_anon! { #[widget { layout = self.cells; }] struct { core: widget_core!(), - #[widget] cells: ScrollBars> = + data: CellData = data, + #[widget(&self.data)] cells: ScrollBars> = ScrollBars::new(cells), } - impl Widget for Self { - fn steal_event(&mut self, mgr: &mut EventMgr, _: &WidgetId, event: &Event) -> Response { + impl Events for Self { + type Data = (); + + fn steal_event(&mut self, cx: &mut EventCx, _: &(), _: &Id, event: &Event) -> IsUsed { match event { - Event::Command(Command::Enter) => { - if let Some((col, row)) = mgr.nav_focus().and_then(|id| { - self.cells.data().reconstruct_key(self.cells.inner().id_ref(), id) + Event::Command(Command::Enter, _) => { + if let Some(Key(col, row)) = cx.nav_focus().and_then(|id| { + Key::reconstruct_key(self.cells.inner().id_ref(), id) }) { - let row = if mgr.modifiers().shift() { + let row = if cx.modifiers().shift_key() { (row - 1).max(1) } else { (row + 1).min(MAX_ROW) }; - let id = self.cells.data().make_id(self.cells.inner().id_ref(), &(col, row)); - mgr.next_nav_focus_from(&mut self.cells, id, true); + let id = Key(col, row).make_id(self.cells.inner().id_ref()); + cx.next_nav_focus(Some(id), false, FocusSource::Synthetic); } - Response::Used + IsUsed::Used }, - _ => Response::Unused + _ => IsUsed::Unused } } - } - impl Window for Self { - fn title(&self) -> &str { - "Cells" + + fn handle_messages(&mut self, cx: &mut EventCx, _: &()) { + if let Some(UpdateInput(key, input)) = cx.try_pop() { + self.data.cells.entry(key).or_default().update(input); + self.data.update_values(); + } } } - }) + }; + Window::new(ui, "Cells") } diff --git a/src/counter.rs b/src/counter.rs index 40b75e5..a35cd9f 100644 --- a/src/counter.rs +++ b/src/counter.rs @@ -5,39 +5,20 @@ //! Counter -use kas::event::EventMgr; use kas::prelude::*; -use kas::singleton; -use kas::widgets::{EditBox, TextButton}; +use kas::widgets::{Adapt, Button, EditBox}; -pub fn window() -> Box { - Box::new(singleton! { - #[derive(Debug)] - #[widget { - layout = row: [ - align(right): self.display, - TextButton::new_msg("Count", ()), - ]; - }] - struct { - core: widget_core!(), - #[widget] display: impl Widget + HasString = EditBox::new("0".to_string()) - .with_width_em(3.0, 3.0) - .with_editable(false), - counter: usize = 0, - } - impl Widget for Self { - fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(()) = mgr.try_pop_msg() { - self.counter = self.counter.saturating_add(1); - *mgr |= self.display.set_string(self.counter.to_string()); - } - } - } - impl Window for Self { - fn title(&self) -> &str { - "Counter" - } - } - }) +#[derive(Clone, Debug)] +struct Incr; + +pub fn window() -> Window<()> { + let ui = kas::row![ + align!( + right, + EditBox::string(|count| format!("{count}")).with_width_em(3.0, 3.0) + ), + Button::label_msg("Count", Incr).map_any(), + ]; + let ui = Adapt::new(ui, 0).on_message(|_, count, Incr| *count += 1); + Window::new(ui, "Counter") } diff --git a/src/crud.rs b/src/crud.rs index d67f8ff..8979e9d 100644 --- a/src/crud.rs +++ b/src/crud.rs @@ -5,14 +5,13 @@ //! Create Read Update Delete -use kas::dir::Down; -use kas::model::filter::{ContainsCaseInsensitive, FilteredList}; -use kas::model::{ListData, SharedData, SharedDataMut}; use kas::prelude::*; -use kas::view::{driver, ListView, SelectionMode, SelectionMsg}; +use kas::view::filter::{ + ContainsCaseInsensitive, Filter, FilterList, KeystrokeGuard, SetFilter, UnsafeFilteredList, +}; +use kas::view::{Driver, ListView, SelectionMode, SelectionMsg}; use kas::widgets::edit::{EditBox, EditField, EditGuard}; -use kas::widgets::{Frame, ScrollBars, TextButton}; -use std::{cell::RefCell, iter, ops, rc::Rc}; +use kas::widgets::{AccessLabel, Button, Frame, NavFrame, ScrollBars, Text}; #[derive(Clone, Debug)] pub struct Entry { @@ -26,98 +25,17 @@ impl Entry { last: last.to_string(), } } - pub fn format(&self) -> String { - format!("{}, {}", self.last, self.first) + pub fn format(_: &ConfigCx, entry: &Entry) -> String { + format!("{}, {}", entry.last, entry.first) } } - -#[derive(Debug)] -struct EntriesInner { - ver: u64, - vec: Vec, -} - -#[derive(Debug)] -pub struct Entries { - inner: RefCell, -} - -// Implement a simple (lazy) CRUD interface -impl Entries { - pub fn new(vec: Vec) -> Self { - let ver = 0; - let inner = RefCell::new(EntriesInner { ver, vec }); - Entries { inner } - } - pub fn create(&self, entry: Entry) -> usize { - let mut inner = self.inner.borrow_mut(); - let index = inner.vec.len(); - inner.ver += 1; - inner.vec.push(entry); - index - } - pub fn read(&self, index: usize) -> Entry { - self.inner.borrow().vec[index].clone() - } - pub fn update_entry(&self, index: usize, entry: Entry) { - let mut inner = self.inner.borrow_mut(); - inner.ver += 1; - inner.vec[index] = entry; - } - pub fn delete(&self, index: usize) { - let mut inner = self.inner.borrow_mut(); - inner.ver += 1; - inner.vec.remove(index); - } -} - -pub type Data = Rc; - -impl SharedData for Entries { - type Key = usize; - type Item = String; - type ItemRef<'b> = Self::Item; - - fn version(&self) -> u64 { - self.inner.borrow().ver - } - - fn contains_key(&self, key: &Self::Key) -> bool { - *key < self.len() - } - - fn borrow(&self, key: &Self::Key) -> Option { - self.inner.borrow().vec.get(*key).map(|e| e.format()) - } -} -impl ListData for Entries { - type KeyIter<'b> = iter::Take>>; - - fn len(&self) -> usize { - self.inner.borrow().vec.len() - } - - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - parent.make_child(*key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - child.next_key_after(parent) - } - - fn iter_from(&self, start: usize, limit: usize) -> Self::KeyIter<'_> { - (0..self.inner.borrow().vec.len()).skip(start).take(limit) +impl Filter for ContainsCaseInsensitive { + fn matches(&self, item: &Entry) -> bool { + Filter::<&str>::matches(self, &item.first.as_str()) + || Filter::<&str>::matches(self, &item.last.as_str()) } } -pub fn make_data() -> Data { - let entries = vec![ - Entry::new("Emil", "Hans"), - Entry::new("Mustermann", "Max"), - Entry::new("Tisch", "Roman"), - ]; - Rc::new(Entries::new(entries)) -} - #[derive(Clone, Debug)] enum Control { Create, @@ -126,28 +44,41 @@ enum Control { } #[derive(Clone, Debug)] -struct NameGuard; +struct NameGuard { + is_last: bool, +} impl EditGuard for NameGuard { - fn update(edit: &mut EditField) { - edit.set_error_state(edit.get_str().is_empty()); + type Data = Option; + + fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &Self::Data) { + let mut act = Action::empty(); + if let Some(entry) = data.as_ref() { + let name = match edit.guard.is_last { + false => &entry.first, + true => &entry.last, + }; + act = edit.set_str(name); + } + act |= edit.set_error_state(edit.get_str().is_empty()); + cx.action(edit, act); } } impl_scope! { - #[derive(Debug)] #[impl_default] #[widget { - layout = grid: { - 0, 0: "First name:"; - 1, 0: self.firstname; - 0, 1: "Surname:"; - 1, 1: self.surname; + Data = Option; + layout = grid! { + (0, 0) => "First name:", + (1, 0) => self.firstname, + (0, 1) => "Surname:", + (1, 1) => self.surname, }; }] struct Editor { core: widget_core!(), - #[widget] firstname: EditBox = EditBox::new("".to_string()).with_guard(NameGuard), - #[widget] surname: EditBox = EditBox::new("".to_string()).with_guard(NameGuard), + #[widget] firstname: EditBox = EditBox::new(NameGuard { is_last: false }), + #[widget] surname: EditBox = EditBox::new(NameGuard { is_last: true }), } impl Self { fn make_item(&self) -> Option { @@ -157,120 +88,123 @@ impl_scope! { } Some(Entry::new(last, self.firstname.get_string())) } - fn set_item(&mut self, item: Entry) -> TkAction { - self.firstname.set_string(item.first) | self.surname.set_string(item.last) - } } } impl_scope! { - #[derive(Debug)] #[impl_default] #[widget { - layout = row: [ - TextButton::new_msg("Create", Control::Create), + layout = row! [ + Button::label_msg("Create", Control::Create).map_any(), self.update, self.delete, ]; }] struct Controls { core: widget_core!(), - #[widget] update: TextButton = TextButton::new_msg("Update", Control::Update), - #[widget] delete: TextButton = TextButton::new_msg("Delete", Control::Delete), - } - impl Self { - fn disable_update_delete(&mut self, mgr: &mut EventMgr, disable: bool) { - mgr.set_disabled(self.update.id(), disable); - mgr.set_disabled(self.delete.id(), disable); - } + #[widget(&())] update: Button = Button::label_msg("Update", Control::Update), + #[widget(&())] delete: Button = Button::label_msg("Delete", Control::Delete), } - impl Widget for Self { - fn configure(&mut self, mgr: &mut ConfigMgr) { - mgr.set_disabled(self.update.id(), true); - mgr.set_disabled(self.delete.id(), true); + impl Events for Self { + type Data = bool; + + fn update(&mut self, cx: &mut ConfigCx, any_selected: &bool) { + if self.update.id_ref().is_valid() { + let disable = !any_selected; + cx.set_disabled(self.update.id(), disable); + cx.set_disabled(self.delete.id(), disable); + } } } } -pub fn window() -> Box { - let data = make_data(); - let filter = ContainsCaseInsensitive::new(""); - - type MyFilteredList = FilteredList; - type FilterList = ListView; - let list_view = FilterList::new(MyFilteredList::new(data.clone(), filter.clone())) - .with_selection_mode(SelectionMode::Single); +pub fn window() -> Window<()> { + struct ListGuard; + type FilteredList = UnsafeFilteredList>; + impl Driver for ListGuard { + type Widget = NavFrame>; + fn make(&mut self, _: &usize) -> Self::Widget { + NavFrame::new(Text::new(Entry::format)) + } + } + let filter = ContainsCaseInsensitive::new(); + let guard = KeystrokeGuard; + type MyListView = ListView>, ListGuard, kas::dir::Down>; + type MyFilterList = FilterList, ContainsCaseInsensitive, MyListView>; + let list_view = MyListView::new(ListGuard).with_selection_mode(SelectionMode::Single); - Box::new(singleton! { - #[derive(Debug)] + let ui = impl_anon! { #[widget { - layout = grid: { - 0, 0: "Filter:"; - 1, 0: self.filter; - 0..2, 1..3: self.list; - 3, 1: self.editor; - 0..4, 3: self.controls; + layout = grid! { + (0, 0) => "Filter:", + (1, 0) => self.filter, + (0..2, 1..3) => self.list, + (3, 1) => self.editor, + (0..4, 3) => self.controls, }; }] struct { core: widget_core!(), - #[widget] filter = EditBox::new("") - .on_edit(move |mgr, s| filter.set(mgr, &(), s.to_string())), - #[widget] list: Frame> = - Frame::new(ScrollBars::new(list_view)), - #[widget] editor: Editor = Editor::default(), - #[widget] controls: Controls = Controls::default(), - data: Data = data, + #[widget(&())] filter: EditBox = EditBox::new(guard), + #[widget(&self.entries)] list: Frame> = + Frame::new(ScrollBars::new(FilterList::new(list_view, filter))), + #[widget(&self.selected)] editor: Editor = Editor::default(), + #[widget(&self.selected.is_some())] controls: Controls = Controls::default(), + entries: Vec = vec![ + Entry::new("Emil", "Hans"), + Entry::new("Mustermann", "Max"), + Entry::new("Tisch", "Roman"), + ], + selected: Option, } impl Self { fn selected(&self) -> Option { self.list.selected_iter().next().cloned() } } - impl Widget for Self { - fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(SelectionMsg::Select(key)) = mgr.try_pop_msg() { - let item = self.data.read(key); - *mgr |= self.editor.set_item(item); - self.controls.disable_update_delete(mgr, false); - } else if let Some(control) = mgr.try_pop_msg() { + impl Events for Self { + type Data = (); + + fn handle_messages(&mut self, cx: &mut EventCx, _: &()) { + if let Some(SetFilter(value)) = cx.try_pop() { + self.list.set_filter(&mut cx.config_cx(), &self.entries, value); + } else if let Some(SelectionMsg::Select(key)) = cx.try_pop() { + self.selected = self.entries.get::(key).cloned(); + cx.update(self.as_node(&())); + } else if let Some(control) = cx.try_pop() { match control { Control::Create => { if let Some(item) = self.editor.make_item() { - let index = self.data.create(item); - mgr.update_all(0); - let _ = self.list.select(index); - self.controls.disable_update_delete(mgr, false); + let index = self.entries.len(); + self.entries.push(item); + let action = self.list.select(index); + cx.action(&self, action); + self.selected = self.entries.get(index).cloned(); + cx.update(self.as_node(&())); } } Control::Update => { if let Some(index) = self.selected() { if let Some(item) = self.editor.make_item() { - self.data.update_entry(index, item); - mgr.update_all(0); + self.entries[index] = item; + cx.update(self.as_node(&())); } } } Control::Delete => { if let Some(index) = self.selected() { - self.data.delete(index); - mgr.update_all(0); - let any_selected = self.list.select(index).is_ok(); - if any_selected { - let item = self.data.read(index); - *mgr |= self.editor.set_item(item); - } - self.controls.disable_update_delete(mgr, !any_selected); + self.entries.remove(index); + let action = self.list.select(index); + cx.action(&self, action); + self.selected = self.entries.get(index).cloned(); + cx.update(self.as_node(&())); } } } } } } - impl Window for Self { - fn title(&self) -> &str { - "Create, Read, Update, Delete" - } - } - }) + }; + + Window::new(ui, "Create, Read, Update, Delete") } diff --git a/src/flight_booker.rs b/src/flight_booker.rs index ac3d75c..f5f17fc 100644 --- a/src/flight_booker.rs +++ b/src/flight_booker.rs @@ -5,129 +5,184 @@ //! Flight booker -use chrono::{Duration, Local, NaiveDate}; +use chrono::{Duration, Local, NaiveDate, ParseError}; use kas::prelude::*; use kas::widgets::dialog::MessageBox; -use kas::widgets::menu::MenuEntry; -use kas::widgets::{ComboBox, EditBox, EditField, EditGuard, TextButton}; +use kas::widgets::{label_any, Adapt, Button, ComboBox, EditBox, EditField, EditGuard, Text}; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum Flight { + #[default] OneWay, Return, } + +#[derive(Debug)] +enum Error { + None, + OutParse(ParseError), + RetParse(ParseError), + OutBeforeToday, + ReturnTooSoon, +} +impl Error { + fn is_none(&self) -> bool { + matches!(self, Error::None) + } +} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::None => Ok(()), + Error::OutParse(err) => f.write_fmt(format_args!("Error: outbound date: {err}")), + Error::RetParse(err) => f.write_fmt(format_args!("Error: return date: {err}")), + Error::OutBeforeToday => f.write_str("Error: outbound date is before today!"), + Error::ReturnTooSoon => f.write_str("Error: return date must be after outbound date!"), + } + } +} + +#[derive(Debug)] +struct Data { + out: Result, + ret: Result, + flight: Flight, + error: Error, +} +impl Data { + fn update_error(&mut self) { + self.error = match self.out { + Ok(out_date) => { + if out_date < Local::now().naive_local().date() { + Error::OutBeforeToday + } else { + match (self.flight, self.ret) { + (Flight::OneWay, _) => Error::None, + (Flight::Return, Ok(ret_date)) => { + if ret_date < out_date { + Error::ReturnTooSoon + } else { + Error::None + } + } + (Flight::Return, Err(err)) => Error::RetParse(err), + } + } + } + Err(err) => Error::OutParse(err), + }; + } +} + #[derive(Clone, Debug)] -struct ActionDates; +struct ActionDate { + result: Result, + is_return_field: bool, +} + #[derive(Clone, Debug)] struct ActionBook; -// TODO: consider adding a view-and-edit widget (like SingleView but supporting -// text editing) so that string representation is just a view of date repr. #[derive(Clone, Debug)] struct Guard { - date: Option, + is_return_field: bool, } impl Guard { - fn new(date: NaiveDate) -> Self { - Guard { date: Some(date) } + fn new(is_return_field: bool) -> Self { + Guard { is_return_field } } } impl EditGuard for Guard { - fn edit(edit: &mut EditField, mgr: &mut EventMgr) { - let date = NaiveDate::parse_from_str(edit.get_str().trim(), "%Y-%m-%d"); - edit.guard.date = match date { - Ok(date) => Some(date), - Err(e) => { - // TODO: display error in GUI - println!("Error parsing date: {e}"); - None - } - }; - edit.set_error_state(edit.guard.date.is_none()); + type Data = Data; - // On any change, we notify the parent that it should update the book button: - mgr.push_msg(ActionDates); + fn edit(edit: &mut EditField, cx: &mut EventCx, _: &Self::Data) { + let result = NaiveDate::parse_from_str(edit.get_str().trim(), "%Y-%m-%d"); + let act = edit.set_error_state(result.is_err()); + cx.action(edit.id(), act); + + cx.push(ActionDate { + result, + is_return_field: edit.guard.is_return_field, + }); } -} -pub fn window() -> Box { - // Default dates: - let out = Local::today().naive_local(); - let back = out + Duration::days(7); - - let d1 = EditBox::new(out.format("%Y-%m-%d").to_string()).with_guard(Guard::new(out)); - let d2 = EditBox::new(back.format("%Y-%m-%d").to_string()).with_guard(Guard::new(back)); - - Box::new(singleton! { - #[derive(Debug)] - #[widget { - layout = column: [ - self.combo, - self.d1, - self.d2, - self.book, - ]; - }] - struct { - core: widget_core!(), - #[widget] combo: ComboBox = ComboBox::new_vec(vec![ - MenuEntry::new("One-way flight", Flight::OneWay), - MenuEntry::new("Return flight", Flight::Return), - ]), - #[widget] d1: EditBox = d1, - #[widget] d2: EditBox = d2, - #[widget] book = TextButton::new_msg("Book", ActionBook), - } - impl Self { - fn update_dates(&mut self, mgr: &mut EventMgr) { - let is_ready = match self.d1.guard.date.as_ref() { - None => false, - Some(_) if mgr.is_disabled(self.d2.id_ref()) => true, - Some(d1) => { - match self.d2.guard.date.as_ref() { - None => false, - Some(d2) if d1 < d2 => true, - _ => { - // TODO: display error in GUI - println!("Out-bound flight must be before return flight!"); - false - } - } - } - }; - mgr.set_disabled(self.book.id(), !is_ready); + fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &Self::Data) { + if !edit.has_edit_focus() && edit.get_str().is_empty() { + if let Ok(date) = match edit.guard.is_return_field { + false => data.out, + true => data.ret, + } { + let act = edit.set_string(date.format("%Y-%m-%d").to_string()); + cx.action(edit.id(), act); } } - impl Widget for Self { - fn configure(&mut self, mgr: &mut ConfigMgr) { - mgr.set_disabled(self.d2.id(), true); + if edit.guard.is_return_field { + cx.set_disabled(edit.id(), data.flight == Flight::OneWay); + } + } +} + +pub fn window() -> Window<()> { + let out_date = Local::now().naive_local().date(); + let data = Data { + out: Ok(out_date), + ret: Ok(out_date + Duration::days(7)), + flight: Flight::OneWay, + error: Error::None, + }; + + let ui = kas::column![ + ComboBox::new( + [ + ("One-way flight", Flight::OneWay), + ("Return flight", Flight::Return) + ], + |_, data: &Data| data.flight + ), + EditBox::new(Guard::new(false)), + EditBox::new(Guard::new(true)), + Text::new(|_, data: &Data| format!("{}", data.error)), + Button::new_msg(label_any("Book"), ActionBook).on_update( + |cx, button, data: &Data| cx.set_disabled(button.id(), !data.error.is_none()) + ), + ]; + + let ui = Adapt::new(ui, data) + .on_message(|_, data, flight| { + data.flight = flight; + data.update_error(); + }) + .on_message(|_, data, parse: ActionDate| { + if parse.is_return_field { + data.ret = parse.result; + } else { + data.out = parse.result; } - fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(flight) = mgr.try_pop_msg::() { - mgr.set_disabled(self.d2.id(), flight == Flight::OneWay); - self.update_dates(mgr); - } else if let Some(ActionDates) = mgr.try_pop_msg() { - self.update_dates(mgr); - } else if let Some(ActionBook) = mgr.try_pop_msg() { - let d1 = self.d1.guard.date.unwrap(); - let msg = if mgr.is_disabled(self.d2.id_ref()) { - format!("You have booked a one-way flight on {}", d1.format("%Y-%m-%d")) - } else { - let d2 = self.d2.guard.date.unwrap(); - format!( + + data.update_error(); + }) + .on_messages(|cx, _, data| { + if cx.try_pop::().is_some() { + let msg = if !data.error.is_none() { + // should be impossible since the button is disabled + format!("{}", data.error) + } else { + match data.flight { + Flight::OneWay => format!( + "You have booked a one-way flight on {}", + data.out.unwrap().format("%Y-%m-%d") + ), + Flight::Return => format!( "You have booked an out-bound flight on {} and a return flight on {}", - d1.format("%Y-%m-%d"), - d2.format("%Y-%m-%d"), - ) - }; - mgr.add_window(Box::new(MessageBox::new("Booker result", msg))); - } - } - } - impl Window for Self { - fn title(&self) -> &str { - "Flight Booker" + data.out.unwrap().format("%Y-%m-%d"), + data.ret.unwrap().format("%Y-%m-%d"), + ), + } + }; + cx.add_window::<()>(MessageBox::new(msg).into_window("Booker result")); } - } - }) + false + }); + + Window::new(ui, "Flight Booker") } diff --git a/src/main.rs b/src/main.rs index 0af6e15..b96eb1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ mod timer; use kas::prelude::*; use kas::widgets::dialog::MessageBox; -use kas::widgets::TextButton; +use kas::widgets::Button; #[derive(Clone, Debug)] enum X { @@ -30,46 +30,43 @@ enum X { fn main() -> Result<(), kas::shell::Error> { env_logger::init(); - let window = singleton! { - #[derive(Debug)] + let ui = impl_anon! { #[widget { - layout = column: [ - TextButton::new_msg("&Counter", X::Counter), - TextButton::new_msg("Tem&perature Converter", X::Temp), - TextButton::new_msg("&Flight &Booker", X::Flight), - TextButton::new_msg("&Timer", X::Timer), - TextButton::new_msg("CRUD (Create, Read, &Update and &Delete)", X::Crud), - TextButton::new_msg("Ci&rcle Drawer", X::Circle), - TextButton::new_msg("Ce&lls", X::Cells), + layout = column! [ + Button::label_msg("&Counter", X::Counter), + Button::label_msg("Tem&perature Converter", X::Temp), + Button::label_msg("&Flight &Booker", X::Flight), + Button::label_msg("&Timer", X::Timer), + Button::label_msg("CRUD (Create, Read, &Update and &Delete)", X::Crud), + Button::label_msg("Ci&rcle Drawer", X::Circle), + Button::label_msg("Ce&lls", X::Cells), ]; }] struct { core: widget_core!(), } - impl Widget for Self { - fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(x) = mgr.try_pop_msg() { - mgr.add_window(match x { + impl Events for Self { + type Data = (); + + fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) { + if let Some(x) = cx.try_pop() { + cx.add_window(match x { X::Counter => counter::window(), X::Temp => temp_conv::window(), X::Flight => flight_booker::window(), X::Timer => timer::window(), X::Crud => crud::window(), X::Cells => cells::window(), - _ => Box::new(MessageBox::new("TODO", "Not implemented yet!")), + _ => MessageBox::new("Not implemented yet!").into_window("TODO"), }); } } } - impl Window for Self { - fn title(&self) -> &str { - "7GUIs Launcher" - } - } }; + let window = Window::new(ui, "7GUIs Launcher"); let theme = kas::theme::FlatTheme::new(); - let mut toolkit = kas::shell::Toolkit::new(theme)?; - toolkit.add(window)?; - toolkit.run() + let mut shell = kas::shell::Default::with_theme(theme).build(())?; + shell.add(window); + shell.run() } diff --git a/src/temp_conv.rs b/src/temp_conv.rs index 7d73ed8..9b0fccd 100644 --- a/src/temp_conv.rs +++ b/src/temp_conv.rs @@ -4,12 +4,9 @@ // https://www.apache.org/licenses/LICENSE-2.0 //! Temperature converter -//! -//! TODO: force single-line labels -use kas::event::EventMgr; use kas::prelude::*; -use kas::widgets::EditBox; +use kas::widgets::{Adapt, EditBox}; #[derive(Clone, Debug)] enum Message { @@ -17,54 +14,40 @@ enum Message { FromFahrenheit(f64), } -pub fn window() -> Box { - Box::new(singleton! { - #[derive(Debug)] - #[widget { - layout = row: [ - self.celsius, - "Celsius =", - self.fahrenheit, - "Fahrenheit", - ]; - }] - struct { - core: widget_core!(), - #[widget] celsius: impl Widget + HasString = EditBox::new("0") - .with_width_em(4.0, 4.0) - .on_edit(|mgr, text| { - if let Ok(c) = text.parse::() { - mgr.push_msg(Message::FromCelsius(c)); - } - }), - #[widget] fahrenheit: impl Widget + HasString = EditBox::new("32") - .with_width_em(4.0, 4.0) - .on_edit(|mgr, text| { - if let Ok(f) = text.parse::() { - mgr.push_msg(Message::FromFahrenheit(f)); - } - }), - } - impl Widget for Self { - fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(msg) = mgr.try_pop_msg() { - match msg { - Message::FromCelsius(c) => { - let f = c * (9.0/5.0) + 32.0; - *mgr |= self.fahrenheit.set_string(f.to_string()); - } - Message::FromFahrenheit(f) => { - let c = (f - 32.0) * (5.0 / 9.0); - *mgr |= self.celsius.set_string(c.to_string()); - } - } +impl_scope! { + #[impl_default] + #[derive(Debug)] + struct Temperature { + celsius: f64 = 0.0, + fahrenheit: f64 = 32.0, + } + + impl Self { + fn handle(&mut self, msg: Message) { + match msg { + Message::FromCelsius(c) => { + self.celsius = c; + self.fahrenheit = c * (9.0/5.0) + 32.0; + } + Message::FromFahrenheit(f) => { + self.celsius = (f - 32.0) * (5.0 / 9.0); + self.fahrenheit = f; } } } - impl Window for Self { - fn title(&self) -> &str { - "Temperature Converter" - } - } - }) + } +} + +pub fn window() -> Window<()> { + let ui = kas::row![ + EditBox::parser(|temp: &Temperature| temp.celsius, Message::FromCelsius), + "Celsius =", + EditBox::parser( + |temp: &Temperature| temp.fahrenheit, + Message::FromFahrenheit + ), + "Fahrenheit", + ]; + let ui = Adapt::new(ui, Temperature::default()).on_message(|_, temp, msg| temp.handle(msg)); + Window::new(ui, "Temperature Converter") } diff --git a/src/timer.rs b/src/timer.rs index 0037125..8edbf84 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -5,107 +5,85 @@ //! Timer -use kas::dir::Right; use kas::prelude::*; -use kas::widgets::{Label, ProgressBar, Slider, TextButton}; +use kas::widgets::{label_any, Adapt, Button, ProgressBar, Slider, Text}; use std::time::{Duration, Instant}; const DUR_MIN: Duration = Duration::from_secs(0); const DUR_MAX: Duration = Duration::from_secs(30); const DUR_STEP: Duration = Duration::from_millis(100); const TIMER_ID: u64 = 0; +const TIMER_SLEEP: Duration = DUR_STEP; #[derive(Clone, Debug)] struct ActionReset; -pub fn window() -> Box { - Box::new(singleton! { - #[derive(Debug)] - #[widget { - layout = grid: { - 0, 0: "Elapsed time:"; - 1, 0: self.progress; - 1, 1: self.elapsed; - 0, 2: "Duration:"; - 1, 2: self.slider; - 0..2, 3: TextButton::new_msg("Reset", ActionReset); - }; - }] - struct { - core: widget_core!(), - #[widget] progress: ProgressBar = ProgressBar::new(), - #[widget] elapsed: Label = Label::new("0.0s".to_string()), - #[widget] slider = - Slider::new_with_direction(DUR_MIN..=DUR_MAX, DUR_STEP, Right) - .with_value(Duration::from_secs(10)) - .on_move(|mgr, value| mgr.push_msg(value)), - dur: Duration = Duration::from_secs(10), - saved: Duration = Duration::default(), - start: Option = None, - } - impl Self { - fn update(&mut self, mgr: &mut EventMgr, elapsed: Duration) { - let frac = elapsed.as_secs_f32() / self.dur.as_secs_f32(); - *mgr |= self.progress.set_value(frac); - *mgr |= self.elapsed.set_string(format!( - "{}.{}s", - elapsed.as_secs(), - elapsed.subsec_millis() / 100 - )); - } - } - impl Widget for Self { - fn configure(&mut self, mgr: &mut ConfigMgr) { - self.start = Some(Instant::now()); - mgr.request_update(self.id(), TIMER_ID, DUR_STEP, true); - } - fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { - match event { - Event::TimerUpdate(TIMER_ID) => { - if let Some(start) = self.start { - let mut elapsed = self.saved + (Instant::now() - start); - if elapsed < self.dur { - mgr.request_update(self.id(), TIMER_ID, DUR_STEP, true); - } else { - elapsed = self.dur; - self.saved = elapsed; - self.start = None; - } - self.update(mgr, elapsed); - } - Response::Used - } - _ => Response::Unused, +pub fn window() -> Window<()> { + #[derive(Debug)] + struct Data { + duration: Duration, + elapsed: Duration, + start: Option, + } + + let ui = kas::grid! { + (0, 0) => "Elapsed time:", + (1, 0) => ProgressBar::right(|_, data: &Data| data.elapsed.as_secs_f32() / data.duration.as_secs_f32()), + (1, 1) => Text::new(|_, data: &Data| { + format!("{}.{}s", data.elapsed.as_secs(), data.elapsed.subsec_millis() / 100) + }), + (0, 2) => "Duration:", + (1, 2) => Slider::right(DUR_MIN..=DUR_MAX, |_, data: &Data| data.duration) + .with_step(DUR_STEP) + .with_msg(|value| value), + (0..2, 3) => Button::new_msg(label_any("Reset"), ActionReset), + }; + + let data = Data { + duration: Duration::from_secs(10), + elapsed: Duration::default(), + start: None, + }; + + let ui = Adapt::new(ui, data) + .on_configure(|cx, data| { + data.start = Some(Instant::now()); + cx.request_timer(TIMER_ID, TIMER_SLEEP); + }) + .on_timer(TIMER_ID, |cx, _, data| { + if let Some(start) = data.start { + data.elapsed = data.duration.min(Instant::now() - start); + if data.elapsed < data.duration { + cx.request_timer(TIMER_ID, TIMER_SLEEP); + } else { + data.start = None; } + true + } else { + false } - fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(dur) = mgr.try_pop_msg() { - self.dur = dur; - let mut elapsed = self.saved; - if let Some(start) = self.start { - elapsed += Instant::now() - start; - if elapsed >= self.dur { - self.saved = elapsed; - self.start = None; - } - } else if self.saved < self.dur { - self.start = Some(Instant::now()); - mgr.request_update(self.id(), TIMER_ID, Duration::ZERO, true); + }) + .on_messages(|cx, _, data| { + if let Some(dur) = cx.try_pop() { + data.duration = dur; + if let Some(start) = data.start { + data.elapsed = data.duration.min(Instant::now() - start); + if data.elapsed >= data.duration { + data.start = None; } - self.update(mgr, elapsed); - } else if let Some(ActionReset) = mgr.try_pop_msg() { - self.saved = Duration::default(); - self.start = Some(Instant::now()); - mgr.request_update(self.id(), TIMER_ID, DUR_STEP, true); - *mgr |= self.progress.set_value(0.0); - *mgr |= self.elapsed.set_string("0.0s".to_string()); + } else if data.elapsed < data.duration { + data.start = Some(Instant::now() - data.elapsed); + cx.request_timer(TIMER_ID, Duration::ZERO); } + true + } else if let Some(ActionReset) = cx.try_pop() { + data.start = Some(Instant::now()); + cx.request_timer(TIMER_ID, TIMER_SLEEP); + true + } else { + false } - } - impl Window for Self { - fn title(&self) -> &str { - "Timer" - } - } - }) + }); + + Window::new(ui, "Timer") }