From 1f743b50b3675df158b16fd3c00985d2f040772d Mon Sep 17 00:00:00 2001 From: Dongcheng Lin Date: Thu, 27 Aug 2026 19:50:55 +0800 Subject: [PATCH] feat(ui): shared clickable-affordance accent for task-card rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-card rows that expand or reveal content on click (processing step rows, CRAFT signal groups, region rows) rendered as plain text until hovered, so users never discovered they were clickable. The Ribbon already has a clickable visual language — enabled button glyphs carry `Visuals::hyperlink_color` — so a new `ui::affordance` module exposes that colour (`clickable_tint`) plus a `selectable_row` helper that tints a row's leading glyph with it while the label keeps the theme text colour. The three click-to-enter rows now render through the helper; Statistics and Curve Fit cards expose their actions through framed buttons and combo boxes already, so they need no change. --- crates/app/src/ui/affordance.rs | 68 +++++++++++++++++++ crates/app/src/ui/mod.rs | 1 + crates/app/src/ui/tools/craft/results.rs | 35 +++++----- crates/app/src/ui/tools/processing/surface.rs | 17 ++--- crates/app/src/ui/tools/region_analysis.rs | 10 ++- 5 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 crates/app/src/ui/affordance.rs 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); }