From e17d66fed1755db3d01dfe9674befcb3a00ba888 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Wed, 10 Sep 2025 15:54:52 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=99=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/grid.rs | 29 +- backend/src/main.rs | 30 +- backend/src/messages.rs | 10 + .../lib/components/grid/cell-header.svelte | 8 +- frontend/src/lib/components/grid/cell.svelte | 6 +- frontend/src/lib/components/grid/cell.ts | 0 frontend/src/lib/components/grid/grid.svelte | 288 +++++------------- frontend/src/lib/components/grid/grid.ts | 208 +++++++++++++ frontend/src/lib/components/grid/messages.ts | 9 +- frontend/src/lib/components/grid/utils.ts | 5 +- 10 files changed, 349 insertions(+), 244 deletions(-) create mode 100644 frontend/src/lib/components/grid/cell.ts create mode 100644 frontend/src/lib/components/grid/grid.ts diff --git a/backend/src/grid.rs b/backend/src/grid.rs index ed969a4..b9600a7 100644 --- a/backend/src/grid.rs +++ b/backend/src/grid.rs @@ -23,13 +23,8 @@ impl Grid { &mut self, cell_ref: CellRef, raw_val: String, - do_propagation: bool, - force_propagation: bool, ) -> Result, String> { - if self.cells.contains_key(&cell_ref) - && self.cells[&cell_ref].raw() == raw_val - && !force_propagation - { + if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val { return Ok(Vec::new()); } @@ -48,14 +43,7 @@ impl Grid { if self.cells.contains_key(&cell_ref) { updated_cells = self - .update_exisiting_cell( - raw_val, - eval, - precs, - cell_ref, - do_propagation, - force_propagation, - )? + .update_exisiting_cell(raw_val, eval, precs, cell_ref)? .into_iter() .chain(updated_cells) .collect(); @@ -66,6 +54,15 @@ impl Grid { Ok(updated_cells) } + pub fn quick_eval(&mut self, raw_val: String) -> Eval { + if raw_val.chars().nth(0) != Some('=') { + Eval::Literal(Literal::String(raw_val.to_owned())) + } else { + let (res_eval, ..) = evaluate(raw_val[1..].to_owned(), Some(&self)); + res_eval + } + } + pub fn get_cell(&self, cell_ref: CellRef) -> Result { if !self.cells.contains_key(&cell_ref) { return Err(format!("Cell at {:?} not found.", cell_ref)); @@ -168,8 +165,6 @@ impl Grid { new_eval: Eval, new_precs: HashSet, cell_ref: CellRef, - do_propagation: bool, - force_propagation: bool, ) -> Result, String> { let (old_precs, old_eval) = match self.cells.get_mut(&cell_ref) { Some(cell) => { @@ -211,7 +206,7 @@ impl Grid { cell.set_precs(new_precs); cell.set_eval(new_eval); - if (eval_changed && do_propagation) || force_propagation { + if eval_changed { self.propagate(cell_ref) } else { Ok(Vec::new()) diff --git a/backend/src/main.rs b/backend/src/main.rs index 3e7a1ef..5a03474 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -62,16 +62,9 @@ async fn accept_connection(stream: TcpStream) { MsgType::Set => { let Some(cell_ref) = req.cell else { continue }; let Some(raw) = req.raw else { continue }; - let Some(config) = req.eval_config else { - continue; - }; + // let config = req.eval_config.unwrap_or_default(); - match grid.update_cell( - cell_ref.clone(), - raw.to_owned(), - config.do_propagation, - config.force_propagation, - ) { + match grid.update_cell(cell_ref.clone(), raw.to_owned()) { Ok(updates) => { let mut msgs = Vec::new(); @@ -122,6 +115,25 @@ async fn accept_connection(stream: TcpStream) { } } } + MsgType::Eval => { + let Some(cell_ref) = req.cell else { continue }; + let Some(raw) = req.raw else { continue }; + + let eval = grid.quick_eval(raw.to_owned()); + + let msg = LeadMsg { + msg_type: MsgType::Eval, + cell: Some(cell_ref), + raw: Some(raw), + eval: Some(eval), + bulk_msgs: None, + eval_config: None, + }; + + let _ = write + .send(serde_json::to_string(&msg).unwrap().into()) + .await; + } _ => { continue; // handle other cases } diff --git a/backend/src/messages.rs b/backend/src/messages.rs index 04b16b1..2e0aefc 100644 --- a/backend/src/messages.rs +++ b/backend/src/messages.rs @@ -6,6 +6,7 @@ use crate::{cell::CellRef, evaluator::Eval}; #[serde(rename_all = "lowercase")] pub enum MsgType { Set, + Eval, Get, Error, Bulk, @@ -17,6 +18,15 @@ pub struct EvalConfig { pub force_propagation: bool, } +impl Default for EvalConfig { + fn default() -> Self { + EvalConfig { + do_propagation: true, + force_propagation: false, + } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct LeadMsg { pub msg_type: MsgType, diff --git a/frontend/src/lib/components/grid/cell-header.svelte b/frontend/src/lib/components/grid/cell-header.svelte index 87ed670..3dea6cf 100644 --- a/frontend/src/lib/components/grid/cell-header.svelte +++ b/frontend/src/lib/components/grid/cell-header.svelte @@ -102,10 +102,10 @@ border-left: none; } - .placeholder.blank, - .placeholder.col { - border-top: none; - } + /* .placeholder.blank, */ + /* .placeholder.col { */ + /* border-top: none; */ + /* } */ .active { background-color: color-mix(in oklab, var(--color-primary) 80%, var(--color-background) 80%); diff --git a/frontend/src/lib/components/grid/cell.svelte b/frontend/src/lib/components/grid/cell.svelte index f02c590..28e61b0 100644 --- a/frontend/src/lib/components/grid/cell.svelte +++ b/frontend/src/lib/components/grid/cell.svelte @@ -13,7 +13,8 @@ startediting = () => {}, stopediting = () => {}, active = false, - editing = false + editing = false, + externalediting = false }: { cla?: string; width?: string; @@ -24,6 +25,7 @@ stopediting?: () => void; active?: boolean; editing?: boolean; + externalediting?: boolean; } = $props(); // focus the first focusable descendant (the inner ) @@ -101,7 +103,7 @@ > {#if cell && (cell.raw_val !== '' || getEvalLiteral(cell.val) !== '')} - {#if cell.val} + {#if cell.val && !externalediting} {getEvalLiteral(cell.val)} {:else} {cell.raw_val} diff --git a/frontend/src/lib/components/grid/cell.ts b/frontend/src/lib/components/grid/cell.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/lib/components/grid/grid.svelte b/frontend/src/lib/components/grid/grid.svelte index 46c16db..981d332 100644 --- a/frontend/src/lib/components/grid/grid.svelte +++ b/frontend/src/lib/components/grid/grid.svelte @@ -1,21 +1,13 @@
-
+
{ + if (e.key === 'Enter' || e.key === 'NumpadEnter') { + e.preventDefault(); // avoid form submit/line break + const el = (e.currentTarget as HTMLElement).querySelector( + 'input' + ) as HTMLInputElement | null; + el?.blur(); // triggers on:blur below + } else if (e.key == 'Escape') { + e.preventDefault(); + + grid.stopEditingActive(); + } + }} + > getActiveCell().raw_val, (raw) => setActiveCellRaw(raw)} - class="relative w-[200px] pl-8" + onmousedown={() => grid.setExternalEdit(grid.getActivePos())} + onblur={() => grid.setExternalEdit(null)} + bind:value={ + () => grid.getActiveCell().raw_val, (raw) => grid.quickEval(grid.getActivePos(), raw) + } + class="relative w-[200px] pl-9" >
@@ -251,8 +122,8 @@
setColWidth(j, width)} + height={grid.getDefaultRowHeight()} + width={grid.getColWidth(j)} + setColWidth={(width) => grid.setColWidth(j, width)} direction="col" val={colToStr(j)} - active={active_cell !== null && active_cell[1] === j} + active={grid.getActivePos() !== null && grid.getActivePos()?.col === j} /> {/each}
@@ -275,28 +146,31 @@
setRowHeight(i, height)} + height={grid.getRowHeight(i)} + width={grid.getDefaultColWidth()} + setRowHeight={(height) => grid.setRowHeight(i, height)} val={(i + 1).toString()} - active={active_cell !== null && active_cell[0] === i} + active={grid.getActivePos() !== null && grid.getActivePos()?.row === i} />
{#each Array(cols) as _, j} startEditing(i, j)} - stopediting={() => stopEditing(i, j)} + height={grid.getRowHeight(i)} + width={grid.getColWidth(j)} + editing={grid.isEditing(new Position(i, j))} + externalediting={grid.isExternalEditing(new Position(i, j))} + startediting={() => grid.startEditing(new Position(i, j))} + stopediting={() => grid.stopEditing(new Position(i, j))} onmousedown={(e) => { handleCellInteraction(i, j, e); }} bind:cell={ - () => getCell(i, j), - (v) => setCell(i, j, v, { do_propagation: false, force_propagation: false }) + () => grid.getCell(new Position(i, j)), (v) => grid.setCell(new Position(i, j), v) } - active={active_cell !== null && active_cell[0] === i && active_cell[1] === j} + active={(() => { + console.log(grid.isActive(new Position(i, j))); + return grid.isActive(new Position(i, j)); + })()} /> {/each}
diff --git a/frontend/src/lib/components/grid/grid.ts b/frontend/src/lib/components/grid/grid.ts new file mode 100644 index 0000000..b248d7f --- /dev/null +++ b/frontend/src/lib/components/grid/grid.ts @@ -0,0 +1,208 @@ +import { toast } from 'svelte-sonner'; +import type { CellRef, CellT, LeadMsg } from './messages'; + +class Position { + public row: number; + public col: number; + + constructor(row: number, col: number) { + this.row = row; + this.col = col; + } + + public key() { + return `${this.row}:${this.col}`; + } + + public static key(row: number, col: number) { + return `${row}:${col}`; + } + + public ref(): CellRef { + return { row: this.row, col: this.col }; + } + + public equals(other: CellRef | null | undefined): boolean { + return !!other && this.row === other.row && this.col === other.col; + } + + public static equals(a: CellRef | null | undefined, b: CellRef | null | undefined): boolean { + return !!a && !!b && a.row === b.row && a.col === b.col; + } +} + +class Grid { + data: Record = {}; + socket: WebSocket; + row_heights: Record = {}; + col_widths: Record = {}; + default_row_height: string; + default_col_width: string; + active_cell: Position | null = null; + editing_cell: Position | null = null; + external_editing_cell: Position | null = null; + + constructor(socket: WebSocket, default_col_width = '80px', default_row_height = '30px') { + this.socket = socket; + this.default_col_width = default_col_width; + this.default_row_height = default_row_height; + } + + public getCell(pos: Position): CellT { + return this.data[pos.key()]; + } + + public setCell(pos: Position, v: CellT) { + if (v?.raw_val == null || v.raw_val === '') { + delete this.data[pos.key()]; + return; + } + + this.data[pos.key()] = { + raw_val: v.raw_val, + val: v.val + }; + + let msg: LeadMsg = { + msg_type: 'set', + cell: pos.ref(), + raw: v.raw_val + }; + + this.socket.send(JSON.stringify(msg)); + } + + public getRowHeight(row: number) { + return this.row_heights[row] ?? this.default_row_height; + } + + public getColWidth(col: number) { + return this.col_widths[col] ?? this.default_col_width; + } + + public getDefaultColWidth() { + return this.default_col_width; + } + public getDefaultRowHeight() { + return this.default_row_height; + } + + public setRowHeight(row: number, height: string) { + if (height === this.default_row_height) { + delete this.row_heights[row]; + } else { + this.row_heights[row] = height; + } + } + + public setColWidth(col: number, width: string) { + if (width === this.default_col_width) { + delete this.col_widths[col]; + } else { + this.col_widths[col] = width; + } + } + + public startEditing(pos: Position) { + this.active_cell = pos; + this.editing_cell = pos; + } + + public stopEditing(pos: Position) { + this.editing_cell = null; + this.setCell(pos, this.getCell(pos)); + } + + public stopEditingActive() { + if (this.active_cell == null) return; + + this.stopEditing(this.active_cell); + } + + public isEditing(pos: Position): boolean { + if (this.editing_cell == null) return false; + return this.editing_cell.equals(pos); + } + + public isExternalEditing(pos: Position): boolean { + if (this.external_editing_cell == null) return false; + return this.external_editing_cell.equals(pos); + } + + public setActive(pos: Position | null) { + this.active_cell = pos; + } + + public setExternalEdit(pos: Position | null) { + this.external_editing_cell = pos; + } + + public getActiveCell(): CellT { + if (this.active_cell === null) return { + raw_val: '', + val: undefined + }; + + return this.getCell(this.active_cell); + } + public getActivePos(): Position | null { + return this.active_cell; + } + + public isActive(pos: Position): boolean { + if (this.active_cell == null) return false; + return this.active_cell.equals(pos); + } + + public quickEval(pos: Position | null, raw: string) { + if (pos === null) return; + + let msg: LeadMsg = { + msg_type: 'eval', + cell: pos.ref(), + raw: raw + }; + + this.socket.send(JSON.stringify(msg)); + } + + public handle_msg(msg: LeadMsg) { + switch (msg.msg_type) { + case 'error': { + toast.error('Error', { + description: msg.raw + }); + break; + } + case 'set': { + if (msg.cell === undefined) { + console.error('Expected cell ref for SET msgponse from server.'); + return; + } else if (msg.eval === undefined) { + console.error('Expected cell value for SET msgponse from server.'); + return; + } + + this.data[Position.key(msg.cell.row, msg.cell.col)] = { + raw_val: msg.raw ?? '', + val: msg.eval + }; + + break; + } + case 'bulk': { + if (msg.bulk_msgs === undefined) { + console.error('Expected bulk_msgs field to be defined for BULK message.'); + return; + } + + for (const m of msg.bulk_msgs) this.handle_msg(m); + } + case 'eval': { + // TODO + } + } + } +} + +export { Grid, Position }; diff --git a/frontend/src/lib/components/grid/messages.ts b/frontend/src/lib/components/grid/messages.ts index 2d7fb39..38d63ae 100644 --- a/frontend/src/lib/components/grid/messages.ts +++ b/frontend/src/lib/components/grid/messages.ts @@ -1,5 +1,5 @@ interface LeadMsg { - msg_type: 'set' | 'get' | 'error' | 'bulk'; + msg_type: 'set' | 'get' | 'error' | 'bulk' | 'eval'; cell?: CellRef; raw?: string; eval?: Eval; @@ -44,3 +44,10 @@ type Eval = | { range: Range } | { err: LeadErr } | 'unset'; + + interface CellT { + raw_val: string; + val?: Eval; +} + +export type { Eval, LeadMsg, LeadErr, Literal, CellRef, LiteralValue, CellT }; diff --git a/frontend/src/lib/components/grid/utils.ts b/frontend/src/lib/components/grid/utils.ts index d3aeea7..43419db 100644 --- a/frontend/src/lib/components/grid/utils.ts +++ b/frontend/src/lib/components/grid/utils.ts @@ -1,7 +1,4 @@ -export interface CellT { - raw_val: string; - val?: Eval; -} +import type { CellRef, Eval, LiteralValue } from './messages'; /** * Zero indexed | A1 == {row: 0, col: 0};