M3.17: the stop popover survives its opening double-click, and the selected stop belongs to its gradient
The double-click that opens the popover lands outside it in the same frame, so the popover closed as soon as it opened; only a later click elsewhere closes it now. The selected stop remembers the document, first selected object and proxy it was picked on (Session::selected_stop), so selecting other art or toggling Fill/Stroke no longer lets Delete or the arrows edit a stop on a different gradient. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3786cfaf36
commit
d3dc4a2af3
|
|
@ -71,10 +71,35 @@ fn select_stop(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Some(i)
|
||||
}
|
||||
};
|
||||
s.gradient_stop = index;
|
||||
s.gradient_stop = index.map(|i| (i, StopOwner::of(s)));
|
||||
Ok(json!({ "index": index }))
|
||||
}
|
||||
|
||||
/// Whose gradient a selected stop belongs to: the active document, its first selected object
|
||||
/// (None: the default paint) and the proxy in front. Selecting other art or toggling the proxy
|
||||
/// leaves no stop selected.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct StopOwner {
|
||||
doc: Option<usize>,
|
||||
object: Option<NodeId>,
|
||||
fill: bool,
|
||||
}
|
||||
|
||||
impl StopOwner {
|
||||
fn of(s: &Session) -> Self {
|
||||
Self { doc: s.active, object: s.active().and_then(|d| d.selection.objects.first().copied()), fill: s.fill_active }
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The selected gradient stop (`gradient.selectStop`), while the gradient it was selected on
|
||||
/// is still the one behind the active proxy. Callers check it against the stop count (an undo
|
||||
/// can remove stops).
|
||||
pub fn selected_stop(&self) -> Option<usize> {
|
||||
self.gradient_stop.filter(|(_, owner)| *owner == StopOwner::of(self)).map(|(i, _)| i)
|
||||
}
|
||||
}
|
||||
|
||||
type Parsed<T> = std::result::Result<T, String>;
|
||||
|
||||
/// Parse a kind name.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ mod edit;
|
|||
mod effectcmd;
|
||||
pub mod fileio;
|
||||
mod fonts;
|
||||
mod gradient;
|
||||
pub(crate) mod gradient;
|
||||
pub(crate) mod graph;
|
||||
pub mod help;
|
||||
mod layer;
|
||||
|
|
|
|||
|
|
@ -443,9 +443,10 @@ pub struct Session {
|
|||
untitled_counter: u32,
|
||||
/// Session-level state of the menu commands (saved selections, guide lock).
|
||||
pub(crate) menu: cmd::menucmds::MenuState,
|
||||
/// The selected gradient stop (`gradient.selectStop`), shared by the Gradient tool's annotator,
|
||||
/// the Gradient and Color panels and agents.
|
||||
pub gradient_stop: Option<usize>,
|
||||
/// The selected gradient stop (`gradient.selectStop`) and whose gradient it was selected on,
|
||||
/// shared by the Gradient tool's annotator, the Gradient and Color panels and agents: read it
|
||||
/// with [`Session::selected_stop`].
|
||||
pub(crate) gradient_stop: Option<(usize, cmd::gradient::StopOwner)>,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
|
|
|
|||
|
|
@ -285,12 +285,12 @@ fn stop_count(s: &Session, id: NodeId) -> usize {
|
|||
fn select_stop_is_validated_and_shared() {
|
||||
let (mut s, _) = annotated();
|
||||
assert_eq!(s.execute("gradient.selectStop", &json!({"index": 1})).unwrap(), json!({"index": 1}));
|
||||
assert_eq!(s.gradient_stop, Some(1));
|
||||
assert_eq!(s.selected_stop(), Some(1));
|
||||
assert!(s.execute("gradient.selectStop", &json!({"index": 2})).is_err(), "two stops");
|
||||
assert!(s.execute("gradient.selectStop", &json!({"index": "a"})).is_err());
|
||||
assert!(s.execute("gradient.selectStop", &json!({})).is_err());
|
||||
s.execute("gradient.selectStop", &json!({"index": null})).unwrap();
|
||||
assert_eq!(s.gradient_stop, None);
|
||||
assert_eq!(s.selected_stop(), None);
|
||||
// The stroke proxy's paint is solid: nothing to select there.
|
||||
s.execute("paint.toggleActive", &json!({})).unwrap();
|
||||
assert!(s.execute("gradient.selectStop", &json!({"index": 0})).is_err());
|
||||
|
|
@ -304,7 +304,7 @@ fn annotator_gestures_edit_the_gradient_as_single_undo_steps() {
|
|||
let stops = fill_gradient(&s, id).gradient.stops;
|
||||
assert_eq!(stops.len(), 3);
|
||||
assert!((stops[1].offset - 0.3).abs() < 1e-6);
|
||||
assert_eq!(s.gradient_stop, Some(1));
|
||||
assert_eq!(s.selected_stop(), Some(1));
|
||||
// Dragging the end handle changes only the end, as one undo step.
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
gesture(
|
||||
|
|
@ -320,9 +320,29 @@ fn annotator_gestures_edit_the_gradient_as_single_undo_steps() {
|
|||
assert!(s.tool_claims_key(ToolKey::Delete, view));
|
||||
s.tool_key(ToolKey::Delete, Mods::default(), view).unwrap();
|
||||
assert_eq!(stop_count(&s, id), 2);
|
||||
assert_eq!(s.gradient_stop, Some(1));
|
||||
assert_eq!(s.selected_stop(), Some(1));
|
||||
// Never below two stops.
|
||||
s.tool_key(ToolKey::Backspace, Mods::default(), view).unwrap();
|
||||
assert_eq!(stop_count(&s, id), 2);
|
||||
assert!(s.doc().unwrap().doc.node(id).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_selected_stop_belongs_to_its_gradient() {
|
||||
let (mut s, a) = annotated();
|
||||
let b = rect(&mut s, 300.0, 100.0, 100.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"gradient": {}})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
s.execute("gradient.selectStop", &json!({"index": 1})).unwrap();
|
||||
let view = crate::ViewInfo::default();
|
||||
assert!(s.tool_claims_key(ToolKey::Delete, view));
|
||||
// Other art selected: no stop is selected there, so Delete is not the tool's.
|
||||
s.execute("select.set", &json!({"ids": [b.0]})).unwrap();
|
||||
assert_eq!(s.selected_stop(), None);
|
||||
assert!(!s.tool_claims_key(ToolKey::Delete, view));
|
||||
// Back on the art it was selected on, it is selected again; the other proxy has none.
|
||||
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
assert_eq!(s.selected_stop(), Some(1));
|
||||
s.execute("paint.toggleActive", &json!({})).unwrap();
|
||||
assert_eq!(s.selected_stop(), None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ impl Session {
|
|||
snap_to_point: view.snap_to_point,
|
||||
corner_widgets: view.corner_widgets,
|
||||
fill_active: self.fill_active,
|
||||
gradient_stop: self.gradient_stop,
|
||||
gradient_stop: self.selected_stop(),
|
||||
};
|
||||
f(self.tool.as_mut(), &cx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec::window(show, confirm);
|
|||
/// The stops of the gradient behind the active proxy and the selected stop's index.
|
||||
fn selected(app: &VectorcraftApp) -> Option<(Vec<GradientStop>, usize)> {
|
||||
let Paint::Gradient(g) = active_paint(app) else { return None };
|
||||
let i = app.session.gradient_stop.filter(|i| *i < g.gradient.stops.len())?;
|
||||
let i = app.session.selected_stop().filter(|i| *i < g.gradient.stops.len())?;
|
||||
Some((g.gradient.stops, i))
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ fn selected(app: &VectorcraftApp) -> Option<(Vec<GradientStop>, usize)> {
|
|||
fn apply(app: &mut VectorcraftApp, stops: &[GradientStop], i: usize, phase: Live) {
|
||||
let params = json!({ "stops": stops_json(stops), "stroke": !app.session.fill_active });
|
||||
live_run(app, "Gradient", "paint.editGradient", params, phase);
|
||||
if phase == Live::Released && app.session.gradient_stop != Some(i) {
|
||||
if phase == Live::Released && app.session.selected_stop() != Some(i) {
|
||||
app.run("gradient.selectStop", json!({ "index": i })).ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +107,9 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
if let (Some(tab), Some(d)) = (tab, app.ui.dialog.as_mut()) {
|
||||
d.fields.insert("tab".into(), json!(tab));
|
||||
}
|
||||
if resp.clicked_elsewhere() {
|
||||
// The double-click that opened the popover lands on its chip, outside it: only a later click
|
||||
// elsewhere closes it.
|
||||
if resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) {
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +202,7 @@ mod tests {
|
|||
crate::canvas::dispatch(&mut app, &PointerEvent { kind: PointerKind::DoubleClick, pos: chip, mods: Default::default(), pressure: 1.0 }, view);
|
||||
let d = app.ui.dialog.clone().expect("the popover opened");
|
||||
assert_eq!((d.kind.as_str(), d.fields["index"].clone(), d.str("tab")), ("gradientStop", json!(1), "color".into()));
|
||||
assert_eq!(app.session.gradient_stop, Some(1));
|
||||
assert_eq!(app.session.selected_stop(), Some(1));
|
||||
frame(&mut app, vec![]);
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("tab".into(), json!("swatches"));
|
||||
frame(&mut app, vec![]);
|
||||
|
|
@ -221,11 +223,28 @@ mod tests {
|
|||
// Moving it past the last stop keeps it selected at its new index.
|
||||
app.ui.dialog = Some(Dialog::new("gradientStop", json!({"location": 100})));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!((app.session.gradient_stop, stops(&app)[2].color.to_hex()), (Some(2), "#ff0000".to_string()));
|
||||
assert_eq!((app.session.selected_stop(), stops(&app)[2].color.to_hex()), (Some(2), "#ff0000".to_string()));
|
||||
app.ui.dialog = Some(Dialog::new("gradientStop", json!({"color": "red"})));
|
||||
assert!(super::super::confirm(&mut app).is_err());
|
||||
}
|
||||
|
||||
fn click(at: egui::Pos2, pressed: bool) -> egui::Event {
|
||||
egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_opening_double_click_keeps_it_open_and_a_later_click_elsewhere_closes_it() {
|
||||
let mut app = app();
|
||||
app.ui.dialog = Some(Dialog::new("gradientStop", json!({"index": 1, "x": 150, "y": 160})));
|
||||
// The frame the double-click lands in (the popover opens beside the pointer, not under it).
|
||||
let at = egui::pos2(5.0, 5.0);
|
||||
let double = vec![egui::Event::PointerMoved(at), click(at, true), click(at, false), click(at, true), click(at, false)];
|
||||
frame(&mut app, double);
|
||||
assert!(app.ui.dialog.is_some(), "the opening double-click must not close it");
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(at), click(at, true), click(at, false)]);
|
||||
assert!(app.ui.dialog.is_none(), "a click elsewhere closes it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_popover_closes_when_its_stop_goes_away() {
|
||||
let mut app = app();
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ fn target(app: &VectorcraftApp) -> Target {
|
|||
let p = active_paint(app);
|
||||
match &p {
|
||||
Paint::Gradient(g) => {
|
||||
let i = app.session.gradient_stop.unwrap_or(0).min(g.gradient.stops.len().saturating_sub(1));
|
||||
let i = app.session.selected_stop().unwrap_or(0).min(g.gradient.stops.len().saturating_sub(1));
|
||||
let color = g.gradient.stops.get(i).map(|s| s.color).unwrap_or(Color::BLACK);
|
||||
Target::Stop { paint: p.clone(), index: i, color }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
ramp(app, ui, &g.gradient, is_grad);
|
||||
ui.add_space(4.0);
|
||||
// Stop fields.
|
||||
let sel = app.session.gradient_stop.filter(|i| *i < g.gradient.stops.len());
|
||||
let sel = app.session.selected_stop().filter(|i| *i < g.gradient.stops.len());
|
||||
ui.horizontal(|ui| {
|
||||
let enabled = is_grad && sel.is_some();
|
||||
let stop = sel.and_then(|i| g.gradient.stops.get(i)).copied();
|
||||
|
|
@ -210,7 +210,7 @@ fn ramp(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) {
|
|||
}
|
||||
ui.painter().rect_stroke(bar, 0.0, Stroke::new(1.0, t.border), StrokeKind::Outside);
|
||||
let x_of = |o: f32| bar.left() + o * bar.width();
|
||||
let sel = app.session.gradient_stop;
|
||||
let sel = app.session.selected_stop();
|
||||
let mut drag: Drag = pstate(ui.ctx(), "grad-drag");
|
||||
let stops = &g.stops;
|
||||
// While dragging, edits are computed against the stops as they were when the drag began (the
|
||||
|
|
|
|||
Loading…
Reference in New Issue