blockworx/widget/
document_ext.rs1use egui::{Pos2, Rect};
6
7use crate::document::{
8 Block, Document, GridRect, GridSize, LineAnchor, TOP_BLOCK_DEFAULT_HEIGHT,
9 TOP_BLOCK_DEFAULT_RECT, TOP_BLOCK_DEFAULT_WIDTH,
10};
11use crate::grid::{GridRectExt, snap_block_height_cells};
12use crate::shape::BaseShape;
13use crate::shape::port::PORT_PIN_ID;
14use crate::store::{IdMapExt, RectId};
15
16pub trait DocumentExt {
19 fn content_bounds(&self, id: RectId) -> Option<Rect>;
24
25 fn anchor_pos(&self, current_id: RectId, anchor: LineAnchor) -> Option<Pos2>;
29
30 fn wrap_top_in_new_parent(&mut self);
36}
37
38impl DocumentExt for Document {
39 fn content_bounds(&self, id: RectId) -> Option<Rect> {
40 let block = self.blocks.get(&id)?;
41 self.child_blocks(id)
42 .map(|(_, b)| b.inner.to_rect())
43 .chain(block.pins.values().map(|p| p.rect.to_rect()))
44 .chain(block.texts.values().map(|t| t.gui_rect()))
45 .chain(block.comments.values().map(|c| c.inner.to_rect()))
46 .reduce(egui::Rect::union)
47 }
48
49 fn anchor_pos(&self, current_id: RectId, anchor: LineAnchor) -> Option<Pos2> {
50 match anchor {
51 LineAnchor::Port(pid) => {
52 let cur = self.blocks.get(¤t_id)?;
53 let p = cur.pins.get(&pid)?;
54 p.anchor_point_with_rect(p.rect.to_rect(), PORT_PIN_ID)
55 }
56 LineAnchor::Pin { block, pin } => {
57 let b = self.blocks.get(&block)?;
58 b.anchor_point_with_rect(b.inner.to_rect(), pin)
59 }
60 }
61 }
62
63 fn wrap_top_in_new_parent(&mut self) {
64 let old_top = self.top_id;
65 let boundary = child_boundary_rect(self, old_top);
66 if let Some(b) = self.blocks.get_mut(&old_top) {
67 b.inner = boundary;
68 }
69 let name = next_top_name(self);
72 let mut parent = Block::default();
73 parent.decorations.title.name = name;
74 let new_id = self.blocks.insert_value(parent);
75 if let Some(parent) = self.blocks.get_mut(&new_id) {
76 parent.children.insert(old_top);
77 }
78 self.top_id = new_id;
79 }
80}
81
82fn next_top_name(doc: &Document) -> String {
85 let last = doc.blocks.len() + 1;
88 (1..=last)
89 .map(|n| format!("top_{n}"))
90 .find(|candidate| {
91 !doc.blocks
92 .values()
93 .any(|b| b.decorations.title.name == *candidate)
94 })
95 .unwrap_or_else(|| format!("top_{}", last + 1))
96}
97
98fn child_boundary_rect(doc: &Document, id: RectId) -> GridRect {
103 let Some(block) = doc.blocks.get(&id) else {
104 return TOP_BLOCK_DEFAULT_RECT;
105 };
106 let max_off = block.pins.values().map(|p| p.offset).max().unwrap_or(0);
107 let min_h = 2 * (max_off + 2); let own = block.inner;
109 let width = if own.size.w == 0 {
112 TOP_BLOCK_DEFAULT_WIDTH
113 } else {
114 own.size.w
115 };
116 let height = if own.size.h == 0 {
117 TOP_BLOCK_DEFAULT_HEIGHT
118 } else {
119 own.size.h
120 };
121 GridRect::new(
122 own.min,
123 GridSize::new(width, snap_block_height_cells(height.max(min_h))),
124 )
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::document::GridPos;
131
132 #[test]
133 fn repeated_wraps_give_each_new_parent_a_distinct_name() {
134 let mut doc = Document::default(); doc.wrap_top_in_new_parent();
136 let first = doc
137 .blocks
138 .get(&doc.top_id)
139 .unwrap()
140 .decorations
141 .title
142 .name
143 .clone();
144 doc.wrap_top_in_new_parent();
145 let second = doc
146 .blocks
147 .get(&doc.top_id)
148 .unwrap()
149 .decorations
150 .title
151 .name
152 .clone();
153 assert_eq!(first, "top_1");
154 assert_eq!(second, "top_2");
155 assert_ne!(first, second);
156 }
157
158 #[test]
161 fn a_demoted_root_keeps_its_own_width_not_its_contents() {
162 let mut doc = Document::default();
163 let old_top = doc.top_id;
164 doc.add_child(
165 old_top,
166 Block {
167 inner: GridRect::new(GridPos::new(4, 4), GridSize::new(40, 10)),
168 ..Block::default()
169 },
170 );
171 let content = doc.content_bounds(old_top).expect("the child block");
174 let content_cells = GridRect::from_rect_snapped(content).size.w;
175 assert!(content_cells > TOP_BLOCK_DEFAULT_WIDTH);
176
177 doc.wrap_top_in_new_parent();
178
179 let demoted = doc.block(old_top).expect("the demoted root");
180 assert_eq!(demoted.inner.size.w, TOP_BLOCK_DEFAULT_WIDTH);
181 let new_top = doc.block(doc.top_id).expect("the new parent");
182 assert_eq!(new_top.inner.size.w, TOP_BLOCK_DEFAULT_WIDTH);
183 }
184
185 #[test]
188 fn a_degenerate_top_demotes_to_the_default_size() {
189 let mut doc = Document::default();
190 let old_top = doc.top_id;
191 doc.block_mut(old_top).unwrap().inner = GridRect::default();
192 assert_eq!(doc.block(old_top).unwrap().inner.size.w, 0);
193
194 doc.wrap_top_in_new_parent();
195
196 let demoted = doc.block(old_top).expect("the demoted root");
197 assert_eq!(demoted.inner.size, TOP_BLOCK_DEFAULT_RECT.size);
198 }
199}