mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 02:30:09 +00:00
Canvas: a dropped press keeps what earlier clicks picked
Escape also clears state that earlier clicks set up, so taking Escape for a dropped press lost it in five tools. Each now drops only the press: - Blend: the object picked first and the blend being added to are put back as they were at the press. - Dimension: a dropped click picks nothing, and the points or lines picked before stay; a dropped drag is canceled as before. - Width: the drag ends and the width points selected at the press are selected again. - Gradient: a dropped click on a freeform gradient leaves the line being drawn open. - Selection: with the pointer loaded with text, a dropped press threads nothing and the text stays loaded; a dropped press on a port loads none.
This commit is contained in:
@@ -390,6 +390,15 @@ impl Tool for DimensionTool {
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
/// A dropped drag is canceled as by Escape. A dropped click picks nothing, and the points or
|
||||
/// lines earlier clicks picked stay.
|
||||
fn drop_press(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
if self.dragging {
|
||||
return self.key(cx, ToolKey::Escape, Mods::default());
|
||||
}
|
||||
self.press = None;
|
||||
vec![]
|
||||
}
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
self.key(cx, ToolKey::Escape, Mods::default())
|
||||
}
|
||||
@@ -465,3 +474,37 @@ impl Tool for DimensionTool {
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testutil::*;
|
||||
use vectorcraft_doc::Selection;
|
||||
|
||||
/// A dropped click picks nothing and keeps the point an earlier click picked. A dropped drag
|
||||
/// out from a segment is canceled as by Escape.
|
||||
#[test]
|
||||
fn a_dropped_press_keeps_the_earlier_picks() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
let ev = |kind, x: f64, y: f64| PointerEvent::new(kind, x, y);
|
||||
let mut t = DimensionTool::default();
|
||||
t.pointer(&c, &ev(PointerKind::Down, 400.0, 400.0));
|
||||
t.pointer(&c, &ev(PointerKind::Up, 400.0, 400.0));
|
||||
assert_eq!(t.points.len(), 1);
|
||||
// The engine sends the release after the drop.
|
||||
t.pointer(&c, &ev(PointerKind::Down, 450.0, 420.0));
|
||||
assert!(t.drop_press(&c).is_empty());
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, 450.0, 420.0)).is_empty());
|
||||
assert_eq!(t.points.len(), 1, "the earlier pick stays and the dropped click picks nothing");
|
||||
assert!(t.pending.is_none() && t.busy());
|
||||
let mut t = DimensionTool::default();
|
||||
t.pointer(&c, &ev(PointerKind::Down, 150.0, 100.0));
|
||||
let acts = t.pointer(&c, &ev(PointerKind::Drag, 150.0, 60.0));
|
||||
assert!(matches!(acts.first(), Some(Action::Begin(_))), "{acts:?}");
|
||||
assert_eq!(t.drop_press(&c), vec![Action::Cancel]);
|
||||
assert!(!t.busy());
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, 150.0, 60.0)).is_empty(), "the release places nothing");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ pub struct WidthTool {
|
||||
drag: Option<Drag>,
|
||||
/// Selected width points: the stroke's owner and their positions t.
|
||||
selected: Option<(NodeId, Vec<f64>)>,
|
||||
/// The selected width points when the button went down: a dropped press puts them back.
|
||||
before_press: Option<(NodeId, Vec<f64>)>,
|
||||
}
|
||||
|
||||
/// The object whose stroke path `id` draws with: its compound path, if it is a member of one.
|
||||
@@ -308,6 +310,7 @@ impl Tool for WidthTool {
|
||||
vec![Action::Dialog("widthPoint".into(), json!({"id": spot.owner.0, "index": w.index, "adjustAdjoining": ev.mods.shift}))]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
self.before_press = self.selected.clone();
|
||||
let Some(spot) = Self::spot_at(cx, p) else {
|
||||
if !ev.mods.shift {
|
||||
self.selected = None;
|
||||
@@ -401,6 +404,13 @@ impl Tool for WidthTool {
|
||||
&& self.drag.is_none()
|
||||
&& self.selected.as_ref().is_some_and(|(id, _)| !self.selected_indices(cx, *id).is_empty())
|
||||
}
|
||||
/// A dropped press ends the drag, and the width points selected when the button went down are
|
||||
/// selected again.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.drag = None;
|
||||
self.selected = self.before_press.take();
|
||||
vec![]
|
||||
}
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.drag.is_none() {
|
||||
self.selected = None;
|
||||
@@ -691,6 +701,38 @@ mod tests {
|
||||
assert!(t.selected.is_some());
|
||||
}
|
||||
|
||||
/// A dropped press keeps the width points earlier clicks selected: a press away from the
|
||||
/// path, dropped, deselects nothing, and a dropped drag of another point ends with the points
|
||||
/// selected before it, and its release commits nothing.
|
||||
#[test]
|
||||
fn a_dropped_press_keeps_the_selected_width_points() {
|
||||
let (d, id) = doc_three_points();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = WidthTool::default();
|
||||
let (none, shift) = (Mods::default(), Mods { shift: true, ..Default::default() });
|
||||
t.pointer(&c, &ev(PointerKind::Down, 150.0, 200.0, none));
|
||||
t.pointer(&c, &ev(PointerKind::Up, 150.0, 200.0, none));
|
||||
t.pointer(&c, &ev(PointerKind::Down, 250.0, 200.0, shift));
|
||||
t.pointer(&c, &ev(PointerKind::Up, 250.0, 200.0, shift));
|
||||
assert_eq!(t.selected_indices(&c, id), vec![1, 3]);
|
||||
// The engine sends the release after the drop.
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, 200.0, 450.0, shift)).is_empty());
|
||||
assert!(t.drop_press(&c).is_empty());
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, 200.0, 450.0, shift)).is_empty());
|
||||
assert_eq!(t.selected_indices(&c, id), vec![1, 3]);
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, 200.0, 450.0, none)).is_empty());
|
||||
assert!(t.drop_press(&c).is_empty());
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, 200.0, 450.0, none)).is_empty());
|
||||
assert_eq!(t.selected_indices(&c, id), vec![1, 3]);
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Down, 200.0, 200.0, none)), vec![Action::Begin("Width Point".into())]);
|
||||
t.pointer(&c, &ev(PointerKind::Drag, 220.0, 200.0, none));
|
||||
assert!(t.drop_press(&c).is_empty());
|
||||
assert!(!t.busy());
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, 220.0, 200.0, none)).is_empty(), "the release commits nothing");
|
||||
assert_eq!(t.selected_indices(&c, id), vec![1, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width_points_show_on_every_subpath() {
|
||||
let (d, c, _) = doc_compound();
|
||||
|
||||
@@ -48,6 +48,9 @@ pub struct BlendTool {
|
||||
first: Option<Pick>,
|
||||
/// A blend was just made: the next click adds to it (the selected blend).
|
||||
chained: bool,
|
||||
/// `first` and `chained` when the button went down: a dropped press puts them back, unless
|
||||
/// the press made a blend.
|
||||
before_press: Option<(Option<Pick>, bool)>,
|
||||
}
|
||||
|
||||
/// The anchor of the first subpath of path or compound path `id` (its first member's) nearest
|
||||
@@ -98,25 +101,32 @@ impl Tool for BlendTool {
|
||||
match ev.kind {
|
||||
PointerKind::DoubleClick => options(self),
|
||||
PointerKind::Down if ev.mods.alt => options(self),
|
||||
PointerKind::Down => match (self.first(cx), pick(cx, ev.pos)) {
|
||||
(_, None) => {
|
||||
(self.first, self.chained) = (None, false);
|
||||
vec![]
|
||||
}
|
||||
(None, Some((top, start))) => {
|
||||
self.first = Some((top, start));
|
||||
vec![Action::Exec("select.set".into(), json!({"ids": [top.0]}))]
|
||||
}
|
||||
(Some((a, sa)), Some((b, sb))) if a != b && cx.doc.node(a).is_some() => {
|
||||
(self.first, self.chained) = (None, true);
|
||||
let mut p = json!({"ids": [a.0, b.0]});
|
||||
if sa.is_some() || sb.is_some() {
|
||||
p["starts"] = json!([sa, sb]);
|
||||
PointerKind::Down => {
|
||||
self.before_press = Some((self.first, self.chained));
|
||||
match (self.first(cx), pick(cx, ev.pos)) {
|
||||
(_, None) => {
|
||||
(self.first, self.chained) = (None, false);
|
||||
vec![]
|
||||
}
|
||||
vec![Action::Exec("object.blend.make".into(), p)]
|
||||
(None, Some((top, start))) => {
|
||||
self.first = Some((top, start));
|
||||
vec![Action::Exec("select.set".into(), json!({"ids": [top.0]}))]
|
||||
}
|
||||
(Some((a, sa)), Some((b, sb))) if a != b && cx.doc.node(a).is_some() => {
|
||||
(self.first, self.chained, self.before_press) = (None, true, None);
|
||||
let mut p = json!({"ids": [a.0, b.0]});
|
||||
if sa.is_some() || sb.is_some() {
|
||||
p["starts"] = json!([sa, sb]);
|
||||
}
|
||||
vec![Action::Exec("object.blend.make".into(), p)]
|
||||
}
|
||||
(Some(_), Some(_)) => vec![],
|
||||
}
|
||||
(Some(_), Some(_)) => vec![],
|
||||
},
|
||||
}
|
||||
PointerKind::Up => {
|
||||
self.before_press = None;
|
||||
vec![]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -151,6 +161,15 @@ impl Tool for BlendTool {
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
/// A dropped press puts back the object picked first and the blend being added to as they
|
||||
/// were when the button went down, so the picks of earlier clicks stay and the press picks
|
||||
/// nothing. A blend the press made stays made.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
if let Some((first, chained)) = self.before_press.take() {
|
||||
(self.first, self.chained) = (first, chained);
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
(self.first, self.chained) = (None, false);
|
||||
vec![]
|
||||
@@ -354,7 +373,7 @@ mod tests {
|
||||
let mut s = Selection::default();
|
||||
s.set([g]);
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = BlendTool { first: None, chained: true };
|
||||
let mut t = BlendTool { first: None, chained: true, ..Default::default() };
|
||||
assert!(t.first(&c).is_some());
|
||||
assert!(t.chosen_again(&c).is_empty());
|
||||
assert!(t.first(&c).is_none(), "choosing the tool again ends adding to the blend just made");
|
||||
@@ -385,7 +404,7 @@ mod tests {
|
||||
// After a blend is made, a click while the selected blend is gone picks the object it hits.
|
||||
let mut s = Selection::default();
|
||||
s.set([NodeId(9999)]);
|
||||
let mut t = BlendTool { first: None, chained: true };
|
||||
let mut t = BlendTool { first: None, chained: true, ..Default::default() };
|
||||
let acts = t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 320.0, 120.0));
|
||||
assert_eq!(acts, vec![Action::Exec("select.set".into(), json!({"ids": [b.0]}))]);
|
||||
}
|
||||
@@ -432,7 +451,7 @@ mod tests {
|
||||
d.insert(Some(l), 1, blend).unwrap();
|
||||
let mut s = Selection::default();
|
||||
let p = paint();
|
||||
let mut t = BlendTool { first: None, chained: true };
|
||||
let mut t = BlendTool { first: None, chained: true, ..Default::default() };
|
||||
s.set([g]);
|
||||
let c = cx(&d, &s, &p);
|
||||
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0));
|
||||
@@ -477,7 +496,7 @@ mod tests {
|
||||
let mut s = Selection::default();
|
||||
s.set([g]);
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = BlendTool { first: None, chained: true };
|
||||
let mut t = BlendTool { first: None, chained: true, ..Default::default() };
|
||||
assert!(t.first(&c).is_some());
|
||||
assert!(t.key(&c, crate::ToolKey::Escape, Mods::default()).is_empty());
|
||||
assert!(t.first(&c).is_none(), "Escape ends adding to the blend just made");
|
||||
@@ -485,6 +504,50 @@ mod tests {
|
||||
assert!(esc(&mut BlendTool::default()).is_empty(), "nothing to drop");
|
||||
}
|
||||
|
||||
/// A dropped press (a long touch, a canceled touch) picks nothing, keeps the pick an earlier
|
||||
/// click made, and keeps adding to the blend just made: the next click blends with them.
|
||||
#[test]
|
||||
fn a_dropped_press_keeps_the_blend_tools_first_pick() {
|
||||
let (mut d, a) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let (b, g) = (d.alloc_id(), d.alloc_id());
|
||||
let r = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(300.0, 100.0, 350.0, 150.0));
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(b, r, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let (k1, k2) = (d.alloc_id(), d.alloc_id());
|
||||
let key = |id: NodeId, x: f64| {
|
||||
let r = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(x, 300.0, x + 20.0, 320.0));
|
||||
std::sync::Arc::new(vectorcraft_doc::Node::path(id, r, vectorcraft_doc::Appearance::default_art()))
|
||||
};
|
||||
let blend = vectorcraft_doc::Node::new(g, NodeKind::Blend { children: vec![key(k1, 0.0), key(k2, 100.0)], spec: Default::default() });
|
||||
d.insert(Some(l), 2, blend).unwrap();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
let ev = |kind, x: f64, y: f64| PointerEvent::new(kind, x, y);
|
||||
// The engine sends the release after the drop.
|
||||
let dropped = |t: &mut BlendTool, c: &ToolContext, x: f64, y: f64| {
|
||||
t.pointer(c, &ev(PointerKind::Down, x, y));
|
||||
assert!(t.drop_press(c).is_empty());
|
||||
assert!(t.pointer(c, &ev(PointerKind::Up, x, y)).is_empty());
|
||||
};
|
||||
let mut t = BlendTool::default();
|
||||
dropped(&mut t, &c, 150.0, 150.0);
|
||||
assert_eq!(t.first(&c), None, "a dropped first press picks nothing");
|
||||
t.pointer(&c, &ev(PointerKind::Down, 150.0, 150.0));
|
||||
t.pointer(&c, &ev(PointerKind::Up, 150.0, 150.0));
|
||||
// A second press on a, and a press on empty canvas, each dropped.
|
||||
dropped(&mut t, &c, 151.0, 151.0);
|
||||
dropped(&mut t, &c, 450.0, 450.0);
|
||||
assert_eq!(t.first(&c), Some((a, None)), "the pick stays");
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Down, 320.0, 120.0)), vec![Action::Exec("object.blend.make".into(), json!({"ids": [a.0, b.0]}))]);
|
||||
// After a blend is made, a dropped press on empty canvas keeps adding to it.
|
||||
let mut s = Selection::default();
|
||||
s.set([g]);
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = BlendTool { first: None, chained: true, ..Default::default() };
|
||||
dropped(&mut t, &c, 450.0, 450.0);
|
||||
assert_eq!(t.first(&c), Some((g, None)), "the blend just made is still added to");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_tool_click_path_creates_mesh() {
|
||||
let (d, a) = doc_with_rect();
|
||||
|
||||
@@ -150,6 +150,9 @@ pub struct SelectionTool {
|
||||
/// The last hover had Alt held: what it measures depends on what lies under the pointer
|
||||
/// ([`Tool::wants_rehover`]).
|
||||
alt_hover: bool,
|
||||
/// The text the pointer was loaded with when the button went down: a dropped press loads it
|
||||
/// again.
|
||||
loaded_at_press: Option<(NodeId, bool)>,
|
||||
}
|
||||
|
||||
/// What Distance Guides measure the selection to.
|
||||
@@ -556,6 +559,21 @@ impl SelectionTool {
|
||||
})
|
||||
}
|
||||
|
||||
/// On a press, notes the text the pointer is loaded with ([`Self::loaded_at_press`]); a
|
||||
/// release clears it.
|
||||
fn note_loaded_at_press(&mut self, kind: PointerKind) {
|
||||
match kind {
|
||||
PointerKind::Down => {
|
||||
self.loaded_at_press = match self.state {
|
||||
State::Loaded { from, before, .. } => Some((from, before)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
PointerKind::Up => self.loaded_at_press = None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer loaded with the text of area type or type on a path `from` ([`State::Loaded`]):
|
||||
/// a press starts placing it, and its release threads it (`text.thread.add`), into a frame put
|
||||
/// `before` `from` or after it. None when `from` can no longer thread: the press is an
|
||||
@@ -669,6 +687,18 @@ impl Tool for SelectionTool {
|
||||
if key == ToolKey::Escape { self.guide.cancel() } else { vec![] }
|
||||
}
|
||||
|
||||
/// A dropped press is canceled as by Escape, so a dropped press on a port loads no text. With
|
||||
/// the pointer loaded with text, the press threads nothing and the text stays loaded for the
|
||||
/// next click, a press on the port that loaded it included.
|
||||
fn drop_press(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
let loaded = self.loaded_at_press.take();
|
||||
let out = if matches!(self.state, State::Loaded { press: Some(_), .. }) { vec![] } else { self.key(cx, ToolKey::Escape, Mods::default()) };
|
||||
if let Some((from, before)) = loaded {
|
||||
self.state = State::Loaded { from, before, press: None };
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
match self.state {
|
||||
State::Marquee { .. } => key == ToolKey::Letter('e'),
|
||||
@@ -703,6 +733,7 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
self.note_loaded_at_press(ev.kind);
|
||||
let hovering = ev.kind == PointerKind::Move && matches!(self.state, State::Idle);
|
||||
self.alt_hover = hovering && ev.mods.alt;
|
||||
self.distance = hovering.then(|| distance_target(cx, ev.pos, ev.mods)).flatten();
|
||||
|
||||
@@ -1871,6 +1871,41 @@ fn escape_drops_the_loaded_text() {
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 30.0, 420.0)), vec![Action::Exec("select.none".into(), json!({}))]);
|
||||
}
|
||||
|
||||
/// A dropped press with the pointer loaded with text threads nothing, and the text stays loaded:
|
||||
/// the next click threads it. A dropped press on the port loads no text.
|
||||
#[test]
|
||||
fn a_dropped_press_keeps_the_text_loaded() {
|
||||
let (d, id, _, port) = overflowing_area_type();
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Down, port.x, port.y));
|
||||
t.drop_press(&cx);
|
||||
t.pointer(&cx, &ev(PointerKind::Up, port.x, port.y));
|
||||
assert_ne!(t.cursor(&cx, Point::new(30.0, 420.0), Mods::default()), Cursor::LoadedText, "the dropped press loads no text");
|
||||
t.pointer(&cx, &ev(PointerKind::Down, port.x, port.y));
|
||||
t.pointer(&cx, &ev(PointerKind::Up, port.x, port.y));
|
||||
// The engine sends the release after the drop.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 20.0, 20.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 80.0, 90.0));
|
||||
assert!(t.drop_press(&cx).is_empty());
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Up, 80.0, 90.0)).is_empty(), "the release threads nothing");
|
||||
assert!(!t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Marquee(_))), "the new frame's outline is removed");
|
||||
assert_eq!(t.cursor(&cx, Point::new(30.0, 420.0), Mods::default()), Cursor::LoadedText);
|
||||
// A press on the port that loaded the text puts it down; dropped, it leaves the text loaded.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, port.x, port.y));
|
||||
assert!(t.drop_press(&cx).is_empty());
|
||||
t.pointer(&cx, &ev(PointerKind::Up, port.x, port.y));
|
||||
assert_eq!(t.cursor(&cx, Point::new(30.0, 420.0), Mods::default()), Cursor::LoadedText, "a dropped press on the port");
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 30.0, 420.0));
|
||||
assert_eq!(
|
||||
t.pointer(&cx, &ev(PointerKind::Up, 30.0, 420.0)),
|
||||
vec![Action::Exec("text.thread.add".into(), json!({"from": id.0, "at": [30.0, 420.0]}))]
|
||||
);
|
||||
}
|
||||
|
||||
/// [`doc_with_area_type`], its text fitting its frame; where its in and out ports are.
|
||||
fn fitting_area_type() -> (vectorcraft_doc::Document, NodeId, Point, Point) {
|
||||
let (d, id) = doc_with_area_type();
|
||||
|
||||
@@ -451,6 +451,31 @@ mod tests {
|
||||
assert!(!t.claims_key(&cx, ToolKey::Escape));
|
||||
}
|
||||
|
||||
/// A dropped click adds no point and leaves the line being drawn open: the next click still
|
||||
/// joins it. A dropped drag of a point is canceled as by Escape.
|
||||
#[test]
|
||||
fn a_dropped_click_keeps_the_line_open() {
|
||||
let (d, s) = doc(FreeformMode::Lines);
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
let mut t = GradientTool::default();
|
||||
let add = |x: f64, y: f64, line: bool| Action::Exec("paint.freeform.addPoint".into(), json!({"at": [x, y], "line": line, "stroke": false}));
|
||||
assert_eq!(click(&mut t, &cx, 140.0, 150.0), vec![add(140.0, 150.0, false)]);
|
||||
cx.freeform_point = Some(2);
|
||||
assert!(t.claims_key(&cx, ToolKey::Escape));
|
||||
// The engine sends the release after the drop.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 170.0, 160.0));
|
||||
assert!(t.drop_press(&cx).is_empty());
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Up, 170.0, 160.0)).is_empty(), "the dropped click adds no point");
|
||||
assert!(t.claims_key(&cx, ToolKey::Escape), "the line stays open");
|
||||
assert_eq!(click(&mut t, &cx, 160.0, 140.0), vec![add(160.0, 140.0, true)]);
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 121.0));
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Drag, 130.0, 140.0))[0], Action::Begin("Gradient".into()));
|
||||
assert_eq!(t.drop_press(&cx), vec![Action::Cancel]);
|
||||
assert!(!t.busy());
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Up, 130.0, 140.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_point_dragged_out_of_the_object_is_removed() {
|
||||
let (d, s) = doc(FreeformMode::Points);
|
||||
|
||||
@@ -511,6 +511,17 @@ impl Tool for GradientTool {
|
||||
&& cx.gradient_stop.is_some_and(|i| Annotator::of(cx).is_some_and(|a| i < a.gradient.stops.len()))
|
||||
}
|
||||
|
||||
/// A dropped drag is canceled as by Escape. A dropped click does nothing, and the line Lines
|
||||
/// mode is drawing stays open.
|
||||
fn drop_press(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
if self.busy() {
|
||||
return self.key(cx, ToolKey::Escape, Mods::default());
|
||||
}
|
||||
(self.gesture, self.free) = (None, None);
|
||||
self.guides.clear();
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.busy() {
|
||||
self.gesture = None;
|
||||
|
||||
Reference in New Issue
Block a user