This commit is contained in:
2025-09-04 15:25:22 +10:00
parent f95f8fcf83
commit 0c68c8cd5b
2 changed files with 162 additions and 154 deletions

View File

@@ -1,7 +1,7 @@
use crate::cell::{Cell, CellRef};
use crate::parser::*;
use crate::tokenizer::Literal;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, PartialEq, Clone)]
@@ -34,15 +34,17 @@ impl Evaluator {
}
let eval: Eval;
let deps: HashSet<CellRef>;
if let Some(c) = raw_val.chars().nth(0)
&& c == '='
{
eval = self.evaluate(raw_val[1..].to_owned())?;
(eval, deps) = self.evaluate(raw_val[1..].to_owned())?;
// for dep in deps {}
} else {
match self.evaluate(raw_val.to_owned()) {
Ok(e) => {
eval = e;
(eval, deps) = e;
}
Err(_) => eval = Eval::Literal(Literal::String(raw_val.to_owned())),
}
@@ -61,14 +63,28 @@ impl Evaluator {
Ok((cell.raw(), cell.eval()))
}
pub fn evaluate(&mut self, str: String) -> Result<Eval, String> {
let (mut expr, mut deps) = parse(&str)?;
self.evaluate_expr(&mut expr)
pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> {
if !self.cells.contains_key(&cell_ref) {
return Err(format!("Cell at {:?} not found.", cell_ref));
}
fn evaluate_expr(&mut self, expr: &mut Expr) -> Result<Eval, String> {
if let Some(cell) = self.cells.get_mut(&cell_ref) {
cell.add_i_dep(dep_ref);
}
Ok(())
}
pub fn evaluate(&mut self, str: String) -> Result<(Eval, HashSet<CellRef>), String> {
let (expr, deps) = parse(&str)?;
match self.evaluate_expr(&expr) {
Ok(it) => Ok((it, deps)),
Err(it) => Err(it),
}
}
fn evaluate_expr(&mut self, expr: &Expr) -> Result<Eval, String> {
let res = match expr {
Expr::Literal(lit) => Eval::Literal(lit.clone()),
Expr::CellRef(re) => self.get_cell(re.to_owned())?.1,

View File

@@ -1,43 +1,30 @@
<script lang="ts">
import { onMount } from "svelte";
const socket = new WebSocket("ws://localhost:7050");
socket.onmessage = (event) => {
const message = event.data;
console.log("Received message from server:", message);
};
socket.onopen = () => {
console.log("WebSocket connection established.");
};
// Connection opened
// --- Configuration ---
const numRows = 1000; // Increased to show performance
const numCols = 100; // Increased to show performance
const numRows = 1000;
const numCols = 100;
const rowHeight = 30; // px
const colWidth = 150; // px
const rowNumberWidth = 50; // px
const colHeaderHeight = 30; // px
// --- State ---
let gridData: string[][];
let columnLabels: string[] = [];
let activeCell: [number, number] | null = null;
let viewportElement: HTMLElement;
let viewportWidth = 0;
let viewportHeight = 0;
let scrollTop = 0;
let scrollLeft = 0;
// --- State (Svelte 5 runes) ---
let gridData = $state<string[][] | null>(new Array());
let columnLabels = $state<string[]>([]);
let activeCell = $state<[number, number] | null>(null);
// --- Helper Functions ---
/**
* Generates Excel-style column labels (A, B, ..., Z, AA, AB, ...).
*/
let viewportEl = $state<HTMLElement | null>(null);
let viewportWidth = $state(0);
let viewportHeight = $state(0);
let scrollTop = $state(0);
let scrollLeft = $state(0);
let socket: WebSocket | null = null;
// --- Helpers ---
function generateColumnLabels(count: number): string[] {
const labels = [];
const labels: string[] = [];
for (let i = 0; i < count; i++) {
let label = "";
let temp = i;
@@ -52,64 +39,71 @@
onMount(() => {
columnLabels = generateColumnLabels(numCols);
gridData = Array(numRows)
.fill(null)
.map(() => Array(numCols).fill(""));
gridData = Array.from({ length: numRows }, () =>
Array<string>(numCols).fill(""),
);
socket = new WebSocket("ws://localhost:7050");
socket.onmessage = (e) =>
console.log("Received message from server:", e.data);
socket.onopen = () => console.log("WebSocket connection established.");
return () => socket?.close();
});
// --- Event Handlers ---
// --- Events ---
function handleGridBlur(e: FocusEvent) {
if (!(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) {
activeCell = null;
}
const target = e.currentTarget as HTMLElement;
const next = e.relatedTarget as Node | null;
if (!target.contains(next)) activeCell = null;
}
function handleCellBlur(row: number, col: number) {
// This function runs when a cell loses focus.
// You can add your logic here, e.g., validation, calculations, etc.
console.log(
`Cell (${row + 1}, ${columnLabels[col]}) lost focus. Value:`,
gridData[row][col],
gridData![row][col],
);
socket.send(
socket?.send(
JSON.stringify({
row: row + 1,
col: columnLabels[col],
value: gridData[row][col],
value: gridData![row][col],
}),
);
}
// --- Reactive Calculations for Virtualization ---
// $: is a Svelte feature that re-runs code when its dependencies change.
// --- Scroll math (account for header/row gutters) ---
// How far into the *grid area* (excluding headers) we've scrolled:
const scrollDX = $derived(Math.max(0, scrollLeft - rowNumberWidth));
const scrollDY = $derived(Math.max(0, scrollTop - colHeaderHeight));
// Calculate which rows/cols are visible based on scroll position
$: startRow = Math.max(0, Math.floor(scrollTop / rowHeight));
$: endRow = Math.min(
numRows,
startRow + Math.ceil(viewportHeight / rowHeight) + 1,
);
$: startCol = Math.max(0, Math.floor(scrollLeft / colWidth));
$: endCol = Math.min(
numCols,
startCol + Math.ceil(viewportWidth / colWidth) + 1,
// Viewport pixels actually available to show cells (excluding sticky gutters):
const innerW = $derived(Math.max(0, viewportWidth - rowNumberWidth));
const innerH = $derived(Math.max(0, viewportHeight - colHeaderHeight));
// Virtualization windows:
const startCol = $derived(Math.max(0, Math.floor(scrollDX / colWidth)));
const endCol = $derived(
Math.min(numCols, startCol + Math.ceil(innerW / colWidth) + 1),
);
// Create arrays of only the visible items to render
$: visibleRows = Array(endRow - startRow)
.fill(0)
.map((_, i) => startRow + i);
$: visibleCols = Array(endCol - startCol)
.fill(0)
.map((_, i) => startCol + i);
const startRow = $derived(Math.max(0, Math.floor(scrollDY / rowHeight)));
const endRow = $derived(
Math.min(numRows, startRow + Math.ceil(innerH / rowHeight) + 1),
);
const visibleCols = $derived(
Array.from({ length: endCol - startCol }, (_, i) => startCol + i),
);
const visibleRows = $derived(
Array.from({ length: endRow - startRow }, (_, i) => startRow + i),
);
</script>
<div
class="w-full h-[85vh] p-4 bg-background text-foreground rounded-lg border flex flex-col"
>
<div
class="relative grid-container"
class="grid-container relative"
on:focusout={handleGridBlur}
style="
--row-height: {rowHeight}px;
@@ -118,43 +112,45 @@
--col-header-height: {colHeaderHeight}px;
"
>
<!-- The scrollable viewport provides the scrollbars -->
<!-- Single, real scroll container -->
<div
class="viewport"
bind:this={viewportElement}
bind:this={viewportEl}
bind:clientWidth={viewportWidth}
bind:clientHeight={viewportHeight}
on:scroll={(e) => {
scrollTop = e.currentTarget.scrollTop;
scrollLeft = e.currentTarget.scrollLeft;
const el = e.currentTarget as HTMLElement;
scrollTop = el.scrollTop;
scrollLeft = el.scrollLeft;
}}
>
<!-- Sizer div creates the full scrollable area -->
<!-- Sizer creates true scrollable area (includes gutters + grid) -->
<div
class="total-sizer"
style:width="{numCols * colWidth}px"
style:height="{numRows * rowHeight}px"
style:width={`${rowNumberWidth + numCols * colWidth}px`}
style:height={`${colHeaderHeight + numRows * rowHeight}px`}
/>
</div>
<!-- The Renderer sits on top of the viewport and handles drawing -->
{#if gridData}
<div class="renderer">
<!-- Top-left corner -->
<!-- Top-left sticky corner -->
<div
class="top-left-corner"
style="top: 0; left: 0;"
class:active-header-corner={activeCell !== null}
/>
<!-- Visible Column Headers -->
<!-- Column headers (stick to top, scroll horizontally with grid) -->
<div
class="col-headers-container"
style:transform="translateX(-{scrollLeft}px)"
style="
top: 0;
left: {rowNumberWidth}px;
transform: translateX(-{scrollDX}px);
"
>
{#each visibleCols as j (j)}
<div
class="header-cell"
style:left="{j * colWidth}px"
style:left={`${j * colWidth}px`}
class:active-header={activeCell !== null && activeCell[1] === j}
>
{columnLabels[j]}
@@ -162,15 +158,19 @@
{/each}
</div>
<!-- Visible Row Headers -->
<!-- Row headers (stick to left, scroll vertically with grid) -->
<div
class="row-headers-container"
style:transform="translateY(-{scrollTop}px)"
style="
top: {colHeaderHeight}px;
left: 0;
transform: translateY(-{scrollDY}px);
"
>
{#each visibleRows as i (i)}
<div
class="row-number-cell"
style:top="{i * rowHeight}px"
style:top={`${i * rowHeight}px`}
class:active-header={activeCell !== null && activeCell[0] === i}
>
{i + 1}
@@ -178,27 +178,34 @@
{/each}
</div>
<!-- Visible Grid Cells -->
<!-- Visible grid cells (overlay; move opposite the inner scroll) -->
{#if gridData}
<div
class="cells-container"
style:transform="translate(-{scrollLeft}px, -{scrollTop}px)"
style="
top: {colHeaderHeight}px;
left: {rowNumberWidth}px;
transform: translate(-{scrollDX}px, -{scrollDY}px);
"
>
{#each visibleRows as i (i)}
{#each visibleCols as j (j)}
<div
class="grid-cell"
style:top="{i * rowHeight}px"
style:left="{j * colWidth}px"
style:top={`${i * rowHeight}px`}
style:left={`${j * colWidth}px`}
>
<!-- Using a standard input to ensure styles are applied correctly -->
<input
type="text"
bind:value={gridData[i][j]}
value={gridData[i][j]}
class="cell-input"
on:input={(e) =>
(gridData[i][j] = (
e.currentTarget as HTMLInputElement
).value)}
on:focus={() => (activeCell = [i, j])}
on:blur={() => handleCellBlur(i, j)}
/>
<!-- Active cell indicator with fill handle -->
{#if activeCell && activeCell[0] === i && activeCell[1] === j}
<div class="active-cell-indicator">
<div class="fill-handle" />
@@ -208,50 +215,39 @@
{/each}
{/each}
</div>
</div>
{/if}
</div>
</div>
</div>
<style>
.grid-container {
flex-grow: 1;
position: relative;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
border: 1px solid hsl(var(--border));
}
.viewport {
position: relative;
width: 100%;
height: 100%;
overflow: auto;
position: absolute;
top: 0;
left: 0;
}
.total-sizer {
position: relative;
pointer-events: none;
}
.renderer {
position: absolute;
top: 0;
left: 0;
.total-sizer {
/* occupies the scroll area; other layers are absolutely positioned on top */
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
/* Layered overlays inside the viewport */
.top-left-corner,
.col-headers-container,
.row-headers-container,
.cells-container {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.top-left-corner {
@@ -262,25 +258,22 @@
border-bottom: 1px solid hsl(var(--border));
z-index: 4;
}
.active-header-corner {
background-color: hsl(var(--accent));
}
.col-headers-container {
top: 0;
left: var(--row-number-width);
height: var(--col-header-height);
z-index: 3;
}
.row-headers-container {
top: var(--col-header-height);
left: 0;
width: var(--row-number-width);
z-index: 2;
}
.cells-container {
top: var(--col-header-height);
left: var(--row-number-width);
z-index: 1;
}
@@ -320,12 +313,10 @@
height: var(--row-height);
border-right: 1px solid hsl(var(--border) / 0.7);
border-bottom: 1px solid hsl(var(--border) / 0.7);
pointer-events: auto;
background-color: hsl(var(--background));
}
.cell-input {
/* Overriding all default input styles */
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
@@ -342,7 +333,7 @@
outline: none;
box-shadow: none;
}
/* Ensure no focus styles are added by the browser or libraries */
.cell-input:focus,
.cell-input:focus-visible {
border: 1px solid red;
@@ -358,6 +349,7 @@
pointer-events: none;
z-index: 5;
}
.fill-handle {
position: absolute;
bottom: -4px;