@@ -98,6 +98,7 @@ pub fn tree_ui(
9898
9999 // Render each favorite (only if there are any)
100100 let favorites_snapshot: Vec < std:: path:: PathBuf > = state. favorites . clone ( ) ;
101+ let mut fav_to_remove: Option < PathBuf > = None ;
101102 for fav_path in & favorites_snapshot {
102103 let fav_name = fav_path
103104 . file_name ( )
@@ -113,7 +114,7 @@ pub fn tree_ui(
113114
114115 let ( rect, response) = ui. allocate_exact_size (
115116 Vec2 :: new ( ui. available_width ( ) , ROW_HEIGHT ) ,
116- Sense :: click ( ) ,
117+ Sense :: click_and_drag ( ) ,
117118 ) ;
118119
119120 if response. hovered ( ) {
@@ -153,6 +154,35 @@ pub fn tree_ui(
153154 if response. clicked ( ) {
154155 state. current_folder = Some ( fav_path. clone ( ) ) ;
155156 }
157+
158+ // Right-click context menu
159+ let fav_path_clone = fav_path. clone ( ) ;
160+ response. context_menu ( |ui| {
161+ if ui. button ( format ! ( "{} Remove from Favorites" , regular:: STAR ) ) . clicked ( ) {
162+ fav_to_remove = Some ( fav_path_clone. clone ( ) ) ;
163+ ui. close ( ) ;
164+ }
165+ } ) ;
166+
167+ // Drag from favorites
168+ if response. drag_started ( ) {
169+ state. drag_moving = vec ! [ fav_path. clone( ) ] ;
170+ let origin = ui. ctx ( ) . pointer_latest_pos ( ) . unwrap_or_default ( ) ;
171+ state. pending_drag_payload = Some ( renzora_editor_framework:: AssetDragPayload {
172+ path : fav_path. clone ( ) ,
173+ paths : vec ! [ fav_path. clone( ) ] ,
174+ name : fav_name. clone ( ) ,
175+ icon : FOLDER . to_string ( ) ,
176+ color : fav_icon_color,
177+ origin,
178+ is_detached : false ,
179+ drag_count : 1 ,
180+ } ) ;
181+ }
182+ }
183+
184+ if let Some ( path) = fav_to_remove {
185+ state. toggle_favorite ( & path) ;
156186 }
157187
158188 // Separator after favorites (only if there are items)
@@ -168,6 +198,186 @@ pub fn tree_ui(
168198 }
169199 }
170200
201+ // Recent files section (collapsible)
202+ {
203+ let recent_count = state. recent_files . len ( ) ;
204+ if recent_count > 0 {
205+ let text_muted = theme. text . muted . to_color32 ( ) ;
206+ let text_secondary = theme. text . secondary . to_color32 ( ) ;
207+ let selection_bg = theme. semantic . selection . to_color32 ( ) ;
208+ let item_hover = theme. panels . item_hover . to_color32 ( ) ;
209+
210+ // Collapsible header with caret + badge
211+ let ( header_rect, header_resp) = ui. allocate_exact_size (
212+ Vec2 :: new ( ui. available_width ( ) , 18.0 ) ,
213+ Sense :: click ( ) ,
214+ ) ;
215+ if header_resp. hovered ( ) {
216+ ui. ctx ( ) . set_cursor_icon ( CursorIcon :: PointingHand ) ;
217+ }
218+
219+ let caret = if state. recent_expanded { CARET_DOWN } else { CARET_RIGHT } ;
220+ ui. painter ( ) . text (
221+ Pos2 :: new ( header_rect. min . x + 4.0 , header_rect. center ( ) . y ) ,
222+ Align2 :: LEFT_CENTER ,
223+ caret,
224+ FontId :: proportional ( 9.0 ) ,
225+ text_muted,
226+ ) ;
227+ ui. painter ( ) . text (
228+ Pos2 :: new ( header_rect. min . x + 16.0 , header_rect. center ( ) . y ) ,
229+ Align2 :: LEFT_CENTER ,
230+ "Recent" ,
231+ FontId :: proportional ( 10.0 ) ,
232+ text_muted,
233+ ) ;
234+
235+ // Badge with count
236+ let badge_text = format ! ( "{}" , recent_count) ;
237+ let badge_font = FontId :: proportional ( 9.0 ) ;
238+ let badge_galley = ui. painter ( ) . layout_no_wrap ( badge_text. clone ( ) , badge_font. clone ( ) , text_muted) ;
239+ let badge_w = badge_galley. size ( ) . x + 8.0 ;
240+ let badge_h = 14.0 ;
241+ let badge_rect = egui:: Rect :: from_center_size (
242+ Pos2 :: new ( header_rect. min . x + 52.0 + badge_w * 0.5 , header_rect. center ( ) . y ) ,
243+ Vec2 :: new ( badge_w, badge_h) ,
244+ ) ;
245+ ui. painter ( ) . rect_filled ( badge_rect, 3.0 , theme. widgets . border . to_color32 ( ) ) ;
246+ ui. painter ( ) . text (
247+ badge_rect. center ( ) ,
248+ Align2 :: CENTER_CENTER ,
249+ badge_text,
250+ badge_font,
251+ text_secondary,
252+ ) ;
253+
254+ if header_resp. clicked ( ) {
255+ state. recent_expanded = !state. recent_expanded ;
256+ }
257+
258+ // Items (only when expanded)
259+ if state. recent_expanded {
260+ let recent_snapshot: Vec < PathBuf > = state. recent_files . clone ( ) ;
261+ let mut to_remove: Option < PathBuf > = None ;
262+
263+ for recent_path in & recent_snapshot {
264+ let name = recent_path
265+ . file_name ( )
266+ . and_then ( |n| n. to_str ( ) )
267+ . unwrap_or ( "???" )
268+ . to_string ( ) ;
269+
270+ let is_selected = state. selected_assets . contains ( recent_path) ;
271+ let ( icon, icon_color) = file_icon ( recent_path) ;
272+
273+ let ( rect, response) = ui. allocate_exact_size (
274+ Vec2 :: new ( ui. available_width ( ) , ROW_HEIGHT ) ,
275+ Sense :: click_and_drag ( ) ,
276+ ) ;
277+
278+ let hovered = response. hovered ( ) ;
279+ if hovered {
280+ ui. ctx ( ) . set_cursor_icon ( CursorIcon :: PointingHand ) ;
281+ }
282+
283+ if is_selected {
284+ ui. painter ( ) . rect_filled ( rect, 2.0 , selection_bg) ;
285+ } else if hovered {
286+ ui. painter ( ) . rect_filled ( rect, 2.0 , item_hover) ;
287+ }
288+
289+ // File icon
290+ ui. painter ( ) . text (
291+ Pos2 :: new ( rect. min . x + 14.0 , rect. center ( ) . y ) ,
292+ Align2 :: LEFT_CENTER ,
293+ icon,
294+ FontId :: proportional ( 12.0 ) ,
295+ icon_color,
296+ ) ;
297+
298+ // Delete button (right side, only on hover)
299+ let delete_w = 16.0 ;
300+ let has_delete = hovered;
301+ let text_right = if has_delete { rect. max . x - delete_w - 4.0 } else { rect. max . x - 4.0 } ;
302+
303+ if has_delete {
304+ let del_rect = egui:: Rect :: from_min_size (
305+ Pos2 :: new ( rect. max . x - delete_w - 2.0 , rect. min . y ) ,
306+ Vec2 :: new ( delete_w, rect. height ( ) ) ,
307+ ) ;
308+ let del_resp = ui. allocate_rect ( del_rect, Sense :: click ( ) ) ;
309+ ui. painter ( ) . text (
310+ del_rect. center ( ) ,
311+ Align2 :: CENTER_CENTER ,
312+ regular:: X ,
313+ FontId :: proportional ( 9.0 ) ,
314+ if del_resp. hovered ( ) { text_secondary } else { text_muted } ,
315+ ) ;
316+ if del_resp. clicked ( ) {
317+ to_remove = Some ( recent_path. clone ( ) ) ;
318+ }
319+ }
320+
321+ // File name
322+ let text_x = rect. min . x + 30.0 ;
323+ let max_w = ( text_right - text_x) . max ( 0.0 ) ;
324+ let text_y = rect. center ( ) . y - 11.0 * 0.5 ;
325+ paint_truncated_text ( ui. painter ( ) , Pos2 :: new ( text_x, text_y) , & name, FontId :: proportional ( 11.0 ) , text_secondary, max_w) ;
326+
327+ // Hover tooltip with folder path
328+ let response = if let Some ( parent) = recent_path. parent ( ) {
329+ if let Some ( ref root) = state. project_root {
330+ if let Ok ( rel) = parent. strip_prefix ( root) {
331+ response. on_hover_text ( rel. to_string_lossy ( ) . to_string ( ) )
332+ } else { response }
333+ } else { response }
334+ } else { response } ;
335+
336+ if response. clicked ( ) {
337+ if let Some ( parent) = recent_path. parent ( ) {
338+ state. current_folder = Some ( parent. to_path_buf ( ) ) ;
339+ }
340+ state. selected_assets . clear ( ) ;
341+ state. selected_assets . insert ( recent_path. clone ( ) ) ;
342+ state. selected_path = Some ( recent_path. clone ( ) ) ;
343+ }
344+ if response. double_clicked ( ) {
345+ state. double_clicked_recent = Some ( recent_path. clone ( ) ) ;
346+ }
347+
348+ // Drag to viewport
349+ if response. drag_started ( ) {
350+ state. drag_moving = vec ! [ recent_path. clone( ) ] ;
351+ let origin = ui. ctx ( ) . pointer_latest_pos ( ) . unwrap_or_default ( ) ;
352+ state. pending_drag_payload = Some ( renzora_editor_framework:: AssetDragPayload {
353+ path : recent_path. clone ( ) ,
354+ paths : vec ! [ recent_path. clone( ) ] ,
355+ name : name. clone ( ) ,
356+ icon : icon. to_string ( ) ,
357+ color : icon_color,
358+ origin,
359+ is_detached : false ,
360+ drag_count : 1 ,
361+ } ) ;
362+ }
363+ }
364+
365+ if let Some ( path) = to_remove {
366+ state. remove_from_recent ( & path) ;
367+ }
368+ }
369+
370+ ui. add_space ( 2.0 ) ;
371+ let sep_rect = ui. allocate_space ( egui:: vec2 ( ui. available_width ( ) , 1.0 ) ) . 1 ;
372+ ui. painter ( ) . hline (
373+ ( sep_rect. min . x + 6.0 ) ..=( sep_rect. max . x - 6.0 ) ,
374+ sep_rect. center ( ) . y ,
375+ egui:: Stroke :: new ( 1.0 , theme. widgets . border . to_color32 ( ) ) ,
376+ ) ;
377+ ui. add_space ( 2.0 ) ;
378+ }
379+ }
380+
171381 // Root node
172382 let is_expanded = state. expanded_folders . contains ( & root) ;
173383 let is_current = state. current_folder . as_ref ( ) == Some ( & root) ;
0 commit comments