From 6fd46e445a9181fb98ca96b93f8f4ffdc4642c6a Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 10:28:55 +0700 Subject: [PATCH 1/4] Color Mixer popover: the Color panel's menu, with its color modes (#4249) --- crates/ui-egui/src/panels.rs | 23 ++++++++++++++++++--- crates/ui-egui/src/tests/paintchips.rs | 28 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 7feed8f95..6015dbaa6 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -236,13 +236,18 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) -> bool true } -/// The ≡ panel-menu button drawn into `rect` (the right end of a panel's title/tab strip). -pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) { +/// The ≡ button drawn into `rect` that opens a panel menu. +fn menu_button(ui: &mut Ui, id: &str, rect: Rect) -> egui::Response { let t = Tokens::get(ui.ctx()); let resp = ui.interact(rect, ui.scope_id().with(("panel-menu", id)), Sense::click()); icons::paint(ui, "menu", rect.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text_dim }); resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Panel menu"))); - let resp = resp.on_hover_text(tl!("Panel menu")); + resp.on_hover_text(tl!("Panel menu")) +} + +/// The ≡ panel-menu button drawn into `rect` (the right end of a panel's title/tab strip). +pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) { + let resp = menu_button(ui, id, rect); egui::Popup::menu(&resp).show(|ui| { crate::widgets::menu_scroll(ui, |ui| { ui.set_min_width(220.0); @@ -431,6 +436,18 @@ fn paint_popover(app: &mut VectorcraftApp, ui: &mut Ui) { set_pstate(ui.ctx(), MIXER, m); } } + // The mixer is the Color panel, so its menu comes along: the color modes (Grayscale, + // RGB, HSB, CMYK, Web Safe RGB), Invert, Complement and the rest (#4249). + if mixer { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let (rect, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover()); + let resp = menu_button(ui, "color-mixer", rect); + egui::Popup::menu(&resp).show(|ui| { + ui.set_min_width(200.0); + color::menu(app, ui); + }); + }); + } }); crate::widgets::divider(ui); if mixer { diff --git a/crates/ui-egui/src/tests/paintchips.rs b/crates/ui-egui/src/tests/paintchips.rs index ba5730238..5ede05407 100644 --- a/crates/ui-egui/src/tests/paintchips.rs +++ b/crates/ui-egui/src/tests/paintchips.rs @@ -322,3 +322,31 @@ fn the_popover_swatch_libraries_button_opens_the_libraries_menu() { let open = crate::panels::library_panel::last_open(&app.ui).map(|o| (o.kind.clone(), o.id.clone())); assert_eq!(open, Some(("swatches".into(), "earth-tones".into()))); } + +/// #4249: the Color Mixer a Shift-click on a Fill chip opens has the Color panel's menu, whose +/// color modes (Grayscale, RGB, HSB, CMYK, Web Safe RGB) switch the color it shows and edits. +#[test] +fn the_mixer_popover_has_the_color_panels_menu() { + let mut app = app(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap(); + let ctx = context(); + frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar); + let (fill, _) = chips(&ctx); + click(&mut app, &ctx, fill.center(), Modifiers::SHIFT, control_bar); + let menu = ctx.viewport(|vp| { + vp.prev_pass.widgets.layers().filter(|(l, _)| l.order != egui::Order::Background).flat_map(|(_, w)| w.iter()).find_map(|w| { + (w.rect.size() == vec2(18.0, 18.0) && w.info.as_ref().and_then(|i| i.label.as_deref()) == Some("Panel menu")).then_some(w.rect) + }) + }); + let menu = menu.expect("the mixer has a panel menu button"); + click(&mut app, &ctx, menu.center(), Modifiers::NONE, control_bar); + let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar); + for mode in ["Grayscale", "RGB", "HSB", "Web Safe RGB", "Invert", "Complement"] { + assert!(menu_item_rect(&shapes, mode).is_some(), "{mode} in the mixer's menu"); + } + let cmyk = menu_item_rect(&shapes, "CMYK").expect("CMYK in the mixer's menu"); + click(&mut app, &ctx, cmyk.center(), Modifiers::NONE, control_bar); + let fill = panels::current_paints(&app).0.color(); + assert!(matches!(fill, Some(vectorcraft_color::Color::Cmyk { .. })), "the fill is CMYK now: {fill:?}"); +} From bd1df49813f47a6a75c61bfd926af93ee8808abe Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 10:47:49 +0700 Subject: [PATCH 2/4] Tests: find the mixer's menu button by its size --- crates/ui-egui/src/tests/paintchips.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/tests/paintchips.rs b/crates/ui-egui/src/tests/paintchips.rs index 5ede05407..38f0fd86c 100644 --- a/crates/ui-egui/src/tests/paintchips.rs +++ b/crates/ui-egui/src/tests/paintchips.rs @@ -335,9 +335,12 @@ fn the_mixer_popover_has_the_color_panels_menu() { let (fill, _) = chips(&ctx); click(&mut app, &ctx, fill.center(), Modifiers::SHIFT, control_bar); let menu = ctx.viewport(|vp| { - vp.prev_pass.widgets.layers().filter(|(l, _)| l.order != egui::Order::Background).flat_map(|(_, w)| w.iter()).find_map(|w| { - (w.rect.size() == vec2(18.0, 18.0) && w.info.as_ref().and_then(|i| i.label.as_deref()) == Some("Panel menu")).then_some(w.rect) - }) + vp.prev_pass + .widgets + .layers() + .filter(|(l, _)| l.order != egui::Order::Background) + .flat_map(|(_, w)| w.iter()) + .find_map(|w| (w.rect.size() == vec2(18.0, 18.0)).then_some(w.rect)) }); let menu = menu.expect("the mixer has a panel menu button"); click(&mut app, &ctx, menu.center(), Modifiers::NONE, control_bar); From cdc8cb88c91c318d7c0aee72ef34e276f04f0297 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 10:51:44 +0700 Subject: [PATCH 3/4] Docs: the Color Mixer popover's menu (#4249) --- docs/ui-parity.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 83749a120..0dafd6bab 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J; one row done; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Color Mixer popover: the Color panel's menu with its color modes (#4249); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -361,6 +361,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Color Mixer popover: the Color panel's menu with its color modes (#4249); scores and estimates unchanged | | 2026-10-30 | minor | Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; one row done; scores and estimates unchanged. | | 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act; scores and estimates unchanged | | 2026-10-30 | minor | Preferences: the workspace, panels, toolbars and other UI settings are saved within 2 seconds of a change instead of only on quitting, and a window saves only what it changed, merged into the file as another VectorCraft window left it, so a second window closing last no longer puts back the settings it started with (#4625). Tests `prefs::tests` in `apps/vectorcraft`; scores and estimates unchanged | From 3e6693045d2ffccc028cacb32b05523f92c31729 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 11:00:51 +0700 Subject: [PATCH 4/4] Tests: the mixer's menu checks the shown mode --- crates/ui-egui/src/tests/paintchips.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/tests/paintchips.rs b/crates/ui-egui/src/tests/paintchips.rs index 38f0fd86c..13da43117 100644 --- a/crates/ui-egui/src/tests/paintchips.rs +++ b/crates/ui-egui/src/tests/paintchips.rs @@ -345,7 +345,9 @@ fn the_mixer_popover_has_the_color_panels_menu() { let menu = menu.expect("the mixer has a panel menu button"); click(&mut app, &ctx, menu.center(), Modifiers::NONE, control_bar); let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar); - for mode in ["Grayscale", "RGB", "HSB", "Web Safe RGB", "Invert", "Complement"] { + // The fill is RGB, so RGB is checked. + assert!(menu_item_rect(&shapes, "✓ RGB").is_some(), "RGB is the checked mode"); + for mode in ["Grayscale", "HSB", "Web Safe RGB", "Invert", "Complement"] { assert!(menu_item_rect(&shapes, mode).is_some(), "{mode} in the mixer's menu"); } let cmyk = menu_item_rect(&shapes, "CMYK").expect("CMYK in the mixer's menu");