1use std::collections::HashSet;
16use std::fmt;
17
18pub const GRID_COLS: u8 = 16;
22
23pub const GRID_ROWS: u8 = 9;
25
26pub const CELL_SIZE: f32 = 120.0;
28
29pub const CELL_PADDING: f32 = 10.0;
31
32pub const INTERNAL_PADDING: f32 = 20.0;
34
35pub const MIN_BLOCK_GAP: f32 = 20.0;
37
38pub const TOTAL_CELLS: usize = (GRID_COLS as usize) * (GRID_ROWS as usize);
40
41pub const CANVAS_WIDTH: f32 = 1920.0;
43
44pub const CANVAS_HEIGHT: f32 = 1080.0;
46
47#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct PixelBounds {
52 pub x: f32,
53 pub y: f32,
54 pub w: f32,
55 pub h: f32,
56}
57
58impl PixelBounds {
59 pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
61 Self { x, y, w, h }
62 }
63
64 pub fn right(&self) -> f32 {
66 self.x + self.w
67 }
68
69 pub fn bottom(&self) -> f32 {
71 self.y + self.h
72 }
73
74 pub fn cx(&self) -> f32 {
76 self.x + self.w / 2.0
77 }
78
79 pub fn cy(&self) -> f32 {
81 self.y + self.h / 2.0
82 }
83}
84
85impl fmt::Display for PixelBounds {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "({}, {}, {}x{})", self.x, self.y, self.w, self.h)
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct GridSpan {
99 pub c1: u8,
101 pub r1: u8,
103 pub c2: u8,
105 pub r2: u8,
107}
108
109impl GridSpan {
110 pub fn new(c1: u8, r1: u8, c2: u8, r2: u8) -> Self {
112 Self { c1, r1, c2, r2 }
113 }
114
115 pub fn pixel_bounds(&self) -> PixelBounds {
117 let x = self.c1 as f32 * CELL_SIZE;
118 let y = self.r1 as f32 * CELL_SIZE;
119 let w = (self.c2 - self.c1 + 1) as f32 * CELL_SIZE;
120 let h = (self.r2 - self.r1 + 1) as f32 * CELL_SIZE;
121 PixelBounds::new(x, y, w, h)
122 }
123
124 pub fn render_bounds(&self) -> PixelBounds {
126 let raw = self.pixel_bounds();
127 PixelBounds::new(
128 raw.x + CELL_PADDING,
129 raw.y + CELL_PADDING,
130 raw.w - 2.0 * CELL_PADDING,
131 raw.h - 2.0 * CELL_PADDING,
132 )
133 }
134
135 pub fn content_zone(&self) -> PixelBounds {
137 let rb = self.render_bounds();
138 PixelBounds::new(
139 rb.x + INTERNAL_PADDING,
140 rb.y + INTERNAL_PADDING,
141 rb.w - 2.0 * INTERNAL_PADDING,
142 rb.h - 2.0 * INTERNAL_PADDING,
143 )
144 }
145
146 pub fn cells(&self) -> Vec<(u8, u8)> {
148 let mut out = Vec::with_capacity(self.cell_count());
149 for r in self.r1..=self.r2 {
150 for c in self.c1..=self.c2 {
151 out.push((c, r));
152 }
153 }
154 out
155 }
156
157 pub fn cell_count(&self) -> usize {
159 (self.c2 - self.c1 + 1) as usize * (self.r2 - self.r1 + 1) as usize
160 }
161
162 fn is_in_bounds(&self) -> bool {
164 self.c1 <= self.c2 && self.r1 <= self.r2 && self.c2 < GRID_COLS && self.r2 < GRID_ROWS
165 }
166}
167
168impl fmt::Display for GridSpan {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 write!(f, "({},{})..({},{})", self.c1, self.r1, self.c2, self.r2)
171 }
172}
173
174#[derive(Debug, Clone)]
178pub enum GridError {
179 CellOccupied { col: u8, row: u8, existing_name: String },
181 OutOfBounds { span: GridSpan },
183}
184
185impl fmt::Display for GridError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::CellOccupied { col, row, existing_name } => {
189 write!(f, "Cell ({}, {}) already occupied by '{}'", col, row, existing_name)
190 }
191 Self::OutOfBounds { span } => {
192 write!(f, "Span {} is outside the {}x{} grid", span, GRID_COLS, GRID_ROWS)
193 }
194 }
195 }
196}
197
198impl std::error::Error for GridError {}
199
200#[derive(Debug, Clone)]
204struct Allocation {
205 name: String,
206 span: GridSpan,
207 step: usize,
208}
209
210#[derive(Debug)]
214pub struct GridProtocol {
215 occupied: HashSet<(u8, u8)>,
217 cell_owner: std::collections::HashMap<(u8, u8), String>,
219 allocations: Vec<Allocation>,
221}
222
223impl GridProtocol {
224 pub fn new() -> Self {
226 Self {
227 occupied: HashSet::new(),
228 cell_owner: std::collections::HashMap::new(),
229 allocations: Vec::new(),
230 }
231 }
232
233 pub fn allocate(&mut self, name: &str, span: GridSpan) -> Result<PixelBounds, GridError> {
235 if !span.is_in_bounds() {
236 return Err(GridError::OutOfBounds { span });
237 }
238
239 for (c, r) in span.cells() {
241 if self.occupied.contains(&(c, r)) {
242 let existing = self.cell_owner.get(&(c, r)).cloned().unwrap_or_default();
243 return Err(GridError::CellOccupied { col: c, row: r, existing_name: existing });
244 }
245 }
246
247 let step = self.allocations.len();
249 for (c, r) in span.cells() {
250 self.occupied.insert((c, r));
251 self.cell_owner.insert((c, r), name.to_string());
252 }
253
254 self.allocations.push(Allocation { name: name.to_string(), span, step });
255
256 Ok(span.render_bounds())
257 }
258
259 pub fn try_allocate(&self, span: &GridSpan) -> bool {
261 if !span.is_in_bounds() {
262 return false;
263 }
264 span.cells().iter().all(|cell| !self.occupied.contains(cell))
265 }
266
267 pub fn cells_used(&self) -> usize {
269 self.occupied.len()
270 }
271
272 pub fn cells_free(&self) -> usize {
274 TOTAL_CELLS - self.occupied.len()
275 }
276
277 pub fn manifest(&self) -> String {
279 let mut out = String::from("<!-- GRID PROTOCOL MANIFEST\n");
280 out.push_str(&format!(
281 " Canvas: {}x{} | Grid: {}x{} | Cell: {}px\n",
282 CANVAS_WIDTH, CANVAS_HEIGHT, GRID_COLS, GRID_ROWS, CELL_SIZE
283 ));
284 out.push_str(&format!(
285 " Cells used: {} / {} ({:.0}%)\n",
286 self.cells_used(),
287 TOTAL_CELLS,
288 self.cells_used() as f32 / TOTAL_CELLS as f32 * 100.0
289 ));
290 out.push_str(" Allocations:\n");
291 for alloc in &self.allocations {
292 let rb = alloc.span.render_bounds();
293 out.push_str(&format!(
294 " [{}] \"{}\" span={} render=({},{},{}x{})\n",
295 alloc.step, alloc.name, alloc.span, rb.x, rb.y, rb.w, rb.h,
296 ));
297 }
298 out.push_str("-->");
299 out
300 }
301}
302
303impl Default for GridProtocol {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum LayoutTemplate {
314 TitleSlide,
316 TwoColumn,
318 Dashboard,
320 CodeWalkthrough,
322 Diagram,
324 KeyConcepts,
326 ReflectionReadings,
328}
329
330impl LayoutTemplate {
331 pub fn allocations(&self) -> Vec<(&'static str, GridSpan)> {
333 match self {
334 Self::TitleSlide => vec![
335 ("title", GridSpan::new(1, 2, 14, 4)),
336 ("subtitle", GridSpan::new(2, 5, 13, 6)),
337 ],
338 Self::TwoColumn => vec![
339 ("header", GridSpan::new(0, 0, 15, 1)),
340 ("left", GridSpan::new(0, 2, 7, 8)),
341 ("right", GridSpan::new(8, 2, 15, 8)),
342 ],
343 Self::Dashboard => vec![
344 ("header", GridSpan::new(0, 0, 15, 1)),
345 ("top_left", GridSpan::new(0, 2, 7, 4)),
346 ("top_right", GridSpan::new(8, 2, 15, 4)),
347 ("bottom_left", GridSpan::new(0, 5, 7, 8)),
348 ("bottom_right", GridSpan::new(8, 5, 15, 8)),
349 ],
350 Self::CodeWalkthrough => vec![
351 ("header", GridSpan::new(0, 0, 15, 1)),
352 ("code", GridSpan::new(0, 2, 9, 8)),
353 ("notes", GridSpan::new(10, 2, 15, 8)),
354 ],
355 Self::Diagram => vec![
356 ("header", GridSpan::new(0, 0, 15, 1)),
357 ("diagram", GridSpan::new(0, 2, 15, 8)),
358 ],
359 Self::KeyConcepts => vec![
360 ("header", GridSpan::new(0, 0, 15, 1)),
361 ("card_left", GridSpan::new(0, 2, 4, 8)),
362 ("card_center", GridSpan::new(5, 2, 10, 8)),
363 ("card_right", GridSpan::new(11, 2, 15, 8)),
364 ],
365 Self::ReflectionReadings => vec![
366 ("header", GridSpan::new(0, 0, 15, 1)),
367 ("reflection", GridSpan::new(0, 2, 15, 5)),
368 ("readings", GridSpan::new(0, 6, 15, 8)),
369 ],
370 }
371 }
372
373 pub fn apply(
375 &self,
376 protocol: &mut GridProtocol,
377 ) -> Result<Vec<(&'static str, PixelBounds)>, GridError> {
378 let mut results = Vec::new();
379 for (name, span) in self.allocations() {
380 let bounds = protocol.allocate(name, span)?;
381 results.push((name, bounds));
382 }
383 Ok(results)
384 }
385}
386
387impl fmt::Display for LayoutTemplate {
388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389 match self {
390 Self::TitleSlide => write!(f, "A: Title Slide"),
391 Self::TwoColumn => write!(f, "B: Two Column"),
392 Self::Dashboard => write!(f, "C: Dashboard"),
393 Self::CodeWalkthrough => write!(f, "D: Code Walkthrough"),
394 Self::Diagram => write!(f, "E: Diagram"),
395 Self::KeyConcepts => write!(f, "F: Key Concepts"),
396 Self::ReflectionReadings => write!(f, "G: Reflection & Readings"),
397 }
398 }
399}
400
401#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_constants() {
409 assert_eq!(GRID_COLS, 16);
410 assert_eq!(GRID_ROWS, 9);
411 assert_eq!(CELL_SIZE, 120.0);
412 assert_eq!(TOTAL_CELLS, 144);
413 assert_eq!(CANVAS_WIDTH, 1920.0);
414 assert_eq!(CANVAS_HEIGHT, 1080.0);
415 }
416
417 #[test]
418 fn test_pixel_bounds() {
419 let pb = PixelBounds::new(10.0, 20.0, 100.0, 50.0);
420 assert_eq!(pb.right(), 110.0);
421 assert_eq!(pb.bottom(), 70.0);
422 assert_eq!(pb.cx(), 60.0);
423 assert_eq!(pb.cy(), 45.0);
424 }
425
426 #[test]
427 fn test_pixel_bounds_display() {
428 let pb = PixelBounds::new(10.0, 20.0, 100.0, 50.0);
429 assert_eq!(format!("{}", pb), "(10, 20, 100x50)");
430 }
431
432 #[test]
433 fn test_grid_span_pixel_bounds() {
434 let span = GridSpan::new(0, 0, 0, 0);
436 let pb = span.pixel_bounds();
437 assert_eq!(pb.x, 0.0);
438 assert_eq!(pb.y, 0.0);
439 assert_eq!(pb.w, 120.0);
440 assert_eq!(pb.h, 120.0);
441
442 let span = GridSpan::new(1, 1, 2, 2);
444 let pb = span.pixel_bounds();
445 assert_eq!(pb.x, 120.0);
446 assert_eq!(pb.y, 120.0);
447 assert_eq!(pb.w, 240.0);
448 assert_eq!(pb.h, 240.0);
449 }
450
451 #[test]
452 fn test_grid_span_render_bounds() {
453 let span = GridSpan::new(0, 0, 0, 0);
454 let rb = span.render_bounds();
455 assert_eq!(rb.x, 10.0);
456 assert_eq!(rb.y, 10.0);
457 assert_eq!(rb.w, 100.0);
458 assert_eq!(rb.h, 100.0);
459 }
460
461 #[test]
462 fn test_grid_span_content_zone() {
463 let span = GridSpan::new(0, 0, 0, 0);
464 let cz = span.content_zone();
465 assert_eq!(cz.x, 30.0);
466 assert_eq!(cz.y, 30.0);
467 assert_eq!(cz.w, 60.0);
468 assert_eq!(cz.h, 60.0);
469 }
470
471 #[test]
472 fn test_grid_span_cells() {
473 let span = GridSpan::new(1, 2, 2, 3);
474 let cells = span.cells();
475 assert_eq!(cells.len(), 4);
476 assert!(cells.contains(&(1, 2)));
477 assert!(cells.contains(&(2, 2)));
478 assert!(cells.contains(&(1, 3)));
479 assert!(cells.contains(&(2, 3)));
480 }
481
482 #[test]
483 fn test_grid_span_cell_count() {
484 assert_eq!(GridSpan::new(0, 0, 0, 0).cell_count(), 1);
485 assert_eq!(GridSpan::new(0, 0, 1, 1).cell_count(), 4);
486 assert_eq!(GridSpan::new(0, 0, 15, 8).cell_count(), 144);
487 }
488
489 #[test]
490 fn test_grid_span_display() {
491 let span = GridSpan::new(1, 2, 3, 4);
492 assert_eq!(format!("{}", span), "(1,2)..(3,4)");
493 }
494
495 #[test]
496 fn test_grid_protocol_allocate() {
497 let mut gp = GridProtocol::new();
498 let result = gp.allocate("header", GridSpan::new(0, 0, 15, 1));
499 assert!(result.is_ok());
500 assert_eq!(gp.cells_used(), 32); assert_eq!(gp.cells_free(), 144 - 32);
502 }
503
504 #[test]
505 fn test_grid_protocol_overlap_rejected() {
506 let mut gp = GridProtocol::new();
507 gp.allocate("header", GridSpan::new(0, 0, 15, 1)).expect("unexpected failure");
508
509 let result = gp.allocate("overlap", GridSpan::new(5, 0, 10, 2));
510 assert!(result.is_err());
511 match result.unwrap_err() {
512 GridError::CellOccupied { col, row, existing_name } => {
513 assert!((5..=10).contains(&col));
514 assert_eq!(row, 0);
515 assert_eq!(existing_name, "header");
516 }
517 other => panic!("Expected CellOccupied, got: {}", other),
518 }
519 }
520
521 #[test]
522 fn test_grid_protocol_out_of_bounds() {
523 let mut gp = GridProtocol::new();
524 let result = gp.allocate("oob", GridSpan::new(0, 0, 16, 0));
525 assert!(result.is_err());
526 assert!(matches!(result.unwrap_err(), GridError::OutOfBounds { .. }));
527 }
528
529 #[test]
530 fn test_grid_protocol_try_allocate() {
531 let mut gp = GridProtocol::new();
532 let span = GridSpan::new(0, 0, 3, 3);
533 assert!(gp.try_allocate(&span));
534
535 gp.allocate("block", span).expect("unexpected failure");
536 assert!(!gp.try_allocate(&span));
537
538 assert!(gp.try_allocate(&GridSpan::new(4, 0, 7, 3)));
540
541 assert!(!gp.try_allocate(&GridSpan::new(0, 0, 16, 0)));
543 }
544
545 #[test]
546 fn test_grid_protocol_manifest() {
547 let mut gp = GridProtocol::new();
548 gp.allocate("header", GridSpan::new(0, 0, 15, 1)).expect("unexpected failure");
549 gp.allocate("body", GridSpan::new(0, 2, 15, 8)).expect("unexpected failure");
550
551 let manifest = gp.manifest();
552 assert!(manifest.contains("GRID PROTOCOL MANIFEST"));
553 assert!(manifest.contains("\"header\""));
554 assert!(manifest.contains("\"body\""));
555 assert!(manifest.contains("Cells used: 144"));
556 }
557
558 #[test]
559 fn test_grid_protocol_default() {
560 let gp = GridProtocol::default();
561 assert_eq!(gp.cells_used(), 0);
562 assert_eq!(gp.cells_free(), 144);
563 }
564
565 #[test]
566 fn test_grid_error_display() {
567 let err = GridError::CellOccupied { col: 5, row: 3, existing_name: "header".to_string() };
568 let msg = format!("{}", err);
569 assert!(msg.contains("(5, 3)"));
570 assert!(msg.contains("header"));
571
572 let err = GridError::OutOfBounds { span: GridSpan::new(0, 0, 16, 0) };
573 let msg = format!("{}", err);
574 assert!(msg.contains("outside"));
575 }
576
577 #[test]
578 fn test_no_cell_in_two_allocations() {
579 let mut gp = GridProtocol::new();
580 gp.allocate("a", GridSpan::new(0, 0, 7, 4)).expect("unexpected failure");
581 gp.allocate("b", GridSpan::new(8, 0, 15, 4)).expect("unexpected failure");
582 gp.allocate("c", GridSpan::new(0, 5, 15, 8)).expect("unexpected failure");
583
584 assert_eq!(gp.cells_used(), 144);
586 assert_eq!(gp.cells_free(), 0);
587 }
588
589 #[test]
592 fn test_layout_template_title_slide() {
593 let mut gp = GridProtocol::new();
594 let result = LayoutTemplate::TitleSlide.apply(&mut gp);
595 assert!(result.is_ok());
596 let allocs = result.expect("operation failed");
597 assert_eq!(allocs.len(), 2);
598 assert_eq!(allocs[0].0, "title");
599 assert_eq!(allocs[1].0, "subtitle");
600 }
601
602 #[test]
603 fn test_layout_template_two_column() {
604 let mut gp = GridProtocol::new();
605 let result = LayoutTemplate::TwoColumn.apply(&mut gp);
606 assert!(result.is_ok());
607 let allocs = result.expect("operation failed");
608 assert_eq!(allocs.len(), 3);
609 }
610
611 #[test]
612 fn test_layout_template_dashboard() {
613 let mut gp = GridProtocol::new();
614 let result = LayoutTemplate::Dashboard.apply(&mut gp);
615 assert!(result.is_ok());
616 let allocs = result.expect("operation failed");
617 assert_eq!(allocs.len(), 5);
618 }
619
620 #[test]
621 fn test_layout_template_code_walkthrough() {
622 let mut gp = GridProtocol::new();
623 let result = LayoutTemplate::CodeWalkthrough.apply(&mut gp);
624 assert!(result.is_ok());
625 let allocs = result.expect("operation failed");
626 assert_eq!(allocs.len(), 3);
627 assert_eq!(allocs[1].0, "code");
628 assert_eq!(allocs[2].0, "notes");
629 }
630
631 #[test]
632 fn test_layout_template_diagram() {
633 let mut gp = GridProtocol::new();
634 let result = LayoutTemplate::Diagram.apply(&mut gp);
635 assert!(result.is_ok());
636 let allocs = result.expect("operation failed");
637 assert_eq!(allocs.len(), 2);
638 assert_eq!(allocs[0].0, "header");
639 assert_eq!(allocs[1].0, "diagram");
640 }
641
642 #[test]
643 fn test_layout_template_key_concepts() {
644 let mut gp = GridProtocol::new();
645 let result = LayoutTemplate::KeyConcepts.apply(&mut gp);
646 assert!(result.is_ok());
647 assert_eq!(result.expect("operation failed").len(), 4);
648 }
649
650 #[test]
651 fn test_layout_template_reflection_readings() {
652 let mut gp = GridProtocol::new();
653 let result = LayoutTemplate::ReflectionReadings.apply(&mut gp);
654 assert!(result.is_ok());
655 assert_eq!(result.expect("operation failed").len(), 3);
656 }
657
658 #[test]
659 fn test_layout_template_no_overlaps() {
660 let templates = [
662 LayoutTemplate::TitleSlide,
663 LayoutTemplate::TwoColumn,
664 LayoutTemplate::Dashboard,
665 LayoutTemplate::CodeWalkthrough,
666 LayoutTemplate::Diagram,
667 LayoutTemplate::KeyConcepts,
668 LayoutTemplate::ReflectionReadings,
669 ];
670
671 for template in &templates {
672 let mut gp = GridProtocol::new();
673 let result = template.apply(&mut gp);
674 assert!(
675 result.is_ok(),
676 "Template {} has overlapping allocations: {:?}",
677 template,
678 result.unwrap_err()
679 );
680 }
681 }
682
683 #[test]
684 fn test_layout_template_display() {
685 assert_eq!(format!("{}", LayoutTemplate::TitleSlide), "A: Title Slide");
686 assert_eq!(format!("{}", LayoutTemplate::TwoColumn), "B: Two Column");
687 assert_eq!(format!("{}", LayoutTemplate::Dashboard), "C: Dashboard");
688 assert_eq!(format!("{}", LayoutTemplate::CodeWalkthrough), "D: Code Walkthrough");
689 assert_eq!(format!("{}", LayoutTemplate::Diagram), "E: Diagram");
690 assert_eq!(format!("{}", LayoutTemplate::KeyConcepts), "F: Key Concepts");
691 assert_eq!(format!("{}", LayoutTemplate::ReflectionReadings), "G: Reflection & Readings");
692 }
693
694 #[test]
695 fn test_canvas_dimensions_match_grid() {
696 assert_eq!(CANVAS_WIDTH, GRID_COLS as f32 * CELL_SIZE);
697 assert_eq!(CANVAS_HEIGHT, GRID_ROWS as f32 * CELL_SIZE);
698 }
699
700 #[test]
701 fn test_full_grid_span() {
702 let full = GridSpan::new(0, 0, 15, 8);
703 assert_eq!(full.cell_count(), 144);
704 let pb = full.pixel_bounds();
705 assert_eq!(pb.w, CANVAS_WIDTH);
706 assert_eq!(pb.h, CANVAS_HEIGHT);
707 }
708
709 #[test]
710 fn test_render_bounds_shrink() {
711 let span = GridSpan::new(0, 0, 1, 1);
712 let raw = span.pixel_bounds();
713 let render = span.render_bounds();
714 assert_eq!(render.x, raw.x + CELL_PADDING);
715 assert_eq!(render.y, raw.y + CELL_PADDING);
716 assert_eq!(render.w, raw.w - 2.0 * CELL_PADDING);
717 assert_eq!(render.h, raw.h - 2.0 * CELL_PADDING);
718 }
719
720 #[test]
721 fn test_content_zone_shrink() {
722 let span = GridSpan::new(0, 0, 3, 3);
723 let render = span.render_bounds();
724 let content = span.content_zone();
725 assert_eq!(content.x, render.x + INTERNAL_PADDING);
726 assert_eq!(content.y, render.y + INTERNAL_PADDING);
727 assert_eq!(content.w, render.w - 2.0 * INTERNAL_PADDING);
728 assert_eq!(content.h, render.h - 2.0 * INTERNAL_PADDING);
729 }
730
731 #[test]
732 fn test_grid_protocol_sequential_allocations() {
733 let mut gp = GridProtocol::new();
734
735 for row_block in 0..3u8 {
737 for col_block in 0..4u8 {
738 let name = format!("block_{}_{}", col_block, row_block);
739 let c1 = col_block * 4;
740 let r1 = row_block * 3;
741 let result = gp.allocate(&name, GridSpan::new(c1, r1, c1 + 3, r1 + 2));
742 assert!(result.is_ok(), "Failed to allocate {}: {:?}", name, result);
743 }
744 }
745 assert_eq!(gp.cells_used(), 144);
746 }
747}