Fortran · 71969 bytes Raw Blame History
1 ! Treemap Renderer Module for Sniffly
2 ! Coordinates scanning, layout calculation, and Cairo rendering
3 module treemap_renderer
4 use, intrinsic :: iso_c_binding
5 use types
6 use disk_scanner, only: build_tree
7 use progressive_scanner, only: start_progressive_scan, stop_progressive_scan, &
8 is_scan_active, register_scan_update_callback, &
9 register_scan_complete_callback, &
10 register_initial_level_complete_callback
11 use squarified_layout, only: calculate_treemap
12 use cairo, only: cairo_set_source_rgb, cairo_rectangle, cairo_fill, &
13 cairo_stroke, cairo_set_line_width, cairo_select_font_face, &
14 cairo_set_font_size, cairo_move_to, cairo_line_to, cairo_show_text, &
15 cairo_set_source_rgba, cairo_text_extents
16 use g, only: g_main_context_default, g_main_context_iteration
17 use gtk, only: gtk_widget_queue_draw
18 use iso_fortran_env, only: int64
19 implicit none
20 private
21
22 public :: scan_and_render, init_renderer, get_root_node, scan_and_render_with_hover, &
23 scan_and_render_with_interaction, find_node_at_position, navigate_into_node, &
24 navigate_up, get_breadcrumb_path, get_path_depth, get_node_count, &
25 get_node_center_by_index, find_node_in_direction, register_progress_callback, &
26 scan_directory, invalidate_layout, get_current_view_node, remove_selected_node_from_view, &
27 clear_cache, toggle_file_extensions, toggle_age_based_coloring, toggle_size_display_mode, &
28 toggle_hidden_files, toggle_render_mode, set_redraw_widget, register_scan_completion_callback, &
29 set_renderer_state_from_tab
30
31 ! Callback interfaces for progress updates
32 abstract interface
33 subroutine show_progress_callback()
34 end subroutine show_progress_callback
35
36 subroutine hide_progress_callback()
37 end subroutine hide_progress_callback
38
39 subroutine update_progress_callback(fraction, message)
40 use, intrinsic :: iso_c_binding
41 real(c_double), intent(in) :: fraction
42 character(len=*), intent(in) :: message
43 end subroutine update_progress_callback
44
45 subroutine scan_completion_callback()
46 end subroutine scan_completion_callback
47 end interface
48
49 ! Directory cache entry
50 type :: cache_entry
51 character(len=512) :: path
52 type(file_node), allocatable :: node
53 logical :: valid
54 end type cache_entry
55
56 ! Global state
57 type(file_node), save, target :: root_node
58 type(file_node), pointer, save :: current_view_node => null()
59 logical, save :: has_data = .false.
60 character(len=512), save :: scanned_path = ""
61
62 ! Directory cache (stores scanned trees)
63 integer, parameter :: MAX_CACHE_SIZE = 50
64 type(cache_entry), dimension(MAX_CACHE_SIZE), save :: dir_cache
65 integer, save :: cache_count = 0
66
67 ! Layout cache state
68 logical, save :: layout_calculated = .false.
69 integer, save :: last_width = 0, last_height = 0
70
71 ! View settings (Phase 5 features)
72 logical, save :: show_file_extensions = .true. ! Toggle file extensions in labels
73 logical, save :: use_age_based_coloring = .false. ! Color by file age instead of type
74 logical, save :: show_allocated_size = .false. ! Show allocated size vs actual size
75 logical, save :: show_hidden_files = .true. ! Toggle visibility of dotfiles/hidden files
76 logical, save :: use_cushion_shading = .true. ! Toggle cushioned (3D) vs flat rendering
77
78 ! Navigation path stack (for breadcrumbs)
79 ! Simple approach: track path as array of names
80 integer, parameter :: MAX_PATH_DEPTH = 100
81 character(len=256), save :: path_names(MAX_PATH_DEPTH)
82 integer, save :: path_depth = 0
83
84 ! Progress callback pointers
85 procedure(show_progress_callback), pointer, save :: show_progress_cb => null()
86 procedure(hide_progress_callback), pointer, save :: hide_progress_cb => null()
87 procedure(update_progress_callback), pointer, save :: update_progress_cb => null()
88 procedure(scan_completion_callback), pointer, save :: scan_completion_cb => null()
89
90 ! Widget pointer for progressive scan redraws
91 type(c_ptr), save :: widget_for_redraw = c_null_ptr
92
93 contains
94
95 ! Initialize renderer
96 subroutine init_renderer()
97 has_data = .false.
98 scanned_path = ""
99 end subroutine init_renderer
100
101 ! Invalidate layout cache to force recalculation
102 subroutine invalidate_layout()
103 layout_calculated = .false.
104 print *, "Layout cache invalidated"
105 end subroutine invalidate_layout
106
107 ! Register progress callbacks
108 subroutine register_progress_callback(show_cb, hide_cb, update_cb)
109 procedure(show_progress_callback) :: show_cb
110 procedure(hide_progress_callback) :: hide_cb
111 procedure(update_progress_callback) :: update_cb
112
113 show_progress_cb => show_cb
114 hide_progress_cb => hide_cb
115 update_progress_cb => update_cb
116 print *, "Progress callbacks registered"
117 end subroutine register_progress_callback
118
119 ! Set widget for progressive scan redraws
120 subroutine set_redraw_widget(widget)
121 type(c_ptr), intent(in) :: widget
122 widget_for_redraw = widget
123 print *, "Redraw widget registered for progressive scanning"
124 end subroutine set_redraw_widget
125
126 ! Callback for progressive scan updates (called after each directory is scanned)
127 subroutine on_progressive_scan_update()
128 use progressive_scanner, only: get_scan_progress
129 real :: progress
130 character(len=256) :: status_msg
131 integer :: dirs_done, total_dirs
132
133 ! Get progress from progressive scanner
134 progress = get_scan_progress()
135
136 ! Update progress bar and status
137 if (associated(update_progress_cb)) then
138 write(status_msg, '(A,F5.1,A)') 'Scanning directories... ', progress * 100.0, '%'
139 call update_progress_cb(real(progress, c_double), trim(status_msg))
140 end if
141
142 ! Decay flash intensities for all visible nodes
143 call decay_flash_highlights()
144
145 ! Invalidate layout to force recalculation with new data
146 call invalidate_layout()
147
148 ! Trigger widget redraw if available
149 if (c_associated(widget_for_redraw)) then
150 call gtk_widget_queue_draw(widget_for_redraw)
151 end if
152 end subroutine on_progressive_scan_update
153
154 ! Register scan completion callback
155 subroutine register_scan_completion_callback(callback)
156 procedure(scan_completion_callback) :: callback
157 scan_completion_cb => callback
158 print *, "Scan completion callback registered"
159 end subroutine register_scan_completion_callback
160
161 ! Callback for initial level completion (depth 0 done)
162 subroutine on_initial_level_complete()
163 print *, "Initial level complete - starting UI rendering (subdirectories will continue scanning)"
164
165 ! Mark initial scan as complete so UI can start rendering
166 if (associated(scan_completion_cb)) then
167 call scan_completion_cb()
168 end if
169
170 ! Trigger first redraw (progress bar stays visible)
171 if (c_associated(widget_for_redraw)) then
172 call gtk_widget_queue_draw(widget_for_redraw)
173 end if
174 end subroutine on_initial_level_complete
175
176 ! Callback for progressive scan completion (fully done)
177 subroutine on_progressive_scan_complete()
178 print *, "Progressive scan FULLY complete - hiding progress bar"
179
180 ! Update progress to 100% and hide progress bar
181 if (associated(update_progress_cb)) then
182 call update_progress_cb(1.0_c_double, 'Scan complete')
183 end if
184 if (associated(hide_progress_cb)) then
185 call hide_progress_cb()
186 end if
187
188 ! Final redraw
189 if (c_associated(widget_for_redraw)) then
190 call gtk_widget_queue_draw(widget_for_redraw)
191 end if
192
193 ! Call scan completion callback to update button states now that scan is inactive
194 if (associated(scan_completion_cb)) then
195 call scan_completion_cb()
196 end if
197
198 print *, "Scan complete. Final root size: ", root_node%size, " bytes"
199 end subroutine on_progressive_scan_complete
200
201 ! Get root node (for external access)
202 function get_root_node() result(node_ptr)
203 type(file_node), pointer :: node_ptr
204 node_ptr => root_node
205 end function get_root_node
206
207 ! Get current view node (for external access)
208 function get_current_view_node() result(node_ptr)
209 type(file_node), pointer :: node_ptr
210 node_ptr => current_view_node
211 end function get_current_view_node
212
213 ! Set renderer state from tab (for tab switching)
214 subroutine set_renderer_state_from_tab(tab_root, tab_current_view, tab_has_data)
215 type(file_node), pointer, intent(in) :: tab_root, tab_current_view
216 logical, intent(in) :: tab_has_data
217
218 print *, "=== SET_RENDERER_STATE_FROM_TAB ==="
219 print *, " tab_has_data: ", tab_has_data
220
221 ! Update global renderer state to match the tab
222 has_data = tab_has_data
223
224 if (tab_has_data .and. associated(tab_root)) then
225 ! Copy tab's tree data to renderer globals
226 root_node = tab_root
227 current_view_node => tab_current_view
228 print *, " Synced renderer to tab's tree data"
229 else
230 ! Empty tab - nullify current view
231 current_view_node => null()
232 has_data = .false.
233 print *, " Tab is empty - cleared renderer state"
234 end if
235
236 ! Invalidate layout to force recalculation
237 layout_calculated = .false.
238 end subroutine set_renderer_state_from_tab
239
240 ! Scan directory and prepare for rendering
241 subroutine scan_directory(path)
242 use, intrinsic :: iso_c_binding
243 use disk_scanner, only: set_progress_callback
244 use file_system, only: get_absolute_path
245 character(len=*), intent(in) :: path
246 character(len=:), allocatable :: expanded_path
247 integer :: cache_index, i
248 character(len=512) :: status_msg
249 type(c_ptr) :: context
250
251 ! Expand relative paths (like ./) to absolute paths for meaningful breadcrumbs
252 expanded_path = get_absolute_path(path)
253 print *, "Scanning: ", trim(expanded_path)
254
255 ! Mark as having data IMMEDIATELY to prevent recursive scans
256 has_data = .true.
257 scanned_path = trim(expanded_path)
258
259 ! Register progress callback with disk_scanner
260 if (associated(update_progress_cb)) then
261 call set_progress_callback(update_progress_cb)
262 end if
263
264 ! Show progress bar and status
265 if (associated(show_progress_cb)) call show_progress_cb()
266
267 ! Process events to make widget visible
268 context = g_main_context_default()
269 do i = 1, 5
270 do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int)
271 end do
272 end do
273
274 ! Now update with initial message
275 write(status_msg, '(A,A)') 'Scanning: ', trim(expanded_path)
276 if (associated(update_progress_cb)) call update_progress_cb(0.1_c_double, status_msg)
277
278 ! Process events again to show the update
279 do i = 1, 5
280 do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int)
281 end do
282 end do
283
284 ! Check cache first
285 cache_index = cache_lookup(expanded_path)
286 if (cache_index > 0) then
287 ! Use cached scan (already colored)
288 if (associated(update_progress_cb)) call update_progress_cb(0.5_c_double, 'Loading from cache...')
289 ! Process events
290 do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int)
291 end do
292 ! Deallocate old root_node before loading from cache
293 call deallocate_tree(root_node)
294 root_node = dir_cache(cache_index)%node ! Deep copy from cache
295 ! Apply colors to cached tree
296 call color_tree(root_node, 0)
297 else
298 ! Use progressive scanning for real-time treemap updates
299 if (associated(update_progress_cb)) call update_progress_cb(0.3_c_double, 'Starting progressive scan...')
300 ! Process events
301 do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int)
302 end do
303
304 ! Register update callback for progressive scanning
305 call register_scan_update_callback(on_progressive_scan_update)
306
307 ! Register initial level completion callback
308 call register_initial_level_complete_callback(on_initial_level_complete)
309
310 ! Register full completion callback
311 call register_scan_complete_callback(on_progressive_scan_complete)
312
313 ! Start progressive scan - this will scan one directory per idle iteration
314 call start_progressive_scan(root_node, expanded_path)
315
316 print *, "Progressive scan started - updates will occur in idle callbacks"
317 ! Note: The scan will continue asynchronously, calling on_progressive_scan_update
318 ! after each directory is scanned. We don't cache during progressive scans.
319 ! Progress bar will be updated by the progressive scanner
320 end if
321
322 ! Start view at root level (showing only top-level items)
323 current_view_node => root_node
324
325 ! Initialize breadcrumb path stack with root path
326 path_depth = 1
327 path_names(1) = trim(scanned_path)
328
329 ! For cached scans, hide progress bar now since scan is complete
330 if (cache_index > 0) then
331 if (associated(update_progress_cb)) call update_progress_cb(1.0_c_double, 'Loaded from cache')
332 if (associated(hide_progress_cb)) call hide_progress_cb()
333 print *, "Scan complete (from cache). Root size: ", root_node%size, " bytes"
334 print *, "Children: ", root_node%num_children
335 else
336 ! For progressive scans, keep progress bar visible - it will be updated during scan
337 print *, "Progressive scan in progress. Root level complete with ", root_node%num_children, " children"
338 end if
339
340 ! Invalidate layout cache to force recalculation with new data
341 call invalidate_layout()
342 end subroutine scan_directory
343
344 ! Main rendering function
345 subroutine scan_and_render(cr, width, height, path)
346 type(c_ptr), intent(in) :: cr
347 integer(c_int), intent(in) :: width, height
348 character(len=*), intent(in), optional :: path
349 type(rect) :: bounds
350
351 ! Scan if needed
352 if (.not. has_data) then
353 if (present(path)) then
354 call scan_directory(path)
355 else
356 ! Default: scan Downloads folder
357 call scan_directory("/Users/matthewwolffe/Downloads")
358 end if
359 end if
360
361 ! Calculate layout for window size using current view
362 bounds%x = 0
363 bounds%y = 0
364 bounds%width = int(width)
365 bounds%height = int(height)
366
367 if (associated(current_view_node) .and. current_view_node%size > 0) then
368 call calculate_treemap(current_view_node, bounds)
369 ! Initialize cushion parameters for 3D shading
370 call init_cushions(current_view_node)
371 end if
372
373 ! Render only the current view (top-level items only)
374 if (associated(current_view_node)) then
375 call render_current_view(cr, current_view_node, bounds)
376 end if
377 end subroutine scan_and_render
378
379 ! Rendering function with hover highlighting
380 subroutine scan_and_render_with_hover(cr, width, height, mouse_x, mouse_y, path)
381 type(c_ptr), intent(in) :: cr
382 integer(c_int), intent(in) :: width, height
383 real(c_double), intent(in) :: mouse_x, mouse_y
384 character(len=*), intent(in), optional :: path
385 type(rect) :: bounds
386 integer :: hovered_index
387
388 ! Scan if needed (only once)
389 if (.not. has_data) then
390 if (present(path)) then
391 call scan_directory(path)
392 else
393 call scan_directory("/Users/matthewwolffe/Downloads")
394 end if
395 end if
396
397 ! Only recalculate layout if window size changed
398 if (.not. layout_calculated .or. last_width /= width .or. last_height /= height) then
399 bounds%x = 0
400 bounds%y = 0
401 bounds%width = int(width)
402 bounds%height = int(height)
403
404 if (associated(current_view_node) .and. current_view_node%size > 0) then
405 call calculate_treemap(current_view_node, bounds)
406 ! Initialize cushion parameters for 3D shading
407 call init_cushions(current_view_node)
408 end if
409
410 layout_calculated = .true.
411 last_width = width
412 last_height = height
413 end if
414
415 ! Render the treemap
416 if (associated(current_view_node)) then
417 call render_current_view(cr, current_view_node, bounds)
418 end if
419
420 ! Find which rectangle is under the mouse
421 hovered_index = find_node_at_position(mouse_x, mouse_y)
422
423 ! Render hover highlight if a node is hovered
424 if (hovered_index > 0 .and. associated(current_view_node)) then
425 if (allocated(current_view_node%children)) then
426 if (hovered_index <= current_view_node%num_children) then
427 call render_hover_highlight(cr, current_view_node%children(hovered_index))
428 end if
429 end if
430 end if
431 end subroutine scan_and_render_with_hover
432
433 ! Rendering function with both hover and selection highlighting
434 subroutine scan_and_render_with_interaction(cr, width, height, mouse_x, mouse_y, &
435 selected_index, path)
436 type(c_ptr), intent(in) :: cr
437 integer(c_int), intent(in) :: width, height
438 real(c_double), intent(in) :: mouse_x, mouse_y
439 integer, intent(in) :: selected_index
440 character(len=*), intent(in), optional :: path
441 type(rect) :: bounds
442 integer :: hovered_index
443
444 ! Scan if needed (only once)
445 if (.not. has_data) then
446 if (present(path)) then
447 call scan_directory(path)
448 else
449 call scan_directory("/Users/matthewwolffe/Downloads")
450 end if
451 end if
452
453 ! Only recalculate layout if window size changed
454 if (.not. layout_calculated .or. last_width /= width .or. last_height /= height) then
455 bounds%x = 0
456 bounds%y = 0
457 bounds%width = int(width)
458 bounds%height = int(height)
459
460 if (associated(current_view_node) .and. current_view_node%size > 0) then
461 call calculate_treemap(current_view_node, bounds)
462 ! Initialize cushion parameters for 3D shading
463 call init_cushions(current_view_node)
464 end if
465
466 layout_calculated = .true.
467 last_width = width
468 last_height = height
469 end if
470
471 ! Render the treemap
472 if (associated(current_view_node)) then
473 call render_current_view(cr, current_view_node, bounds)
474 end if
475
476 ! Render selection highlight first (under hover)
477 if (selected_index > 0 .and. associated(current_view_node)) then
478 print *, "DEBUG: selected_index =", selected_index
479 if (allocated(current_view_node%children)) then
480 if (selected_index <= current_view_node%num_children) then
481 print *, "DEBUG: Rendering selection highlight for node:", selected_index
482 call render_selection_highlight(cr, current_view_node%children(selected_index))
483 else
484 print *, "DEBUG: selected_index out of bounds:", selected_index, ">", current_view_node%num_children
485 end if
486 else
487 print *, "DEBUG: current_view_node has no children"
488 end if
489 else
490 if (selected_index == 0) then
491 print *, "DEBUG: selected_index is 0 (no selection)"
492 end if
493 end if
494
495 ! Find which rectangle is under the mouse
496 hovered_index = find_node_at_position(mouse_x, mouse_y)
497
498 ! Render hover highlight on top (only if not the selected node)
499 if (hovered_index > 0 .and. hovered_index /= selected_index) then
500 if (associated(current_view_node)) then
501 if (allocated(current_view_node%children)) then
502 if (hovered_index <= current_view_node%num_children) then
503 call render_hover_highlight(cr, current_view_node%children(hovered_index))
504 end if
505 end if
506 end if
507 end if
508 end subroutine scan_and_render_with_interaction
509
510 ! Find which node is at the given position
511 function find_node_at_position(mouse_x, mouse_y) result(index)
512 real(c_double), intent(in) :: mouse_x, mouse_y
513 integer :: index, i
514 real :: x, y, w, h
515
516 index = 0
517
518 ! Check if mouse position is valid
519 if (mouse_x < 0 .or. mouse_y < 0) return
520 if (.not. associated(current_view_node)) return
521 if (.not. allocated(current_view_node%children)) return
522
523 ! Hit test against all visible rectangles (access bounds directly, no copying)
524 do i = 1, current_view_node%num_children
525 x = real(current_view_node%children(i)%bounds%x)
526 y = real(current_view_node%children(i)%bounds%y)
527 w = real(current_view_node%children(i)%bounds%width)
528 h = real(current_view_node%children(i)%bounds%height)
529
530 ! Check if mouse is inside this rectangle
531 if (mouse_x >= x .and. mouse_x <= x + w .and. &
532 mouse_y >= y .and. mouse_y <= y + h) then
533 index = i
534 return
535 end if
536 end do
537 end function find_node_at_position
538
539 ! Navigate into a directory node (zoom in)
540 subroutine navigate_into_node(index)
541 integer, intent(in) :: index
542
543 ! Block navigation if scan is active
544 if (is_scan_active()) then
545 print *, "Navigation blocked: Scan in progress"
546 return
547 end if
548
549 ! Validate inputs
550 if (.not. associated(current_view_node)) then
551 print *, "ERROR: No current view node!"
552 return
553 end if
554
555 if (.not. allocated(current_view_node%children)) then
556 print *, "ERROR: Current node has no children!"
557 return
558 end if
559
560 if (index < 1 .or. index > current_view_node%num_children) then
561 print *, "ERROR: Invalid child index: ", index
562 return
563 end if
564
565 ! Get the target node
566 if (.not. current_view_node%children(index)%is_directory) then
567 print *, "Cannot navigate into file (not a directory)"
568 return
569 end if
570
571 ! Restore all sizes before navigating (in case they were modified by filtering/deletion)
572 print *, "Restoring sizes before navigation..."
573 call recalculate_sizes(root_node)
574
575 ! Check if the directory needs to be scanned (progressive scan didn't populate children)
576 if (current_view_node%children(index)%num_children == 0 .and. &
577 allocated(current_view_node%children(index)%path)) then
578 ! This directory was created by progressive scan but never had children populated
579 ! We need to scan it now
580 print *, "Directory has no children - triggering rescan: ", &
581 trim(current_view_node%children(index)%path)
582
583 call scan_directory(trim(current_view_node%children(index)%path))
584 return ! scan_directory will set current_view_node appropriately
585 end if
586
587 ! Navigate into the directory
588 current_view_node => current_view_node%children(index)
589
590 ! Push onto path stack for breadcrumbs
591 if (path_depth < MAX_PATH_DEPTH) then
592 path_depth = path_depth + 1
593 if (allocated(current_view_node%name)) then
594 path_names(path_depth) = current_view_node%name
595 else
596 path_names(path_depth) = "(unnamed)"
597 end if
598 else
599 print *, "WARNING: Max path depth reached!"
600 end if
601
602 ! Reset layout cache to force recalculation
603 layout_calculated = .false.
604
605 if (allocated(current_view_node%name)) then
606 print *, "Navigated into: ", trim(current_view_node%name)
607 print *, "Children: ", current_view_node%num_children
608 print *, "Path depth: ", path_depth
609 else
610 print *, "Navigated into directory (no name)"
611 end if
612 end subroutine navigate_into_node
613
614 ! Navigate up one level (back button / breadcrumb click)
615 subroutine navigate_up(levels)
616 use disk_scanner, only: build_tree
617 use file_system, only: get_path_separator
618 integer, intent(in), optional :: levels
619 integer :: levels_to_go
620 integer :: i, last_sep, cache_index
621 character(len=512) :: parent_path, current_root_path
622 character(len=1) :: sep
623 type(file_node), pointer :: temp_node
624
625 ! Block navigation if scan is active
626 if (is_scan_active()) then
627 print *, "Navigation blocked: Scan in progress"
628 return
629 end if
630
631 if (present(levels)) then
632 levels_to_go = levels
633 else
634 levels_to_go = 1
635 end if
636
637 ! If at root depth, re-scan parent directory
638 if (path_depth <= 1) then
639 ! Get parent directory path
640 if (allocated(root_node%path)) then
641 current_root_path = trim(root_node%path)
642 sep = get_path_separator()
643
644 ! Strip trailing separator if present
645 if (len_trim(current_root_path) > 1) then
646 if (current_root_path(len_trim(current_root_path):len_trim(current_root_path)) == sep) then
647 current_root_path = current_root_path(1:len_trim(current_root_path)-1)
648 end if
649 end if
650
651 ! Find last path separator (after stripping trailing slash)
652 last_sep = 0
653 do i = len_trim(current_root_path), 1, -1
654 if (current_root_path(i:i) == sep) then
655 last_sep = i
656 exit
657 end if
658 end do
659
660 ! Can't go above filesystem root
661 if (last_sep <= 1 .and. sep == '/') then
662 print *, "Already at filesystem root: /"
663 return
664 else if (last_sep == 0) then
665 print *, "Cannot determine parent directory"
666 return
667 end if
668
669 ! Get parent path
670 if (last_sep > 1) then
671 parent_path = current_root_path(1:last_sep-1)
672 else
673 parent_path = sep ! Root directory
674 end if
675
676 print *, "Re-scanning parent directory: ", trim(parent_path)
677
678 ! Use scan_directory instead of build_tree to ensure colors are applied
679 ! and progress is shown
680 call scan_directory(trim(parent_path))
681
682 ! Update path names
683 path_depth = 1
684 if (allocated(root_node%name)) then
685 path_names(1) = trim(root_node%path)
686 else
687 path_names(1) = trim(parent_path)
688 end if
689
690 ! Reset layout cache
691 layout_calculated = .false.
692
693 print *, "Navigated up to parent: ", trim(parent_path)
694 return
695 else
696 print *, "Cannot navigate up: root path not set"
697 return
698 end if
699 end if
700
701 ! Go up the specified number of levels
702 path_depth = max(1, path_depth - levels_to_go)
703
704 ! Restore all sizes before navigating (in case they were modified by filtering/deletion)
705 print *, "Restoring sizes before navigation..."
706 call recalculate_sizes(root_node)
707
708 ! Navigate back up to the correct node by traversing from root
709 current_view_node => root_node
710
711 ! If we're deeper than root, traverse down to the correct node
712 if (path_depth > 1) then
713 do i = 2, path_depth
714 ! Find child matching path_names(i)
715 if (.not. allocated(current_view_node%children)) then
716 print *, "ERROR: Cannot navigate - current node has no children"
717 current_view_node => root_node
718 path_depth = 1
719 exit
720 end if
721
722 ! Search for matching child by name
723 temp_node => null()
724 do cache_index = 1, current_view_node%num_children
725 if (allocated(current_view_node%children(cache_index)%name)) then
726 if (trim(current_view_node%children(cache_index)%name) == trim(path_names(i))) then
727 temp_node => current_view_node%children(cache_index)
728 exit
729 end if
730 end if
731 end do
732
733 if (associated(temp_node)) then
734 current_view_node => temp_node
735 else
736 print *, "WARNING: Could not find child for path: ", trim(path_names(i))
737 current_view_node => root_node
738 path_depth = 1
739 exit
740 end if
741 end do
742 end if
743
744 ! Reset layout cache
745 layout_calculated = .false.
746
747 print *, "Navigated up to depth: ", path_depth
748 if (allocated(current_view_node%path)) then
749 print *, "Current view: ", trim(current_view_node%path)
750 end if
751 end subroutine navigate_up
752
753 ! Remove selected node from view (after deletion)
754 ! This marks the node with size 0 so it won't be rendered
755 subroutine remove_selected_node_from_view(selected_index)
756 integer, intent(in) :: selected_index
757
758 print *, "Removing node from view: index=", selected_index
759
760 ! Validate inputs
761 if (.not. associated(current_view_node)) then
762 print *, "ERROR: No current view node"
763 return
764 end if
765
766 if (.not. allocated(current_view_node%children)) then
767 print *, "ERROR: Current node has no children"
768 return
769 end if
770
771 if (selected_index < 1 .or. selected_index > current_view_node%num_children) then
772 print *, "ERROR: Invalid selected index: ", selected_index
773 return
774 end if
775
776 ! Mark the node as deleted by setting its size to 0
777 ! This will cause the layout algorithm to skip it
778 current_view_node%children(selected_index)%size = 0_int64
779
780 print *, "Node marked as deleted (size = 0)"
781
782 ! Invalidate layout so it gets recalculated without this node
783 layout_calculated = .false.
784 end subroutine remove_selected_node_from_view
785
786 ! Get current path depth
787 function get_path_depth() result(depth)
788 integer :: depth
789 depth = path_depth
790 end function get_path_depth
791
792 ! Get breadcrumb path (returns array of names)
793 subroutine get_breadcrumb_path(names, count)
794 character(len=256), dimension(:), intent(out) :: names
795 integer, intent(out) :: count
796 integer :: i
797
798 count = path_depth
799 do i = 1, min(path_depth, size(names))
800 names(i) = path_names(i)
801 end do
802 end subroutine get_breadcrumb_path
803
804 ! Get number of visible nodes in current view
805 function get_node_count() result(count)
806 integer :: count
807
808 if (associated(current_view_node)) then
809 count = current_view_node%num_children
810 else
811 count = 0
812 end if
813 end function get_node_count
814
815 ! Get center coordinates of a node by index (for keyboard navigation)
816 subroutine get_node_center_by_index(index, center_x, center_y, success)
817 integer, intent(in) :: index
818 real(c_double), intent(out) :: center_x, center_y
819 logical, intent(out) :: success
820
821 success = .false.
822
823 if (.not. associated(current_view_node)) return
824 if (index < 1 .or. index > current_view_node%num_children) return
825
826 ! Get the node's bounds and calculate center
827 center_x = real(current_view_node%children(index)%bounds%x, c_double) + &
828 real(current_view_node%children(index)%bounds%width, c_double) / 2.0d0
829 center_y = real(current_view_node%children(index)%bounds%y, c_double) + &
830 real(current_view_node%children(index)%bounds%height, c_double) / 2.0d0
831
832 success = .true.
833 end subroutine get_node_center_by_index
834
835 ! Find the best node in a given direction from current position
836 ! direction: 1=up, 2=down, 3=left, 4=right
837 function find_node_in_direction(from_x, from_y, direction) result(best_index)
838 use iso_fortran_env, only: real64
839 real(c_double), intent(in) :: from_x, from_y
840 integer, intent(in) :: direction
841 integer :: best_index
842 integer :: i
843 real(real64) :: cx, cy, dx, dy, score, best_score
844 real(real64) :: directional_component, perpendicular_component
845
846 best_index = 0
847 best_score = 1.0d20 ! Large number
848
849 if (.not. associated(current_view_node)) return
850 if (current_view_node%num_children == 0) return
851
852 ! For each child node, calculate score based on direction
853 do i = 1, current_view_node%num_children
854 ! Get center of this node
855 cx = real(current_view_node%children(i)%bounds%x, real64) + &
856 real(current_view_node%children(i)%bounds%width, real64) / 2.0d0
857 cy = real(current_view_node%children(i)%bounds%y, real64) + &
858 real(current_view_node%children(i)%bounds%height, real64) / 2.0d0
859
860 dx = cx - real(from_x, real64)
861 dy = cy - real(from_y, real64)
862
863 ! Check if node is in the correct direction
864 select case (direction)
865 case (1) ! Up
866 if (dy >= 0.0d0) cycle ! Skip nodes below or at same level
867 directional_component = abs(dy) ! Distance upward
868 perpendicular_component = abs(dx) ! Horizontal offset
869 case (2) ! Down
870 if (dy <= 0.0d0) cycle ! Skip nodes above or at same level
871 directional_component = abs(dy) ! Distance downward
872 perpendicular_component = abs(dx) ! Horizontal offset
873 case (3) ! Left
874 if (dx >= 0.0d0) cycle ! Skip nodes to right or at same position
875 directional_component = abs(dx) ! Distance leftward
876 perpendicular_component = abs(dy) ! Vertical offset
877 case (4) ! Right
878 if (dx <= 0.0d0) cycle ! Skip nodes to left or at same position
879 directional_component = abs(dx) ! Distance rightward
880 perpendicular_component = abs(dy) ! Vertical offset
881 case default
882 cycle
883 end select
884
885 ! Score: prioritize alignment (low perpendicular) and closeness (low directional)
886 ! Weight perpendicular offset more heavily to prefer aligned nodes
887 score = directional_component + perpendicular_component * 2.0d0
888
889 if (score < best_score) then
890 best_score = score
891 best_index = i
892 end if
893 end do
894
895 ! If no node found in direction, wrap around to opposite edge
896 if (best_index == 0 .and. current_view_node%num_children > 0) then
897 select case (direction)
898 case (1) ! Up - wrap to bottom (max Y)
899 best_index = 1
900 best_score = real(current_view_node%children(1)%bounds%y + &
901 current_view_node%children(1)%bounds%height, real64)
902 do i = 2, current_view_node%num_children
903 score = real(current_view_node%children(i)%bounds%y + &
904 current_view_node%children(i)%bounds%height, real64)
905 if (score > best_score) then
906 best_score = score
907 best_index = i
908 end if
909 end do
910
911 case (2) ! Down - wrap to top (min Y)
912 best_index = 1
913 best_score = real(current_view_node%children(1)%bounds%y, real64)
914 do i = 2, current_view_node%num_children
915 score = real(current_view_node%children(i)%bounds%y, real64)
916 if (score < best_score) then
917 best_score = score
918 best_index = i
919 end if
920 end do
921
922 case (3) ! Left - wrap to right (max X)
923 best_index = 1
924 best_score = real(current_view_node%children(1)%bounds%x + &
925 current_view_node%children(1)%bounds%width, real64)
926 do i = 2, current_view_node%num_children
927 score = real(current_view_node%children(i)%bounds%x + &
928 current_view_node%children(i)%bounds%width, real64)
929 if (score > best_score) then
930 best_score = score
931 best_index = i
932 end if
933 end do
934
935 case (4) ! Right - wrap to left (min X)
936 best_index = 1
937 best_score = real(current_view_node%children(1)%bounds%x, real64)
938 do i = 2, current_view_node%num_children
939 score = real(current_view_node%children(i)%bounds%x, real64)
940 if (score < best_score) then
941 best_score = score
942 best_index = i
943 end if
944 end do
945
946 case default
947 best_index = 1 ! Fallback
948 end select
949 end if
950
951 end function find_node_in_direction
952
953 ! Render hover highlight overlay
954 subroutine render_hover_highlight(cr, node)
955 type(c_ptr), intent(in) :: cr
956 type(file_node), intent(in) :: node
957 real(c_double) :: x, y, w, h
958
959 x = real(node%bounds%x, c_double)
960 y = real(node%bounds%y, c_double)
961 w = real(node%bounds%width, c_double)
962 h = real(node%bounds%height, c_double)
963
964 ! Draw semi-transparent yellow overlay (30% opacity) - closer to selection color
965 call cairo_set_source_rgba(cr, 1.0d0, 0.9d0, 0.3d0, 0.3d0)
966 call cairo_rectangle(cr, x, y, w, h)
967 call cairo_fill(cr)
968
969 ! Draw thicker yellow highlight border
970 call cairo_set_source_rgb(cr, 1.0d0, 0.9d0, 0.2d0)
971 call cairo_set_line_width(cr, 2.5d0)
972 call cairo_rectangle(cr, x, y, w, h)
973 call cairo_stroke(cr)
974 end subroutine render_hover_highlight
975
976 ! Render selection highlight overlay (different from hover)
977 subroutine render_selection_highlight(cr, node)
978 type(c_ptr), intent(in) :: cr
979 type(file_node), intent(in) :: node
980 real(c_double) :: x, y, w, h
981
982 x = real(node%bounds%x, c_double)
983 y = real(node%bounds%y, c_double)
984 w = real(node%bounds%width, c_double)
985 h = real(node%bounds%height, c_double)
986
987 ! Draw thick colored border (yellow/gold for selection)
988 call cairo_set_source_rgb(cr, 1.0d0, 0.84d0, 0.0d0) ! Gold color
989 call cairo_set_line_width(cr, 4.0d0)
990 call cairo_rectangle(cr, x, y, w, h)
991 call cairo_stroke(cr)
992
993 ! Draw inner border for extra emphasis
994 call cairo_set_source_rgb(cr, 1.0d0, 1.0d0, 0.0d0) ! Bright yellow
995 call cairo_set_line_width(cr, 2.0d0)
996 call cairo_rectangle(cr, x + 2.0d0, y + 2.0d0, w - 4.0d0, h - 4.0d0)
997 call cairo_stroke(cr)
998 end subroutine render_selection_highlight
999
1000 ! Render flash highlight overlay (for actively resizing items during scan)
1001 subroutine render_flash_highlight(cr, node)
1002 type(c_ptr), intent(in) :: cr
1003 type(file_node), intent(in) :: node
1004 real(c_double) :: x, y, w, h
1005 real(c_double) :: intensity
1006
1007 ! Skip if flash intensity is too low
1008 if (node%flash_intensity < 0.05d0) return
1009
1010 x = real(node%bounds%x, c_double)
1011 y = real(node%bounds%y, c_double)
1012 w = real(node%bounds%width, c_double)
1013 h = real(node%bounds%height, c_double)
1014 intensity = real(node%flash_intensity, c_double)
1015
1016 ! Draw bright cyan/white flash overlay (intensity-based opacity)
1017 ! Cyan gives that "electric" SpaceSniffer feel
1018 call cairo_set_source_rgba(cr, 0.3d0, 1.0d0, 1.0d0, intensity * 0.6d0)
1019 call cairo_rectangle(cr, x, y, w, h)
1020 call cairo_fill(cr)
1021
1022 ! Draw bright flash border
1023 call cairo_set_source_rgba(cr, 0.5d0, 1.0d0, 1.0d0, intensity * 0.9d0)
1024 call cairo_set_line_width(cr, 2.0d0)
1025 call cairo_rectangle(cr, x, y, w, h)
1026 call cairo_stroke(cr)
1027 end subroutine render_flash_highlight
1028
1029 ! Get file extension from filename
1030 function get_file_extension(filename) result(ext)
1031 character(len=*), intent(in) :: filename
1032 character(len=:), allocatable :: ext
1033 integer :: dot_pos, i, ascii_val
1034 character(len=256) :: temp_ext
1035
1036 ! Find last dot in filename
1037 dot_pos = 0
1038 do i = len_trim(filename), 1, -1
1039 if (filename(i:i) == '.') then
1040 dot_pos = i
1041 exit
1042 end if
1043 end do
1044
1045 if (dot_pos > 0 .and. dot_pos < len_trim(filename)) then
1046 temp_ext = trim(filename(dot_pos+1:))
1047 ! Convert to lowercase for comparison
1048 do i = 1, len_trim(temp_ext)
1049 ascii_val = iachar(temp_ext(i:i))
1050 if (ascii_val >= 65 .and. ascii_val <= 90) then ! A-Z
1051 temp_ext(i:i) = achar(ascii_val + 32)
1052 end if
1053 end do
1054 ext = trim(temp_ext)
1055 else
1056 ext = ""
1057 end if
1058 end function get_file_extension
1059
1060 ! Strip file extension from a filename (modifies in place)
1061 subroutine strip_extension(filename)
1062 character(len=*), intent(inout) :: filename
1063 integer :: dot_pos, i
1064
1065 ! Find the last dot in the filename
1066 dot_pos = 0
1067 do i = len_trim(filename), 1, -1
1068 if (filename(i:i) == '.') then
1069 dot_pos = i
1070 exit
1071 end if
1072 ! Stop at path separators (no dot in filename)
1073 if (filename(i:i) == '/' .or. filename(i:i) == '\') then
1074 exit
1075 end if
1076 end do
1077
1078 ! If dot found and not at start of filename, strip from dot onward
1079 if (dot_pos > 1) then
1080 filename(dot_pos:) = ' ' ! Replace with spaces
1081 end if
1082 end subroutine strip_extension
1083
1084 ! Get color hue based on file type
1085 function get_file_type_hue(filename) result(hue)
1086 use iso_fortran_env, only: real64
1087 character(len=*), intent(in) :: filename
1088 real(real64) :: hue
1089 character(len=:), allocatable :: ext
1090
1091 ext = get_file_extension(filename)
1092
1093 ! Assign hue based on file type categories
1094 ! Images: Green (120)
1095 if (ext == "jpg" .or. ext == "jpeg" .or. ext == "png" .or. ext == "gif" .or. &
1096 ext == "bmp" .or. ext == "svg" .or. ext == "ico" .or. ext == "webp" .or. &
1097 ext == "tiff" .or. ext == "tif") then
1098 hue = 120.0d0
1099 ! Videos: Magenta (300)
1100 else if (ext == "mp4" .or. ext == "avi" .or. ext == "mov" .or. ext == "mkv" .or. &
1101 ext == "flv" .or. ext == "wmv" .or. ext == "webm" .or. ext == "m4v" .or. &
1102 ext == "mpg" .or. ext == "mpeg") then
1103 hue = 300.0d0
1104 ! Audio: Cyan (180)
1105 else if (ext == "mp3" .or. ext == "wav" .or. ext == "flac" .or. ext == "aac" .or. &
1106 ext == "ogg" .or. ext == "wma" .or. ext == "m4a" .or. ext == "opus") then
1107 hue = 180.0d0
1108 ! Documents: Yellow (60)
1109 else if (ext == "pdf" .or. ext == "doc" .or. ext == "docx" .or. ext == "txt" .or. &
1110 ext == "rtf" .or. ext == "odt" .or. ext == "pages" .or. ext == "md") then
1111 hue = 60.0d0
1112 ! Archives: Red (0)
1113 else if (ext == "zip" .or. ext == "tar" .or. ext == "gz" .or. ext == "rar" .or. &
1114 ext == "7z" .or. ext == "bz2" .or. ext == "xz" .or. ext == "tgz" .or. &
1115 ext == "dmg" .or. ext == "iso") then
1116 hue = 0.0d0
1117 ! Code: Orange (30)
1118 else if (ext == "py" .or. ext == "js" .or. ext == "java" .or. ext == "c" .or. &
1119 ext == "cpp" .or. ext == "h" .or. ext == "rs" .or. ext == "go" .or. &
1120 ext == "rb" .or. ext == "php" .or. ext == "f90" .or. ext == "f95" .or. &
1121 ext == "f03" .or. ext == "f08" .or. ext == "ts" .or. ext == "jsx" .or. &
1122 ext == "tsx" .or. ext == "swift" .or. ext == "kt") then
1123 hue = 30.0d0
1124 ! Spreadsheets: Lime (90)
1125 else if (ext == "xls" .or. ext == "xlsx" .or. ext == "csv" .or. ext == "ods" .or. &
1126 ext == "numbers") then
1127 hue = 90.0d0
1128 ! Presentations: Rose (330)
1129 else if (ext == "ppt" .or. ext == "pptx" .or. ext == "odp" .or. ext == "key") then
1130 hue = 330.0d0
1131 ! Executables: Dark Red (15)
1132 else if (ext == "exe" .or. ext == "app" .or. ext == "bin" .or. ext == "sh" .or. &
1133 ext == "bat" .or. ext == "com") then
1134 hue = 15.0d0
1135 ! Default: Gray tone (0 with low saturation handled by caller)
1136 else
1137 hue = 0.0d0
1138 end if
1139 end function get_file_type_hue
1140
1141 ! Assign colors based on file type
1142 recursive subroutine color_tree(node, depth)
1143 use iso_fortran_env, only: real64
1144 type(file_node), intent(inout) :: node
1145 integer, intent(in) :: depth
1146 integer :: i
1147 real(real64) :: hue, hue_offset
1148
1149 if (node%is_directory) then
1150 ! Directories: blue-ish tones with depth variation
1151 hue = mod(depth * 60.0, 360.0) ! 0, 60, 120, 180, 240, 300
1152 node%color = hsv_to_rgb(hue, 0.6d0, 0.8d0)
1153 else
1154 ! Files: color by file type
1155 hue = get_file_type_hue(node%name)
1156 if (abs(hue) < 0.01d0 .and. len_trim(get_file_extension(node%name)) == 0) then
1157 ! No extension - use gray
1158 node%color = hsv_to_rgb(0.0d0, 0.1d0, 0.8d0)
1159 else
1160 node%color = hsv_to_rgb(hue, 0.7d0, 0.9d0)
1161 end if
1162 end if
1163
1164 ! Recurse to children with varying hues for siblings
1165 if (allocated(node%children)) then
1166 do i = 1, node%num_children
1167 ! For directories, calculate hue offset based on sibling index
1168 if (node%children(i)%is_directory) then
1169 hue = mod(depth * 60.0, 360.0)
1170 hue_offset = real(mod(i * 37, 360), real64) ! 37 is prime for good distribution
1171 node%children(i)%color = hsv_to_rgb(hue + hue_offset, 0.6d0, 0.8d0)
1172 else
1173 ! For files, use file type color
1174 hue = get_file_type_hue(node%children(i)%name)
1175 if (abs(hue) < 0.01d0 .and. len_trim(get_file_extension(node%children(i)%name)) == 0) then
1176 node%children(i)%color = hsv_to_rgb(0.0d0, 0.1d0, 0.8d0)
1177 else
1178 ! Add slight variation based on sibling index
1179 hue_offset = real(mod(i * 5, 30), real64) - 15.0d0 ! Vary by ±15 degrees
1180 node%children(i)%color = hsv_to_rgb(hue + hue_offset, 0.7d0, 0.9d0)
1181 end if
1182 end if
1183
1184 ! Recurse with increased depth
1185 call color_tree(node%children(i), depth + 1)
1186 end do
1187 end if
1188 end subroutine color_tree
1189
1190 ! Initialize cushion parameters for treemap nodes
1191 ! Based on Van Wijk & Van de Wetering algorithm
1192 recursive subroutine init_cushions(node, parent_cushion)
1193 use iso_fortran_env, only: real64
1194 type(file_node), intent(inout) :: node
1195 type(cushion_params), intent(in), optional :: parent_cushion
1196 real(real64) :: x, y, w, h, cx, cy
1197 real(real64) :: f ! Ridge height factor
1198 integer :: i
1199
1200 ! Ridge height factor (controls the "bumpiness" of the cushion)
1201 ! Higher values = more pronounced 3D effect
1202 ! Need very large values because we divide by w² (pixels²)
1203 f = 50000.0d0
1204
1205 ! Get rectangle bounds
1206 x = real(node%bounds%x, real64)
1207 y = real(node%bounds%y, real64)
1208 w = real(node%bounds%width, real64)
1209 h = real(node%bounds%height, real64)
1210
1211 ! Calculate center
1212 cx = x + w / 2.0d0
1213 cy = y + h / 2.0d0
1214
1215 ! Initialize or inherit cushion parameters
1216 if (present(parent_cushion)) then
1217 ! Inherit parent cushion and add our own
1218 node%cushion%ax = parent_cushion%ax
1219 node%cushion%ay = parent_cushion%ay
1220 node%cushion%bx = parent_cushion%bx
1221 node%cushion%by = parent_cushion%by
1222 node%cushion%c = parent_cushion%c
1223 node%cushion%depth = parent_cushion%depth + 1
1224 else
1225 ! Root node - initialize to zero
1226 node%cushion%ax = 0.0d0
1227 node%cushion%ay = 0.0d0
1228 node%cushion%bx = 0.0d0
1229 node%cushion%by = 0.0d0
1230 node%cushion%c = 0.0d0
1231 node%cushion%depth = 0
1232 end if
1233
1234 ! Add this node's cushion ridge
1235 if (w > 0.0d0 .and. h > 0.0d0) then
1236 ! Add quadratic terms (Van Wijk algorithm)
1237 node%cushion%ax = node%cushion%ax + f / (w * w)
1238 node%cushion%ay = node%cushion%ay + f / (h * h)
1239
1240 ! Add linear terms
1241 node%cushion%bx = node%cushion%bx - 2.0d0 * (f / (w * w)) * cx
1242 node%cushion%by = node%cushion%by - 2.0d0 * (f / (h * h)) * cy
1243
1244 ! Add constant term
1245 node%cushion%c = node%cushion%c + (f / (w * w)) * cx * cx + (f / (h * h)) * cy * cy
1246 end if
1247
1248 ! Recursively init children
1249 if (allocated(node%children)) then
1250 do i = 1, node%num_children
1251 call init_cushions(node%children(i), node%cushion)
1252 end do
1253 end if
1254 end subroutine init_cushions
1255
1256 ! Simple HSV to RGB conversion
1257 function hsv_to_rgb(h, s, v) result(color)
1258 use iso_fortran_env, only: real64
1259 real(real64), intent(in) :: h, s, v
1260 type(rgb_color) :: color
1261 real(real64) :: c, x, m, h_prime
1262 integer :: sector
1263
1264 h_prime = h / 60.0d0
1265 c = v * s
1266 x = c * (1.0d0 - abs(mod(h_prime, 2.0d0) - 1.0d0))
1267 m = v - c
1268
1269 sector = int(h_prime)
1270
1271 select case (sector)
1272 case (0)
1273 color%r = c + m; color%g = x + m; color%b = m
1274 case (1)
1275 color%r = x + m; color%g = c + m; color%b = m
1276 case (2)
1277 color%r = m; color%g = c + m; color%b = x + m
1278 case (3)
1279 color%r = m; color%g = x + m; color%b = c + m
1280 case (4)
1281 color%r = x + m; color%g = m; color%b = c + m
1282 case default
1283 color%r = c + m; color%g = m; color%b = x + m
1284 end select
1285 end function hsv_to_rgb
1286
1287 ! Render only the current view (direct children only, no recursion)
1288 subroutine render_current_view(cr, view_node, bounds)
1289 use cairo, only: cairo_set_source_rgb, cairo_move_to, cairo_show_text, &
1290 cairo_set_font_size, cairo_select_font_face
1291 type(c_ptr), intent(in) :: cr
1292 type(file_node), intent(in) :: view_node
1293 type(rect), intent(in) :: bounds
1294 integer :: i, visible_count
1295 real(c_double) :: center_x, center_y
1296
1297 ! Render only the direct children of the current view
1298 if (allocated(view_node%children) .and. view_node%num_children > 0) then
1299 print *, "DEBUG: Rendering", view_node%num_children, "children"
1300
1301 ! Count visible nodes
1302 visible_count = 0
1303 do i = 1, view_node%num_children
1304 ! Skip nodes without names (shouldn't happen but be defensive)
1305 if (.not. allocated(view_node%children(i)%name)) cycle
1306
1307 if (view_node%children(i)%size > 0) then
1308 visible_count = visible_count + 1
1309 if (visible_count <= 5) then
1310 print *, "DEBUG: Visible child", i, ":", trim(view_node%children(i)%name), &
1311 "size=", view_node%children(i)%size, "original=", view_node%children(i)%original_size
1312 end if
1313 end if
1314 end do
1315 print *, "DEBUG: Total visible children:", visible_count, "of", view_node%num_children
1316
1317 do i = 1, view_node%num_children
1318 ! Skip nodes without names (shouldn't happen but be defensive)
1319 if (.not. allocated(view_node%children(i)%name)) cycle
1320
1321 ! Skip nodes with size 0 (deleted)
1322 if (view_node%children(i)%size == 0) then
1323 cycle
1324 end if
1325
1326 call render_node(cr, view_node%children(i))
1327 end do
1328 else
1329 ! Empty directory - show warning message
1330 print *, "DEBUG: Empty directory - showing warning"
1331
1332 ! Draw warning text in center
1333 center_x = real(bounds%width, c_double) / 2.0_c_double
1334 center_y = real(bounds%height, c_double) / 2.0_c_double
1335
1336 ! Set color to yellow/orange for warning
1337 call cairo_set_source_rgb(cr, 0.9_c_double, 0.6_c_double, 0.0_c_double)
1338 ! cairo font: family, slant (0=normal), weight (1=bold)
1339 call cairo_select_font_face(cr, "Sans"//c_null_char, 0_c_int, 1_c_int)
1340 call cairo_set_font_size(cr, 24.0_c_double)
1341
1342 ! Center the text (approximate)
1343 call cairo_move_to(cr, center_x - 100.0_c_double, center_y - 30.0_c_double)
1344 call cairo_show_text(cr, "Empty Directory"//c_null_char)
1345
1346 call cairo_set_font_size(cr, 14.0_c_double)
1347 call cairo_move_to(cr, center_x - 120.0_c_double, center_y + 10.0_c_double)
1348 call cairo_show_text(cr, "Press Backspace to go up"//c_null_char)
1349 end if
1350 end subroutine render_current_view
1351
1352 ! Render a single node (non-recursive)
1353 subroutine render_node(cr, node)
1354 type(c_ptr), intent(in) :: cr
1355 type(file_node), intent(in) :: node
1356 real(c_double) :: x, y, w, h
1357 logical :: can_show_label
1358
1359 ! Don't render tiny rectangles
1360 if (node%bounds%width < 2 .or. node%bounds%height < 2) return
1361
1362 x = real(node%bounds%x, c_double)
1363 y = real(node%bounds%y, c_double)
1364 w = real(node%bounds%width, c_double)
1365 h = real(node%bounds%height, c_double)
1366
1367 ! Check if we can show a full label
1368 can_show_label = (w >= 50.0d0 .and. h >= 20.0d0)
1369
1370 ! Fill rectangle with base color (same for both modes)
1371 call cairo_set_source_rgb(cr, node%color%r, node%color%g, node%color%b)
1372 call cairo_rectangle(cr, x, y, w, h)
1373 call cairo_fill(cr)
1374
1375 ! DEBUG: Check flag value (print only once per render)
1376 ! if (w > 100.0d0) print *, "DEBUG render_node: use_cushion_shading=", use_cushion_shading
1377
1378 if (use_cushion_shading) then
1379 ! 3D mode: Draw highlight and shadow lines for embossed effect
1380 ! Only draw if rectangle is large enough
1381 if (w > 6.0d0 .and. h > 6.0d0) then
1382 ! Top-left highlight (inset by 1px to be inside border)
1383 call cairo_set_source_rgba(cr, 1.0d0, 1.0d0, 1.0d0, 0.6d0)
1384 call cairo_set_line_width(cr, 3.0d0)
1385 call cairo_move_to(cr, x + 2.0d0, y + h - 2.0d0)
1386 call cairo_line_to(cr, x + 2.0d0, y + 2.0d0)
1387 call cairo_line_to(cr, x + w - 2.0d0, y + 2.0d0)
1388 call cairo_stroke(cr)
1389
1390 ! Bottom-right shadow (inset by 1px to be inside border)
1391 call cairo_set_source_rgba(cr, 0.0d0, 0.0d0, 0.0d0, 0.5d0)
1392 call cairo_set_line_width(cr, 3.0d0)
1393 call cairo_move_to(cr, x + 2.0d0, y + h - 2.0d0)
1394 call cairo_line_to(cr, x + w - 2.0d0, y + h - 2.0d0)
1395 call cairo_line_to(cr, x + w - 2.0d0, y + 2.0d0)
1396 call cairo_stroke(cr)
1397 end if
1398 end if
1399
1400 ! Always draw border (for both flat and 3D modes)
1401 call cairo_set_source_rgb(cr, 0.0d0, 0.0d0, 0.0d0)
1402 call cairo_set_line_width(cr, 1.0d0)
1403 call cairo_rectangle(cr, x, y, w, h)
1404 call cairo_stroke(cr)
1405
1406 ! Render flash highlight if node is actively being updated
1407 if (node%flash_intensity > 0.05d0) then
1408 call render_flash_highlight(cr, node)
1409 end if
1410
1411 ! Render text label if rectangle is large enough
1412 if (can_show_label) then
1413 call render_label(cr, node, x, y, w, h)
1414 else if (w >= 10.0d0 .and. h >= 10.0d0) then
1415 ! Show "..." for rectangles too small for full labels
1416 call render_ellipsis(cr, x, y, w, h)
1417 else if (w >= 2.0d0 .and. h >= 2.0d0) then
1418 ! Debug: print info about unlabeled rectangles
1419 if (allocated(node%name)) then
1420 print *, "Unlabeled rect: ", trim(node%name), " size=", w, "x", h
1421 else
1422 print *, "Unlabeled rect: (no name) size=", w, "x", h
1423 end if
1424 end if
1425 end subroutine render_node
1426
1427 ! Calculate cushion shading intensity using Van Wijk algorithm
1428 ! Returns a factor between 0.0 (dark) and 1.0 (bright)
1429 function calculate_cushion_shading(x, y, w, h, cushion) result(intensity)
1430 use iso_fortran_env, only: real64
1431 real(c_double), intent(in) :: x, y, w, h
1432 type(cushion_params), intent(in) :: cushion
1433 real(real64) :: intensity
1434 real(real64) :: cx, cy ! Center of rectangle
1435 real(real64) :: nx, ny, nz, norm ! Normal vector
1436 real(real64) :: lx, ly, lz ! Light direction (from top-left)
1437 real(real64) :: dot_product
1438 real(real64) :: ambient, diffuse
1439
1440 ! Light source direction (normalized) - coming from top-left at 45 degrees
1441 lx = -0.5d0
1442 ly = -0.5d0
1443 lz = 0.707d0 ! sqrt(1 - lx^2 - ly^2)
1444
1445 ! Ambient and diffuse lighting coefficients
1446 ambient = 0.3d0 ! Base lighting (lowered for more contrast)
1447 diffuse = 0.7d0 ! Directional lighting strength (increased for more pronounced effect)
1448
1449 ! Calculate center of rectangle
1450 cx = x + w / 2.0d0
1451 cy = y + h / 2.0d0
1452
1453 ! Calculate surface gradient (partial derivatives)
1454 ! h(x,y) = ax*x² + bx*x + ay*y² + by*y + c
1455 ! ∂h/∂x = 2*ax*x + bx
1456 ! ∂h/∂y = 2*ay*y + by
1457 nx = -(2.0d0 * cushion%ax * cx + cushion%bx)
1458 ny = -(2.0d0 * cushion%ay * cy + cushion%by)
1459 nz = 1.0d0
1460
1461 ! Debug: Print cushion params for first rectangle
1462 if (abs(cushion%ax) > 1e-10 .or. abs(cushion%ay) > 1e-10) then
1463 ! Only print if we have non-zero cushion params (skip spam)
1464 end if
1465
1466 ! Normalize the normal vector
1467 norm = sqrt(nx*nx + ny*ny + nz*nz)
1468 if (norm > 0.0d0) then
1469 nx = nx / norm
1470 ny = ny / norm
1471 nz = nz / norm
1472 else
1473 nx = 0.0d0
1474 ny = 0.0d0
1475 nz = 1.0d0
1476 end if
1477
1478 ! Calculate Lambertian shading (dot product of normal and light direction)
1479 dot_product = nx*lx + ny*ly + nz*lz
1480 dot_product = max(0.0d0, dot_product) ! Clamp negative values
1481
1482 ! Combine ambient and diffuse lighting
1483 intensity = ambient + diffuse * dot_product
1484 intensity = min(1.0d0, max(0.0d0, intensity)) ! Clamp to [0,1]
1485
1486 ! Debug output (only print occasionally to avoid spam)
1487 ! print *, "Shading: cushion%ax=", cushion%ax, " intensity=", intensity
1488 end function calculate_cushion_shading
1489
1490 ! Render ellipsis for small rectangles
1491 subroutine render_ellipsis(cr, x, y, w, h)
1492 type(c_ptr), intent(in) :: cr
1493 real(c_double), intent(in) :: x, y, w, h
1494 real(c_double) :: font_size, text_x, text_y
1495
1496 ! Small font for ellipsis
1497 font_size = min(h * 0.5d0, 12.0d0)
1498 if (font_size < 6.0d0) return
1499
1500 call cairo_select_font_face(cr, "Sans"//c_null_char, 0_c_int, 0_c_int)
1501 call cairo_set_font_size(cr, font_size)
1502
1503 ! Center the ellipsis
1504 text_x = x + w / 2.0d0 - font_size * 0.5d0
1505 text_y = y + h / 2.0d0 + font_size * 0.3d0
1506
1507 ! Draw with contrast
1508 call cairo_set_source_rgb(cr, 1.0d0, 1.0d0, 1.0d0)
1509 call cairo_move_to(cr, text_x, text_y)
1510 call cairo_show_text(cr, "..."//c_null_char)
1511 end subroutine render_ellipsis
1512
1513 ! Format file size in human-readable format
1514 function format_size(size_bytes) result(size_str)
1515 use iso_fortran_env, only: int64, real64
1516 integer(int64), intent(in) :: size_bytes
1517 character(len=20) :: size_str
1518 real(real64) :: size_val
1519
1520 if (size_bytes < 1024_int64) then
1521 write(size_str, '(I0, A)') size_bytes, ' B'
1522 else if (size_bytes < 1024_int64 * 1024_int64) then
1523 size_val = real(size_bytes, real64) / 1024.0d0
1524 write(size_str, '(F0.1, A)') size_val, ' KB'
1525 else if (size_bytes < 1024_int64 * 1024_int64 * 1024_int64) then
1526 size_val = real(size_bytes, real64) / (1024.0d0 * 1024.0d0)
1527 write(size_str, '(F0.1, A)') size_val, ' MB'
1528 else
1529 size_val = real(size_bytes, real64) / (1024.0d0 * 1024.0d0 * 1024.0d0)
1530 write(size_str, '(F0.1, A)') size_val, ' GB'
1531 end if
1532 end function format_size
1533
1534 ! Cache helper functions
1535 ! Look up cached directory scan by path
1536 function cache_lookup(path) result(found_index)
1537 character(len=*), intent(in) :: path
1538 integer :: found_index, i
1539
1540 found_index = 0
1541 do i = 1, cache_count
1542 if (dir_cache(i)%valid .and. trim(dir_cache(i)%path) == trim(path)) then
1543 found_index = i
1544 print *, "Cache hit for: ", trim(path)
1545 return
1546 end if
1547 end do
1548 print *, "Cache miss for: ", trim(path)
1549 end function cache_lookup
1550
1551 ! Store scanned directory in cache
1552 subroutine cache_store(path, node)
1553 character(len=*), intent(in) :: path
1554 type(file_node), intent(in) :: node
1555 integer :: store_index
1556
1557 ! Simple strategy: if cache full, overwrite oldest (index 1)
1558 if (cache_count < MAX_CACHE_SIZE) then
1559 cache_count = cache_count + 1
1560 store_index = cache_count
1561 else
1562 ! Cache full - simple FIFO: overwrite first entry
1563 print *, "Cache full - evicting oldest entry"
1564 store_index = 1
1565 ! Deallocate old entry before overwriting
1566 if (allocated(dir_cache(store_index)%node)) then
1567 call deallocate_tree(dir_cache(store_index)%node)
1568 deallocate(dir_cache(store_index)%node)
1569 end if
1570 end if
1571
1572 ! Store in cache (deep copy via allocatable assignment)
1573 dir_cache(store_index)%path = trim(path)
1574 allocate(dir_cache(store_index)%node)
1575 dir_cache(store_index)%node = node ! Deep copy
1576 dir_cache(store_index)%valid = .true.
1577
1578 print *, "Cached scan for: ", trim(path), " at index ", store_index
1579 end subroutine cache_store
1580
1581 ! Clear all cached directory scans
1582 subroutine clear_cache()
1583 integer :: i
1584
1585 ! Deallocate cache entries (they are deep copies with own memory)
1586 do i = 1, cache_count
1587 if (allocated(dir_cache(i)%node)) then
1588 call deallocate_tree(dir_cache(i)%node)
1589 deallocate(dir_cache(i)%node)
1590 end if
1591 dir_cache(i)%valid = .false.
1592 dir_cache(i)%path = ""
1593 end do
1594 cache_count = 0
1595 print *, "Directory cache cleared and deallocated"
1596 end subroutine clear_cache
1597
1598 ! Decay flash highlights for all visible nodes
1599 subroutine decay_flash_highlights()
1600 use iso_fortran_env, only: int64, real64
1601 integer :: i
1602 integer(int64) :: current_time, elapsed_ms
1603 real(real64) :: decay_rate
1604
1605 if (.not. allocated(root_node%children)) return
1606
1607 ! Get current time
1608 current_time = get_current_time_ms()
1609
1610 ! Decay rate: flash fades out over ~200ms
1611 decay_rate = 0.15d0 ! Decay by 15% per update
1612
1613 ! Decay flash intensity for all root children
1614 do i = 1, root_node%num_children
1615 if (root_node%children(i)%flash_intensity > 0.01d0) then
1616 ! Calculate time-based decay
1617 elapsed_ms = current_time - root_node%children(i)%last_update_time
1618
1619 ! If it's been more than 50ms since last update, start decaying
1620 if (elapsed_ms > 50_int64) then
1621 root_node%children(i)%flash_intensity = &
1622 root_node%children(i)%flash_intensity * (1.0d0 - decay_rate)
1623
1624 ! Clamp to zero when very small
1625 if (root_node%children(i)%flash_intensity < 0.01d0) then
1626 root_node%children(i)%flash_intensity = 0.0d0
1627 end if
1628 end if
1629 end if
1630 end do
1631 end subroutine decay_flash_highlights
1632
1633 ! Get current time in milliseconds (wrapper for progressive_scanner function)
1634 function get_current_time_ms() result(time_ms)
1635 use iso_fortran_env, only: int64
1636 integer(int64) :: time_ms
1637 integer :: count, count_rate, count_max
1638
1639 call system_clock(count, count_rate, count_max)
1640 time_ms = int(count * 1000_int64 / count_rate, int64)
1641 end function get_current_time_ms
1642
1643 ! Recursively deallocate a file tree
1644 recursive subroutine deallocate_tree(node)
1645 type(file_node), intent(inout) :: node
1646 integer :: i
1647
1648 ! Deallocate children recursively
1649 if (allocated(node%children)) then
1650 do i = 1, node%num_children
1651 call deallocate_tree(node%children(i))
1652 end do
1653 deallocate(node%children)
1654 end if
1655
1656 ! Deallocate strings
1657 if (allocated(node%name)) deallocate(node%name)
1658 if (allocated(node%path)) deallocate(node%path)
1659 end subroutine deallocate_tree
1660
1661 ! Toggle file extensions in labels
1662 subroutine toggle_file_extensions()
1663 use gtk, only: gtk_widget_queue_draw
1664 show_file_extensions = .not. show_file_extensions
1665 if (show_file_extensions) then
1666 print *, "File extensions enabled"
1667 else
1668 print *, "File extensions disabled"
1669 end if
1670 ! Invalidate layout to force redraw
1671 call invalidate_layout()
1672 ! Trigger widget redraw to show the change
1673 if (c_associated(widget_for_redraw)) then
1674 call gtk_widget_queue_draw(widget_for_redraw)
1675 print *, "Redraw queued for file extension toggle"
1676 else
1677 print *, "ERROR: widget_for_redraw not associated!"
1678 end if
1679 end subroutine toggle_file_extensions
1680
1681 ! Toggle age-based coloring
1682 subroutine toggle_age_based_coloring()
1683 use_age_based_coloring = .not. use_age_based_coloring
1684 if (use_age_based_coloring) then
1685 print *, "Age-based coloring enabled"
1686 else
1687 print *, "File type coloring enabled"
1688 end if
1689 ! Need to recolor tree and redraw
1690 if (has_data) then
1691 call color_tree(root_node, 0)
1692 call invalidate_layout()
1693 end if
1694 end subroutine toggle_age_based_coloring
1695
1696 ! Toggle size display mode (actual vs allocated)
1697 subroutine toggle_size_display_mode()
1698 show_allocated_size = .not. show_allocated_size
1699 if (show_allocated_size) then
1700 print *, "Showing allocated size (disk usage)"
1701 else
1702 print *, "Showing actual size"
1703 end if
1704 ! For now just a stub - would need to rescan with disk usage info
1705 ! Just invalidate layout to force redraw
1706 call invalidate_layout()
1707 end subroutine toggle_size_display_mode
1708
1709 ! Toggle hidden files (dotfiles) visibility
1710 subroutine toggle_hidden_files()
1711 use disk_scanner, only: disk_scanner_set_show_hidden_files => set_show_hidden_files
1712 use progressive_scanner, only: progressive_scanner_set_show_hidden_files => set_show_hidden_files
1713
1714 show_hidden_files = .not. show_hidden_files
1715
1716 ! Update scanner settings for both scanners
1717 call disk_scanner_set_show_hidden_files(show_hidden_files)
1718 call progressive_scanner_set_show_hidden_files(show_hidden_files)
1719
1720 if (show_hidden_files) then
1721 print *, "Hidden files (dotfiles) shown - rescan needed"
1722 else
1723 print *, "Hidden files (dotfiles) hidden - rescan needed"
1724 end if
1725
1726 ! Clear cache to force rescan with new filter
1727 call clear_cache()
1728 call invalidate_layout()
1729 end subroutine toggle_hidden_files
1730
1731 ! Toggle render mode (flat vs cushioned/3D)
1732 subroutine toggle_render_mode()
1733 print *, "=== TOGGLE_RENDER_MODE CALLED ==="
1734 print *, "use_cushion_shading before:", use_cushion_shading
1735 use_cushion_shading = .not. use_cushion_shading
1736 print *, "use_cushion_shading after:", use_cushion_shading
1737 if (use_cushion_shading) then
1738 print *, "Cushioned (3D) rendering enabled"
1739 else
1740 print *, "Flat rendering enabled"
1741 end if
1742 ! Invalidate layout and trigger redraw
1743 call invalidate_layout()
1744 if (c_associated(widget_for_redraw)) then
1745 call gtk_widget_queue_draw(widget_for_redraw)
1746 print *, "Redraw queued for render mode change"
1747 else
1748 print *, "ERROR: widget_for_redraw not associated!"
1749 end if
1750 end subroutine toggle_render_mode
1751
1752 ! Wrap text to fit within a given width, returning lines
1753 subroutine wrap_text(cr, text, max_width, lines, line_count, max_lines)
1754 type(c_ptr), intent(in) :: cr
1755 character(len=*), intent(in) :: text
1756 real(c_double), intent(in) :: max_width
1757 character(len=256), dimension(:), intent(out) :: lines
1758 integer, intent(out) :: line_count
1759 integer, intent(in) :: max_lines
1760
1761 ! Cairo text extents structure
1762 type, bind(c) :: cairo_text_extents_t
1763 real(c_double) :: x_bearing
1764 real(c_double) :: y_bearing
1765 real(c_double) :: width
1766 real(c_double) :: height
1767 real(c_double) :: x_advance
1768 real(c_double) :: y_advance
1769 end type cairo_text_extents_t
1770
1771 type(cairo_text_extents_t), target :: extents
1772 character(len=256) :: current_line, test_line, word
1773 integer :: text_len, i, word_start, word_len
1774 logical :: in_word
1775
1776 line_count = 0
1777 current_line = ""
1778 word = ""
1779 word_start = 1
1780 in_word = .false.
1781 text_len = len_trim(text)
1782
1783 ! If text is empty, return
1784 if (text_len == 0) return
1785
1786 ! Check if full text fits
1787 call cairo_text_extents(cr, trim(text)//c_null_char, c_loc(extents))
1788 if (extents%width <= max_width) then
1789 line_count = 1
1790 lines(1) = trim(text)
1791 return
1792 end if
1793
1794 ! Split text into words and wrap
1795 i = 1
1796 do while (i <= text_len .and. line_count < max_lines)
1797 ! Get current character
1798 if (text(i:i) == ' ' .or. i == text_len) then
1799 ! End of word
1800 if (i == text_len .and. text(i:i) /= ' ') then
1801 word_len = i - word_start + 1
1802 else
1803 word_len = i - word_start
1804 end if
1805
1806 if (word_len > 0) then
1807 word = text(word_start:word_start + word_len - 1)
1808
1809 ! Try adding word to current line
1810 if (len_trim(current_line) == 0) then
1811 test_line = trim(word)
1812 else
1813 test_line = trim(current_line) // " " // trim(word)
1814 end if
1815
1816 ! Measure the test line
1817 call cairo_text_extents(cr, trim(test_line)//c_null_char, c_loc(extents))
1818
1819 if (extents%width <= max_width) then
1820 ! Word fits, add it to current line
1821 current_line = trim(test_line)
1822 else
1823 ! Word doesn't fit
1824 if (len_trim(current_line) > 0) then
1825 ! Save current line and start new one with this word
1826 line_count = line_count + 1
1827 lines(line_count) = trim(current_line)
1828 current_line = trim(word)
1829 else
1830 ! Word is too long for a single line, truncate it
1831 current_line = trim(word)
1832 ! Truncate word to fit
1833 do while (extents%width > max_width .and. len_trim(current_line) > 3)
1834 current_line = current_line(1:len_trim(current_line)-1)
1835 call cairo_text_extents(cr, trim(current_line)//"..."//c_null_char, c_loc(extents))
1836 end do
1837 current_line = trim(current_line) // "..."
1838 line_count = line_count + 1
1839 lines(line_count) = trim(current_line)
1840 current_line = ""
1841 end if
1842 end if
1843 end if
1844
1845 word_start = i + 1
1846 end if
1847 i = i + 1
1848 end do
1849
1850 ! Add remaining text as last line
1851 if (len_trim(current_line) > 0 .and. line_count < max_lines) then
1852 line_count = line_count + 1
1853 lines(line_count) = trim(current_line)
1854 end if
1855
1856 ! If we hit max_lines, add ellipsis to last line
1857 if (line_count == max_lines .and. i < text_len) then
1858 lines(line_count) = trim(lines(line_count)) // "..."
1859 end if
1860 end subroutine wrap_text
1861
1862 ! Render text label for a node
1863 subroutine render_label(cr, node, x, y, w, h)
1864 use iso_fortran_env, only: int64
1865 type(c_ptr), intent(in) :: cr
1866 type(file_node), intent(in) :: node
1867 real(c_double), intent(in) :: x, y, w, h
1868 real(c_double) :: font_size, text_x, text_y, size_font, max_text_width, bg_height
1869 integer :: min_width, min_height, max_name_lines, i, line_count
1870 character(len=256) :: name_copy
1871 character(len=256), dimension(5) :: wrapped_lines
1872 character(len=20) :: size_text
1873
1874 ! Minimum rectangle size for text (pixels)
1875 min_width = 50
1876 min_height = 20
1877
1878 ! Don't render text in tiny rectangles
1879 if (w < min_width .or. h < min_height) return
1880
1881 ! Calculate font size based on rectangle height
1882 font_size = min(h / 3.0d0, 14.0d0)
1883 if (font_size < 8.0d0) return ! Text too small to be readable
1884
1885 ! Get the file/directory name
1886 if (allocated(node%name)) then
1887 name_copy = node%name
1888
1889 ! Strip extension if show_file_extensions is false and it's a file
1890 if (.not. show_file_extensions .and. .not. node%is_directory) then
1891 call strip_extension(name_copy)
1892 end if
1893 else
1894 return ! No name to display
1895 end if
1896
1897 ! Set up font (0 = normal slant, 0 = normal weight)
1898 call cairo_select_font_face(cr, "Sans"//c_null_char, 0_c_int, 0_c_int)
1899 call cairo_set_font_size(cr, font_size)
1900
1901 ! Calculate maximum text width (box width minus padding)
1902 max_text_width = w - 8.0d0
1903
1904 ! Determine how many lines we can fit for the name
1905 if (h > 60) then
1906 max_name_lines = 3 ! Tall box: allow 3 lines for name + size line
1907 else if (h > 40) then
1908 max_name_lines = 2 ! Medium box: allow 2 lines for name + size line
1909 else
1910 max_name_lines = 1 ! Short box: only 1 line for name
1911 end if
1912
1913 ! Wrap text to fit width
1914 call wrap_text(cr, name_copy, max_text_width, wrapped_lines, line_count, max_name_lines)
1915
1916 ! Position text (top-left with small padding)
1917 text_x = x + 4.0d0
1918 text_y = y + font_size + 2.0d0
1919
1920 ! Calculate background height based on number of lines
1921 if (h > 40 .and. line_count > 0) then
1922 ! Account for wrapped name lines + size line
1923 bg_height = font_size * real(line_count + 1, c_double) + 6.0d0
1924 else if (line_count > 0) then
1925 ! Just name lines, no size
1926 bg_height = font_size * real(line_count, c_double) + 4.0d0
1927 else
1928 bg_height = font_size + 6.0d0
1929 end if
1930
1931 ! Draw semi-transparent dark background behind text for contrast
1932 call cairo_set_source_rgba(cr, 0.0d0, 0.0d0, 0.0d0, 0.7d0) ! Black with 70% opacity
1933 call cairo_rectangle(cr, x + 2.0d0, y + 2.0d0, w - 4.0d0, bg_height)
1934 call cairo_fill(cr)
1935
1936 ! Draw each line of wrapped text
1937 call cairo_set_source_rgb(cr, 1.0d0, 1.0d0, 1.0d0)
1938 do i = 1, line_count
1939 call cairo_move_to(cr, text_x, text_y)
1940 call cairo_show_text(cr, trim(wrapped_lines(i))//c_null_char)
1941 text_y = text_y + font_size + 2.0d0
1942 end do
1943
1944 ! Draw size label on next line if rectangle is tall enough
1945 if (h > 40 .and. line_count > 0) then
1946 size_text = format_size(node%size)
1947 size_font = max(font_size * 0.8d0, 8.0d0) ! Slightly smaller font for size
1948
1949 call cairo_set_font_size(cr, size_font)
1950
1951 ! Draw size in light gray/white
1952 call cairo_set_source_rgb(cr, 0.9d0, 0.9d0, 0.9d0)
1953 call cairo_move_to(cr, text_x, text_y)
1954 call cairo_show_text(cr, trim(size_text)//c_null_char)
1955 end if
1956 end subroutine render_label
1957
1958 ! Recursively recalculate directory sizes from their children
1959 ! This restores sizes that may have been set to 0 by filtering
1960 recursive subroutine recalculate_sizes(node)
1961 type(file_node), intent(inout) :: node
1962 integer :: i
1963
1964 ! If this is a file, restore from original_size backup
1965 ! (original_size is always set during scanning, even for empty files)
1966 if (.not. node%is_directory) then
1967 node%size = node%original_size
1968 return
1969 end if
1970
1971 ! Directories: recalculate from children
1972 if (allocated(node%children) .and. node%num_children > 0) then
1973 ! First recalculate all children recursively
1974 do i = 1, node%num_children
1975 call recalculate_sizes(node%children(i))
1976 end do
1977
1978 ! Then sum up children sizes
1979 node%size = 0_int64
1980 do i = 1, node%num_children
1981 node%size = node%size + node%children(i)%size
1982 end do
1983 else
1984 ! Empty directory
1985 node%size = 0_int64
1986 end if
1987 end subroutine recalculate_sizes
1988
1989 end module treemap_renderer
1990