diff --git a/crates/app/src/ui/affordance.rs b/crates/app/src/ui/affordance.rs new file mode 100644 index 0000000..53e20e7 --- /dev/null +++ b/crates/app/src/ui/affordance.rs @@ -0,0 +1,68 @@ +//! Shared visual language for click-to-enter surfaces. +//! +//! Ribbon buttons signal clickability by tinting their leading glyph with the +//! theme accent (`Visuals::hyperlink_color`, see `ribbon_button`). Task-card +//! rows that open an editor or reveal content on click reuse the same colour +//! through these helpers, so "this text is clickable" reads identically on +//! every surface instead of each card inventing its own (or, worse, plain +//! text that gives no signal until hovered). + +use egui::{Color32, Response, TextFormat, TextStyle, Ui, Visuals, text::LayoutJob}; + +/// The accent that marks a clickable surface — the exact colour the Ribbon +/// paints its enabled, unchecked button glyphs with. +pub(crate) fn clickable_tint(visuals: &Visuals) -> Color32 { + visuals.hyperlink_color +} + +/// A selectable row that reads as clickable while idle: the leading glyph +/// carries the clickable accent while the label keeps the theme text colour, +/// mirroring Ribbon buttons. A selected row falls back to the selection +/// styling wholesale so the accent never fights the checked state. +pub(crate) fn selectable_row( + ui: &mut Ui, + selected: bool, + glyph: &str, + label: impl Into, +) -> Response { + let font_id = TextStyle::Body.resolve(ui.style()); + let glyph_color = if selected { + Color32::PLACEHOLDER + } else { + clickable_tint(ui.visuals()) + }; + let mut job = LayoutJob::default(); + job.append( + glyph, + 0.0, + TextFormat { + font_id: font_id.clone(), + color: glyph_color, + ..Default::default() + }, + ); + job.append( + &format!(" {}", label.into()), + 0.0, + TextFormat { + font_id, + color: Color32::PLACEHOLDER, + ..Default::default() + }, + ); + ui.selectable_label(selected, job) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The clickable accent must stay the colour Ribbon glyphs use, in both + /// themes, so every "this is clickable" mark reads as one language. + #[test] + fn clickable_tint_matches_the_ribbon_glyph_colour() { + for visuals in [Visuals::light(), Visuals::dark()] { + assert_eq!(clickable_tint(&visuals), visuals.hyperlink_color); + } + } +} diff --git a/crates/app/src/ui/mod.rs b/crates/app/src/ui/mod.rs index 551420b..61808ab 100644 --- a/crates/app/src/ui/mod.rs +++ b/crates/app/src/ui/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod affordance; pub(crate) mod align; pub(crate) mod arithmetic; pub(crate) mod batch_workflow; diff --git a/crates/app/src/ui/tools/craft/results.rs b/crates/app/src/ui/tools/craft/results.rs index f8247c2..3184780 100644 --- a/crates/app/src/ui/tools/craft/results.rs +++ b/crates/app/src/ui/tools/craft/results.rs @@ -234,23 +234,24 @@ fn overview( }); for (position, summary) in run.region_summaries.iter().enumerate() { let selected = app.session.ui.craft_component_region == Some(summary.region); - if ui - .selectable_label( - selected, - format!( - "{} · {:.4}–{:.4} ppm · coherent amplitude {:.4} · {} component(s)", - if exploratory { - "Full bandwidth".into() - } else { - format!("Signal {}", position + 1) - }, - summary.start_ppm, - summary.end_ppm, - summary.coherent_amplitude_t0, - summary.component_count, - ), - ) - .clicked() + if crate::ui::affordance::selectable_row( + ui, + selected, + egui_phosphor::regular::WAVEFORM, + format!( + "{} · {:.4}–{:.4} ppm · coherent amplitude {:.4} · {} component(s)", + if exploratory { + "Full bandwidth".into() + } else { + format!("Signal {}", position + 1) + }, + summary.start_ppm, + summary.end_ppm, + summary.coherent_amplitude_t0, + summary.component_count, + ), + ) + .clicked() { app.session.ui.craft_component_region = Some(summary.region); app.session.ui.craft_result_tab = CraftResultTab::Components; diff --git a/crates/app/src/ui/tools/processing/surface.rs b/crates/app/src/ui/tools/processing/surface.rs index 8346c22..bf43cd5 100644 --- a/crates/app/src/ui/tools/processing/surface.rs +++ b/crates/app/src/ui/tools/processing/surface.rs @@ -482,16 +482,13 @@ fn step_row( egui::Frame::group(ui.style()).show(ui, |ui| { ui.horizontal_wrapped(|ui| { crate::ui::properties::panel::processing_step_section(app, &target, ui); - let response = ui - .selectable_label( - expanded, - format!( - "{} {}", - editors::kind_icon(&step.kind), - editors::kind_label(&step.kind) - ), - ) - .on_hover_text(editors::kind_summary(&step.kind)); + let response = crate::ui::affordance::selectable_row( + ui, + expanded, + editors::kind_icon(&step.kind), + editors::kind_label(&step.kind), + ) + .on_hover_text(editors::kind_summary(&step.kind)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.menu_button(icon::DOTS_THREE, |ui| { row_menu( diff --git a/crates/app/src/ui/tools/region_analysis.rs b/crates/app/src/ui/tools/region_analysis.rs index 185e030..2d6fe2f 100644 --- a/crates/app/src/ui/tools/region_analysis.rs +++ b/crates/app/src/ui/tools/region_analysis.rs @@ -247,9 +247,13 @@ fn region_task_body(app: &mut PlotxApp, di: usize, ui: &mut Ui) { } else { format!("{:.3}–{:.3} {axis_unit}", region.lo_min(), region.hi_max()) }; - if ui - .add(Button::selectable(selected == Some(region.id), interval)) - .clicked() + if crate::ui::affordance::selectable_row( + ui, + selected == Some(region.id), + icon::SELECTION, + interval, + ) + .clicked() { select_id = Some(region.id); }