1818use super :: * ;
1919use crate :: storage:: * ;
2020use crate :: ui:: cached:: Cached ;
21- use crate :: ui:: util:: { clearable_line_edit , frame_kind_color, humanize_count} ;
21+ use crate :: ui:: util:: { clearable_line_edit_with_status , frame_kind_color, humanize_count} ;
2222use base64:: Engine ;
2323use egui:: emath:: RectTransform ;
2424use egui:: Stroke ;
@@ -92,7 +92,34 @@ impl TabWidget for FlameGraphTab {
9292 } ) ;
9393 ui[ 1 ] . with_layout ( Layout :: right_to_left ( Align :: Min ) , |ui| {
9494 let hint = format ! ( "{} Filter ..." , icons:: FUNNEL ) ;
95- clearable_line_edit ( ui, & hint, & mut self . widget . filter ) ;
95+ let prev_filter = self . widget . filter . clone ( ) ;
96+
97+ let status_text = if self . widget . filter . len ( ) >= 3 {
98+ if self . widget . match_count > 0 {
99+ let current = self . widget . current_match_index + 1 ;
100+ Some ( (
101+ format ! ( "{}/{}" , current, self . widget. match_count) ,
102+ Color32 :: from_rgb ( 100 , 200 , 100 ) ,
103+ ) )
104+ } else {
105+ Some ( ( "No matches" . to_string ( ) , Color32 :: from_rgb ( 200 , 100 , 100 ) ) )
106+ }
107+ } else {
108+ None
109+ } ;
110+
111+ clearable_line_edit_with_status (
112+ ui,
113+ & hint,
114+ & mut self . widget . filter ,
115+ status_text
116+ . as_ref ( )
117+ . map ( |( text, color) | ( text. as_str ( ) , * color) ) ,
118+ ) ;
119+
120+ if prev_filter != self . widget . filter {
121+ self . widget . current_match_index = 0 ;
122+ }
96123 } ) ;
97124 } ) ;
98125 ui. add_space ( 5.0 ) ;
@@ -101,6 +128,13 @@ impl TabWidget for FlameGraphTab {
101128 }
102129}
103130
131+ /// MatchingFrame is a helper struct to navigate filtered frames.
132+ struct MatchingFrame {
133+ id : FrameId ,
134+ pos : Pos2 ,
135+ width_ratio : f32 ,
136+ }
137+
104138/// Widget drawing a flame-graph.
105139///
106140/// Separate from [`FlameGraphTab`] to allow reusing it later (e.g. for
@@ -110,6 +144,12 @@ struct FlameGraphWidget {
110144 x_zoom : f32 ,
111145 filter : String ,
112146 sandwich_view : Option < SandwichView > ,
147+
148+ matching_frames : Vec < MatchingFrame > ,
149+ current_match_index : usize ,
150+ match_count : usize ,
151+ cached_filter : String ,
152+ rebuild_matches : bool ,
113153}
114154
115155/// Sandwich view showing callers above and callees below a selected frame
@@ -128,6 +168,11 @@ impl Default for FlameGraphWidget {
128168 x_zoom : 1.0 ,
129169 filter : "" . to_string ( ) ,
130170 sandwich_view : None ,
171+ matching_frames : Vec :: new ( ) ,
172+ current_match_index : 0 ,
173+ match_count : 0 ,
174+ cached_filter : "" . to_string ( ) ,
175+ rebuild_matches : true ,
131176 }
132177 }
133178}
@@ -140,6 +185,13 @@ impl FlameGraphWidget {
140185
141186 self . process_inputs ( ui, size, & response, root) ;
142187
188+ if self . filter != self . cached_filter {
189+ self . matching_frames . clear ( ) ;
190+ self . match_count = 0 ;
191+ self . cached_filter = self . filter . clone ( ) ;
192+ self . rebuild_matches = true ;
193+ }
194+
143195 let to_screen = RectTransform :: from_to (
144196 Rect :: from_min_size ( self . origin , response. rect . size ( ) ) ,
145197 response. rect ,
@@ -180,6 +232,8 @@ impl FlameGraphWidget {
180232 root,
181233 ) ;
182234 }
235+
236+ self . rebuild_matches = false ;
183237 } ) ;
184238 }
185239
@@ -353,10 +407,35 @@ impl FlameGraphWidget {
353407 _root : & FlameGraphNode ,
354408 ) {
355409 let Some ( cursor) = response. hover_pos ( ) else {
356- // Ignore inputs when not hovered.
410+ // Check for Enter key even when not hovered (for filter navigation)
411+ if ui. input ( |i| i. key_pressed ( Key :: Enter ) )
412+ && self . filter . len ( ) >= 3
413+ && !self . matching_frames . is_empty ( )
414+ {
415+ let shift_held = ui. input ( |i| i. modifiers . shift ) ;
416+ if shift_held {
417+ self . navigate_to_prev_match ( size) ;
418+ } else {
419+ self . navigate_to_next_match ( size) ;
420+ }
421+ }
357422 return ;
358423 } ;
359424
425+ // Handle Enter key for navigating through matches
426+ if ui. input ( |i| i. key_pressed ( Key :: Enter ) )
427+ && self . filter . len ( ) >= 3
428+ && !self . matching_frames . is_empty ( )
429+ {
430+ let shift_held = ui. input ( |i| i. modifiers . shift ) ;
431+ if shift_held {
432+ self . navigate_to_prev_match ( size) ;
433+ } else {
434+ self . navigate_to_next_match ( size) ;
435+ }
436+ return ;
437+ }
438+
360439 // Double-click -> reset the view.
361440 if response. double_clicked ( ) {
362441 self . origin = Pos2 :: ZERO ;
@@ -430,12 +509,49 @@ impl FlameGraphWidget {
430509 return flame_width;
431510 }
432511
433- let bg_color = if flame. text . contains ( & self . filter ) {
512+ let bg_color = if self . filter . len ( ) >= 3 {
513+ if flame. text . contains ( & self . filter ) {
514+ flame. bg_color
515+ } else {
516+ flame. bg_color . gamma_multiply ( 0.5 )
517+ }
518+ } else {
434519 flame. bg_color
520+ } ;
521+
522+ // Track matching frames for navigation (only if filter changed)
523+ let is_match = self . filter . len ( ) >= 3 && flame. text . contains ( & self . filter ) ;
524+ let is_focused = if is_match {
525+ if self . rebuild_matches {
526+ let unscaled_pos = pos2 ( draw_pos. x / self . x_zoom , draw_pos. y ) ;
527+ let width_ratio = flame. weight as f32 / root. weight . max ( 1 ) as f32 ;
528+
529+ self . matching_frames . push ( MatchingFrame {
530+ id : flame. id ,
531+ pos : unscaled_pos,
532+ width_ratio,
533+ } ) ;
534+ self . match_count += 1 ;
535+ }
536+
537+ // Check if this frame is the focused one by comparing IDs
538+ self . matching_frames
539+ . get ( self . current_match_index )
540+ . map ( |m| m. id == flame. id )
541+ . unwrap_or ( false )
435542 } else {
436- flame . bg_color . gamma_multiply ( 0.5 )
543+ false
437544 } ;
438545
546+ // Highlight the currently focused match
547+ if is_focused {
548+ painter. add ( Shape :: rect_stroke (
549+ screen_rect,
550+ Rounding :: ZERO ,
551+ Stroke :: new ( 3.0 , Color32 :: from_rgb ( 255 , 215 , 0 ) ) , // Gold color
552+ ) ) ;
553+ }
554+
439555 painter. add ( Shape :: rect_filled ( screen_rect, Rounding :: ZERO , bg_color) ) ;
440556
441557 painter. add ( Shape :: rect_stroke (
@@ -501,6 +617,67 @@ impl FlameGraphWidget {
501617 flame_width
502618 }
503619
620+ /// Navigate to the next matching frame
621+ fn navigate_to_next_match ( & mut self , size : Vec2 ) {
622+ if self . matching_frames . is_empty ( ) {
623+ return ;
624+ }
625+
626+ // Cycle to next match
627+ self . current_match_index = ( self . current_match_index + 1 ) % self . match_count ;
628+ self . center_on_current_match ( size) ;
629+ }
630+
631+ /// Navigate to the previous matching frame
632+ fn navigate_to_prev_match ( & mut self , size : Vec2 ) {
633+ if self . matching_frames . is_empty ( ) {
634+ return ;
635+ }
636+
637+ // Cycle to previous match (wrap around)
638+ if self . current_match_index == 0 {
639+ self . current_match_index = self . match_count - 1 ;
640+ } else {
641+ self . current_match_index -= 1 ;
642+ }
643+ self . center_on_current_match ( size) ;
644+ }
645+
646+ /// Center the view on the currently selected match
647+ fn center_on_current_match ( & mut self , size : Vec2 ) {
648+ if let Some ( frame_info) = self . matching_frames . get ( self . current_match_index ) {
649+ let base_width = size. x * frame_info. width_ratio ;
650+
651+ // Make sure text in frame is readable
652+ let min_visible_width = size. x * 0.3 ;
653+ let desired_zoom = if base_width < min_visible_width {
654+ ( min_visible_width / base_width) . min ( 20.0 )
655+ } else {
656+ 1.0
657+ } ;
658+
659+ // Update zoom
660+ self . x_zoom = desired_zoom;
661+
662+ // Recalculate frame position and center with new zoom
663+ let frame_x = frame_info. pos . x * self . x_zoom ;
664+ let frame_width_zoomed = base_width * self . x_zoom ;
665+ let frame_center_x = frame_x + frame_width_zoomed / 2.0 ;
666+ let frame_center_y = frame_info. pos . y + FLAME_HEIGHT / 2.0 ;
667+
668+ let target_x = frame_center_x - size. x / 2.0 ;
669+ let target_y = frame_center_y - size. y / 2.0 ;
670+
671+ self . origin . x = target_x. max ( 0.0 ) ;
672+ self . origin . y = target_y. max ( 0.0 ) ;
673+
674+ // Clamp to visible region
675+ let virt_width = size. x * self . x_zoom ;
676+ self . origin . x = self . origin . x . clamp ( 0.0 , ( virt_width - size. x ) . max ( 0.0 ) ) ;
677+ self . origin . y = self . origin . y . clamp ( 0.0 , MAX_FRAMES * FLAME_HEIGHT ) ;
678+ }
679+ }
680+
504681 /// Populates the on-hover tooltip UI.
505682 fn draw_tooltip (
506683 & self ,
0 commit comments