1use crate::rect::LayoutRect;
2use dumap_core::tree::{FileTree, NodeId, NodeKind};
3
4#[derive(Debug, Clone)]
6pub struct LayoutConfig {
7 pub max_depth: u16,
9 pub min_rect_size: f64,
12 pub padding: f64,
14 pub header_height: f64,
17}
18
19impl Default for LayoutConfig {
20 fn default() -> Self {
21 Self {
22 max_depth: 3,
23 min_rect_size: 2.0,
24 padding: 3.0,
25 header_height: 20.0,
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct LayoutEntry {
33 pub node_id: NodeId,
34 pub rect: LayoutRect,
35 pub depth: u16,
37}
38
39pub struct TreemapLayout {
41 entries: Vec<LayoutEntry>,
43}
44
45impl TreemapLayout {
46 pub fn entries(&self) -> &[LayoutEntry] {
48 &self.entries
49 }
50
51 pub fn len(&self) -> usize {
53 self.entries.len()
54 }
55
56 pub fn is_empty(&self) -> bool {
58 self.entries.is_empty()
59 }
60
61 pub fn hit_test(&self, x: f64, y: f64) -> Option<&LayoutEntry> {
64 self.entries.iter().rev().find(|entry| {
66 let r = &entry.rect;
67 x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h
68 })
69 }
70}
71
72pub fn squarify_layout(
74 tree: &FileTree,
75 root_id: NodeId,
76 bounds: LayoutRect,
77 config: &LayoutConfig,
78) -> TreemapLayout {
79 let estimated_capacity = tree.len().min(10_000);
80 let mut entries = Vec::with_capacity(estimated_capacity);
81 layout_node(tree, root_id, bounds, 0, config, &mut entries);
82 TreemapLayout { entries }
83}
84
85fn layout_node(
86 tree: &FileTree,
87 node_id: NodeId,
88 rect: LayoutRect,
89 depth: u16,
90 config: &LayoutConfig,
91 out: &mut Vec<LayoutEntry>,
92) {
93 let node = tree.node(node_id);
94
95 out.push(LayoutEntry {
97 node_id,
98 rect,
99 depth,
100 });
101
102 let children = match &node.kind {
104 NodeKind::File { .. } => return,
105 NodeKind::Directory { children } => children,
106 };
107 if depth >= config.max_depth || !rect.is_visible(config.min_rect_size) || children.is_empty() {
108 return;
109 }
110
111 let padded = rect.inset(config.padding);
113 if padded.w <= 0.0 || padded.h <= 0.0 {
114 return;
115 }
116
117 let inner =
119 if config.header_height > 0.0 && padded.h > config.header_height + config.min_rect_size {
120 LayoutRect::new(
121 padded.x,
122 padded.y + config.header_height,
123 padded.w,
124 padded.h - config.header_height,
125 )
126 } else {
127 padded
128 };
129 if inner.w <= 0.0 || inner.h <= 0.0 {
130 return;
131 }
132
133 let sized_children: Vec<(NodeId, f64)> = children
136 .iter()
137 .map(|&cid| (cid, tree.node(cid).total_size as f64))
138 .filter(|(_, size)| *size > 0.0)
139 .collect();
140
141 if sized_children.is_empty() {
142 return;
143 }
144
145 let child_rects = squarify_children(&sized_children, inner);
147
148 for (child_id, child_rect) in child_rects {
150 layout_node(tree, child_id, child_rect, depth + 1, config, out);
151 }
152}
153
154fn squarify_children(children: &[(NodeId, f64)], bounds: LayoutRect) -> Vec<(NodeId, LayoutRect)> {
166 let total_size: f64 = children.iter().map(|(_, s)| *s).sum();
167 if total_size <= 0.0 {
168 return Vec::new();
169 }
170
171 let mut result = Vec::with_capacity(children.len());
172 let mut remaining = bounds;
173 let mut i = 0;
174
175 while i < children.len() {
176 if remaining.w <= 0.0 || remaining.h <= 0.0 {
177 break;
178 }
179
180 let remaining_size: f64 = children[i..].iter().map(|(_, s)| *s).sum();
181 let shorter = remaining.shorter_side();
182
183 let mut row: Vec<(NodeId, f64)> = Vec::new();
185 let mut row_size = 0.0;
186
187 row.push(children[i]);
189 row_size += children[i].1;
190 i += 1;
191
192 while i < children.len() {
194 let worst_before =
195 worst_aspect_ratio(&row, row_size, shorter, remaining_size, remaining.area());
196
197 row.push(children[i]);
198 let candidate_size = row_size + children[i].1;
199 let worst_after = worst_aspect_ratio(
200 &row,
201 candidate_size,
202 shorter,
203 remaining_size,
204 remaining.area(),
205 );
206
207 if worst_after > worst_before {
208 row.pop();
210 break;
211 }
212 row_size = candidate_size;
213 i += 1;
214 }
215
216 let row_fraction = row_size / remaining_size;
218 let (strip, rest) = remaining.split_strip(row_fraction);
219 layout_row(&row, row_size, strip, &mut result);
220 remaining = rest;
221 }
222
223 result
224}
225
226fn worst_aspect_ratio(
228 row: &[(NodeId, f64)],
229 row_size: f64,
230 shorter_side: f64,
231 total_remaining: f64,
232 remaining_area: f64,
233) -> f64 {
234 if row_size <= 0.0 || shorter_side <= 0.0 || total_remaining <= 0.0 {
235 return f64::MAX;
236 }
237
238 let strip_area = (row_size / total_remaining) * remaining_area;
239 let strip_thickness = strip_area / shorter_side;
240
241 if strip_thickness <= 0.0 {
242 return f64::MAX;
243 }
244
245 let mut worst = 1.0_f64;
246 for &(_, item_size) in row {
247 let item_length = (item_size / row_size) * shorter_side;
248 if item_length <= 0.0 {
249 continue;
250 }
251 let ratio = if item_length > strip_thickness {
252 item_length / strip_thickness
253 } else {
254 strip_thickness / item_length
255 };
256 worst = worst.max(ratio);
257 }
258 worst
259}
260
261fn layout_row(
263 row: &[(NodeId, f64)],
264 row_size: f64,
265 strip: LayoutRect,
266 out: &mut Vec<(NodeId, LayoutRect)>,
267) {
268 if row_size <= 0.0 {
269 return;
270 }
271
272 let mut offset = 0.0;
273 for &(node_id, item_size) in row {
274 let fraction = item_size / row_size;
275 let item_rect = if strip.is_wide() {
276 LayoutRect {
277 x: strip.x + offset * strip.w,
278 y: strip.y,
279 w: fraction * strip.w,
280 h: strip.h,
281 }
282 } else {
283 LayoutRect {
284 x: strip.x,
285 y: strip.y + offset * strip.h,
286 w: strip.w,
287 h: fraction * strip.h,
288 }
289 };
290 out.push((node_id, item_rect));
291 offset += fraction;
292 }
293}
294
295#[cfg(test)]
296#[path = "squarify_tests.rs"]
297mod squarify_tests;