1use fc_core::FcError;
26use serde::{Deserialize, Serialize};
27#[cfg(feature = "bindgen")]
28use wasm_bindgen::prelude::*;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct CellRef {
33 pub row: usize,
34 pub col: usize,
35}
36
37impl CellRef {
38 pub fn new(row: usize, col: usize) -> Self {
39 Self { row, col }
40 }
41}
42
43#[cfg_attr(feature = "bindgen", wasm_bindgen)]
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub struct CellRange {
49 pub start_row: usize,
50 pub start_col: usize,
51 pub end_row: usize,
52 pub end_col: usize,
53 #[serde(default)]
60 anchor_row: usize,
61 #[serde(default)]
62 anchor_col: usize,
63}
64
65impl CellRange {
66 pub fn from_corners(a: CellRef, b: CellRef) -> Self {
72 Self {
73 start_row: a.row.min(b.row),
74 start_col: a.col.min(b.col),
75 end_row: a.row.max(b.row),
76 end_col: a.col.max(b.col),
77 anchor_row: a.row,
79 anchor_col: a.col,
80 }
81 }
82
83 pub fn single(cell: CellRef) -> Self {
84 Self {
85 start_row: cell.row,
86 start_col: cell.col,
87 end_row: cell.row,
88 end_col: cell.col,
89 anchor_row: cell.row,
90 anchor_col: cell.col,
91 }
92 }
93
94 pub fn contains(&self, cell: CellRef) -> bool {
95 cell.row >= self.start_row
96 && cell.row <= self.end_row
97 && cell.col >= self.start_col
98 && cell.col <= self.end_col
99 }
100
101 pub fn row_count(&self) -> usize {
102 self.end_row - self.start_row + 1
103 }
104
105 pub fn col_count(&self) -> usize {
106 self.end_col - self.start_col + 1
107 }
108
109 pub fn cell_count(&self) -> u64 {
113 (self.row_count() as u64).saturating_mul(self.col_count() as u64)
114 }
115
116
117 pub fn with_anchor(mut self, cell: CellRef) -> Self {
119 self.anchor_row = cell.row;
120 self.anchor_col = cell.col;
121 self
122 }
123
124 pub fn anchor(&self) -> CellRef {
126 CellRef::new(self.anchor_row, self.anchor_col)
127 }
128
129 pub fn extend_to(&self, cell: CellRef) -> Self {
132 Self::from_corners(self.anchor(), cell).with_anchor(self.anchor())
133 }
134
135 pub fn iter_cells(&self) -> Vec<CellRef> {
137 let cap = self.cell_count().min(65_536) as usize;
140 let mut out = Vec::with_capacity(cap);
141 for row in self.start_row..=self.end_row {
142 for col in self.start_col..=self.end_col {
143 out.push(CellRef::new(row, col));
144 }
145 }
146 out
147 }
148}
149
150#[cfg_attr(feature = "bindgen", wasm_bindgen)]
152#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
153pub struct Viewport {
154 pub first_row: usize,
155 pub first_col: usize,
156 pub rows: usize,
157 pub cols: usize,
158 pub overscan: usize,
160}
161
162impl Viewport {
163 pub fn visible_range(&self) -> CellRange {
165 CellRange::from_corners(
168 CellRef::new(self.first_row, self.first_col),
169 CellRef::new(
170 self.first_row.saturating_add(self.rows.saturating_sub(1)),
171 self.first_col.saturating_add(self.cols.saturating_sub(1)),
172 ),
173 )
174 }
175
176 pub fn render_range(&self) -> CellRange {
178 let v = self.visible_range();
179 CellRange::from_corners(
180 CellRef::new(
181 v.start_row.saturating_sub(self.overscan),
182 v.start_col.saturating_sub(self.overscan),
183 ),
184 CellRef::new(
185 v.end_row.saturating_add(self.overscan),
186 v.end_col.saturating_add(self.overscan),
187 ),
188 )
189 }
190
191 pub fn active_cell(&self) -> CellRef {
193 CellRef::new(self.first_row, self.first_col)
194 }
195}
196
197#[cfg_attr(feature = "bindgen", wasm_bindgen)]
202#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203pub struct Grid {
204 cells: std::collections::HashMap<(usize, usize), String>,
205}
206
207impl Grid {
208 pub fn new() -> Self {
209 Self::default()
210 }
211
212 pub fn set(&mut self, cell: CellRef, value: String) {
213 if value.is_empty() {
214 self.cells.remove(&(cell.row, cell.col));
217 } else {
218 self.cells.insert((cell.row, cell.col), value);
219 }
220 }
221
222 pub fn get(&self, cell: CellRef) -> Option<&str> {
223 self.cells.get(&(cell.row, cell.col)).map(String::as_str)
224 }
225
226 pub fn is_empty(&self) -> bool {
227 self.cells.is_empty()
228 }
229
230 pub fn len(&self) -> usize {
231 self.cells.len()
232 }
233
234 pub fn count_in(&self, range: CellRange) -> usize {
236 self.cells
237 .keys()
238 .filter(|(r, c)| range.contains(CellRef::new(*r, *c)))
239 .count()
240 }
241
242 pub fn used_rows(&self) -> usize {
244 self.cells.keys().map(|(r, _)| r + 1).max().unwrap_or(0)
245 }
246
247 pub fn used_cols(&self) -> usize {
249 self.cells.keys().map(|(_, c)| c + 1).max().unwrap_or(0)
250 }
251
252 pub fn fill_down(&mut self, from: CellRef, count: usize, max_rows: usize) -> Result<usize, FcError> {
257 if count > max_rows {
258 return Err(FcError::InvalidInput {
259 message: format!("fill count {count} exceeds the {max_rows} row limit"),
260 });
261 }
262 let Some(value) = self.get(from).map(str::to_string) else {
263 return Err(FcError::InvalidInput {
264 message: "fill source cell is empty".to_string(),
265 });
266 };
267 for offset in 1..=count {
268 self.set(CellRef::new(from.row + offset, from.col), value.clone());
269 }
270 Ok(count)
271 }
272}
273fn range_to_js(r: CellRange) -> Vec<u32> {
280 vec![r.start_row as u32, r.start_col as u32, r.end_row as u32, r.end_col as u32]
281}
282
283#[cfg_attr(feature = "bindgen", wasm_bindgen)]
284impl CellRange {
285 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = fromCorners))]
286 pub fn js_from_corners(r1: usize, c1: usize, r2: usize, c2: usize) -> CellRange {
287 CellRange::from_corners(CellRef::new(r1, c1), CellRef::new(r2, c2))
288 }
289
290 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = single))]
291 pub fn js_single(row: usize, col: usize) -> CellRange {
292 CellRange::single(CellRef::new(row, col))
293 }
294
295 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = toArray))]
296 pub fn js_to_array(&self) -> Vec<u32> {
297 range_to_js(*self)
298 }
299
300 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = cellCount))]
301 pub fn js_cell_count(&self) -> u64 {
302 self.cell_count()
303 }
304
305 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = contains))]
306 pub fn js_contains(&self, row: usize, col: usize) -> bool {
307 self.contains(CellRef::new(row, col))
308 }
309
310 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = cells))]
312 pub fn js_cells(&self) -> Vec<u32> {
313 self.iter_cells().into_iter().flat_map(|c| [c.row as u32, c.col as u32]).collect()
314 }
315
316}
317
318#[cfg_attr(feature = "bindgen", wasm_bindgen)]
319impl Viewport {
320 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = new))]
321 #[allow(clippy::too_many_arguments)]
322 pub fn js_new(
323 first_row: usize,
324 first_col: usize,
325 rows: usize,
326 cols: usize,
327 overscan: usize,
328 ) -> Viewport {
329 Viewport { first_row, first_col, rows, cols, overscan }
330 }
331
332 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = visibleRange))]
333 pub fn js_visible_range(&self) -> Vec<u32> {
334 range_to_js(self.visible_range())
335 }
336
337 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = renderRange))]
338 pub fn js_render_range(&self) -> Vec<u32> {
339 range_to_js(self.render_range())
340 }
341}
342
343#[cfg_attr(feature = "bindgen", wasm_bindgen)]
344impl Grid {
345 #[cfg_attr(feature = "bindgen", wasm_bindgen(constructor))]
346 pub fn js_new() -> Grid {
347 Grid::new()
348 }
349
350 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = set))]
351 pub fn js_set(&mut self, row: usize, col: usize, value: String) {
352 self.set(CellRef::new(row, col), value);
353 }
354
355 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = get))]
356 pub fn js_get(&self, row: usize, col: usize) -> Option<String> {
357 self.get(CellRef::new(row, col)).map(str::to_string)
358 }
359
360 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = cellCount))]
361 pub fn js_len(&self) -> usize {
362 self.len()
363 }
364
365 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = usedRows))]
366 pub fn js_used_rows(&self) -> usize {
367 self.used_rows()
368 }
369
370 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = usedCols))]
371 pub fn js_used_cols(&self) -> usize {
372 self.used_cols()
373 }
374
375 #[cfg(feature = "bindgen")]
376 #[wasm_bindgen(js_name = fillDown)]
377 pub fn js_fill_down(
378 &mut self,
379 row: usize,
380 col: usize,
381 count: usize,
382 max_rows: usize,
383 ) -> Result<usize, JsValue> {
384 self.fill_down(CellRef::new(row, col), count, max_rows).map_err(Into::into)
385 }
386
387 #[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = countIn))]
389 pub fn js_count_in(&self, r1: usize, c1: usize, r2: usize, c2: usize) -> usize {
390 self.count_in(CellRange::from_corners(CellRef::new(r1, c1), CellRef::new(r2, c2)))
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 fn cell(row: usize, col: usize) -> CellRef {
399 CellRef::new(row, col)
400 }
401
402 #[test]
403 fn drag_is_normalized_regardless_of_direction() {
404 let up_left = CellRange::from_corners(cell(5, 5), cell(1, 1));
406 assert_eq!(
407 (up_left.start_row, up_left.start_col, up_left.end_row, up_left.end_col),
408 (1, 1, 5, 5)
409 );
410 }
411
412 #[test]
413 fn single_cell_range_counts_one() {
414 let r = CellRange::single(cell(3, 3));
415 assert_eq!(r.cell_count(), 1);
416 assert_eq!(r.row_count(), 1);
417 assert_eq!(r.col_count(), 1);
418 }
419
420 #[test]
421 fn range_dimensions_are_inclusive() {
422 let r = CellRange::from_corners(cell(0, 0), cell(4, 4));
425 assert_eq!(r.row_count(), 5);
426 assert_eq!(r.col_count(), 5);
427 assert_eq!(r.cell_count(), 25);
428 }
429
430 #[test]
431 fn contains_respects_bounds() {
432 let r = CellRange::from_corners(cell(1, 1), cell(3, 3));
433 assert!(r.contains(cell(1, 1)), "top-left corner is inside");
434 assert!(r.contains(cell(3, 3)), "bottom-right corner is inside");
435 assert!(!r.contains(cell(0, 1)), "above is outside");
436 assert!(!r.contains(cell(4, 3)), "below is outside");
437 }
438
439 #[test]
440 fn extending_keeps_the_anchor() {
441 let base = CellRange::single(cell(5, 5));
443 let extended = base.extend_to(cell(2, 8));
444 assert_eq!(extended.anchor(), cell(5, 5), "anchor must not move");
445 assert_eq!((extended.end_row, extended.end_col), (5, 8));
446 }
447
448 #[test]
449 fn extending_upward_still_produces_an_ordered_range() {
450 let extended = CellRange::single(cell(5, 5)).extend_to(cell(2, 2));
451 assert_eq!((extended.start_row, extended.start_col), (2, 2));
452 assert_eq!((extended.end_row, extended.end_col), (5, 5));
453 }
454
455 #[test]
456 fn huge_range_does_not_overflow() {
457 let huge = CellRange::from_corners(
460 CellRef::new(0, 0),
461 CellRef::new(usize::MAX - 1, usize::MAX - 1),
462 );
463 assert_eq!(huge.cell_count(), u64::MAX);
464 }
465
466 #[test]
467 fn cells_enumerate_in_row_major_order() {
468 let r = CellRange::from_corners(cell(0, 0), cell(1, 1));
469 let cells = r.iter_cells();
470 assert_eq!(cells.len(), 4);
471 assert_eq!(cells[0], cell(0, 0));
472 assert_eq!(cells[1], cell(0, 1));
473 assert_eq!(cells[2], cell(1, 0));
474 assert_eq!(cells[3], cell(1, 1));
475 }
476
477 #[test]
478 fn viewport_visible_range_is_sized_correctly() {
479 let vp = Viewport { first_row: 10, first_col: 5, rows: 20, cols: 4, overscan: 2 };
480 let v = vp.visible_range();
481 assert_eq!((v.start_row, v.end_row), (10, 29), "20 rows inclusive");
482 assert_eq!((v.start_col, v.end_col), (5, 8), "4 cols inclusive");
483 }
484
485 #[test]
486 fn viewport_overscan_expands_but_clamps_at_zero() {
487 let vp = Viewport { first_row: 1, first_col: 1, rows: 10, cols: 10, overscan: 5 };
488 assert_eq!(vp.render_range().start_row, 0);
490 }
491
492 #[test]
493 fn zero_sized_viewport_does_not_underflow() {
494 let vp = Viewport { first_row: 5, first_col: 5, rows: 0, cols: 0, overscan: 0 };
496 let v = vp.visible_range();
497 assert_eq!(v.end_row, 5);
498 assert_eq!(v.end_col, 5);
499 }
500
501 #[test]
502 fn sparse_grid_stores_only_populated_cells() {
503 let mut g = Grid::new();
504 assert!(g.is_empty());
505 g.set(cell(1000, 1000), "x".into());
506 assert_eq!(g.len(), 1, "one cell must not imply a million allocations");
507 assert_eq!(g.used_rows(), 1001);
508 assert_eq!(g.used_cols(), 1001);
509 }
510
511 #[test]
512 fn setting_an_empty_value_removes_the_cell() {
513 let mut g = Grid::new();
514 g.set(cell(0, 0), "x".into());
515 g.set(cell(0, 0), String::new());
516 assert!(g.is_empty(), "an empty cell must not be stored");
517 assert_eq!(g.get(cell(0, 0)), None);
518 }
519
520 #[test]
521 fn count_in_counts_only_cells_inside_the_range() {
522 let mut g = Grid::new();
523 g.set(cell(0, 0), "a".into());
524 g.set(cell(1, 1), "b".into());
525 g.set(cell(9, 9), "c".into());
526 assert_eq!(g.count_in(CellRange::from_corners(cell(0, 0), cell(1, 1))), 2);
527 assert_eq!(g.count_in(CellRange::from_corners(cell(0, 0), cell(4, 4))), 2);
528 assert_eq!(g.count_in(CellRange::from_corners(cell(5, 5), cell(6, 6))), 0);
529 }
530
531 #[test]
532 fn fill_down_copies_the_source_value() {
533 let mut g = Grid::new();
534 g.set(cell(0, 0), "seed".into());
535 assert_eq!(g.fill_down(cell(0, 0), 3, 100).unwrap(), 3);
536 assert_eq!(g.get(cell(1, 0)), Some("seed"));
537 assert_eq!(g.get(cell(3, 0)), Some("seed"));
538 assert_eq!(g.get(cell(4, 0)), None, "must not overshoot the requested count");
539 }
540
541 #[test]
542 fn fill_down_from_an_empty_cell_is_an_error() {
543 let mut g = Grid::new();
544 assert_eq!(
545 g.fill_down(cell(0, 0), 5, 100).unwrap_err().kind(),
546 "invalidInput"
547 );
548 }
549
550 #[test]
551 fn fill_down_is_bounded() {
552 let mut g = Grid::new();
554 g.set(cell(0, 0), "x".into());
555 assert_eq!(g.fill_down(cell(0, 0), 10_000_000, 1000).unwrap_err().kind(), "invalidInput");
556 }
557}