Fortran · 28729 bytes Raw Blame History
1 module squarified_layout
2 use types
3 use iso_fortran_env, only: int64, real64
4 implicit none
5 private
6
7 public :: calculate_treemap
8
9 contains
10
11 ! Calculate treemap layout using alternating slice-and-dice (creates spiral effect)
12 recursive subroutine calculate_treemap(node, bounds)
13 type(file_node), intent(inout) :: node
14 type(rect), intent(in) :: bounds
15
16 call calculate_treemap_internal(node, bounds, 0)
17 end subroutine calculate_treemap
18
19 ! Internal recursive function with depth tracking
20 recursive subroutine calculate_treemap_internal(node, bounds, depth)
21 type(file_node), intent(inout) :: node
22 type(rect), intent(in) :: bounds
23 integer, intent(in) :: depth
24 integer :: i, visible_count
25 real(real64) :: size_threshold
26
27 ! Set this node's bounds
28 node%bounds = bounds
29
30 ! If no children or zero size, nothing to layout
31 if (.not. allocated(node%children) .or. node%num_children == 0) then
32 return
33 end if
34
35 if (node%size == 0) return
36
37 ! Sort children by size (descending) for spiral effect
38 call sort_by_size(node%children, node%num_children)
39
40 ! Group very small files to prevent visual clutter
41 ! Only show files that are at least 0.5% of parent, or top 50 items
42 size_threshold = real(node%size, real64) * 0.005_real64
43 visible_count = node%num_children
44
45 ! Find cutoff point: files must be >= 0.5% of total OR in top 50
46 do i = 1, node%num_children
47 if (i > 50 .and. real(node%children(i)%size, real64) < size_threshold) then
48 visible_count = i - 1
49 exit
50 end if
51 end do
52
53 ! Layout only visible children
54 call slice_and_dice(node%children, visible_count, bounds, node%size, depth)
55
56 ! Recursively layout each visible child's children
57 do i = 1, visible_count
58 if (allocated(node%children(i)%children)) then
59 ! Pass child's bounds and increment depth for alternating direction
60 call calculate_treemap_internal(node%children(i), node%children(i)%bounds, depth + 1)
61 end if
62 end do
63 end subroutine calculate_treemap_internal
64
65 ! True conch shell spiral with fixed clockwise rotation
66 ! TOP → RIGHT → BOTTOM → LEFT, converging on bottom-right interior
67 recursive subroutine slice_and_dice(nodes, num_nodes, bounds, total_size, depth)
68 type(file_node), dimension(:), intent(inout) :: nodes
69 integer, intent(in) :: num_nodes
70 type(rect), intent(in) :: bounds
71 integer(int64), intent(in) :: total_size
72 integer, intent(in) :: depth
73 integer :: pivot_count, i, spiral_direction
74 integer :: max_stackable, stack_count
75 integer(int64) :: pivot_size, remaining_size, stack_size
76 type(rect) :: pivot_area, remaining_area
77 real(real64) :: size_ratio
78
79 if (num_nodes == 0 .or. total_size == 0) return
80 if (bounds%width <= 0 .or. bounds%height <= 0) return
81
82 ! Base case: single item gets full bounds
83 if (num_nodes == 1) then
84 nodes(1)%bounds = bounds
85 return
86 end if
87
88 ! Base case: very small area, just stack items
89 ! But limit how many items we try to stack in tight spaces
90 if (bounds%width < 100 .or. bounds%height < 100) then
91 ! Calculate how many items we can reasonably stack
92 ! Each item needs at least 30px to be useful (clickable, readable)
93 if (bounds%width >= bounds%height) then
94 ! Horizontal stacking
95 max_stackable = max(1, bounds%width / 30)
96 else
97 ! Vertical stacking
98 max_stackable = max(1, bounds%height / 30)
99 end if
100
101 ! Only stack the largest items that fit
102 stack_count = min(num_nodes, max_stackable)
103
104 ! Recalculate total size for items we're actually stacking
105 if (stack_count < num_nodes) then
106 stack_size = 0
107 do i = 1, stack_count
108 stack_size = stack_size + nodes(i)%size
109 end do
110
111 ! CRITICAL: Zero out bounds for culled items to prevent phantom rendering
112 do i = stack_count + 1, num_nodes
113 nodes(i)%bounds%x = 0
114 nodes(i)%bounds%y = 0
115 nodes(i)%bounds%width = 0
116 nodes(i)%bounds%height = 0
117 end do
118 else
119 stack_size = total_size
120 end if
121
122 call simple_stack(nodes, stack_count, bounds, stack_size, bounds%width >= bounds%height)
123 return
124 end if
125
126 ! Fixed spiral rotation: 0=TOP, 1=RIGHT, 2=BOTTOM, 3=LEFT
127 spiral_direction = mod(depth, 4)
128
129 ! Take largest item (or small group) as pivot
130 pivot_count = 1
131 pivot_size = nodes(1)%size
132
133 ! Check if we should take more items (if first few are similar size)
134 do i = 2, min(3, num_nodes)
135 size_ratio = real(nodes(i)%size, real64) / real(nodes(1)%size, real64)
136 if (size_ratio > 0.6_real64) then
137 pivot_count = i
138 pivot_size = pivot_size + nodes(i)%size
139 else
140 exit
141 end if
142 end do
143
144 ! Limit pivot group size
145 ! Conservative: don't take more than 1/3 of items, and consider space constraints
146 ! If bounds are tight (< 100px), be even more conservative
147 if (bounds%width < 100 .or. bounds%height < 100) then
148 ! Tight space: limit to max 3 items in pivot group
149 pivot_count = min(pivot_count, min(3, max(1, num_nodes / 3)))
150 else
151 ! Normal space: standard 1/3 rule
152 pivot_count = min(pivot_count, max(1, num_nodes / 3))
153 end if
154
155 remaining_size = total_size - pivot_size
156
157 if (pivot_count >= num_nodes) then
158 call simple_stack(nodes, num_nodes, bounds, total_size, bounds%width >= bounds%height)
159 return
160 end if
161
162 ! Calculate pivot placement based on fixed spiral direction
163 if (spiral_direction == 0) then
164 ! TOP: Pivot at top, remaining below
165 pivot_area%x = bounds%x
166 pivot_area%y = bounds%y
167 pivot_area%width = bounds%width
168 pivot_area%height = nint((real(pivot_size, real64) / real(total_size, real64)) * real(bounds%height, real64))
169 pivot_area%height = max(30, min(pivot_area%height, bounds%height - 30))
170
171 remaining_area%x = bounds%x
172 remaining_area%y = bounds%y + pivot_area%height
173 remaining_area%width = bounds%width
174 remaining_area%height = bounds%height - pivot_area%height
175
176 else if (spiral_direction == 1) then
177 ! RIGHT: Pivot on right, remaining on left
178 pivot_area%width = nint((real(pivot_size, real64) / real(total_size, real64)) * real(bounds%width, real64))
179 pivot_area%width = max(30, min(pivot_area%width, bounds%width - 30))
180 pivot_area%x = bounds%x + bounds%width - pivot_area%width
181 pivot_area%y = bounds%y
182 pivot_area%height = bounds%height
183
184 remaining_area%x = bounds%x
185 remaining_area%y = bounds%y
186 remaining_area%width = bounds%width - pivot_area%width
187 remaining_area%height = bounds%height
188
189 else if (spiral_direction == 2) then
190 ! BOTTOM: Pivot at bottom, remaining above
191 pivot_area%height = nint((real(pivot_size, real64) / real(total_size, real64)) * real(bounds%height, real64))
192 pivot_area%height = max(30, min(pivot_area%height, bounds%height - 30))
193 pivot_area%x = bounds%x
194 pivot_area%y = bounds%y + bounds%height - pivot_area%height
195 pivot_area%width = bounds%width
196
197 remaining_area%x = bounds%x
198 remaining_area%y = bounds%y
199 remaining_area%width = bounds%width
200 remaining_area%height = bounds%height - pivot_area%height
201
202 else ! spiral_direction == 3
203 ! LEFT: Pivot on left, remaining on right
204 pivot_area%x = bounds%x
205 pivot_area%y = bounds%y
206 pivot_area%width = nint((real(pivot_size, real64) / real(total_size, real64)) * real(bounds%width, real64))
207 pivot_area%width = max(30, min(pivot_area%width, bounds%width - 30))
208 pivot_area%height = bounds%height
209
210 remaining_area%x = bounds%x + pivot_area%width
211 remaining_area%y = bounds%y
212 remaining_area%width = bounds%width - pivot_area%width
213 remaining_area%height = bounds%height
214 end if
215
216 ! Layout pivot items in their area
217 if (spiral_direction == 0 .or. spiral_direction == 2) then
218 ! Top or bottom: stack horizontally
219 call simple_stack(nodes(1:pivot_count), pivot_count, pivot_area, pivot_size, .true.)
220 else
221 ! Right or left: stack vertically
222 call simple_stack(nodes(1:pivot_count), pivot_count, pivot_area, pivot_size, .false.)
223 end if
224
225 ! Recursively layout remaining items (continues spiral clockwise)
226 if (num_nodes > pivot_count) then
227 call slice_and_dice(nodes(pivot_count+1:num_nodes), num_nodes - pivot_count, &
228 remaining_area, remaining_size, depth + 1)
229 end if
230 end subroutine slice_and_dice
231
232 ! Simple stacking helper - stacks items in one direction
233 subroutine simple_stack(nodes, num_nodes, bounds, total_size, horizontal)
234 type(file_node), dimension(:), intent(inout) :: nodes
235 integer, intent(in) :: num_nodes
236 type(rect), intent(in) :: bounds
237 integer(int64), intent(in) :: total_size
238 logical, intent(in) :: horizontal
239 integer :: i, offset, item_size, dynamic_min, available_space
240 integer :: actual_num_nodes, max_stackable
241 integer(int64) :: actual_total_size
242
243 ! CRITICAL: Check if we can actually fit all items with reasonable sizes
244 ! This prevents thin stacks from appearing anywhere in the spiral
245 if (horizontal) then
246 available_space = bounds%width
247 ! Each item needs at least 40px to be useful
248 max_stackable = max(1, available_space / 40)
249 else
250 available_space = bounds%height
251 ! Each item needs at least 40px to be useful
252 max_stackable = max(1, available_space / 40)
253 end if
254
255 ! Limit to what we can actually render
256 actual_num_nodes = min(num_nodes, max_stackable)
257
258 ! If we're culling items, recalculate total_size
259 if (actual_num_nodes < num_nodes) then
260 actual_total_size = 0
261 do i = 1, actual_num_nodes
262 actual_total_size = actual_total_size + nodes(i)%size
263 end do
264
265 ! CRITICAL: Zero out bounds for culled items to prevent phantom rendering
266 do i = actual_num_nodes + 1, num_nodes
267 nodes(i)%bounds%x = 0
268 nodes(i)%bounds%y = 0
269 nodes(i)%bounds%width = 0
270 nodes(i)%bounds%height = 0
271 end do
272 else
273 actual_total_size = total_size
274 end if
275
276 if (horizontal) then
277 ! Calculate dynamic minimum based on available space
278 ! If we have N items and W width, ensure each item gets at most W/N
279 available_space = bounds%width
280 dynamic_min = max(1, available_space / actual_num_nodes) ! At least 1 pixel per item
281
282 ! Stack left-to-right
283 offset = bounds%x
284 do i = 1, actual_num_nodes
285 if (i < actual_num_nodes) then
286 ! Calculate proportional size
287 item_size = nint((real(nodes(i)%size, real64) / real(actual_total_size, real64)) * real(bounds%width, real64))
288 ! Apply minimum, but check we won't overflow
289 item_size = max(dynamic_min, item_size)
290 ! Ensure we don't exceed remaining space
291 item_size = min(item_size, (bounds%x + bounds%width) - offset - (actual_num_nodes - i))
292 else
293 ! Last item gets ALL remaining space (prevents gaps/overlaps)
294 item_size = (bounds%x + bounds%width) - offset
295 end if
296
297 ! Ensure at least 1 pixel
298 item_size = max(1, item_size)
299
300 nodes(i)%bounds%x = offset
301 nodes(i)%bounds%y = bounds%y
302 nodes(i)%bounds%width = item_size
303 nodes(i)%bounds%height = bounds%height
304
305 offset = offset + item_size
306 end do
307 else
308 ! Calculate dynamic minimum based on available space
309 available_space = bounds%height
310 dynamic_min = max(1, available_space / actual_num_nodes) ! At least 1 pixel per item
311
312 ! Stack top-to-bottom
313 offset = bounds%y
314 do i = 1, actual_num_nodes
315 if (i < actual_num_nodes) then
316 ! Calculate proportional size
317 item_size = nint((real(nodes(i)%size, real64) / real(actual_total_size, real64)) * real(bounds%height, real64))
318 ! Apply minimum, but check we won't overflow
319 item_size = max(dynamic_min, item_size)
320 ! Ensure we don't exceed remaining space
321 item_size = min(item_size, (bounds%y + bounds%height) - offset - (actual_num_nodes - i))
322 else
323 ! Last item gets ALL remaining space (prevents gaps/overlaps)
324 item_size = (bounds%y + bounds%height) - offset
325 end if
326
327 ! Ensure at least 1 pixel
328 item_size = max(1, item_size)
329
330 nodes(i)%bounds%x = bounds%x
331 nodes(i)%bounds%y = offset
332 nodes(i)%bounds%width = bounds%width
333 nodes(i)%bounds%height = item_size
334
335 offset = offset + item_size
336 end do
337 end if
338 end subroutine simple_stack
339
340 ! OLD squarified code - keeping for reference but not used
341 recursive subroutine squarify_OLD(nodes, num_nodes, bounds, total_size)
342 type(file_node), dimension(:), intent(inout) :: nodes
343 integer, intent(in) :: num_nodes
344 type(rect), intent(in) :: bounds
345 integer(int64), intent(in) :: total_size
346
347 integer :: i, row_start, row_end
348 real(real64) :: remaining_area, row_area, scale_factor, total_pixel_area
349 type(rect) :: remaining_bounds, row_bounds
350 logical :: layout_horizontal
351
352 ! Only check for invalid inputs, not space constraints (we support scrolling!)
353 if (num_nodes == 0 .or. total_size == 0) return
354 if (bounds%width <= 0 .or. bounds%height <= 0) return
355
356 ! Calculate scaling factor: maps bytes to pixels²
357 total_pixel_area = real(bounds%width, real64) * real(bounds%height, real64)
358 scale_factor = total_pixel_area / real(total_size, real64)
359
360 ! Determine layout direction (use shorter dimension for rows)
361 layout_horizontal = bounds%width >= bounds%height
362
363 remaining_bounds = bounds
364 remaining_area = real(total_size, real64)
365 row_start = 1
366
367 do while (row_start <= num_nodes)
368
369 ! Find best row: add items while aspect ratio improves
370 row_end = find_best_row(nodes(row_start:num_nodes), &
371 num_nodes - row_start + 1, &
372 remaining_bounds, &
373 layout_horizontal, &
374 scale_factor)
375 row_end = row_start + row_end - 1
376
377 ! Calculate row area
378 row_area = 0.0_real64
379 do i = row_start, row_end
380 row_area = row_area + real(nodes(i)%size, real64)
381 end do
382
383 ! Layout this row
384 if (layout_horizontal) then
385 ! Horizontal row (items placed left-to-right)
386 call layout_row_horizontal(nodes(row_start:row_end), &
387 row_end - row_start + 1, &
388 remaining_bounds, &
389 row_area, &
390 scale_factor, &
391 row_bounds)
392 ! Update remaining bounds (move down)
393 remaining_bounds%y = remaining_bounds%y + row_bounds%height
394 remaining_bounds%height = max(1, remaining_bounds%height - row_bounds%height)
395
396 ! Note: remaining_bounds%height might go to 1 (minimum), but that's OK
397 ! The layout_row_horizontal will still calculate proper row heights based on scale_factor
398 else
399 ! Vertical row (items placed top-to-bottom)
400 call layout_row_vertical(nodes(row_start:row_end), &
401 row_end - row_start + 1, &
402 remaining_bounds, &
403 row_area, &
404 scale_factor, &
405 row_bounds)
406 ! Update remaining bounds (move right)
407 remaining_bounds%x = remaining_bounds%x + row_bounds%width
408 remaining_bounds%width = max(1, remaining_bounds%width - row_bounds%width)
409 end if
410
411 row_start = row_end + 1
412 end do
413 end subroutine squarify_OLD
414
415 ! Find best row: add items while worst aspect ratio improves
416 function find_best_row(nodes, num_nodes, bounds, horizontal, scale_factor) result(row_size)
417 type(file_node), dimension(:), intent(in) :: nodes
418 integer, intent(in) :: num_nodes
419 type(rect), intent(in) :: bounds
420 logical, intent(in) :: horizontal
421 real(real64), intent(in) :: scale_factor
422 integer :: row_size
423
424 real(real64) :: current_area, new_area
425 real :: current_worst, new_worst
426 integer :: i
427
428 if (num_nodes == 0) then
429 row_size = 0
430 return
431 end if
432
433 row_size = 1
434 current_area = real(nodes(1)%size, real64)
435 current_worst = calc_worst_aspect_ratio(nodes(1:1), 1, bounds, &
436 horizontal, current_area, scale_factor)
437
438 ! Try adding items while aspect ratio improves
439 do i = 2, num_nodes
440 new_area = current_area + real(nodes(i)%size, real64)
441 new_worst = calc_worst_aspect_ratio(nodes(1:i), i, bounds, &
442 horizontal, new_area, scale_factor)
443
444 ! Limit items per row to prevent too many tiny boxes
445 ! Check if adding this item would make boxes too small
446 if (horizontal .and. bounds%width / i < 10) then
447 ! Would make boxes < 10 chars wide, stop here
448 exit
449 else if (i > 5) then
450 ! Max 5 items per row for readability (was 6, reducing further)
451 exit
452 else if (new_worst <= current_worst * 1.2) then
453 ! Aspect ratio acceptable (within 20% tolerance), add to row
454 row_size = i
455 current_area = new_area
456 current_worst = new_worst
457 else
458 ! Aspect ratio too bad, stop here
459 exit
460 end if
461 end do
462 end function find_best_row
463
464 ! Calculate worst aspect ratio for a row of items
465 function calc_worst_aspect_ratio(nodes, num_nodes, bounds, horizontal, row_area, scale_factor) result(worst)
466 type(file_node), dimension(:), intent(in) :: nodes
467 integer, intent(in) :: num_nodes
468 type(rect), intent(in) :: bounds
469 logical, intent(in) :: horizontal
470 real(real64), intent(in) :: row_area, scale_factor
471 real :: worst
472
473 real(real64) :: row_dim, other_dim, item_dim, row_pixel_area
474 real :: item_aspect
475 integer :: i
476
477 worst = 0.0
478
479 if (row_area <= 0.0_real64) then
480 worst = huge(1.0)
481 return
482 end if
483
484 ! Convert row area from bytes to pixels using scale factor
485 row_pixel_area = row_area * scale_factor
486
487 if (horizontal) then
488 ! Horizontal row: height is fixed, widths vary
489 other_dim = real(bounds%width, real64)
490 if (other_dim <= 0.0_real64) then
491 worst = huge(1.0)
492 return
493 end if
494 row_dim = row_pixel_area / other_dim ! Height of row in PIXELS
495
496 do i = 1, num_nodes
497 ! Convert item size to pixels and calculate width
498 item_dim = (real(nodes(i)%size, real64) * scale_factor) / row_dim
499 item_aspect = aspect_ratio(int(item_dim), int(row_dim))
500 worst = max(worst, item_aspect)
501 end do
502 else
503 ! Vertical row: width is fixed, heights vary
504 other_dim = real(bounds%height, real64)
505 if (other_dim <= 0.0_real64) then
506 worst = huge(1.0)
507 return
508 end if
509 row_dim = row_pixel_area / other_dim ! Width of row in PIXELS
510
511 do i = 1, num_nodes
512 ! Convert item size to pixels and calculate height
513 item_dim = (real(nodes(i)%size, real64) * scale_factor) / row_dim
514 item_aspect = aspect_ratio(int(row_dim), int(item_dim))
515 worst = max(worst, item_aspect)
516 end do
517 end if
518 end function calc_worst_aspect_ratio
519
520 ! Layout a horizontal row (items left-to-right)
521 subroutine layout_row_horizontal(nodes, num_nodes, bounds, row_area, scale_factor, row_bounds)
522 type(file_node), dimension(:), intent(inout) :: nodes
523 integer, intent(in) :: num_nodes
524 type(rect), intent(in) :: bounds
525 real(real64), intent(in) :: row_area, scale_factor
526 type(rect), intent(out) :: row_bounds
527
528 integer :: i, x_offset, item_width, remaining_width
529 real(real64) :: row_height, row_pixel_area
530 integer, parameter :: MIN_HEIGHT = 3 ! Minimum to show text: 2 content lines + borders
531
532 ! Convert row area from bytes to pixels² using scale factor
533 row_pixel_area = row_area * scale_factor
534
535 ! Calculate row height
536 if (bounds%width > 0) then
537 row_height = row_pixel_area / real(bounds%width, real64)
538 else
539 row_height = 0.0_real64
540 end if
541
542 row_bounds%x = bounds%x
543 row_bounds%y = bounds%y
544 row_bounds%width = bounds%width
545 row_bounds%height = max(MIN_HEIGHT, int(row_height))
546
547 x_offset = bounds%x
548 remaining_width = bounds%width
549
550 ! First pass: calculate ideal widths with minimums
551 do i = 1, num_nodes
552 if (row_height > 0.0_real64) then
553 ! Calculate width: (item_size / row_total_size) * row_width
554 item_width = int((real(nodes(i)%size, real64) / row_area) * real(bounds%width, real64))
555 else
556 item_width = 0
557 end if
558
559 ! Enforce minimum width for text
560 item_width = max(10, item_width)
561 nodes(i)%bounds%width = item_width
562 end do
563
564 ! Second pass: adjust if total width exceeds bounds
565 ! Calculate total width needed
566 item_width = 0
567 do i = 1, num_nodes
568 item_width = item_width + nodes(i)%bounds%width
569 end do
570
571 ! If overflow, scale all widths proportionally to fit
572 if (item_width > bounds%width) then
573 do i = 1, num_nodes
574 ! Scale width down proportionally (keep minimum for text)
575 nodes(i)%bounds%width = max(10, &
576 int((real(nodes(i)%bounds%width, real64) / real(item_width, real64)) * real(bounds%width, real64)))
577 end do
578 end if
579
580 ! Third pass: assign x positions and handle rounding
581 do i = 1, num_nodes
582 nodes(i)%bounds%x = x_offset
583 nodes(i)%bounds%y = bounds%y
584 nodes(i)%bounds%height = row_bounds%height
585
586 if (i == num_nodes) then
587 ! Last item gets remaining width to avoid rounding gaps
588 remaining_width = bounds%x + bounds%width - x_offset
589 ! Ensure last item has at least minimum width
590 nodes(i)%bounds%width = max(2, remaining_width)
591 end if
592
593 x_offset = x_offset + nodes(i)%bounds%width
594 end do
595 end subroutine layout_row_horizontal
596
597 ! Layout a vertical row (items top-to-bottom)
598 subroutine layout_row_vertical(nodes, num_nodes, bounds, row_area, scale_factor, row_bounds)
599 type(file_node), dimension(:), intent(inout) :: nodes
600 integer, intent(in) :: num_nodes
601 type(rect), intent(in) :: bounds
602 real(real64), intent(in) :: row_area, scale_factor
603 type(rect), intent(out) :: row_bounds
604
605 integer :: i, y_offset, item_height, remaining_height
606 real(real64) :: row_width, row_pixel_area
607 integer, parameter :: MIN_WIDTH = 10 ! Minimum to show text: 2 borders + 8 chars
608
609 ! Convert row area from bytes to pixels² using scale factor
610 row_pixel_area = row_area * scale_factor
611
612 ! Calculate row width
613 if (bounds%height > 0) then
614 row_width = row_pixel_area / real(bounds%height, real64)
615 else
616 row_width = 0.0_real64
617 end if
618
619 row_bounds%x = bounds%x
620 row_bounds%y = bounds%y
621 row_bounds%width = max(MIN_WIDTH, int(row_width))
622 row_bounds%height = bounds%height
623
624 y_offset = bounds%y
625 remaining_height = bounds%height
626
627 ! First pass: calculate ideal heights with minimums
628 do i = 1, num_nodes
629 if (row_width > 0.0_real64) then
630 ! Calculate height: (item_size / row_total_size) * row_height
631 item_height = int((real(nodes(i)%size, real64) / row_area) * real(bounds%height, real64))
632 else
633 item_height = 0
634 end if
635
636 ! Enforce minimum height for text
637 item_height = max(3, item_height)
638 nodes(i)%bounds%height = item_height
639 end do
640
641 ! Second pass: adjust if total height exceeds bounds (allow overflow for scrolling)
642 ! Calculate total height needed
643 item_height = 0
644 do i = 1, num_nodes
645 item_height = item_height + nodes(i)%bounds%height
646 end do
647
648 ! Note: For vertical, we allow overflow (scrolling), but scale if severely over
649 ! to prevent extremely tall boxes
650 if (item_height > bounds%height * 10) then
651 do i = 1, num_nodes
652 ! Scale height down proportionally
653 nodes(i)%bounds%height = max(2, &
654 int((real(nodes(i)%bounds%height, real64) / real(item_height, real64)) * real(bounds%height * 10, real64)))
655 end do
656 end if
657
658 ! Third pass: assign y positions
659 do i = 1, num_nodes
660 nodes(i)%bounds%x = bounds%x
661 nodes(i)%bounds%y = y_offset
662 nodes(i)%bounds%width = row_bounds%width
663
664 y_offset = y_offset + nodes(i)%bounds%height
665 end do
666 end subroutine layout_row_vertical
667
668 ! Calculate aspect ratio for a rectangle (always >= 1.0)
669 pure function aspect_ratio(width, height) result(ratio)
670 integer, intent(in) :: width, height
671 real :: ratio
672
673 if (height > 0 .and. width > 0) then
674 ratio = real(width) / real(height)
675 if (ratio < 1.0) ratio = 1.0 / ratio
676 else
677 ratio = huge(1.0)
678 end if
679 end function aspect_ratio
680
681 ! Sort nodes by size (descending) using quicksort
682 ! Uses index-based sorting to avoid deep-copying file_node objects
683 subroutine sort_by_size(nodes, num_nodes)
684 type(file_node), dimension(:), intent(inout) :: nodes
685 integer, intent(in) :: num_nodes
686 integer, dimension(:), allocatable :: indices
687 type(file_node), dimension(:), allocatable :: temp_nodes
688 integer :: i
689
690 if (num_nodes <= 1) return
691
692 ! Create index array
693 allocate(indices(num_nodes))
694 do i = 1, num_nodes
695 indices(i) = i
696 end do
697
698 ! Quicksort indices by node size (descending)
699 call quicksort_indices(nodes, indices, 1, num_nodes)
700
701 ! Reorder nodes according to sorted indices
702 ! Use move_alloc to transfer allocatable components without deep copying
703 allocate(temp_nodes(num_nodes))
704 do i = 1, num_nodes
705 ! Move allocatable components (avoids deep copy)
706 call move_alloc(nodes(indices(i))%name, temp_nodes(i)%name)
707 call move_alloc(nodes(indices(i))%path, temp_nodes(i)%path)
708 call move_alloc(nodes(indices(i))%children, temp_nodes(i)%children)
709 ! Copy simple types
710 temp_nodes(i)%size = nodes(indices(i))%size
711 temp_nodes(i)%is_directory = nodes(indices(i))%is_directory
712 temp_nodes(i)%access_denied = nodes(indices(i))%access_denied
713 temp_nodes(i)%bounds = nodes(indices(i))%bounds
714 temp_nodes(i)%num_children = nodes(indices(i))%num_children
715 end do
716
717 ! Move back to original array
718 do i = 1, num_nodes
719 call move_alloc(temp_nodes(i)%name, nodes(i)%name)
720 call move_alloc(temp_nodes(i)%path, nodes(i)%path)
721 call move_alloc(temp_nodes(i)%children, nodes(i)%children)
722 nodes(i)%size = temp_nodes(i)%size
723 nodes(i)%is_directory = temp_nodes(i)%is_directory
724 nodes(i)%access_denied = temp_nodes(i)%access_denied
725 nodes(i)%bounds = temp_nodes(i)%bounds
726 nodes(i)%num_children = temp_nodes(i)%num_children
727 end do
728
729 deallocate(indices)
730 deallocate(temp_nodes)
731 end subroutine sort_by_size
732
733 ! Quicksort indices array based on node sizes (descending order)
734 recursive subroutine quicksort_indices(nodes, indices, left, right)
735 type(file_node), dimension(:), intent(in) :: nodes
736 integer, dimension(:), intent(inout) :: indices
737 integer, intent(in) :: left, right
738 integer :: pivot_idx
739
740 if (left < right) then
741 call partition_indices(nodes, indices, left, right, pivot_idx)
742 call quicksort_indices(nodes, indices, left, pivot_idx - 1)
743 call quicksort_indices(nodes, indices, pivot_idx + 1, right)
744 end if
745 end subroutine quicksort_indices
746
747 ! Partition for quicksort (sorts in descending order by size)
748 subroutine partition_indices(nodes, indices, left, right, pivot_idx)
749 type(file_node), dimension(:), intent(in) :: nodes
750 integer, dimension(:), intent(inout) :: indices
751 integer, intent(in) :: left, right
752 integer, intent(out) :: pivot_idx
753 integer(int64) :: pivot_size
754 integer :: i, j, temp
755
756 ! Use middle element as pivot
757 pivot_idx = (left + right) / 2
758 pivot_size = nodes(indices(pivot_idx))%size
759
760 ! Move pivot to end
761 temp = indices(pivot_idx)
762 indices(pivot_idx) = indices(right)
763 indices(right) = temp
764
765 ! Partition: larger sizes go to the left
766 i = left - 1
767 do j = left, right - 1
768 if (nodes(indices(j))%size >= pivot_size) then
769 i = i + 1
770 temp = indices(i)
771 indices(i) = indices(j)
772 indices(j) = temp
773 end if
774 end do
775
776 ! Move pivot to its final position
777 i = i + 1
778 temp = indices(i)
779 indices(i) = indices(right)
780 indices(right) = temp
781 pivot_idx = i
782 end subroutine partition_indices
783
784 end module squarified_layout
785