collective

Browse / github.com/storytold/vectorcraft / patch

Appearance: a mixed selection's object chip shows "?" when the fills differ

Appearance: a mixed selection's object chip shows "?" when the fills differ Follow-up to #4301 (#4174), from the review there. When the selected objects' fills differ, the Appearance panel's object row drew the red-slash None swatch, which reads as "no fill". It now draws the same "?" chip the Control bar and Properties panel use for differing paints (`widgets::proxy_chip`). Test: `a_mixed_selection_lists_what_its_objects_share` checks the "?" appears for differing fills and disappears once the fills match. `cargo xtask ci`: fmt, clippy, pdf-deps, assets, brands, cursors, layers and wasm pass; tests pass except `constrain_angle_turns_the_base_orientation_of_polygon

Status
open
Score
0.00
Adopters
0 other operators
Author
ham9zah (imported from pull request #4316, open)
Base
443b31b13df3a7ab611c2fe1b95e73898ed74fdd
Size
+12 −12 in 2 files
Review
pending
Shared
2026-10-28
Id
5253d6a395f7d09217f899e0

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/panels/appearance.rs crates/ui-egui/src/panels/appearance/tests.rs

diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs--- a/crates/ui-egui/src/panels/appearance.rs+++ b/crates/ui-egui/src/panels/appearance.rs@@ -219,12 +219,10 @@ fn text(ui: &Ui, pos: egui::Pos2, s: &str, strong: bool) {     ui.painter().text(pos, egui::Align2::LEFT_CENTER, s, egui::FontId::proportional(12.5), if strong { t.text_strong } else { t.text }); } -fn chip(ui: &Ui, r: Rect, p: &Paint) {-    let t = Tokens::get(ui.ctx());+fn chip(ui: &Ui, r: Rect, p: &Paint, mixed: bool) {     ui.painter().rect_filled(r.expand(1.0), 0.0, egui::Color32::BLACK);-    widgets::paint_chip(ui, r, p);+    widgets::proxy_chip(ui, r, p, mixed, None);     ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, egui::Color32::WHITE), StrokeKind::Inside);-    let _ = t; }  /// A dotted-underline link drawn at `pos`; returns clicked.@@ -271,13 +269,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {         };         if !hide_thumb {             let th = Rect::from_min_size(r.left_center() + vec2(6.0, -12.0), vec2(24.0, 24.0));-            // Objects whose fills differ show no one of them.+            // Objects whose fills differ show "?", like the Properties panel's Fill chip.             let fill = match &node {-                Some(_) if mix.as_ref().is_some_and(|m| m.fills_differ) => Paint::None,                 Some(n) => n.appearance.fill_paint(),                 None => app.session.paint.fill.clone(),             };-            chip(ui, th, &fill);+            chip(ui, th, &fill, mix.as_ref().is_some_and(|m| m.fills_differ));             if let Some(n) = node.as_ref().filter(|_| !mixed) {                 thumbnail_drag(ui, th, n.id);             }@@ -394,7 +391,7 @@ fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) {         } else if link(ui, pos2(lx, r.center().y), ("ap-def-link", i), tl!("Stroke:")) {             app.ui.open_panel = Some("stroke".into());         }-        chip(ui, Rect::from_min_size(pos2(r.left() + EYE_W + 76.0, r.center().y - 9.0), vec2(18.0, 18.0)), it.paint());+        chip(ui, Rect::from_min_size(pos2(r.left() + EYE_W + 76.0, r.center().y - 9.0), vec2(18.0, 18.0)), it.paint(), false);         if let AppearanceItem::Stroke(st) = it {             let w = app.session.stroke_unit().format(st.width);             text(ui, pos2(r.left() + EYE_W + 106.0, r.center().y), &w, false);@@ -496,7 +493,7 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel, object_row:         // Swatch with a swatches popup that sets this item's paint; Shift-click opens the Color         // panel's mixer on this row instead.         let cr = Rect::from_min_size(pos2(r.left() + EYE_W + 76.0, r.center().y - 9.0), vec2(18.0, 18.0));-        chip(ui, cr, paint);+        chip(ui, cr, paint, false);         let cresp = ui             .interact(cr.expand(2.0), ui.scope_id().with(("ap-chip", i)), Sense::click())             .on_hover_text(tl!("Click to choose a swatch, Shift-click to mix a colour"));@@ -683,7 +680,7 @@ fn contents_row(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, label: &str, se     if characters {         for (k, stroke) in [false, true].into_iter().enumerate() {             let cr = Rect::from_min_size(pos2(r.left() + EYE_W + 100.0 + 24.0 * k as f32, r.center().y - 9.0), vec2(18.0, 18.0));-            chip(ui, cr, n.proxy_paint(stroke, None).map_or(&Paint::None, |(p, ..)| p));+            chip(ui, cr, n.proxy_paint(stroke, None).map_or(&Paint::None, |(p, ..)| p), false);         }     }     // An image's row has no members to target.diff --git a/crates/ui-egui/src/panels/appearance/tests.rs b/crates/ui-egui/src/panels/appearance/tests.rs--- a/crates/ui-egui/src/panels/appearance/tests.rs+++ b/crates/ui-egui/src/panels/appearance/tests.rs@@ -554,17 +554,20 @@ fn a_mixed_selection_lists_what_its_objects_share() {     run(&mut app, "select.set", json!({"ids": ids}));     let shown = frame(&mut app);     let kind = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(ids[0].as_u64().unwrap())).unwrap().kind_label();-    assert_eq!(shown.first().map(String::as_str), Some(kind), "the object row names their kind: {shown:?}");+    assert_eq!(shown.iter().find(|t| *t != "?").map(String::as_str), Some(kind), "the object row names their kind: {shown:?}");     assert!(shown.iter().any(|t| t == "Mixed Appearances"), "{shown:?}");     assert!(shown.iter().any(|t| t == "Stroke:") && !shown.iter().any(|t| t == "Fill:"), "the stroke, not the fills: {shown:?}");     assert!(shown.iter().any(|t| t.contains("Feather")), "the shared effect: {shown:?}");     assert!(shown.iter().any(|t| t == "Opacity:"), "{shown:?}");+    // The object row's chip says the fills differ, like the Properties panel's "?" chip.+    assert!(shown.iter().any(|t| t == "?"), "{shown:?}");     // Alike objects list their whole stack, without the note.     run(&mut app, "paint.setFill", json!({"ids": ids, "color": "#ffff00"}));     let shown = frame(&mut app);     assert!(!shown.iter().any(|t| t == "Mixed Appearances") && shown.iter().any(|t| t == "Fill:"), "{shown:?}");+    assert!(!shown.iter().any(|t| t == "?"), "{shown:?}");     // A path and type.     let t = run(&mut app, "text.create", json!({"x": 0, "y": 180, "text": "Hi"}))["id"].clone();     run(&mut app, "select.set", json!({"ids": [ids[0].clone(), t]}));-    assert_eq!(frame(&mut app).first().map(String::as_str), Some("Mixed Objects"));+    assert_eq!(frame(&mut app).iter().find(|t| *t != "?").map(String::as_str), Some("Mixed Objects")); }