11//! Control panel (left sidebar) and top/bottom bars rendering.
22
3- use eframe:: egui:: { self , Color32 , FontFamily , FontId , Rounding , Stroke } ;
3+ use eframe:: egui:: { self , Color32 , FontFamily , FontId , Rect , Rounding , Stroke } ;
44use rfd:: FileDialog ;
55
66use super :: theme:: { accent, form_label, primary_button, ThemeColors } ;
@@ -18,6 +18,13 @@ enum FileToolbarIcon {
1818 Gear ,
1919}
2020
21+ #[ derive( Clone , Copy ) ]
22+ enum SectionHelp {
23+ Clustering ,
24+ View ,
25+ PixelHealth ,
26+ }
27+
2128impl RustpixApp {
2229 /// Render the top panel with RUSTPIX branding, file info, and view mode toggle.
2330 pub ( crate ) fn render_top_panel ( & mut self , ctx : & egui:: Context ) {
@@ -551,24 +558,42 @@ impl RustpixApp {
551558 . auto_shrink ( [ false , false ] )
552559 . show ( ui, |ui| {
553560 // Statistics section
554- self . render_section ( ui, "Statistics" , true , |app, ui| {
561+ self . render_section ( ui, "Statistics" , true , None , |app, ui| {
555562 app. render_statistics ( ui) ;
556563 } ) ;
557564
558565 // Clustering section
559- self . render_section ( ui, "Clustering" , true , |app, ui| {
560- app. render_clustering_controls ( ui) ;
561- } ) ;
566+ self . render_section (
567+ ui,
568+ "Clustering" ,
569+ true ,
570+ Some ( SectionHelp :: Clustering ) ,
571+ |app, ui| {
572+ app. render_clustering_controls ( ui) ;
573+ } ,
574+ ) ;
562575
563576 // View section
564- self . render_section ( ui, "View" , true , |app, ui| {
565- app. render_view_options ( ui) ;
566- } ) ;
577+ self . render_section (
578+ ui,
579+ "View" ,
580+ true ,
581+ Some ( SectionHelp :: View ) ,
582+ |app, ui| {
583+ app. render_view_options ( ui) ;
584+ } ,
585+ ) ;
567586
568587 // Pixel Health section
569- self . render_section ( ui, "Pixel Health" , false , |app, ui| {
570- app. render_pixel_health ( ui) ;
571- } ) ;
588+ self . render_section (
589+ ui,
590+ "Pixel Health" ,
591+ false ,
592+ Some ( SectionHelp :: PixelHealth ) ,
593+ |app, ui| {
594+ app. render_pixel_health ( ui) ;
595+ } ,
596+ ) ;
572597
573598 // Progress indicator (when active)
574599 self . render_progress_status ( ui) ;
@@ -579,52 +604,105 @@ impl RustpixApp {
579604 }
580605
581606 /// Render a collapsible section with header.
582- fn render_section < F > ( & mut self , ui : & mut egui:: Ui , title : & str , default_open : bool , content : F )
583- where
607+ #[ allow( clippy:: too_many_lines) ]
608+ fn render_section < F > (
609+ & mut self ,
610+ ui : & mut egui:: Ui ,
611+ title : & str ,
612+ default_open : bool ,
613+ help : Option < SectionHelp > ,
614+ content : F ,
615+ ) where
584616 F : FnOnce ( & mut Self , & mut egui:: Ui ) ,
585617 {
586618 // Section container
587619 ui. push_id ( title, |ui| {
588620 let colors = ThemeColors :: from_ui ( ui) ;
589- // Header
590- let header_response = ui
591- . scope ( |ui| {
592- let old_padding = ui. spacing ( ) . button_padding ;
593- ui. spacing_mut ( ) . button_padding = egui:: vec2 ( 16.0 , old_padding. y ) ;
594- let response = ui. add (
595- egui:: Button :: new (
596- egui:: RichText :: new ( title. to_uppercase ( ) )
597- . size ( 11.0 )
598- . strong ( )
599- . color ( colors. text_primary ) ,
600- )
601- . fill ( Color32 :: TRANSPARENT )
602- . stroke ( Stroke :: NONE )
603- . rounding ( Rounding :: ZERO )
604- . min_size ( egui:: vec2 ( ui. available_width ( ) , 0.0 ) ) ,
605- ) ;
606- ui. spacing_mut ( ) . button_padding = old_padding;
607- response
608- } )
609- . inner ;
621+ let header_height = ui. spacing ( ) . interact_size . y . max ( 28.0 ) ;
622+ let ( header_rect, header_response) = ui. allocate_exact_size (
623+ egui:: vec2 ( ui. available_width ( ) , header_height) ,
624+ egui:: Sense :: click ( ) ,
625+ ) ;
610626
611- // Get/toggle state
612627 let id = ui. make_persistent_id ( format ! ( "{title}_open" ) ) ;
613628 let mut is_open = ui. data_mut ( |d| * d. get_temp_mut_or_insert_with ( id, || default_open) ) ;
614629
615- if header_response. clicked ( ) {
630+ let help_state = match help {
631+ Some ( SectionHelp :: Clustering ) => self . ui_state . panel_popups . show_clustering_help ,
632+ Some ( SectionHelp :: View ) => self . ui_state . panel_popups . show_view_help ,
633+ Some ( SectionHelp :: PixelHealth ) => self . ui_state . panel_popups . show_pixel_health_help ,
634+ None => false ,
635+ } ;
636+
637+ let help_rect = help. map ( |_| {
638+ let size = egui:: vec2 ( 18.0 , 18.0 ) ;
639+ Rect :: from_center_size ( header_rect. right_center ( ) - egui:: vec2 ( 36.0 , 0.0 ) , size)
640+ } ) ;
641+ let help_clicked = help_rect. is_some_and ( |rect| {
642+ let help_id = ui. make_persistent_id ( format ! ( "{title}_help" ) ) ;
643+ let response = ui. interact ( rect, help_id, egui:: Sense :: click ( ) ) ;
644+ let response = response. on_hover_text ( "Help" ) ;
645+ if response. clicked ( ) {
646+ match help {
647+ Some ( SectionHelp :: Clustering ) => {
648+ self . ui_state . panel_popups . show_clustering_help = !help_state;
649+ }
650+ Some ( SectionHelp :: View ) => {
651+ self . ui_state . panel_popups . show_view_help = !help_state;
652+ }
653+ Some ( SectionHelp :: PixelHealth ) => {
654+ self . ui_state . panel_popups . show_pixel_health_help = !help_state;
655+ }
656+ None => { }
657+ }
658+ }
659+ response. clicked ( )
660+ } ) ;
661+
662+ if header_response. clicked ( ) && !help_clicked {
616663 is_open = !is_open;
617664 ui. data_mut ( |d| d. insert_temp ( id, is_open) ) ;
618665 }
619666
620- // Draw the header with proper styling
621- let header_rect = header_response. rect ;
622- ui. painter ( )
623- . rect_filled ( header_rect, 0.0 , Color32 :: TRANSPARENT ) ;
667+ let header_fill = if header_response. hovered ( ) {
668+ colors. bg_header
669+ } else {
670+ Color32 :: TRANSPARENT
671+ } ;
672+ ui. painter ( ) . rect_filled ( header_rect, 0.0 , header_fill) ;
673+
674+ let text_pos = header_rect. left_center ( ) + egui:: vec2 ( 16.0 , 0.0 ) ;
675+ ui. painter ( ) . text (
676+ text_pos,
677+ egui:: Align2 :: LEFT_CENTER ,
678+ title. to_uppercase ( ) ,
679+ FontId :: new ( 11.0 , FontFamily :: Proportional ) ,
680+ colors. text_primary ,
681+ ) ;
682+
683+ if let Some ( rect) = help_rect {
684+ let fill = if help_state {
685+ colors. bg_header
686+ } else {
687+ Color32 :: TRANSPARENT
688+ } ;
689+ ui. painter ( ) . rect_stroke (
690+ rect,
691+ Rounding :: same ( 3.0 ) ,
692+ Stroke :: new ( 1.0 , colors. border_light ) ,
693+ ) ;
694+ ui. painter ( ) . rect_filled ( rect, Rounding :: same ( 3.0 ) , fill) ;
695+ ui. painter ( ) . text (
696+ rect. center ( ) ,
697+ egui:: Align2 :: CENTER_CENTER ,
698+ "?" ,
699+ FontId :: new ( 11.0 , FontFamily :: Proportional ) ,
700+ colors. text_dim ,
701+ ) ;
702+ }
624703
625- // Arrow indicator
626704 let arrow = if is_open { "▼" } else { "▶" } ;
627- let arrow_pos = header_rect. right_center ( ) - egui:: vec2 ( 20 .0, 0.0 ) ;
705+ let arrow_pos = header_rect. right_center ( ) - egui:: vec2 ( 16 .0, 0.0 ) ;
628706 ui. painter ( ) . text (
629707 arrow_pos,
630708 egui:: Align2 :: CENTER_CENTER ,
@@ -1557,6 +1635,88 @@ impl RustpixApp {
15571635 if self . ui_state . export . show_dialog {
15581636 self . render_export_dialog ( ctx) ;
15591637 }
1638+
1639+ self . render_help_windows ( ctx) ;
1640+ }
1641+
1642+ fn render_help_windows ( & mut self , ctx : & egui:: Context ) {
1643+ self . render_clustering_help_panel ( ctx) ;
1644+ self . render_view_help_panel ( ctx) ;
1645+ self . render_pixel_health_help_panel ( ctx) ;
1646+ }
1647+
1648+ fn render_clustering_help_panel ( & mut self , ctx : & egui:: Context ) {
1649+ if !self . ui_state . panel_popups . show_clustering_help {
1650+ return ;
1651+ }
1652+ let mut open = self . ui_state . panel_popups . show_clustering_help ;
1653+ egui:: Window :: new ( "Clustering Help" )
1654+ . open ( & mut open)
1655+ . collapsible ( false )
1656+ . resizable ( false )
1657+ . default_width ( 300.0 )
1658+ . show ( ctx, |ui| {
1659+ ui. label ( egui:: RichText :: new ( "Algorithm" ) . strong ( ) ) ;
1660+ ui. label ( "• ABS / DBSCAN / Grid: choose clustering method." ) ;
1661+ ui. label ( "• Parameters control spatial radius + time window." ) ;
1662+ ui. add_space ( 6.0 ) ;
1663+ ui. label ( egui:: RichText :: new ( "Extraction" ) . strong ( ) ) ;
1664+ ui. label ( "• Super-res scales sub-pixel neutron positions." ) ;
1665+ ui. label ( "• Weighted by TOT improves centroid stability." ) ;
1666+ ui. label ( "• Min TOT filters low signal hits." ) ;
1667+ ui. add_space ( 6.0 ) ;
1668+ ui. label ( egui:: RichText :: new ( "Run" ) . strong ( ) ) ;
1669+ ui. label ( "• Click Run Clustering to generate neutrons." ) ;
1670+ } ) ;
1671+ self . ui_state . panel_popups . show_clustering_help = open;
1672+ }
1673+
1674+ fn render_view_help_panel ( & mut self , ctx : & egui:: Context ) {
1675+ if !self . ui_state . panel_popups . show_view_help {
1676+ return ;
1677+ }
1678+ let mut open = self . ui_state . panel_popups . show_view_help ;
1679+ egui:: Window :: new ( "View Help" )
1680+ . open ( & mut open)
1681+ . collapsible ( false )
1682+ . resizable ( false )
1683+ . default_width ( 280.0 )
1684+ . show ( ctx, |ui| {
1685+ ui. label ( egui:: RichText :: new ( "Colormap" ) . strong ( ) ) ;
1686+ ui. label ( "• Change display palette only (data unchanged)." ) ;
1687+ ui. add_space ( 6.0 ) ;
1688+ ui. label ( egui:: RichText :: new ( "TOF Slicer" ) . strong ( ) ) ;
1689+ ui. label ( "• Show a single TOF bin instead of full projection." ) ;
1690+ ui. add_space ( 6.0 ) ;
1691+ ui. label ( egui:: RichText :: new ( "Spectrum" ) . strong ( ) ) ;
1692+ ui. label ( "• Toggle spectrum panel visibility." ) ;
1693+ ui. add_space ( 6.0 ) ;
1694+ ui. label ( egui:: RichText :: new ( "Log scale" ) . strong ( ) ) ;
1695+ ui. label ( "• Use log intensity for histogram display." ) ;
1696+ } ) ;
1697+ self . ui_state . panel_popups . show_view_help = open;
1698+ }
1699+
1700+ fn render_pixel_health_help_panel ( & mut self , ctx : & egui:: Context ) {
1701+ if !self . ui_state . panel_popups . show_pixel_health_help {
1702+ return ;
1703+ }
1704+ let mut open = self . ui_state . panel_popups . show_pixel_health_help ;
1705+ egui:: Window :: new ( "Pixel Health Help" )
1706+ . open ( & mut open)
1707+ . collapsible ( false )
1708+ . resizable ( false )
1709+ . default_width ( 300.0 )
1710+ . show ( ctx, |ui| {
1711+ ui. label ( egui:: RichText :: new ( "Dead/Hot pixels" ) . strong ( ) ) ;
1712+ ui. label ( "• Computed from hit statistics in the current dataset." ) ;
1713+ ui. label ( "• Hot pixel overlay marks outliers." ) ;
1714+ ui. add_space ( 6.0 ) ;
1715+ ui. label ( egui:: RichText :: new ( "Masks" ) . strong ( ) ) ;
1716+ ui. label ( "• Exclude masked pixels from spectra/stats." ) ;
1717+ ui. label ( "• Recompute masks after changing thresholds." ) ;
1718+ } ) ;
1719+ self . ui_state . panel_popups . show_pixel_health_help = open;
15601720 }
15611721
15621722 fn render_export_dialog ( & mut self , ctx : & egui:: Context ) {
0 commit comments