Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ resolver = "2"
publish = false

[dependencies]
kas = { version = "0.14.2" }
kas = "0.15.0"
chrono = "0.4"
env_logger = "0.8"
env_logger = "0.11.8"
pest = "2.1"
pest_derive = "2.1"

[patch.crates-io.kas]
git = "https://github.com/kas-gui/kas.git"
rev = "4bf71367f6a57c727a689097302de40274f5830a"

[patch.crates-io.kas-text]
git = "https://github.com/kas-gui/kas-text.git"
rev = "0bc2629df690ecfd562a7bf7b6ac096ad0d8f1a9"
175 changes: 74 additions & 101 deletions src/cells.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,28 @@

//! Cells: a mini spreadsheet

use kas::event::{Command, FocusSource};
use kas::prelude::*;
use kas::view::{DataKey, Driver, MatrixData, MatrixView, SharedData};
use kas::view::{
DataChanges, DataClerk, DataKey, DataLen, Driver, GridIndex, GridView, TokenChanges,
};
use kas::widgets::{EditBox, EditField, EditGuard, ScrollBars};
use kas::{prelude::*, TextOrSource};
use std::collections::HashMap;
use std::{fmt, iter, ops};
use std::fmt;

#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Hash)]
pub struct ColKey(u8);
type ColKeyIter = iter::Map<ops::RangeInclusive<u8>, fn(u8) -> ColKey>;
impl ColKey {
const LEN: u8 = 26;
fn try_from_u8(n: u8) -> Option<Self> {
if (b'A'..=b'Z').contains(&n) {
Some(ColKey(n))
Some(ColKey(n - b'A'))
} else {
None
}
}
fn from_u8(n: u8) -> Self {
Self::try_from_u8(n).expect("bad column key")
}
fn iter_keys() -> ColKeyIter {
(b'A'..=b'Z').map(ColKey::from_u8)
}
}

impl fmt::Display for ColKey {
Expand All @@ -39,7 +36,7 @@ impl fmt::Display for ColKey {
}
}

const MAX_ROW: u8 = 99;
const ROW_LEN: u32 = 100;

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Key(ColKey, u8);
Expand Down Expand Up @@ -265,15 +262,6 @@ impl Cell {
self.input = input;
}

/// Get display string
fn display(&self) -> String {
if !self.display.is_empty() {
self.display.clone()
} else {
self.input.clone()
}
}

fn try_eval(&mut self, values: &HashMap<Key, f64>) -> Result<Option<f64>, EvalError> {
if self.parse_error {
// Display the error locally; propegate NaN
Expand Down Expand Up @@ -345,60 +333,50 @@ impl CellData {
}
}

#[derive(Clone, Debug, Default)]
struct Item {
input: String,
display: String,
error: bool,
struct Clerk {
empty_cell: Cell,
}

impl SharedData for CellData {
impl DataClerk<GridIndex> for Clerk {
type Data = CellData;
type Key = Key;
type Item = Item;
type ItemRef<'b> = Self::Item;
type Item = Cell;
type Token = Key;

fn contains_key(&self, _: &Self::Key) -> bool {
// we know both sub-keys are valid and that the length is fixed
true
fn update(&mut self, _: &mut ConfigCx<'_>, _: Id, _: &Self::Data) -> DataChanges {
DataChanges::Any
}

fn borrow(&self, key: &Self::Key) -> Option<Self::Item> {
self.cells
.get(key)
.map(|cell| Item {
input: cell.input.clone(),
display: cell.display(),
error: cell.parse_error,
})
.or_else(|| Some(Item::default()))
}
}

impl MatrixData for CellData {
type ColKey = ColKey;
type RowKey = u8;
type ColKeyIter<'b> = iter::Take<iter::Skip<ColKeyIter>>;
type RowKeyIter<'b> = iter::Take<iter::Skip<ops::RangeInclusive<u8>>>;

fn is_empty(&self) -> bool {
false
}
fn len(&self) -> (usize, usize) {
(ColKey::LEN.cast(), 99)
fn len(&self, _: &CellData, _: GridIndex) -> DataLen<GridIndex> {
DataLen::Known(GridIndex {
col: ColKey::LEN.cast(),
row: ROW_LEN,
})
}

fn col_iter_from(&self, start: usize, limit: usize) -> Self::ColKeyIter<'_> {
ColKey::iter_keys().skip(start).take(limit)
}
fn update_token(
&self,
_: &CellData,
index: GridIndex,
_: bool,
token: &mut Option<Key>,
) -> TokenChanges {
if index.col >= ColKey::LEN as u32 || index.row >= ROW_LEN {
*token = None;
return TokenChanges::Any;
}

fn row_iter_from(&self, start: usize, limit: usize) -> Self::RowKeyIter<'_> {
// NOTE: for strict compliance with the 7GUIs challenge the rows should
// start from 0, but any other spreadsheet I've seen starts from 1!
(1..=MAX_ROW).skip(start).take(limit)
let key = Key(ColKey(index.col as u8), index.row as u8);
if *token == Some(key) {
TokenChanges::None
} else {
*token = Some(key);
TokenChanges::Any
}
}

fn make_key(&self, col: &Self::ColKey, row: &Self::RowKey) -> Self::Key {
Key(*col, *row)
fn item<'r>(&'r self, data: &'r CellData, key: &'r Key) -> &'r Cell {
data.cells.get(key).unwrap_or(&self.empty_cell)
}
}

Expand All @@ -411,39 +389,42 @@ struct CellGuard {
is_input: bool,
}
impl EditGuard for CellGuard {
type Data = Item;
type Data = Cell;

fn update(edit: &mut EditField<Self>, cx: &mut ConfigCx, item: &Item) {
let mut action = edit.set_error_state(item.error);
fn update(edit: &mut EditField<Self>, cx: &mut ConfigCx, item: &Cell) {
edit.set_error_state(cx, item.parse_error);
if !edit.has_edit_focus() {
action |= edit.set_str(&item.display);
let text = if !item.display.is_empty() {
&item.display
} else {
&item.input
};
edit.set_str(cx, text);
edit.guard.is_input = false;
}
cx.action(edit, action);
}

fn activate(edit: &mut EditField<Self>, cx: &mut EventCx, item: &Item) -> IsUsed {
fn activate(edit: &mut EditField<Self>, cx: &mut EventCx, item: &Cell) -> IsUsed {
Self::focus_lost(edit, cx, item);
IsUsed::Used
}

fn focus_gained(edit: &mut EditField<Self>, cx: &mut EventCx, item: &Item) {
cx.action(edit.id(), edit.set_str(&item.input));
fn focus_gained(edit: &mut EditField<Self>, cx: &mut EventCx, item: &Cell) {
edit.set_str(cx, &item.input);
edit.guard.is_input = true;
}

fn focus_lost(edit: &mut EditField<Self>, 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));
fn focus_lost(edit: &mut EditField<Self>, cx: &mut EventCx, item: &Cell) {
if edit.guard.is_input && edit.as_str() != item.input {
cx.push(UpdateInput(edit.guard.key, edit.clone_string()));
}
}
}

#[derive(Debug)]
struct CellDriver;

impl Driver<Item, CellData> for CellDriver {
impl Driver<Key, Cell> 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
Expand All @@ -454,6 +435,15 @@ impl Driver<Item, CellData> for CellDriver {
key: *key,
is_input: false,
})
.with_width_em(6.0, 6.0)
}

fn navigable(_: &Self::Widget) -> bool {
false
}

fn label(widget: &Self::Widget) -> Option<TextOrSource<'_>> {
Some(widget.as_str().into())
}
}

Expand All @@ -470,46 +460,29 @@ pub fn window() -> Window<()> {
cells.insert(make_key("C2"), Cell::new("= A2 * A3 * A4"));
data.update_values();

let cells = MatrixView::new(CellDriver).with_num_visible(5, 20);
let clerk = Clerk {
empty_cell: Cell::default(),
};

let cells = GridView::new(clerk, CellDriver).with_num_visible(5, 20);

let ui = impl_anon! {
#[widget {
layout = self.cells;
}]
#[widget]
#[layout(self.cells)]
struct {
core: widget_core!(),
data: CellData = data,
#[widget(&self.data)] cells: ScrollBars<MatrixView<CellData, CellDriver>> =
#[widget(&self.data)] cells: ScrollBars<GridView<Clerk, CellDriver>> =
ScrollBars::new(cells),
}
impl Events for Self {
type Data = ();

fn steal_event(&mut self, cx: &mut EventCx, _: &(), _: &Id, event: &Event) -> IsUsed {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't supported any more, as such we cannot intercept the Enter key and use it to navigate down/up.

Something else is needed. Considering the hacky nature and that nothing else uses event stealing, this was not the right approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is #20.

match event {
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 cx.modifiers().shift_key() {
(row - 1).max(1)
} else {
(row + 1).min(MAX_ROW)
};
let id = Key(col, row).make_id(self.cells.inner().id_ref());
cx.next_nav_focus(Some(id), false, FocusSource::Synthetic);
}
IsUsed::Used
},
_ => IsUsed::Unused
}
}

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();
cx.update(self.cells.as_node(&self.data));
}
}
}
Expand Down
15 changes: 7 additions & 8 deletions src/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@
//! Counter

use kas::prelude::*;
use kas::widgets::{Adapt, Button, EditBox};
use kas::widgets::{row, Button, EditBox};

#[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 = row![
EditBox::string(|count| format!("{count}"))
.with_width_em(3.0, 3.0)
.align(AlignHints::RIGHT),
Button::label_msg("&Count", Incr).map_any(),
];
let ui = Adapt::new(ui, 0).on_message(|_, count, Incr| *count += 1);
let ui = ui.with_state(0).on_message(|_, count, Incr| *count += 1);
Window::new(ui, "Counter")
}
Loading
Loading