diff --git a/crates/ui-egui/src/panels/color_wheel.rs b/crates/ui-egui/src/panels/color_wheel.rs index 7011c6c70..a2108d8cf 100644 --- a/crates/ui-egui/src/panels/color_wheel.rs +++ b/crates/ui-egui/src/panels/color_wheel.rs @@ -23,16 +23,22 @@ const MAX_WHEEL: f32 = 200.0; /// Panel state: the HSB picked last, its [`Owner`] and the display sRGB of the color it stored. const MEMORY: &str = "color-wheel-hsb"; -/// What the HSB picked last belongs to: the document, the selected objects, the proxy, the -/// gradient stop or freeform point, and the color the pick stored. The wheel shows that HSB only -/// while all of them are unchanged. +/// What the HSB picked last belongs to: the document, the selected objects or text characters, +/// the appearance item, the proxy, the gradient stop or freeform point, its paint, and the color +/// the pick stored. The wheel shows that HSB only while all of them are unchanged. #[derive(Clone, Debug, PartialEq)] struct Owner { doc: Option, selection: Vec, + /// The Type tool's selected characters, when it edits the paint. + text_selection: Option<(NodeId, usize, usize)>, + /// The Appearance panel's active fill or stroke row. + appearance_item: Option, fill: bool, /// The selected gradient stop (`Some((false, i))`) or freeform point (`Some((true, i))`). slot: Option<(bool, usize)>, + /// The gradient behind a selected stop or point, including its other colors. + gradient: Option, color: Color, } @@ -47,8 +53,11 @@ impl Owner { Self { doc: st.map(|d| d.uid), selection: st.map(|d| d.selection.objects.clone()).unwrap_or_default(), + text_selection: super::character::text_editing(app), + appearance_item: app.session.appearance_item(), fill: app.session.fill_active, slot, + gradient: slot.map(|_| super::active_paint(app)), color, } } @@ -150,8 +159,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let behind = super::alt_held(ui); let cmyk_doc = app.session.active().is_some_and(|d| d.doc.color_mode == ColorMode::Cmyk); let c = stored(&cms, hsb, replaced(app, &tgt, behind).as_ref(), cmyk_doc); + apply(app, &tgt, c, phase, behind); if !behind { set_pstate(ui.ctx(), MEMORY, Some((Owner::of(app, &tgt, c), cms.display_rgb(&c), hsb))); } - apply(app, &tgt, c, phase, behind); } diff --git a/crates/ui-egui/src/panels/tests_color_wheel.rs b/crates/ui-egui/src/panels/tests_color_wheel.rs index be9856ed4..798a842e4 100644 --- a/crates/ui-egui/src/panels/tests_color_wheel.rs +++ b/crates/ui-egui/src/panels/tests_color_wheel.rs @@ -308,6 +308,75 @@ fn the_remembered_hue_stays_with_its_object() { assert!(!(1.0..359.0).contains(&hue), "the white object's own hue is 0°, not 120°: {hue}"); } +/// Equal colors on two appearance fills have separate remembered hues. A square pick on the +/// second white fill uses its own 0° hue after the first fill was changed from green to white. +#[test] +fn the_remembered_hue_stays_with_its_appearance_item() { + let mut app = app(json!("#00ff00")); + let id = app.session.active().unwrap().selection.objects[0]; + app.run("appearance.addFill", json!({})).unwrap(); // [Fill, Stroke, Fill] + app.run("paint.setFill", json!({"item": 2, "color": "#ffffff"})).unwrap(); + app.run("appearance.setActiveItem", json!({"index": 0})).unwrap(); + let mut h = harness(app); + let sq = rect_of(&h, "Saturation and brightness"); + drag(&mut h, &[in_square(sq, 0.995, 0.995), Pos2::new(sq.left() - 5.0, sq.top() - 3.0)], Modifiers::NONE); + let item_fill = |app: &VectorcraftApp, index| { + app.session.doc().unwrap().doc.node(id).unwrap().appearance.fill_at(Some(index)).unwrap().paint.color().unwrap() + }; + assert_eq!(item_fill(h.state(), 0), Color::WHITE); + assert_eq!(item_fill(h.state(), 2), Color::WHITE); + h.state_mut().run("appearance.setActiveItem", json!({"index": 2})).unwrap(); + h.run_steps(1); + click(&mut h, in_square(sq, 0.995, 0.995), Modifiers::NONE); + let hue = hsb(&item_fill(h.state(), 2))[0]; + assert!(!(1.0..359.0).contains(&hue), "the second fill's own hue is 0°, not 120°: {hue}"); + assert_eq!(item_fill(h.state(), 0), Color::WHITE); +} + +/// The Type tool can recolor a selected character range without changing the selected object. +/// The next range of the same color starts with its own hue. +#[test] +fn the_remembered_hue_stays_with_its_text_selection() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let id = app.run("text.create", json!({"x": 10, "y": 100, "text": "AB", "color": "#00ff00"})).unwrap()["id"].as_u64().unwrap(); + app.run("text.setRangeStyle", json!({"id": id, "start": 1, "end": 2, "fill": "#ffffff"})).unwrap(); + app.select_tool("type"); + app.session.set_tool_option("textSelection", &json!({"id": id, "start": 0, "end": 1})); + let mut h = harness(app); + let sq = rect_of(&h, "Saturation and brightness"); + drag(&mut h, &[in_square(sq, 0.995, 0.995), Pos2::new(sq.left() - 5.0, sq.top() - 3.0)], Modifiers::NONE); + h.state_mut().session.set_tool_option("textSelection", &json!({"id": id, "start": 1, "end": 2})); + h.run_steps(1); + click(&mut h, in_square(sq, 0.995, 0.995), Modifiers::NONE); + let node = h.state().session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap(); + let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { panic!("still text") }; + assert_eq!(t.runs.first().unwrap().style.fill.color(), Some(Color::WHITE)); + let hue = hsb(&t.runs.last().unwrap().style.fill.color().unwrap())[0]; + assert!(!(1.0..359.0).contains(&hue), "the second character's own hue is 0°, not 120°: {hue}"); +} + +/// Replacing a gradient can keep the same selected stop and color while changing the paint it +/// belongs to. The new gradient starts with that stop's own hue. +#[test] +fn the_remembered_hue_stays_with_its_gradient() { + let mut app = app(json!("#00ff00")); + let first = json!([{"offset": 0, "color": "#00ff00"}, {"offset": 1, "color": "#0000ff"}]); + app.run("paint.setFill", json!({"gradient": {"stops": first}})).unwrap(); + app.run("gradient.selectStop", json!({"index": 0})).unwrap(); + let mut h = harness(app); + let sq = rect_of(&h, "Saturation and brightness"); + drag(&mut h, &[in_square(sq, 0.995, 0.995), Pos2::new(sq.left() - 5.0, sq.top() - 3.0)], Modifiers::NONE); + let second = json!([{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#ff0000"}]); + h.state_mut().run("paint.setFill", json!({"gradient": {"stops": second}})).unwrap(); + assert_eq!(h.state().session.selected_stop(), Some(0)); + h.run_steps(1); + click(&mut h, in_square(sq, 0.995, 0.995), Modifiers::NONE); + let Paint::Gradient(g) = crate::panels::current_paints(h.state()).0 else { panic!("still a gradient") }; + let hue = hsb(&g.gradient.stops[0].color)[0]; + assert!(!(1.0..359.0).contains(&hue), "the new gradient's white stop has hue 0°, not 120°: {hue}"); +} + /// The remembered HSB holds only while the color displays as it did when picked: in another /// working RGB space the wheel shows the color's own HSB there. #[test]