From e1439bfe33fb2514dccbbc1bca2eee762f5d22ac Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:53:37 -0500 Subject: [PATCH] Scrubby labels: the Control bar's Stroke: and Opacity: links scrub their fields (#4088) The Control bar's Stroke: and Opacity: and the Properties panel's Stroke are links to their panels, so a drag on them did nothing while every other field label scrubbed its field. A drag on such a link now scrubs the field after it (same steps and modifiers, one undo step, Escape cancels); a click still opens the panel, and the link's text is not selected by the drag. --- crates/ui-egui/src/chrome/control_bar.rs | 3 +- crates/ui-egui/src/panels/stroke.rs | 6 ++- crates/ui-egui/src/scrub.rs | 52 +++++++++++++++----- crates/ui-egui/src/tests/scrub.rs | 61 ++++++++++++++++++++++++ docs/target-app-parity.md | 5 +- docs/ui-parity.md | 5 +- 6 files changed, 113 insertions(+), 19 deletions(-) diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 88cc6dde0..9afbe838e 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -338,7 +338,8 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(4.0); ui.separator(); } - if ui.link(egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline()).clicked() { + // A click opens the Transparency panel; a drag scrubs the field (#4088). + if crate::scrub::link_label(ui, egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline()).clicked() { app.ui.open_panel = Some("transparency".into()); } if let Some(o) = widgets::plain_field(ui, "cb-opacity", opacity as f64 * 100.0, "%", 0, 56.0) diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index fb9269fcf..f6ece0ac5 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -137,10 +137,12 @@ pub(crate) fn weight_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, weig } /// The underlined Stroke link (`label`: the Control bar's "Stroke:", the Properties panel's -/// "Stroke"): a click toggles the Stroke panel in a popover under it. +/// "Stroke"): a click toggles the Stroke panel in a popover under it, and a drag scrubs the weight +/// field after it ([`crate::scrub`]). pub(crate) fn link(app: &mut VectorcraftApp, ui: &mut Ui, label: &str) { let t = Tokens::get(ui.ctx()); - let resp = ui.link(egui::RichText::new(label).size(12.0).color(t.text).underline()).on_hover_text(tl!("Stroke options")); + // A drag on it scrubs the weight field after it (#4088). + let resp = crate::scrub::link_label(ui, egui::RichText::new(label).size(12.0).color(t.text).underline()).on_hover_text(tl!("Stroke options")); widgets::popover(&resp, resp.clicked(), |ui| { ui.set_width(260.0); show(app, ui); diff --git a/crates/ui-egui/src/scrub.rs b/crates/ui-egui/src/scrub.rs index 832b82870..70ceb5060 100644 --- a/crates/ui-egui/src/scrub.rs +++ b/crates/ui-egui/src/scrub.rs @@ -1,8 +1,10 @@ //! Scrubbing numeric fields (#400, beyond Illustrator; Photoshop's scrubby labels): a horizontal //! drag on a numeric field's label (`W:`, `Opacity:`, the Character panel's size icon) steps the //! field's value by one unit every [`PX_PER_STEP`] points, ten with Shift and a tenth with -//! Ctrl/Cmd, as the arrow keys step a focused field; a click on the label focuses the field. The -//! text box itself never scrubs, and a field without a label doesn't. +//! Ctrl/Cmd, as the arrow keys step a focused field; a click on the label focuses the field. A +//! label that is a link (the Control bar's `Stroke:` and `Opacity:`, which open their panels; +//! #4088) scrubs its field the same way, and a click on it still follows the link. The text box +//! itself never scrubs, and a field without a label doesn't. //! //! The panels apply each value the drag passes as they apply a typed one, so the art follows the //! drag live; the app holds an undo group open meanwhile ([`begin_frame`], [`end_frame`]), so the @@ -113,24 +115,44 @@ pub(crate) fn end_frame(app: &mut VectorcraftApp, ctx: &Context) { struct Label { rect: Rect, pass: u64, + /// The label is a link, this widget: a click follows it instead of focusing the field. + link: Option, } /// Note the label (text or icon) drawn at `rect`: a numeric field drawn right after it on the /// same line takes it as its label, which scrubs the field. pub(crate) fn note_label(ui: &Ui, rect: Rect) { + note(ui, rect, None); +} + +/// A link that labels the numeric field drawn after it (it opens a panel when clicked): a drag on +/// it scrubs the field, and a click stays the link's. Its text can't be selected, which a drag +/// across it would otherwise do. +pub(crate) fn link_label(ui: &mut Ui, text: egui::RichText) -> Response { + let link = ui + .scope(|ui| { + ui.style_mut().interaction.selectable_labels = false; + ui.link(text) + }) + .inner; + note(ui, link.rect, Some(link.id)); + link +} + +fn note(ui: &Ui, rect: Rect, link: Option) { let pass = ui.ctx().cumulative_pass_nr(); - ui.data_mut(|d| d.insert_temp(label_id(), Label { rect, pass })); + ui.data_mut(|d| d.insert_temp(label_id(), Label { rect, pass, link })); } /// The label noted just before the field at `field`, on its line. -fn take_label(ui: &Ui, field: Rect) -> Option { +fn take_label(ui: &Ui, field: Rect) -> Option