Skip to content

Commit d87792e

Browse files
committed
feat(ui): add help panels for clustering, view, pixel health, and spectrum
1 parent 7bfe2f1 commit d87792e

3 files changed

Lines changed: 258 additions & 42 deletions

File tree

rustpix-gui/src/state/ui.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ pub struct UiPanelToggles {
153153
pub show_spectrum_settings: bool,
154154
}
155155

156+
#[allow(clippy::struct_excessive_bools)]
156157
#[derive(Clone, Copy, Default)]
157158
pub struct UiPanelPopups {
158159
/// Whether the spectrum data selection panel is open.
@@ -161,6 +162,14 @@ pub struct UiPanelPopups {
161162
pub show_spectrum_range: bool,
162163
/// Whether the ROI help panel is open.
163164
pub show_roi_help: bool,
165+
/// Whether the clustering help panel is open.
166+
pub show_clustering_help: bool,
167+
/// Whether the view help panel is open.
168+
pub show_view_help: bool,
169+
/// Whether the pixel health help panel is open.
170+
pub show_pixel_health_help: bool,
171+
/// Whether the spectrum help panel is open.
172+
pub show_spectrum_help: bool,
164173
}
165174

166175
#[derive(Clone, Copy, Default)]

rustpix-gui/src/ui/control_panel.rs

Lines changed: 202 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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};
44
use rfd::FileDialog;
55

66
use 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+
2128
impl 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) {

rustpix-gui/src/ui/main_view.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,7 @@ impl RustpixApp {
14461446
ui.add_space(4.0);
14471447
self.render_roi_data_panel(ctx);
14481448
self.render_spectrum_range_panel(ctx);
1449+
self.render_spectrum_help_panel(ctx);
14491450

14501451
let Some(plot_data) = self.build_spectrum_plot_data(&colors, inputs) else {
14511452
Self::render_spectrum_empty(ui);
@@ -1498,6 +1499,7 @@ impl RustpixApp {
14981499
}
14991500
self.render_spectrum_settings_button(ui, &colors);
15001501
self.render_spectrum_data_button(ui, &colors);
1502+
self.render_spectrum_help_button(ui, &colors);
15011503

15021504
ui.add_space(8.0);
15031505
if self.render_spectrum_log_toggles(ui, &colors) {
@@ -1617,6 +1619,22 @@ impl RustpixApp {
16171619
}
16181620
}
16191621

1622+
fn render_spectrum_help_button(&mut self, ui: &mut egui::Ui, colors: &ThemeColors) {
1623+
let help_button = egui::Button::new("?")
1624+
.min_size(egui::vec2(22.0, 22.0))
1625+
.fill(if self.ui_state.panel_popups.show_spectrum_help {
1626+
colors.bg_header
1627+
} else {
1628+
Color32::TRANSPARENT
1629+
})
1630+
.stroke(Stroke::new(1.0, colors.border_light))
1631+
.rounding(Rounding::same(4.0));
1632+
if ui.add(help_button).on_hover_text("Spectrum help").clicked() {
1633+
self.ui_state.panel_popups.show_spectrum_help =
1634+
!self.ui_state.panel_popups.show_spectrum_help;
1635+
}
1636+
}
1637+
16201638
fn render_spectrum_export_buttons(
16211639
&mut self,
16221640
ui: &mut egui::Ui,
@@ -2442,6 +2460,35 @@ impl RustpixApp {
24422460
self.ui_state.panel_popups.show_spectrum_range = open;
24432461
}
24442462

2463+
fn render_spectrum_help_panel(&mut self, ctx: &egui::Context) {
2464+
if !self.ui_state.panel_popups.show_spectrum_help {
2465+
return;
2466+
}
2467+
let mut open = self.ui_state.panel_popups.show_spectrum_help;
2468+
egui::Window::new("Spectrum Help")
2469+
.open(&mut open)
2470+
.collapsible(false)
2471+
.resizable(false)
2472+
.default_width(320.0)
2473+
.show(ctx, |ui| {
2474+
ui.label(egui::RichText::new("Axes").strong());
2475+
ui.label("• Switch TOF / Energy in the dropdown.");
2476+
ui.label("• Energy axis needs flight path + TOF offset.");
2477+
ui.add_space(6.0);
2478+
ui.label(egui::RichText::new("Visibility").strong());
2479+
ui.label("• Use the data button to toggle Full FOV and ROIs.");
2480+
ui.add_space(6.0);
2481+
ui.label(egui::RichText::new("Scaling & range").strong());
2482+
ui.label("• logX/logY toggles adjust scaling.");
2483+
ui.label("• Range panel constrains x/y bounds.");
2484+
ui.add_space(6.0);
2485+
ui.label(egui::RichText::new("Zoom & export").strong());
2486+
ui.label("• Zoom with buttons or selection box.");
2487+
ui.label("• Export PNG/CSV from the toolbar.");
2488+
});
2489+
self.ui_state.panel_popups.show_spectrum_help = open;
2490+
}
2491+
24452492
fn render_spectrum_range_contents(&mut self, ui: &mut egui::Ui, axis_label: &str) {
24462493
let colors = ThemeColors::from_ui(ui);
24472494
ui.label(

0 commit comments

Comments
 (0)