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:
bflatastic
2026-10-04 17:52:28 +07:00
co-authored by Claude Opus 5.5
parent 3786cfaf36
commit d3dc4a2af3
8 changed files with 83 additions and 18 deletions
+26 -1
View File
@@ -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.
+1 -1
View File
@@ -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;
+4 -3
View File
@@ -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 {
+24 -4
View File
@@ -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);
}
+1 -1
View File
@@ -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)
}