From f730fa54d9bb6f9b514c23e74d037bfee9ddf825 Mon Sep 17 00:00:00 2001 From: tauseefk Date: Tue, 19 May 2026 23:25:06 -0700 Subject: [PATCH 1/8] account for subtree depth --- Cargo.lock | 76 +++++++++------- src/algo/calendar_tree.rs | 174 ++++++++++++++++++++++++++++++++++-- src/components/calendar.rs | 2 +- src/get_position_offsets.rs | 59 +++++++++++- src/main.rs | 5 +- 5 files changed, 267 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01fc023..e226011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -151,7 +151,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -272,7 +272,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -480,7 +480,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "version_check", ] @@ -497,18 +497,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -549,6 +549,12 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.12" @@ -583,7 +589,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -633,6 +639,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -650,7 +667,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -725,26 +742,14 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" -dependencies = [ - "bumpalo", - "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -762,9 +767,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -772,22 +777,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" diff --git a/src/algo/calendar_tree.rs b/src/algo/calendar_tree.rs index 82ef235..ce86e60 100644 --- a/src/algo/calendar_tree.rs +++ b/src/algo/calendar_tree.rs @@ -4,6 +4,7 @@ use crate::prelude::*; pub struct FlattenedCalendarBlock { pub block: CalendarBlock, pub stack_position: usize, + pub subtree_height: usize, } pub struct CalendarBlockTree { @@ -112,17 +113,17 @@ impl CalendarBlockTree { } pub fn traverse(&self) -> Vec { - let mut traversal_queue: VecDeque<(NodeIndex, usize)> = + let mut traversal_queue: VecDeque<(NodeIndex, usize, usize)> = VecDeque::with_capacity(self.id_to_block_map.iter().len()); - let mut buffer: Vec<(NodeIndex, usize)> = + let mut buffer: Vec<(NodeIndex, usize, usize)> = Vec::with_capacity(self.id_to_block_map.iter().len()); - traversal_queue.push_back((self.root_idx, 0)); + traversal_queue.push_back((self.root_idx, 0, 0)); while !traversal_queue.is_empty() { - let (node_idx, stack_position) = traversal_queue.pop_front().unwrap(); - buffer.push((node_idx, stack_position)); + let (node_idx, stack_position, subtree_height) = traversal_queue.pop_front().unwrap(); + buffer.push((node_idx, stack_position, subtree_height)); let forward_neighbors = self .adjacency @@ -130,20 +131,179 @@ impl CalendarBlockTree { .map(|e| e.target()); forward_neighbors.for_each(|n| { - traversal_queue.push_back((n, stack_position + 1)); + // Cluster tops are the root's direct children; a top's subtree_depth is the + // longest chain in its cluster, so its subtree height is 1 + subtree_depth. + // Deeper nodes inherit their cluster top's height, so a node's column grid + // never depends on its own depth. + let child_subtree_height = if stack_position == 0 { + let block_id = self.adjacency[n]; + 1 + self.id_to_block_map.get(&block_id).unwrap().subtree_depth + } else { + subtree_height + }; + traversal_queue.push_back((n, stack_position + 1, child_subtree_height)); }); } buffer .iter() - .map(|(node_idx, stack_position)| { + .map(|(node_idx, stack_position, subtree_height)| { let current_block_id = self.adjacency[*node_idx]; let current_block = self.id_to_block_map.get(¤t_block_id).unwrap(); FlattenedCalendarBlock { block: current_block.clone(), stack_position: *stack_position, + subtree_height: *subtree_height, } }) .collect() } } + +#[cfg(test)] +mod tests { + use super::*; + + /// The default dataset from `main.rs` (`app`'s `calendar_blocks`), including the same + /// `start/end - BLOCK_TOP_OFFSET` minutes and the same `sort_by` ordering, so the tree + /// built here matches what the app builds. + fn default_blocks() -> Vec { + let mut blocks = vec![ + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 530 - BLOCK_TOP_OFFSET, + end_minute: 830 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Available, + subtree_depth: 0, + label: String::from("Available"), + }, + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 550 - BLOCK_TOP_OFFSET, + end_minute: 590 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Busy, + subtree_depth: 0, + label: String::from("Shower"), + }, + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 550 - BLOCK_TOP_OFFSET, + end_minute: 580 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Busy, + subtree_depth: 0, + label: String::from("Shower Thoughts"), + }, + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 605 - BLOCK_TOP_OFFSET, + end_minute: 665 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Busy, + subtree_depth: 0, + label: String::from("Coffee"), + }, + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 605 - BLOCK_TOP_OFFSET, + end_minute: 630 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Busy, + subtree_depth: 0, + label: String::from("Brew"), + }, + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 635 - BLOCK_TOP_OFFSET, + end_minute: 710 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Busy, + subtree_depth: 0, + label: String::from("Contemplation"), + }, + CalendarBlock { + id: Uuid::new_v4(), + start_minute: 650 - BLOCK_TOP_OFFSET, + end_minute: 830 - BLOCK_TOP_OFFSET, + block_type: CalendarBlockType::Busy, + subtree_depth: 0, + label: String::from("Code"), + }, + ]; + + blocks.sort_by(|a, b| { + if a.start_minute < b.start_minute + || a.start_minute == b.start_minute && a.end_minute >= b.end_minute + { + Ordering::Less + } else { + Ordering::Greater + } + }); + + blocks + } + + fn build_default_tree() -> CalendarBlockTree { + let mut tree = CalendarBlockTree::new(); + for block in default_blocks() { + let _ = tree.add(block, None); + } + tree + } + + fn flattened_by_label() -> HashMap { + build_default_tree() + .traverse() + .into_iter() + .map(|f| (f.block.label.clone(), f)) + .collect() + } + + #[test] + fn traverse_default_structure() { + let by_label = flattened_by_label(); + + // (stack_position, subtree_depth, subtree_height) per block. The single synthetic + // root has stack_position 0; the cluster height is 1 + Available.subtree_depth = 4. + let expected: &[(&str, usize, usize, usize)] = &[ + ("Available", 1, 3, 4), + ("Shower", 2, 1, 4), + ("Shower Thoughts", 3, 0, 4), + ("Coffee", 2, 2, 4), + ("Brew", 3, 0, 4), + ("Contemplation", 3, 1, 4), + ("Code", 4, 0, 4), + ]; + + for (label, s, sd, height) in expected { + let f = by_label + .get(*label) + .unwrap_or_else(|| panic!("missing block: {label}")); + assert_eq!( + (f.stack_position, f.block.subtree_depth, f.subtree_height), + (*s, *sd, *height), + "block {label}: (stack_position, subtree_depth, subtree_height)" + ); + } + + // The wrapper root is the only node at stack_position 0. + let root = by_label + .values() + .find(|f| f.block.block_type == CalendarBlockType::Wrapper) + .expect("wrapper root present"); + assert_eq!(root.stack_position, 0); + } + + #[test] + fn brew_and_shower_thoughts_have_identical_inputs() { + // Brew (shallow sibling of a deeper branch) and Shower Thoughts (a nested-only + // leaf) share `(stack_position, subtree_depth, subtree_height)`, so both render to + // the same span [50%, 100%]. That is the intended result, not a defect: both are + // leaves filling to the right edge. + let by_label = flattened_by_label(); + let brew = &by_label["Brew"]; + let st = &by_label["Shower Thoughts"]; + + assert_eq!( + (brew.stack_position, brew.block.subtree_depth, brew.subtree_height), + (st.stack_position, st.block.subtree_depth, st.subtree_height), + ); + } +} diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 473f4b9..710f688 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -129,7 +129,7 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { }; let (left, width) = match use_subtree_depth_algorithm.get() { - true => get_subtree_depth_transforms(flattened_block.stack_position, flattened_block.block.subtree_depth), + true => get_subtree_depth_transforms(flattened_block.stack_position, flattened_block.block.subtree_depth, flattened_block.subtree_height), false => get_position_offsets(flattened_block.stack_position) }; let top = format!("{}px", flattened_block.block.start_minute); diff --git a/src/get_position_offsets.rs b/src/get_position_offsets.rs index c688742..d038c68 100644 --- a/src/get_position_offsets.rs +++ b/src/get_position_offsets.rs @@ -17,24 +17,75 @@ pub fn get_position_offsets(stack_position: usize) -> (String, String) { pub fn get_subtree_depth_transforms( stack_position: usize, subtree_depth: usize, + subtree_height: usize, ) -> (String, String) { let stack_position = stack_position as f64; let subtree_depth = subtree_depth as f64; - let width_divisor = stack_position + subtree_depth; + // Divide the row by the cluster's subtree height so siblings share one column grid; a + // node off the longest path no longer shrinks itself by its own (shorter) depth. + let height = subtree_height as f64; match stack_position < 1.0 { true => (0.to_string(), MAX_COL_WIDTH.to_string()), false => { let width = match subtree_depth > 0.0 { - true => 1.8 / width_divisor, - false => 1.0 / width_divisor, + // non-leaf: cover its own subtree, filling behind its children + true => (subtree_depth + 1.0) / height, + // leaf: fill from its left edge to the right edge + false => (height - stack_position + 1.0) / height, }; ( - format!("calc(100% * {})", (stack_position - 1.0) / width_divisor), + format!("calc(100% * {})", (stack_position - 1.0) / height), format!("calc(100% * {width})"), ) } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Expected transform from explicit `numerator/denominator` fractions, so each case + /// reads as intent rather than re-deriving the function's arithmetic. + fn transform(left_num: f64, width_num: f64, denominator: f64) -> (String, String) { + ( + format!("calc(100% * {})", left_num / denominator), + format!("calc(100% * {})", width_num / denominator), + ) + } + + #[test] + fn transforms_for_default_blocks() { + // (stack_position, subtree_depth, subtree_height) per block of the default dataset, + // matching `calendar_tree::tests::traverse_default_structure`. height = 4. + // left = (s-1)/H; non-leaf width = (t+1)/H; leaf width = (H-s+1)/H. + let h = 4.0; + + // Available: s=1, t=3, non-leaf -> left 0/4, width 4/4 (fills, covers subtree). + assert_eq!(get_subtree_depth_transforms(1, 3, 4), transform(0.0, 4.0, h)); + // Shower: s=2, t=1, non-leaf -> left 1/4, width 2/4. + assert_eq!(get_subtree_depth_transforms(2, 1, 4), transform(1.0, 2.0, h)); + // Coffee: s=2, t=2, non-leaf -> left 1/4, width 3/4. + assert_eq!(get_subtree_depth_transforms(2, 2, 4), transform(1.0, 3.0, h)); + // Contemplation: s=3, t=1, non-leaf -> left 2/4, width 2/4. + assert_eq!(get_subtree_depth_transforms(3, 1, 4), transform(2.0, 2.0, h)); + // Code: s=4, t=0, leaf -> left 3/4, width (4-4+1)/4 = 1/4. + assert_eq!(get_subtree_depth_transforms(4, 0, 4), transform(3.0, 1.0, h)); + // Brew & Shower Thoughts: s=3, t=0, leaf -> left 2/4, width (4-3+1)/4 = 2/4. + assert_eq!(get_subtree_depth_transforms(3, 0, 4), transform(2.0, 2.0, h)); + } + + #[test] + fn sibling_leaves_share_transform() { + // Brew and Shower Thoughts share (stack_position, subtree_depth, subtree_height) = + // (3, 0, 4), so they intentionally render to the same span [50%, 100%]. Both are + // leaves filling to the right edge; sharing a transform is correct, not a defect. + assert_eq!( + get_subtree_depth_transforms(3, 0, 4), + transform(2.0, 2.0, 4.0), + ); + } +} diff --git a/src/main.rs b/src/main.rs index 952c231..7f9c7be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,12 +12,11 @@ mod prelude { pub use core::fmt; pub use dioxus::events::MouseEvent; pub use dioxus::prelude::*; - pub use log::{info, Level}; + pub use log::info; pub use petgraph::dot::Dot; pub use petgraph::graph::{Graph, NodeIndex}; pub use petgraph::visit::EdgeRef; - pub use serde::Deserialize; - pub use thiserror::Error; + pub use uuid::Uuid; pub use crate::algo::calendar_block::*; From 76b4dcfc8b69a751c4f0968301372852f1695c9a Mon Sep 17 00:00:00 2001 From: tauseefk Date: Wed, 20 May 2026 00:52:50 -0700 Subject: [PATCH 2/8] expand shallower subtrees to touch right edfge --- src/algo/calendar_tree.rs | 152 ------------------------------------ src/get_position_offsets.rs | 57 ++------------ 2 files changed, 6 insertions(+), 203 deletions(-) diff --git a/src/algo/calendar_tree.rs b/src/algo/calendar_tree.rs index ce86e60..a1c1e8a 100644 --- a/src/algo/calendar_tree.rs +++ b/src/algo/calendar_tree.rs @@ -131,10 +131,6 @@ impl CalendarBlockTree { .map(|e| e.target()); forward_neighbors.for_each(|n| { - // Cluster tops are the root's direct children; a top's subtree_depth is the - // longest chain in its cluster, so its subtree height is 1 + subtree_depth. - // Deeper nodes inherit their cluster top's height, so a node's column grid - // never depends on its own depth. let child_subtree_height = if stack_position == 0 { let block_id = self.adjacency[n]; 1 + self.id_to_block_map.get(&block_id).unwrap().subtree_depth @@ -159,151 +155,3 @@ impl CalendarBlockTree { .collect() } } - -#[cfg(test)] -mod tests { - use super::*; - - /// The default dataset from `main.rs` (`app`'s `calendar_blocks`), including the same - /// `start/end - BLOCK_TOP_OFFSET` minutes and the same `sort_by` ordering, so the tree - /// built here matches what the app builds. - fn default_blocks() -> Vec { - let mut blocks = vec![ - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 530 - BLOCK_TOP_OFFSET, - end_minute: 830 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Available, - subtree_depth: 0, - label: String::from("Available"), - }, - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 550 - BLOCK_TOP_OFFSET, - end_minute: 590 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Busy, - subtree_depth: 0, - label: String::from("Shower"), - }, - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 550 - BLOCK_TOP_OFFSET, - end_minute: 580 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Busy, - subtree_depth: 0, - label: String::from("Shower Thoughts"), - }, - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 605 - BLOCK_TOP_OFFSET, - end_minute: 665 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Busy, - subtree_depth: 0, - label: String::from("Coffee"), - }, - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 605 - BLOCK_TOP_OFFSET, - end_minute: 630 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Busy, - subtree_depth: 0, - label: String::from("Brew"), - }, - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 635 - BLOCK_TOP_OFFSET, - end_minute: 710 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Busy, - subtree_depth: 0, - label: String::from("Contemplation"), - }, - CalendarBlock { - id: Uuid::new_v4(), - start_minute: 650 - BLOCK_TOP_OFFSET, - end_minute: 830 - BLOCK_TOP_OFFSET, - block_type: CalendarBlockType::Busy, - subtree_depth: 0, - label: String::from("Code"), - }, - ]; - - blocks.sort_by(|a, b| { - if a.start_minute < b.start_minute - || a.start_minute == b.start_minute && a.end_minute >= b.end_minute - { - Ordering::Less - } else { - Ordering::Greater - } - }); - - blocks - } - - fn build_default_tree() -> CalendarBlockTree { - let mut tree = CalendarBlockTree::new(); - for block in default_blocks() { - let _ = tree.add(block, None); - } - tree - } - - fn flattened_by_label() -> HashMap { - build_default_tree() - .traverse() - .into_iter() - .map(|f| (f.block.label.clone(), f)) - .collect() - } - - #[test] - fn traverse_default_structure() { - let by_label = flattened_by_label(); - - // (stack_position, subtree_depth, subtree_height) per block. The single synthetic - // root has stack_position 0; the cluster height is 1 + Available.subtree_depth = 4. - let expected: &[(&str, usize, usize, usize)] = &[ - ("Available", 1, 3, 4), - ("Shower", 2, 1, 4), - ("Shower Thoughts", 3, 0, 4), - ("Coffee", 2, 2, 4), - ("Brew", 3, 0, 4), - ("Contemplation", 3, 1, 4), - ("Code", 4, 0, 4), - ]; - - for (label, s, sd, height) in expected { - let f = by_label - .get(*label) - .unwrap_or_else(|| panic!("missing block: {label}")); - assert_eq!( - (f.stack_position, f.block.subtree_depth, f.subtree_height), - (*s, *sd, *height), - "block {label}: (stack_position, subtree_depth, subtree_height)" - ); - } - - // The wrapper root is the only node at stack_position 0. - let root = by_label - .values() - .find(|f| f.block.block_type == CalendarBlockType::Wrapper) - .expect("wrapper root present"); - assert_eq!(root.stack_position, 0); - } - - #[test] - fn brew_and_shower_thoughts_have_identical_inputs() { - // Brew (shallow sibling of a deeper branch) and Shower Thoughts (a nested-only - // leaf) share `(stack_position, subtree_depth, subtree_height)`, so both render to - // the same span [50%, 100%]. That is the intended result, not a defect: both are - // leaves filling to the right edge. - let by_label = flattened_by_label(); - let brew = &by_label["Brew"]; - let st = &by_label["Shower Thoughts"]; - - assert_eq!( - (brew.stack_position, brew.block.subtree_depth, brew.subtree_height), - (st.stack_position, st.block.subtree_depth, st.subtree_height), - ); - } -} diff --git a/src/get_position_offsets.rs b/src/get_position_offsets.rs index d038c68..7055e6b 100644 --- a/src/get_position_offsets.rs +++ b/src/get_position_offsets.rs @@ -22,17 +22,18 @@ pub fn get_subtree_depth_transforms( let stack_position = stack_position as f64; let subtree_depth = subtree_depth as f64; - // Divide the row by the cluster's subtree height so siblings share one column grid; a - // node off the longest path no longer shrinks itself by its own (shorter) depth. + // The cluster height is the shared column grid, so siblings on different-depth + // branches stay horizontally aligned. let height = subtree_height as f64; match stack_position < 1.0 { true => (0.to_string(), MAX_COL_WIDTH.to_string()), false => { let width = match subtree_depth > 0.0 { - // non-leaf: cover its own subtree, filling behind its children - true => (subtree_depth + 1.0) / height, - // leaf: fill from its left edge to the right edge + // non-leaf: one column plus a half-column overlap into its child, so + // nesting stays visible but the block never reaches the right edge. + true => 1.5 / height, + // leaf: fill from its own column to the container's right edge. false => (height - stack_position + 1.0) / height, }; @@ -43,49 +44,3 @@ pub fn get_subtree_depth_transforms( } } } - -#[cfg(test)] -mod tests { - use super::*; - - /// Expected transform from explicit `numerator/denominator` fractions, so each case - /// reads as intent rather than re-deriving the function's arithmetic. - fn transform(left_num: f64, width_num: f64, denominator: f64) -> (String, String) { - ( - format!("calc(100% * {})", left_num / denominator), - format!("calc(100% * {})", width_num / denominator), - ) - } - - #[test] - fn transforms_for_default_blocks() { - // (stack_position, subtree_depth, subtree_height) per block of the default dataset, - // matching `calendar_tree::tests::traverse_default_structure`. height = 4. - // left = (s-1)/H; non-leaf width = (t+1)/H; leaf width = (H-s+1)/H. - let h = 4.0; - - // Available: s=1, t=3, non-leaf -> left 0/4, width 4/4 (fills, covers subtree). - assert_eq!(get_subtree_depth_transforms(1, 3, 4), transform(0.0, 4.0, h)); - // Shower: s=2, t=1, non-leaf -> left 1/4, width 2/4. - assert_eq!(get_subtree_depth_transforms(2, 1, 4), transform(1.0, 2.0, h)); - // Coffee: s=2, t=2, non-leaf -> left 1/4, width 3/4. - assert_eq!(get_subtree_depth_transforms(2, 2, 4), transform(1.0, 3.0, h)); - // Contemplation: s=3, t=1, non-leaf -> left 2/4, width 2/4. - assert_eq!(get_subtree_depth_transforms(3, 1, 4), transform(2.0, 2.0, h)); - // Code: s=4, t=0, leaf -> left 3/4, width (4-4+1)/4 = 1/4. - assert_eq!(get_subtree_depth_transforms(4, 0, 4), transform(3.0, 1.0, h)); - // Brew & Shower Thoughts: s=3, t=0, leaf -> left 2/4, width (4-3+1)/4 = 2/4. - assert_eq!(get_subtree_depth_transforms(3, 0, 4), transform(2.0, 2.0, h)); - } - - #[test] - fn sibling_leaves_share_transform() { - // Brew and Shower Thoughts share (stack_position, subtree_depth, subtree_height) = - // (3, 0, 4), so they intentionally render to the same span [50%, 100%]. Both are - // leaves filling to the right edge; sharing a transform is correct, not a defect. - assert_eq!( - get_subtree_depth_transforms(3, 0, 4), - transform(2.0, 2.0, 4.0), - ); - } -} From c7dd2c80d22d249f00a454b10968c138aaebb36b Mon Sep 17 00:00:00 2001 From: tauseefk Date: Wed, 20 May 2026 01:07:32 -0700 Subject: [PATCH 3/8] cleanup --- src/algo/calendar_tree.rs | 2 ++ src/components/calendar_block_list.rs | 19 ------------------- src/get_position_offsets.rs | 9 ++++----- 3 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 src/components/calendar_block_list.rs diff --git a/src/algo/calendar_tree.rs b/src/algo/calendar_tree.rs index a1c1e8a..8e619c2 100644 --- a/src/algo/calendar_tree.rs +++ b/src/algo/calendar_tree.rs @@ -3,7 +3,9 @@ use crate::prelude::*; #[derive(Debug, Clone)] pub struct FlattenedCalendarBlock { pub block: CalendarBlock, + /// The block's distance from the root in the flattened tree pub stack_position: usize, + /// The block's cluster height pub subtree_height: usize, } diff --git a/src/components/calendar_block_list.rs b/src/components/calendar_block_list.rs deleted file mode 100644 index af0efd9..0000000 --- a/src/components/calendar_block_list.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::prelude::*; - -#[derive(Props, PartialEq)] -pub struct CalendarBlockListProps { - flattened_blocks: Vec, -} - -#[allow(non_snake_case)] -pub fn CalendarBlockList(cx: Scope) -> Element { - return cx.render(rsx!(div { - class: "calendar flex noselect", - cx.props.flattened_blocks.iter().map(move |flattened_block| - rsx!(calendar_block::CalendarBlockListItem { - calendar_block: flattened_block.block, - stack_position: flattened_block.stack_position, - }) - ) - })); -} diff --git a/src/get_position_offsets.rs b/src/get_position_offsets.rs index 7055e6b..652373d 100644 --- a/src/get_position_offsets.rs +++ b/src/get_position_offsets.rs @@ -22,18 +22,17 @@ pub fn get_subtree_depth_transforms( let stack_position = stack_position as f64; let subtree_depth = subtree_depth as f64; - // The cluster height is the shared column grid, so siblings on different-depth - // branches stay horizontally aligned. + // height is used to ensure that siblings on different-depth + // branches stay horizontally aligned let height = subtree_height as f64; match stack_position < 1.0 { true => (0.to_string(), MAX_COL_WIDTH.to_string()), false => { let width = match subtree_depth > 0.0 { - // non-leaf: one column plus a half-column overlap into its child, so - // nesting stays visible but the block never reaches the right edge. + // non-leaf: one column plus a half-column overlap into its child true => 1.5 / height, - // leaf: fill from its own column to the container's right edge. + // leaf: fill from its own column to the container's right edge false => (height - stack_position + 1.0) / height, }; From 850ed88e5888a4f6c24bac7470c69a66e7f9210d Mon Sep 17 00:00:00 2001 From: tauseefk Date: Wed, 20 May 2026 01:17:39 -0700 Subject: [PATCH 4/8] cleanup --- src/algo/calendar_tree.rs | 12 ---------- src/components/calendar.rs | 13 +++++++++- src/get_position_offsets.rs | 47 +++++++++++++++++-------------------- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/algo/calendar_tree.rs b/src/algo/calendar_tree.rs index 8e619c2..927901a 100644 --- a/src/algo/calendar_tree.rs +++ b/src/algo/calendar_tree.rs @@ -45,18 +45,6 @@ impl CalendarBlockTree { block: CalendarBlock, destination: Option, ) -> Result<(), Box> { - // Recursive Add - // 1. find overlaps - // if no overlap - // add edge from destination to new block - // return - // else if new block gets swallowed - // call add with new destination - // TODO:else - // add edge from destination to new block - // add edges from new block to overlapping blocks - // remove edges from destination to overlapping blocks - let destination = destination.unwrap_or(self.root_idx); let mut forward_neighbors = self diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 710f688..528b80d 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -129,7 +129,18 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { }; let (left, width) = match use_subtree_depth_algorithm.get() { - true => get_subtree_depth_transforms(flattened_block.stack_position, flattened_block.block.subtree_depth, flattened_block.subtree_height), + true => match flattened_block.stack_position { + // synthetic root spans the full container width + 0 => (0.to_string(), MAX_COL_WIDTH.to_string()), + stack_position => { + let height = flattened_block.subtree_height as f64; + get_subtree_depth_transforms( + (stack_position as f64 - 1.0) / height, + 1.0 / height, + flattened_block.block.subtree_depth == 0, + ) + } + }, false => get_position_offsets(flattened_block.stack_position) }; let top = format!("{}px", flattened_block.block.start_minute); diff --git a/src/get_position_offsets.rs b/src/get_position_offsets.rs index 652373d..c71cd5c 100644 --- a/src/get_position_offsets.rs +++ b/src/get_position_offsets.rs @@ -14,32 +14,29 @@ pub fn get_position_offsets(stack_position: usize) -> (String, String) { } } +/// Returns the CSS `transform` offsets for a block. +/// +/// The block's left edge as a fraction of the container +/// left_fraction: f64, +/// One column's width as a fraction of the container +/// column_fraction: f64, +/// Whether the block is a leaf (i.e., spans the full container width) +/// is_leaf: bool, +/// pub fn get_subtree_depth_transforms( - stack_position: usize, - subtree_depth: usize, - subtree_height: usize, + left_fraction: f64, + column_fraction: f64, + is_leaf: bool, ) -> (String, String) { - let stack_position = stack_position as f64; - let subtree_depth = subtree_depth as f64; - - // height is used to ensure that siblings on different-depth - // branches stay horizontally aligned - let height = subtree_height as f64; + let width = match is_leaf { + // leaf: fill from its own left edge to the container's right edge + true => 1.0 - left_fraction, + // non-leaf: one column plus a half-column overlap into its child + false => 1.5 * column_fraction, + }; - match stack_position < 1.0 { - true => (0.to_string(), MAX_COL_WIDTH.to_string()), - false => { - let width = match subtree_depth > 0.0 { - // non-leaf: one column plus a half-column overlap into its child - true => 1.5 / height, - // leaf: fill from its own column to the container's right edge - false => (height - stack_position + 1.0) / height, - }; - - ( - format!("calc(100% * {})", (stack_position - 1.0) / height), - format!("calc(100% * {width})"), - ) - } - } + ( + format!("calc(100% * {left_fraction})"), + format!("calc(100% * {width})"), + ) } From a5794e50891542df56ea8e8a21649cf7250cbe00 Mon Sep 17 00:00:00 2001 From: tauseefk Date: Wed, 20 May 2026 23:43:16 -0700 Subject: [PATCH 5/8] simplify offsets --- src/components/calendar.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 528b80d..4ab1950 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -129,17 +129,13 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { }; let (left, width) = match use_subtree_depth_algorithm.get() { - true => match flattened_block.stack_position { - // synthetic root spans the full container width - 0 => (0.to_string(), MAX_COL_WIDTH.to_string()), - stack_position => { + true => { let height = flattened_block.subtree_height as f64; get_subtree_depth_transforms( - (stack_position as f64 - 1.0) / height, + (flattened_block.stack_position as f64 - 1.0) / height, 1.0 / height, flattened_block.block.subtree_depth == 0, ) - } }, false => get_position_offsets(flattened_block.stack_position) }; From 541dc7199bfdcf3f265c6086b3531abc973b69bb Mon Sep 17 00:00:00 2001 From: tauseefk Date: Thu, 21 May 2026 00:50:40 -0700 Subject: [PATCH 6/8] rename to cluster_height --- src/algo/calendar_tree.rs | 16 ++++++++-------- src/components/calendar.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/algo/calendar_tree.rs b/src/algo/calendar_tree.rs index 927901a..b30149b 100644 --- a/src/algo/calendar_tree.rs +++ b/src/algo/calendar_tree.rs @@ -6,7 +6,7 @@ pub struct FlattenedCalendarBlock { /// The block's distance from the root in the flattened tree pub stack_position: usize, /// The block's cluster height - pub subtree_height: usize, + pub cluster_height: usize, } pub struct CalendarBlockTree { @@ -112,8 +112,8 @@ impl CalendarBlockTree { traversal_queue.push_back((self.root_idx, 0, 0)); while !traversal_queue.is_empty() { - let (node_idx, stack_position, subtree_height) = traversal_queue.pop_front().unwrap(); - buffer.push((node_idx, stack_position, subtree_height)); + let (node_idx, stack_position, cluster_height) = traversal_queue.pop_front().unwrap(); + buffer.push((node_idx, stack_position, cluster_height)); let forward_neighbors = self .adjacency @@ -121,25 +121,25 @@ impl CalendarBlockTree { .map(|e| e.target()); forward_neighbors.for_each(|n| { - let child_subtree_height = if stack_position == 0 { + let child_cluster_height = if stack_position == 0 { let block_id = self.adjacency[n]; 1 + self.id_to_block_map.get(&block_id).unwrap().subtree_depth } else { - subtree_height + cluster_height }; - traversal_queue.push_back((n, stack_position + 1, child_subtree_height)); + traversal_queue.push_back((n, stack_position + 1, child_cluster_height)); }); } buffer .iter() - .map(|(node_idx, stack_position, subtree_height)| { + .map(|(node_idx, stack_position, cluster_height)| { let current_block_id = self.adjacency[*node_idx]; let current_block = self.id_to_block_map.get(¤t_block_id).unwrap(); FlattenedCalendarBlock { block: current_block.clone(), stack_position: *stack_position, - subtree_height: *subtree_height, + cluster_height: *cluster_height, } }) .collect() diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 4ab1950..1341e58 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -130,7 +130,7 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { let (left, width) = match use_subtree_depth_algorithm.get() { true => { - let height = flattened_block.subtree_height as f64; + let height = flattened_block.cluster_height as f64; get_subtree_depth_transforms( (flattened_block.stack_position as f64 - 1.0) / height, 1.0 / height, From e0953bf0a99b91978f46d5d21de9eb567079183d Mon Sep 17 00:00:00 2001 From: tauseefk Date: Thu, 21 May 2026 00:52:27 -0700 Subject: [PATCH 7/8] fix doc --- src/algo/calendar_tree.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algo/calendar_tree.rs b/src/algo/calendar_tree.rs index b30149b..69946e4 100644 --- a/src/algo/calendar_tree.rs +++ b/src/algo/calendar_tree.rs @@ -3,9 +3,9 @@ use crate::prelude::*; #[derive(Debug, Clone)] pub struct FlattenedCalendarBlock { pub block: CalendarBlock, - /// The block's distance from the root in the flattened tree + /// The block's distance from the root pub stack_position: usize, - /// The block's cluster height + /// Height of the block's cluster (deepest leaf node to subtree parent) pub cluster_height: usize, } From 9e4a9c6d5b5ca8e7ce4cd0b320b6364fb78bf7a3 Mon Sep 17 00:00:00 2001 From: tauseefk Date: Thu, 21 May 2026 00:55:36 -0700 Subject: [PATCH 8/8] clippy --- src/components/calendar.rs | 10 +++++----- src/components/calendar_block.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 1341e58..b709c7e 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -14,7 +14,7 @@ fn get_time_from_minutes(minutes: u32) -> String { } #[allow(non_snake_case)] -pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { +pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element<'app> { let ghost_block_top = use_state(&cx, || 0_f64); let click_offset = use_state(&cx, || 0_f64); let dragged_block = use_state(&cx, || None::); @@ -104,7 +104,7 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { None => rsx!(empty_element::EmptyElement {}), }; - return cx.render(rsx! { + cx.render(rsx! { button { class: "btn", onclick: move |_| { @@ -149,7 +149,7 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { let id = flattened_block.block.id; let block_type = flattened_block.block.block_type; - return rsx!(calendar_block::CalendarBlockListItem { + rsx!(calendar_block::CalendarBlockListItem { key: "{id}", left: left, top: top, @@ -164,11 +164,11 @@ pub fn Calendar<'app>(cx: Scope<'app, CalendarProps<'app>>) -> Element { click_offset.set(evt.client_y as f64 - flattened_block.block.start_minute as f64); }, onmouseup: handle_move_calendar_block, - }); + }) } ) rsx!(ghost_block) } } - }); + }) } diff --git a/src/components/calendar_block.rs b/src/components/calendar_block.rs index 2fbf33e..349bfba 100644 --- a/src/components/calendar_block.rs +++ b/src/components/calendar_block.rs @@ -18,7 +18,7 @@ pub struct CalendarBlockListItemProps<'block> { #[allow(non_snake_case)] pub fn CalendarBlockListItem<'block>( cx: Scope<'block, CalendarBlockListItemProps<'block>>, -) -> Element { +) -> Element<'block> { let block_type_class = match cx.props.block_type { CalendarBlockType::Wrapper => "wrapper", CalendarBlockType::Busy => "busy", @@ -29,7 +29,7 @@ pub fn CalendarBlockListItem<'block>( None => "".to_string(), }; - return cx.render(rsx!(div { + cx.render(rsx!(div { class: "absolute calendar-block {block_type_class} {classes}", title: "{cx.props.label}", top: "{cx.props.top}", @@ -49,5 +49,5 @@ pub fn CalendarBlockListItem<'block>( } }, "{cx.props.label}" - })); + })) }