@@ -5,7 +5,7 @@ use egui_phosphor::regular;
55use renzora_editor:: { split_label_two_lines, AssetDragPayload , TileGrid , TileState } ;
66use renzora_theme:: Theme ;
77
8- use crate :: state:: { file_icon, folder_icon_color, is_hidden, AssetBrowserState } ;
8+ use crate :: state:: { file_icon, folder_icon_color, format_file_size , is_hidden, AssetBrowserState , SortDirection , SortMode } ;
99use crate :: thumbnails:: supports_thumbnail;
1010
1111/// Entry in the file grid (folder or file).
@@ -60,10 +60,40 @@ pub(crate) fn collect_entries(state: &AssetBrowserState) -> Option<Vec<GridEntry
6060 Err ( _) => return None ,
6161 } ;
6262
63+ // Folders always sort before files, then apply sort mode
64+ let sort_mode = state. sort_mode ;
65+ let sort_dir = state. sort_direction ;
6366 entries. sort_by ( |a, b| {
64- b. is_dir
65- . cmp ( & a. is_dir )
66- . then_with ( || a. name . to_lowercase ( ) . cmp ( & b. name . to_lowercase ( ) ) )
67+ b. is_dir . cmp ( & a. is_dir ) . then_with ( || {
68+ let cmp = match sort_mode {
69+ SortMode :: Name => {
70+ a. name . to_lowercase ( ) . cmp ( & b. name . to_lowercase ( ) )
71+ }
72+ SortMode :: DateModified => {
73+ let time_a = std:: fs:: metadata ( & a. path )
74+ . and_then ( |m| m. modified ( ) )
75+ . unwrap_or ( std:: time:: SystemTime :: UNIX_EPOCH ) ;
76+ let time_b = std:: fs:: metadata ( & b. path )
77+ . and_then ( |m| m. modified ( ) )
78+ . unwrap_or ( std:: time:: SystemTime :: UNIX_EPOCH ) ;
79+ time_a. cmp ( & time_b)
80+ }
81+ SortMode :: Type => {
82+ let ext_a = a. path . extension ( ) . and_then ( |e| e. to_str ( ) ) . unwrap_or ( "" ) . to_lowercase ( ) ;
83+ let ext_b = b. path . extension ( ) . and_then ( |e| e. to_str ( ) ) . unwrap_or ( "" ) . to_lowercase ( ) ;
84+ ext_a. cmp ( & ext_b) . then_with ( || a. name . to_lowercase ( ) . cmp ( & b. name . to_lowercase ( ) ) )
85+ }
86+ SortMode :: Size => {
87+ let size_a = std:: fs:: metadata ( & a. path ) . map ( |m| m. len ( ) ) . unwrap_or ( 0 ) ;
88+ let size_b = std:: fs:: metadata ( & b. path ) . map ( |m| m. len ( ) ) . unwrap_or ( 0 ) ;
89+ size_a. cmp ( & size_b)
90+ }
91+ } ;
92+ match sort_dir {
93+ SortDirection :: Ascending => cmp,
94+ SortDirection :: Descending => cmp. reverse ( ) ,
95+ }
96+ } )
6797 } ) ;
6898
6999 // Apply search filter
@@ -296,6 +326,18 @@ pub fn grid_ui_interactive(
296326 ) ;
297327 }
298328
329+ // Star badge on favorited folders
330+ if entry. is_dir && state. is_favorite ( & entry. path ) {
331+ let star_pos = egui:: pos2 ( tile. rect . max . x - 10.0 , tile. rect . min . y + 10.0 ) ;
332+ ui. painter ( ) . text (
333+ star_pos,
334+ Align2 :: CENTER_CENTER ,
335+ regular:: STAR ,
336+ FontId :: proportional ( 10.0 ) ,
337+ Color32 :: from_rgb ( 255 , 200 , 60 ) ,
338+ ) ;
339+ }
340+
299341 // Draw label (skip if renaming)
300342 if !is_renaming {
301343 let ( line1, line2) =
@@ -318,6 +360,13 @@ pub fn grid_ui_interactive(
318360 }
319361 }
320362
363+ // File hover tooltip (suppress during drag)
364+ if !entry. is_dir && tile. response . hovered ( ) && state. drag_moving . is_empty ( ) {
365+ tile. response . clone ( ) . on_hover_ui_at_pointer ( |ui| {
366+ file_hover_tooltip ( ui, & entry. path ) ;
367+ } ) ;
368+ }
369+
321370 TileState {
322371 is_selected,
323372 is_hovered,
@@ -498,20 +547,21 @@ pub fn grid_ui_interactive(
498547 state. drag_moving = vec ! [ entry. path. clone( ) ] ;
499548 }
500549
501- // Only produce viewport drag payload for files (not folders)
502- if !entry. is_dir {
503- let ( icon, color) = file_icon ( & entry. path ) ;
504- let origin = ui. ctx ( ) . pointer_latest_pos ( ) . unwrap_or_default ( ) ;
505- drag_payload = Some ( AssetDragPayload {
506- path : entry. path . clone ( ) ,
507- name : entry. name . clone ( ) ,
508- icon : icon. to_string ( ) ,
509- color,
510- origin,
511- is_detached : false ,
512- drag_count : state. drag_moving . len ( ) ,
513- } ) ;
514- }
550+ let ( icon, color) = if entry. is_dir {
551+ ( regular:: FOLDER , folder_icon_color ( & entry. name ) )
552+ } else {
553+ file_icon ( & entry. path )
554+ } ;
555+ let origin = ui. ctx ( ) . pointer_latest_pos ( ) . unwrap_or_default ( ) ;
556+ drag_payload = Some ( AssetDragPayload {
557+ path : entry. path . clone ( ) ,
558+ name : entry. name . clone ( ) ,
559+ icon : icon. to_string ( ) ,
560+ color,
561+ origin,
562+ is_detached : false ,
563+ drag_count : state. drag_moving . len ( ) ,
564+ } ) ;
515565 }
516566
517567 // Update drop target for ghost label
@@ -534,3 +584,62 @@ pub fn grid_ui_interactive(
534584 thumbnail_requests,
535585 }
536586}
587+
588+ /// Render a tooltip with file info (name, type, size, date modified).
589+ pub ( crate ) fn file_hover_tooltip ( ui : & mut egui:: Ui , path : & std:: path:: Path ) {
590+ let name = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) . unwrap_or ( "unknown" ) ;
591+ ui. label ( egui:: RichText :: new ( name) . strong ( ) ) ;
592+
593+ if let Some ( ext) = path. extension ( ) . and_then ( |e| e. to_str ( ) ) {
594+ ui. label ( format ! ( "Type: {}" , ext. to_uppercase( ) ) ) ;
595+ }
596+
597+ if let Ok ( meta) = std:: fs:: metadata ( path) {
598+ ui. label ( format ! ( "Size: {}" , format_file_size( meta. len( ) ) ) ) ;
599+ if let Ok ( modified) = meta. modified ( ) {
600+ if let Ok ( duration) = modified. duration_since ( std:: time:: SystemTime :: UNIX_EPOCH ) {
601+ let secs = duration. as_secs ( ) ;
602+ // Simple date formatting: YYYY-MM-DD HH:MM
603+ let days = secs / 86400 ;
604+ let time_of_day = secs % 86400 ;
605+ let hours = time_of_day / 3600 ;
606+ let minutes = ( time_of_day % 3600 ) / 60 ;
607+
608+ // Approximate date from days since epoch
609+ let ( year, month, day) = days_to_date ( days) ;
610+ ui. label ( format ! ( "Modified: {}-{:02}-{:02} {:02}:{:02}" , year, month, day, hours, minutes) ) ;
611+ }
612+ }
613+ }
614+ }
615+
616+ /// Convert days since Unix epoch to (year, month, day).
617+ fn days_to_date ( mut days : u64 ) -> ( u64 , u64 , u64 ) {
618+ let mut year = 1970 ;
619+ loop {
620+ let days_in_year = if is_leap ( year) { 366 } else { 365 } ;
621+ if days < days_in_year {
622+ break ;
623+ }
624+ days -= days_in_year;
625+ year += 1 ;
626+ }
627+ let month_days: & [ u64 ] = if is_leap ( year) {
628+ & [ 31 , 29 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
629+ } else {
630+ & [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
631+ } ;
632+ let mut month = 1 ;
633+ for & md in month_days {
634+ if days < md {
635+ break ;
636+ }
637+ days -= md;
638+ month += 1 ;
639+ }
640+ ( year, month, days + 1 )
641+ }
642+
643+ fn is_leap ( year : u64 ) -> bool {
644+ ( year % 4 == 0 && year % 100 != 0 ) || year % 400 == 0
645+ }
0 commit comments