use std::collections::HashSet;
use std::fmt;
pub const GRID_COLS: u8 = 16;
pub const GRID_ROWS: u8 = 9;
pub const CELL_SIZE: f32 = 120.0;
pub const CELL_PADDING: f32 = 10.0;
pub const INTERNAL_PADDING: f32 = 20.0;
pub const MIN_BLOCK_GAP: f32 = 20.0;
pub const TOTAL_CELLS: usize = (GRID_COLS as usize) * (GRID_ROWS as usize);
pub const CANVAS_WIDTH: f32 = 1920.0;
pub const CANVAS_HEIGHT: f32 = 1080.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PixelBounds {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl PixelBounds {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
pub fn right(&self) -> f32 {
self.x + self.w
}
pub fn bottom(&self) -> f32 {
self.y + self.h
}
pub fn cx(&self) -> f32 {
self.x + self.w / 2.0
}
pub fn cy(&self) -> f32 {
self.y + self.h / 2.0
}
}
impl fmt::Display for PixelBounds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {}, {}x{})", self.x, self.y, self.w, self.h)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GridSpan {
pub c1: u8,
pub r1: u8,
pub c2: u8,
pub r2: u8,
}
impl GridSpan {
pub fn new(c1: u8, r1: u8, c2: u8, r2: u8) -> Self {
Self { c1, r1, c2, r2 }
}
pub fn pixel_bounds(&self) -> PixelBounds {
let x = self.c1 as f32 * CELL_SIZE;
let y = self.r1 as f32 * CELL_SIZE;
let w = (self.c2 - self.c1 + 1) as f32 * CELL_SIZE;
let h = (self.r2 - self.r1 + 1) as f32 * CELL_SIZE;
PixelBounds::new(x, y, w, h)
}
pub fn render_bounds(&self) -> PixelBounds {
let raw = self.pixel_bounds();
PixelBounds::new(
raw.x + CELL_PADDING,
raw.y + CELL_PADDING,
raw.w - 2.0 * CELL_PADDING,
raw.h - 2.0 * CELL_PADDING,
)
}
pub fn content_zone(&self) -> PixelBounds {
let rb = self.render_bounds();
PixelBounds::new(
rb.x + INTERNAL_PADDING,
rb.y + INTERNAL_PADDING,
rb.w - 2.0 * INTERNAL_PADDING,
rb.h - 2.0 * INTERNAL_PADDING,
)
}
pub fn cells(&self) -> Vec<(u8, u8)> {
let mut out = Vec::with_capacity(self.cell_count());
for r in self.r1..=self.r2 {
for c in self.c1..=self.c2 {
out.push((c, r));
}
}
out
}
pub fn cell_count(&self) -> usize {
(self.c2 - self.c1 + 1) as usize * (self.r2 - self.r1 + 1) as usize
}
fn is_in_bounds(&self) -> bool {
self.c1 <= self.c2 && self.r1 <= self.r2 && self.c2 < GRID_COLS && self.r2 < GRID_ROWS
}
}
impl fmt::Display for GridSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({},{})..({},{})", self.c1, self.r1, self.c2, self.r2)
}
}
#[derive(Debug, Clone)]
pub enum GridError {
CellOccupied { col: u8, row: u8, existing_name: String },
OutOfBounds { span: GridSpan },
}
impl fmt::Display for GridError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CellOccupied { col, row, existing_name } => {
write!(f, "Cell ({}, {}) already occupied by '{}'", col, row, existing_name)
}
Self::OutOfBounds { span } => {
write!(f, "Span {} is outside the {}x{} grid", span, GRID_COLS, GRID_ROWS)
}
}
}
}
impl std::error::Error for GridError {}
#[derive(Debug, Clone)]
struct Allocation {
name: String,
span: GridSpan,
step: usize,
}
#[derive(Debug)]
pub struct GridProtocol {
occupied: HashSet<(u8, u8)>,
cell_owner: std::collections::HashMap<(u8, u8), String>,
allocations: Vec<Allocation>,
}
impl GridProtocol {
pub fn new() -> Self {
Self {
occupied: HashSet::new(),
cell_owner: std::collections::HashMap::new(),
allocations: Vec::new(),
}
}
pub fn allocate(&mut self, name: &str, span: GridSpan) -> Result<PixelBounds, GridError> {
if !span.is_in_bounds() {
return Err(GridError::OutOfBounds { span });
}
for (c, r) in span.cells() {
if self.occupied.contains(&(c, r)) {
let existing = self.cell_owner.get(&(c, r)).cloned().unwrap_or_default();
return Err(GridError::CellOccupied { col: c, row: r, existing_name: existing });
}
}
let step = self.allocations.len();
for (c, r) in span.cells() {
self.occupied.insert((c, r));
self.cell_owner.insert((c, r), name.to_string());
}
self.allocations.push(Allocation { name: name.to_string(), span, step });
Ok(span.render_bounds())
}
pub fn try_allocate(&self, span: &GridSpan) -> bool {
if !span.is_in_bounds() {
return false;
}
span.cells().iter().all(|cell| !self.occupied.contains(cell))
}
pub fn cells_used(&self) -> usize {
self.occupied.len()
}
pub fn cells_free(&self) -> usize {
TOTAL_CELLS - self.occupied.len()
}
pub fn manifest(&self) -> String {
let mut out = String::from("<!-- GRID PROTOCOL MANIFEST\n");
out.push_str(&format!(
" Canvas: {}x{} | Grid: {}x{} | Cell: {}px\n",
CANVAS_WIDTH, CANVAS_HEIGHT, GRID_COLS, GRID_ROWS, CELL_SIZE
));
out.push_str(&format!(
" Cells used: {} / {} ({:.0}%)\n",
self.cells_used(),
TOTAL_CELLS,
self.cells_used() as f32 / TOTAL_CELLS as f32 * 100.0
));
out.push_str(" Allocations:\n");
for alloc in &self.allocations {
let rb = alloc.span.render_bounds();
out.push_str(&format!(
" [{}] \"{}\" span={} render=({},{},{}x{})\n",
alloc.step, alloc.name, alloc.span, rb.x, rb.y, rb.w, rb.h,
));
}
out.push_str("-->");
out
}
}
impl Default for GridProtocol {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutTemplate {
TitleSlide,
TwoColumn,
Dashboard,
CodeWalkthrough,
Diagram,
KeyConcepts,
ReflectionReadings,
}
impl LayoutTemplate {
pub fn allocations(&self) -> Vec<(&'static str, GridSpan)> {
match self {
Self::TitleSlide => vec![
("title", GridSpan::new(1, 2, 14, 4)),
("subtitle", GridSpan::new(2, 5, 13, 6)),
],
Self::TwoColumn => vec![
("header", GridSpan::new(0, 0, 15, 1)),
("left", GridSpan::new(0, 2, 7, 8)),
("right", GridSpan::new(8, 2, 15, 8)),
],
Self::Dashboard => vec![
("header", GridSpan::new(0, 0, 15, 1)),
("top_left", GridSpan::new(0, 2, 7, 4)),
("top_right", GridSpan::new(8, 2, 15, 4)),
("bottom_left", GridSpan::new(0, 5, 7, 8)),
("bottom_right", GridSpan::new(8, 5, 15, 8)),
],
Self::CodeWalkthrough => vec![
("header", GridSpan::new(0, 0, 15, 1)),
("code", GridSpan::new(0, 2, 9, 8)),
("notes", GridSpan::new(10, 2, 15, 8)),
],
Self::Diagram => vec![
("header", GridSpan::new(0, 0, 15, 1)),
("diagram", GridSpan::new(0, 2, 15, 8)),
],
Self::KeyConcepts => vec![
("header", GridSpan::new(0, 0, 15, 1)),
("card_left", GridSpan::new(0, 2, 4, 8)),
("card_center", GridSpan::new(5, 2, 10, 8)),
("card_right", GridSpan::new(11, 2, 15, 8)),
],
Self::ReflectionReadings => vec![
("header", GridSpan::new(0, 0, 15, 1)),
("reflection", GridSpan::new(0, 2, 15, 5)),
("readings", GridSpan::new(0, 6, 15, 8)),
],
}
}
pub fn apply(
&self,
protocol: &mut GridProtocol,
) -> Result<Vec<(&'static str, PixelBounds)>, GridError> {
let mut results = Vec::new();
for (name, span) in self.allocations() {
let bounds = protocol.allocate(name, span)?;
results.push((name, bounds));
}
Ok(results)
}
}
impl fmt::Display for LayoutTemplate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TitleSlide => write!(f, "A: Title Slide"),
Self::TwoColumn => write!(f, "B: Two Column"),
Self::Dashboard => write!(f, "C: Dashboard"),
Self::CodeWalkthrough => write!(f, "D: Code Walkthrough"),
Self::Diagram => write!(f, "E: Diagram"),
Self::KeyConcepts => write!(f, "F: Key Concepts"),
Self::ReflectionReadings => write!(f, "G: Reflection & Readings"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constants() {
assert_eq!(GRID_COLS, 16);
assert_eq!(GRID_ROWS, 9);
assert_eq!(CELL_SIZE, 120.0);
assert_eq!(TOTAL_CELLS, 144);
assert_eq!(CANVAS_WIDTH, 1920.0);
assert_eq!(CANVAS_HEIGHT, 1080.0);
}
#[test]
fn test_pixel_bounds() {
let pb = PixelBounds::new(10.0, 20.0, 100.0, 50.0);
assert_eq!(pb.right(), 110.0);
assert_eq!(pb.bottom(), 70.0);
assert_eq!(pb.cx(), 60.0);
assert_eq!(pb.cy(), 45.0);
}
#[test]
fn test_pixel_bounds_display() {
let pb = PixelBounds::new(10.0, 20.0, 100.0, 50.0);
assert_eq!(format!("{}", pb), "(10, 20, 100x50)");
}
#[test]
fn test_grid_span_pixel_bounds() {
let span = GridSpan::new(0, 0, 0, 0);
let pb = span.pixel_bounds();
assert_eq!(pb.x, 0.0);
assert_eq!(pb.y, 0.0);
assert_eq!(pb.w, 120.0);
assert_eq!(pb.h, 120.0);
let span = GridSpan::new(1, 1, 2, 2);
let pb = span.pixel_bounds();
assert_eq!(pb.x, 120.0);
assert_eq!(pb.y, 120.0);
assert_eq!(pb.w, 240.0);
assert_eq!(pb.h, 240.0);
}
#[test]
fn test_grid_span_render_bounds() {
let span = GridSpan::new(0, 0, 0, 0);
let rb = span.render_bounds();
assert_eq!(rb.x, 10.0);
assert_eq!(rb.y, 10.0);
assert_eq!(rb.w, 100.0);
assert_eq!(rb.h, 100.0);
}
#[test]
fn test_grid_span_content_zone() {
let span = GridSpan::new(0, 0, 0, 0);
let cz = span.content_zone();
assert_eq!(cz.x, 30.0);
assert_eq!(cz.y, 30.0);
assert_eq!(cz.w, 60.0);
assert_eq!(cz.h, 60.0);
}
#[test]
fn test_grid_span_cells() {
let span = GridSpan::new(1, 2, 2, 3);
let cells = span.cells();
assert_eq!(cells.len(), 4);
assert!(cells.contains(&(1, 2)));
assert!(cells.contains(&(2, 2)));
assert!(cells.contains(&(1, 3)));
assert!(cells.contains(&(2, 3)));
}
#[test]
fn test_grid_span_cell_count() {
assert_eq!(GridSpan::new(0, 0, 0, 0).cell_count(), 1);
assert_eq!(GridSpan::new(0, 0, 1, 1).cell_count(), 4);
assert_eq!(GridSpan::new(0, 0, 15, 8).cell_count(), 144);
}
#[test]
fn test_grid_span_display() {
let span = GridSpan::new(1, 2, 3, 4);
assert_eq!(format!("{}", span), "(1,2)..(3,4)");
}
#[test]
fn test_grid_protocol_allocate() {
let mut gp = GridProtocol::new();
let result = gp.allocate("header", GridSpan::new(0, 0, 15, 1));
assert!(result.is_ok());
assert_eq!(gp.cells_used(), 32); assert_eq!(gp.cells_free(), 144 - 32);
}
#[test]
fn test_grid_protocol_overlap_rejected() {
let mut gp = GridProtocol::new();
gp.allocate("header", GridSpan::new(0, 0, 15, 1)).expect("unexpected failure");
let result = gp.allocate("overlap", GridSpan::new(5, 0, 10, 2));
assert!(result.is_err());
match result.unwrap_err() {
GridError::CellOccupied { col, row, existing_name } => {
assert!((5..=10).contains(&col));
assert_eq!(row, 0);
assert_eq!(existing_name, "header");
}
other => panic!("Expected CellOccupied, got: {}", other),
}
}
#[test]
fn test_grid_protocol_out_of_bounds() {
let mut gp = GridProtocol::new();
let result = gp.allocate("oob", GridSpan::new(0, 0, 16, 0));
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), GridError::OutOfBounds { .. }));
}
#[test]
fn test_grid_protocol_try_allocate() {
let mut gp = GridProtocol::new();
let span = GridSpan::new(0, 0, 3, 3);
assert!(gp.try_allocate(&span));
gp.allocate("block", span).expect("unexpected failure");
assert!(!gp.try_allocate(&span));
assert!(gp.try_allocate(&GridSpan::new(4, 0, 7, 3)));
assert!(!gp.try_allocate(&GridSpan::new(0, 0, 16, 0)));
}
#[test]
fn test_grid_protocol_manifest() {
let mut gp = GridProtocol::new();
gp.allocate("header", GridSpan::new(0, 0, 15, 1)).expect("unexpected failure");
gp.allocate("body", GridSpan::new(0, 2, 15, 8)).expect("unexpected failure");
let manifest = gp.manifest();
assert!(manifest.contains("GRID PROTOCOL MANIFEST"));
assert!(manifest.contains("\"header\""));
assert!(manifest.contains("\"body\""));
assert!(manifest.contains("Cells used: 144"));
}
#[test]
fn test_grid_protocol_default() {
let gp = GridProtocol::default();
assert_eq!(gp.cells_used(), 0);
assert_eq!(gp.cells_free(), 144);
}
#[test]
fn test_grid_error_display() {
let err = GridError::CellOccupied { col: 5, row: 3, existing_name: "header".to_string() };
let msg = format!("{}", err);
assert!(msg.contains("(5, 3)"));
assert!(msg.contains("header"));
let err = GridError::OutOfBounds { span: GridSpan::new(0, 0, 16, 0) };
let msg = format!("{}", err);
assert!(msg.contains("outside"));
}
#[test]
fn test_no_cell_in_two_allocations() {
let mut gp = GridProtocol::new();
gp.allocate("a", GridSpan::new(0, 0, 7, 4)).expect("unexpected failure");
gp.allocate("b", GridSpan::new(8, 0, 15, 4)).expect("unexpected failure");
gp.allocate("c", GridSpan::new(0, 5, 15, 8)).expect("unexpected failure");
assert_eq!(gp.cells_used(), 144);
assert_eq!(gp.cells_free(), 0);
}
#[test]
fn test_layout_template_title_slide() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::TitleSlide.apply(&mut gp);
assert!(result.is_ok());
let allocs = result.expect("operation failed");
assert_eq!(allocs.len(), 2);
assert_eq!(allocs[0].0, "title");
assert_eq!(allocs[1].0, "subtitle");
}
#[test]
fn test_layout_template_two_column() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::TwoColumn.apply(&mut gp);
assert!(result.is_ok());
let allocs = result.expect("operation failed");
assert_eq!(allocs.len(), 3);
}
#[test]
fn test_layout_template_dashboard() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::Dashboard.apply(&mut gp);
assert!(result.is_ok());
let allocs = result.expect("operation failed");
assert_eq!(allocs.len(), 5);
}
#[test]
fn test_layout_template_code_walkthrough() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::CodeWalkthrough.apply(&mut gp);
assert!(result.is_ok());
let allocs = result.expect("operation failed");
assert_eq!(allocs.len(), 3);
assert_eq!(allocs[1].0, "code");
assert_eq!(allocs[2].0, "notes");
}
#[test]
fn test_layout_template_diagram() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::Diagram.apply(&mut gp);
assert!(result.is_ok());
let allocs = result.expect("operation failed");
assert_eq!(allocs.len(), 2);
assert_eq!(allocs[0].0, "header");
assert_eq!(allocs[1].0, "diagram");
}
#[test]
fn test_layout_template_key_concepts() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::KeyConcepts.apply(&mut gp);
assert!(result.is_ok());
assert_eq!(result.expect("operation failed").len(), 4);
}
#[test]
fn test_layout_template_reflection_readings() {
let mut gp = GridProtocol::new();
let result = LayoutTemplate::ReflectionReadings.apply(&mut gp);
assert!(result.is_ok());
assert_eq!(result.expect("operation failed").len(), 3);
}
#[test]
fn test_layout_template_no_overlaps() {
let templates = [
LayoutTemplate::TitleSlide,
LayoutTemplate::TwoColumn,
LayoutTemplate::Dashboard,
LayoutTemplate::CodeWalkthrough,
LayoutTemplate::Diagram,
LayoutTemplate::KeyConcepts,
LayoutTemplate::ReflectionReadings,
];
for template in &templates {
let mut gp = GridProtocol::new();
let result = template.apply(&mut gp);
assert!(
result.is_ok(),
"Template {} has overlapping allocations: {:?}",
template,
result.unwrap_err()
);
}
}
#[test]
fn test_layout_template_display() {
assert_eq!(format!("{}", LayoutTemplate::TitleSlide), "A: Title Slide");
assert_eq!(format!("{}", LayoutTemplate::TwoColumn), "B: Two Column");
assert_eq!(format!("{}", LayoutTemplate::Dashboard), "C: Dashboard");
assert_eq!(format!("{}", LayoutTemplate::CodeWalkthrough), "D: Code Walkthrough");
assert_eq!(format!("{}", LayoutTemplate::Diagram), "E: Diagram");
assert_eq!(format!("{}", LayoutTemplate::KeyConcepts), "F: Key Concepts");
assert_eq!(format!("{}", LayoutTemplate::ReflectionReadings), "G: Reflection & Readings");
}
#[test]
fn test_canvas_dimensions_match_grid() {
assert_eq!(CANVAS_WIDTH, GRID_COLS as f32 * CELL_SIZE);
assert_eq!(CANVAS_HEIGHT, GRID_ROWS as f32 * CELL_SIZE);
}
#[test]
fn test_full_grid_span() {
let full = GridSpan::new(0, 0, 15, 8);
assert_eq!(full.cell_count(), 144);
let pb = full.pixel_bounds();
assert_eq!(pb.w, CANVAS_WIDTH);
assert_eq!(pb.h, CANVAS_HEIGHT);
}
#[test]
fn test_render_bounds_shrink() {
let span = GridSpan::new(0, 0, 1, 1);
let raw = span.pixel_bounds();
let render = span.render_bounds();
assert_eq!(render.x, raw.x + CELL_PADDING);
assert_eq!(render.y, raw.y + CELL_PADDING);
assert_eq!(render.w, raw.w - 2.0 * CELL_PADDING);
assert_eq!(render.h, raw.h - 2.0 * CELL_PADDING);
}
#[test]
fn test_content_zone_shrink() {
let span = GridSpan::new(0, 0, 3, 3);
let render = span.render_bounds();
let content = span.content_zone();
assert_eq!(content.x, render.x + INTERNAL_PADDING);
assert_eq!(content.y, render.y + INTERNAL_PADDING);
assert_eq!(content.w, render.w - 2.0 * INTERNAL_PADDING);
assert_eq!(content.h, render.h - 2.0 * INTERNAL_PADDING);
}
#[test]
fn test_grid_protocol_sequential_allocations() {
let mut gp = GridProtocol::new();
for row_block in 0..3u8 {
for col_block in 0..4u8 {
let name = format!("block_{}_{}", col_block, row_block);
let c1 = col_block * 4;
let r1 = row_block * 3;
let result = gp.allocate(&name, GridSpan::new(c1, r1, c1 + 3, r1 + 2));
assert!(result.is_ok(), "Failed to allocate {}: {:?}", name, result);
}
}
assert_eq!(gp.cells_used(), 144);
}
}