mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
Merge pull request #5234 from dotCooCoo/fix/dropped-press-cancels-tool
Canvas: a dropped press cancels the tool's drag instead of ending it
This commit is contained in:
@@ -203,6 +203,12 @@ pub struct Session {
|
||||
/// The pointer button is held: the last pointer event was a press or a drag, not a release or
|
||||
/// a move ([`Session::pointer_held`]).
|
||||
pub(crate) pointer_held: bool,
|
||||
/// The id and persistent options of the tool when the press under way began, which a dropped
|
||||
/// press puts back ([`Session::drop_tool_press`]).
|
||||
pub(crate) press_options: Option<(String, serde_json::Map<String, serde_json::Value>)>,
|
||||
/// The freeform gradient point selected once the tool took the press under way, which a
|
||||
/// dropped press selects again ([`Session::drop_tool_press`]).
|
||||
pub(crate) press_point: Option<Option<(usize, cmd::gradient::StopOwner)>>,
|
||||
/// A guide being dragged out of a ruler ([`Session::ruler_guide`]).
|
||||
pub(crate) ruler_guide: Option<vectorcraft_tools::rulerguide::NewGuide>,
|
||||
/// Which selected type overflows its frame, for the ports every tool but the Type tools shows.
|
||||
|
||||
@@ -92,6 +92,8 @@ impl Session {
|
||||
liquify_stroke: None,
|
||||
plane_widget_press: false,
|
||||
pointer_held: false,
|
||||
press_options: None,
|
||||
press_point: None,
|
||||
ruler_guide: None,
|
||||
port_marks: Default::default(),
|
||||
font_search: None,
|
||||
|
||||
@@ -331,3 +331,29 @@ fn type_objects_keep_points_in_text_space() {
|
||||
assert!(close(f.points[i].at, Point::new(90.0, 150.0)), "back in document coordinates: {:?}", f.points[i].at);
|
||||
assert_eq!(f.points[i].color, Color::rgb(1.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
/// A dropped press (a long touch, a canceled touch) of the Gradient tool that dragged a selected
|
||||
/// point out of the object, and out and back in, keeps the point and its selection.
|
||||
#[test]
|
||||
fn a_dropped_drag_of_a_point_keeps_it_selected() {
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
let (mut s, id) = freeform_rect();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("gradient", v).unwrap();
|
||||
let count = points(&s, id).points.len();
|
||||
assert!(count > 1, "a point can be dragged out");
|
||||
let at = s.proxy_freeform(false).unwrap().0.points[0].at;
|
||||
for back in [false, true] {
|
||||
run(&mut s, "paint.freeform.selectPoint", json!({"index": 0}));
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, at.x, at.y), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 400.0, 400.0), v).unwrap();
|
||||
assert_eq!(s.selected_freeform_point(), None, "the removal the drag previews deselects the point");
|
||||
if back {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, at.x + 5.0, at.y + 5.0), v).unwrap();
|
||||
}
|
||||
s.drop_tool_press(v).unwrap();
|
||||
assert!(!s.tool_busy(), "the tool is at rest");
|
||||
assert_eq!(points(&s, id).points.len(), count, "back inside: {back}");
|
||||
assert_eq!(s.selected_freeform_point(), Some(0), "back inside: {back}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Tool options that persist across tool switches, documents and launches (`Prefs::tool_settings`).
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -90,6 +91,115 @@ fn a_gesture_keeps_what_it_changed_and_a_document_switch_keeps_options() {
|
||||
assert_eq!(s.tool_options()["segments"], json!(14));
|
||||
}
|
||||
|
||||
/// A dropped press (a long touch, a canceled touch, a second finger) keeps nothing it began: the
|
||||
/// tool ends at rest with the options it had before the press, and the options the drag changed
|
||||
/// are not stored. A release stores them.
|
||||
#[test]
|
||||
fn a_dropped_press_does_not_keep_what_its_drag_changed() {
|
||||
let mut s = session();
|
||||
select(&mut s, "polygon");
|
||||
let v = ViewInfo::default();
|
||||
let drag_with_one_more_side = |s: &mut Session| {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 150.0), v).unwrap();
|
||||
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
||||
};
|
||||
assert_eq!(s.tool_options()["sides"], json!(6));
|
||||
drag_with_one_more_side(&mut s);
|
||||
assert_eq!(s.tool_options()["sides"], json!(7));
|
||||
s.drop_tool_press(v).unwrap();
|
||||
assert!(!s.tool_busy(), "the tool is at rest");
|
||||
let st = s.active().unwrap();
|
||||
assert!(st.interaction.is_none() && st.doc.art_bounds().is_none(), "the polygon is not drawn");
|
||||
assert_eq!(s.tool_options()["sides"], json!(6), "the tool has its sides from before the press");
|
||||
assert_eq!(s.prefs.tool_settings.get("polygon").and_then(|o| o.get("sides")), None, "the dropped drag's sides are not stored");
|
||||
drag_with_one_more_side(&mut s);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0), v).unwrap();
|
||||
assert!(s.active().unwrap().doc.art_bounds().is_some(), "a release draws the polygon");
|
||||
assert_eq!(s.prefs.tool_settings["polygon"]["sides"], json!(7), "and stores its sides");
|
||||
}
|
||||
|
||||
/// A dropped Pen press drops only the anchor it was placing: the path being drawn goes on with
|
||||
/// the next click.
|
||||
#[test]
|
||||
fn a_dropped_pen_press_keeps_the_path_being_drawn() {
|
||||
let mut s = session();
|
||||
select(&mut s, "pen");
|
||||
let v = ViewInfo::default();
|
||||
let click = |s: &mut Session, x: f64, y: f64| {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();
|
||||
};
|
||||
click(&mut s, 10.0, 10.0);
|
||||
click(&mut s, 100.0, 10.0);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 140.0, 120.0), v).unwrap();
|
||||
s.drop_tool_press(v).unwrap();
|
||||
click(&mut s, 10.0, 100.0);
|
||||
let d = &s.active().unwrap().doc;
|
||||
let paths = d.layers[0].children().unwrap();
|
||||
assert_eq!(paths.len(), 1, "no new path starts");
|
||||
assert_eq!(paths[0].path_data().unwrap().anchor_count(), 3, "the dropped anchor is not placed");
|
||||
}
|
||||
|
||||
/// A dropped Pen press on the first anchor does not close the path or end it.
|
||||
#[test]
|
||||
fn a_dropped_pen_close_keeps_the_path_open_and_being_drawn() {
|
||||
let mut s = session();
|
||||
select(&mut s, "pen");
|
||||
let v = ViewInfo::default();
|
||||
let click = |s: &mut Session, x: f64, y: f64| {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();
|
||||
};
|
||||
click(&mut s, 10.0, 10.0);
|
||||
click(&mut s, 100.0, 10.0);
|
||||
click(&mut s, 100.0, 100.0);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 10.0, 10.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 40.0, 30.0), v).unwrap();
|
||||
s.drop_tool_press(v).unwrap();
|
||||
click(&mut s, 10.0, 100.0);
|
||||
let d = &s.active().unwrap().doc;
|
||||
let paths = d.layers[0].children().unwrap();
|
||||
assert_eq!(paths.len(), 1, "no new path starts");
|
||||
let pd = paths[0].path_data().unwrap();
|
||||
assert_eq!(pd.anchor_count(), 4, "the next click goes on the path");
|
||||
assert!(!pd.subpaths[0].closed, "the path is not closed");
|
||||
}
|
||||
|
||||
/// A dropped marquee selects nothing: the selection stays as it was before the press.
|
||||
#[test]
|
||||
fn a_dropped_marquee_leaves_the_selection_alone() {
|
||||
let mut s = session();
|
||||
s.execute("shape.rectangle", &json!({"x": 600, "y": 400, "width": 100, "height": 100})).unwrap();
|
||||
let before = s.doc().unwrap().selection.objects.clone();
|
||||
assert_eq!(before.len(), 1);
|
||||
select(&mut s, "selection");
|
||||
let v = ViewInfo::default();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 300.0, 300.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 350.0, 350.0), v).unwrap();
|
||||
s.drop_tool_press(v).unwrap();
|
||||
assert!(!s.tool_busy(), "the tool is at rest");
|
||||
assert_eq!(s.doc().unwrap().selection.objects, before);
|
||||
}
|
||||
|
||||
/// A dropped Shift-drag marquee of the Artboard tool toggles no artboard: the tool takes Escape,
|
||||
/// as during a drag, before its release.
|
||||
#[test]
|
||||
fn a_dropped_artboard_marquee_toggles_nothing() {
|
||||
let mut s = session();
|
||||
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 400, "height": 400})).unwrap();
|
||||
select(&mut s, "artboard");
|
||||
let before = s.tool_options()["selected"].clone();
|
||||
let v = ViewInfo::default();
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 1200.0, 500.0).with_mods(shift), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 1250.0, 550.0).with_mods(shift), v).unwrap();
|
||||
s.drop_tool_press(v).unwrap();
|
||||
assert!(!s.tool_busy(), "the tool is at rest");
|
||||
assert_eq!(s.tool_options()["selected"], before, "no artboard is toggled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_are_saved_with_the_preferences() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -358,26 +358,87 @@ impl Session {
|
||||
|
||||
/// Feed a pointer event to the active tool. Returns requests for the UI.
|
||||
pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
self.pointer_event(ev, view, true)
|
||||
}
|
||||
|
||||
/// Drop the press in progress (a long touch, a canceled touch, a second finger): nothing it
|
||||
/// began is kept. Its interaction is undone, the tool drops the press ([`Tool::drop_press`]:
|
||||
/// Escape, as during a drag, for most tools), and a release returns the tool to rest. Of what
|
||||
/// the tool asks for along the way, only a cancel and a notify run, so a marquee selects
|
||||
/// nothing and a click opens no size dialog. The tool gets back the persistent options it had
|
||||
/// when the press began, and none of the press's changes to them are stored. The freeform
|
||||
/// gradient point selected once the tool took the press is selected again.
|
||||
pub fn drop_tool_press(&mut self, view: ViewInfo) -> Result<()> {
|
||||
// Nothing to roll back without a document; the tool still drops the press and gets the release.
|
||||
let _ = self.cancel_interaction();
|
||||
let before = self.press_options.take();
|
||||
let point = self.press_point.take();
|
||||
let mut acts = self.with_tool_cx(view, |t, cx| t.drop_press(cx));
|
||||
acts.retain(|a| matches!(a, Action::Cancel | Action::Notify(_)));
|
||||
let dropped = self.take_tool_panic().and_then(|()| self.apply_actions(acts));
|
||||
let up = PointerEvent { kind: PointerKind::Up, pos: Point::ZERO, mods: Mods::default(), backquote: false, pressure: 1.0, pen: None };
|
||||
let r = dropped.and(self.pointer_event(&up, view, false));
|
||||
// A drag out of the object previews the point's removal, which deselects it.
|
||||
if let Some(p) = point {
|
||||
self.freeform_point = p;
|
||||
}
|
||||
if let Some((id, opts)) = before
|
||||
&& id == self.tool.id()
|
||||
{
|
||||
let now = self.tool.options();
|
||||
for (k, v) in &opts {
|
||||
if now.get(k) != Some(v) {
|
||||
self.tool.set_option(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
r.map(drop)
|
||||
}
|
||||
|
||||
/// The options of `t` that [`Self::store_tool_settings`] stores.
|
||||
fn persistent_options(t: &dyn Tool) -> Map<String, Value> {
|
||||
let opts = t.options();
|
||||
let radius = (t.id() == ROUNDED_RECTANGLE).then_some("cornerRadius");
|
||||
let keys = settings::stores(t.id()).flat_map(|(_, keys)| keys.iter().copied()).chain(radius);
|
||||
keys.filter_map(|k| opts.get(k).map(|v| (k.to_string(), v.clone()))).collect()
|
||||
}
|
||||
|
||||
/// [`Self::pointer`]. Without `keep` (a dropped press's release), the tool's options are not
|
||||
/// stored and only the tool's `Cancel` and `Notify` actions run.
|
||||
fn pointer_event(&mut self, ev: &PointerEvent, view: ViewInfo, keep: bool) -> Result<Vec<UiRequest>> {
|
||||
self.last_view = view;
|
||||
match ev.kind {
|
||||
PointerKind::Down | PointerKind::Drag => self.pointer_held = true,
|
||||
PointerKind::Up | PointerKind::Move => self.pointer_held = false,
|
||||
PointerKind::DoubleClick => {}
|
||||
}
|
||||
match ev.kind {
|
||||
PointerKind::Down => self.press_options = Some((self.tool.id().to_string(), Self::persistent_options(self.tool.as_ref()))),
|
||||
PointerKind::Up => self.press_options = None,
|
||||
_ => {}
|
||||
}
|
||||
// The Plane Switching Widget takes its clicks whatever the tool.
|
||||
if let Some(r) = self.plane_widget_pointer(ev, view) {
|
||||
return r;
|
||||
}
|
||||
self.leave_draw_inside(ev, view)?;
|
||||
let ev = &self.lend_selection_tool(ev);
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
let mut acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
if !keep {
|
||||
acts.retain(|a| matches!(a, Action::Cancel | Action::Notify(_)));
|
||||
}
|
||||
let r = self.take_tool_panic().and_then(|()| {
|
||||
// A gesture may change options (Alt-drag sizes a Liquify brush): keep them.
|
||||
if ev.kind == PointerKind::Up {
|
||||
if keep && ev.kind == PointerKind::Up {
|
||||
self.keep_tool_settings();
|
||||
}
|
||||
self.apply_actions(acts)
|
||||
});
|
||||
match ev.kind {
|
||||
PointerKind::Down => self.press_point = Some(self.freeform_point),
|
||||
PointerKind::Up => self.press_point = None,
|
||||
_ => {}
|
||||
}
|
||||
if ev.kind == PointerKind::Up {
|
||||
self.give_back_tool(view)?;
|
||||
}
|
||||
|
||||
@@ -534,6 +534,15 @@ impl Tool for SmartPenTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// A dropped press does not end the path being drawn, as Escape would: the press and its
|
||||
/// marquee are forgotten, so the release does nothing.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.press = None;
|
||||
self.marquee = None;
|
||||
self.snap.clear();
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
key == ToolKey::Tab && (self.press.is_some() || self.active(cx).is_some())
|
||||
}
|
||||
|
||||
@@ -184,6 +184,16 @@ impl Tool for CropImageTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// A dropped press puts the box back as it was at the press; earlier changes to it stay.
|
||||
fn drop_press(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
if let Some(Drag::Handle(_, from) | Drag::Move(_, from)) = self.drag.take()
|
||||
&& let Some((id, _, _)) = self.current(cx)
|
||||
{
|
||||
self.rect = Some((Some(id), from));
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Enter | ToolKey::Escape) && self.current(cx).is_some()
|
||||
}
|
||||
@@ -368,6 +378,27 @@ mod tests {
|
||||
assert!(t.overlays(&cx).is_empty() && !t.claims_key(&cx, ToolKey::Enter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dropped_press_puts_the_box_back_as_it_was_at_the_press() {
|
||||
let (d, s) = doc_with_image((50.0, 60.0));
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = CropImageTool::default();
|
||||
drag(&mut t, &cx, (50.0, 60.0), (80.0, 70.0));
|
||||
assert_eq!(t.options()["rect"], json!([80.0, 70.0, 170.0, 90.0]));
|
||||
// A handle pressed and dragged, then the press dropped: the box is the adjusted one.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 250.0, 160.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 130.0));
|
||||
assert!(t.drop_press(&cx).is_empty());
|
||||
assert!(!t.busy());
|
||||
assert_eq!(t.options()["rect"], json!([80.0, 70.0, 170.0, 90.0]));
|
||||
// So is a moved box.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 110.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0));
|
||||
assert!(t.drop_press(&cx).is_empty());
|
||||
assert_eq!(t.options()["rect"], json!([80.0, 70.0, 170.0, 90.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_box_option_requires_exactly_four_finite_numeric_values() {
|
||||
let mut tool = CropImageTool::default();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -35,6 +35,9 @@ pub struct CurvatureTool {
|
||||
id: Option<NodeId>,
|
||||
/// The point being dragged, the pointer at the press, and whether the drag began.
|
||||
drag: Option<(Grab, Point, bool)>,
|
||||
/// Where the point of the path being drawn that the press grabbed was at the press, and the
|
||||
/// current point before the press.
|
||||
grabbed: Option<(Point, Option<usize>)>,
|
||||
/// Last touched point of the path being drawn (Backspace removes it).
|
||||
current: Option<usize>,
|
||||
hover: Option<Point>,
|
||||
@@ -564,7 +567,7 @@ impl Tool for CurvatureTool {
|
||||
PointerKind::Down => {
|
||||
if let Some(id) = active {
|
||||
if let Some(i) = self.hit_point(cx, p) {
|
||||
self.current = Some(i);
|
||||
let was_current = self.current.replace(i);
|
||||
if ev.mods.alt {
|
||||
self.pts[i].1 = !self.pts[i].1;
|
||||
return vec![Action::Exec("path.curvature".into(), self.params())];
|
||||
@@ -576,6 +579,7 @@ impl Tool for CurvatureTool {
|
||||
// The point dragged and the curve through it move: the rest pull.
|
||||
hold_targets(&mut self.snap, cx, id, 0, &[i]);
|
||||
self.drag = Some((Grab::Drawn(i), p, false));
|
||||
self.grabbed = self.pts.get(i).map(|pt| (pt.0, was_current));
|
||||
return vec![];
|
||||
}
|
||||
if !self.closed {
|
||||
@@ -650,6 +654,18 @@ impl Tool for CurvatureTool {
|
||||
&& self.drag.is_none()
|
||||
&& if self.active(cx).is_some() { self.pts.len() > 1 } else { current_point(cx).is_some() }
|
||||
}
|
||||
/// A dropped press does not end the path being drawn, as Escape would: a point it grabbed
|
||||
/// goes back to where it was at the press, and the current point to the one before the press.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
if let (Some((Grab::Drawn(i), _, _)), Some((at, current))) = (self.drag.take(), self.grabbed.take())
|
||||
&& let Some(pt) = self.pts.get_mut(i)
|
||||
{
|
||||
pt.0 = at;
|
||||
self.current = current;
|
||||
}
|
||||
self.snap.clear();
|
||||
vec![]
|
||||
}
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _m: Mods) -> Vec<Action> {
|
||||
match key {
|
||||
ToolKey::Escape | ToolKey::Enter => {
|
||||
|
||||
@@ -370,6 +370,34 @@ fn a_press_on_a_point_selects_it_and_a_drag_moves_it() {
|
||||
assert_eq!(press(&mut t, &cx, 100.0, 0.0, false).len(), 1);
|
||||
}
|
||||
|
||||
/// A dropped drag of a point of the path being drawn puts the point back where it was at the
|
||||
/// press, and the path goes on: the next click adds a point to it.
|
||||
#[test]
|
||||
fn a_dropped_drag_puts_the_point_back_and_keeps_drawing() {
|
||||
let (d, id) = doc_with(SubPath::polyline(&[pt(100.0, 100.0), pt(200.0, 100.0), pt(300.0, 100.0)], false));
|
||||
let (s, p) = (selected(id, None), paint());
|
||||
let cx = quiet(&d, &s, &p);
|
||||
let mut t = CurvatureTool {
|
||||
pts: vec![(pt(100.0, 100.0), false), (pt(200.0, 100.0), false), (pt(300.0, 100.0), false)],
|
||||
id: Some(id),
|
||||
current: Some(2),
|
||||
..Default::default()
|
||||
};
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 100.0));
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 220.0, 160.0));
|
||||
assert_eq!(a[0], Action::Begin("Curvature".into()));
|
||||
assert_eq!(t.pts[1].0, pt(220.0, 160.0));
|
||||
assert!(t.drop_press(&cx).is_empty(), "the engine has undone the preview");
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 0.0, 0.0)), vec![]);
|
||||
assert_eq!(t.pts[1].0, pt(200.0, 100.0), "the point is back");
|
||||
assert_eq!(t.current, Some(2), "the current point is the one before the press");
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 400.0, 200.0));
|
||||
assert!(
|
||||
matches!(&a[0], Action::Exec(_, v) if v["id"] == id.0 && v["points"][1]["y"] == 100.0 && v["points"].as_array().unwrap().len() == 4),
|
||||
"{a:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #2120: the dragged end of a slanted straight path keeps to its extension (Smart Guides).
|
||||
#[test]
|
||||
fn a_dragged_end_of_a_straight_path_keeps_to_its_extension() {
|
||||
|
||||
@@ -124,6 +124,12 @@ pub trait Tool: Send {
|
||||
fn key(&mut self, _cx: &ToolContext, _key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
vec![]
|
||||
}
|
||||
/// The press in progress is dropped (a long touch, a canceled touch, a second finger): the
|
||||
/// engine has undone its interaction and sends a release after this. By default the tool takes
|
||||
/// Escape, as during a drag.
|
||||
fn drop_press(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
self.key(cx, ToolKey::Escape, Mods::default())
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
@@ -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 picked an object or 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, self.before_press) = (Some((top, start)), None);
|
||||
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. An object the press
|
||||
/// picked stays picked and selected, and 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,51 @@ mod tests {
|
||||
assert!(esc(&mut BlendTool::default()).is_empty(), "nothing to drop");
|
||||
}
|
||||
|
||||
/// A dropped press (a long touch, a canceled touch) keeps the pick an earlier click made and
|
||||
/// keeps adding to the blend just made: the next click blends with them. A dropped press
|
||||
/// that picked an object keeps that pick, as the object stays selected.
|
||||
#[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();
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Down, 150.0, 150.0)), vec![Action::Exec("select.set".into(), json!({"ids": [a.0]}))]);
|
||||
assert!(t.drop_press(&c).is_empty());
|
||||
t.pointer(&c, &ev(PointerKind::Up, 150.0, 150.0));
|
||||
assert_eq!(t.first(&c), Some((a, None)), "a dropped first press keeps the object it picked and selected");
|
||||
// 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();
|
||||
|
||||
@@ -445,6 +445,15 @@ impl Tool for PenTool {
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
/// A dropped press drops only the anchor it was placing, a handle it was dragging or the
|
||||
/// close it was making: the path being drawn goes on with the next click.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.drag = None;
|
||||
self.handle = None;
|
||||
self.convert = None;
|
||||
self.snap.clear();
|
||||
vec![]
|
||||
}
|
||||
/// Deactivate the pen tool, committing in-flight operations and restoring compound path selection.
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
let path = self.path;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -189,6 +189,9 @@ pub struct TypeTool {
|
||||
snap: DrawSnap,
|
||||
/// The press landed in the edited text: dragging selects.
|
||||
selecting: bool,
|
||||
/// The caret and anchor from before a press in type, kept until its release; a dropped press
|
||||
/// puts them back.
|
||||
before_press: Option<(usize, usize)>,
|
||||
/// Click counting for double/triple click (position of the last click, count).
|
||||
clicks: (Option<Point>, u8),
|
||||
/// Styled copy of the last copied range (pasting the same plain text keeps its styles).
|
||||
|
||||
@@ -603,6 +603,41 @@ fn a_drag_across_type_not_being_edited_selects_its_text() {
|
||||
assert_eq!((tool.editing, tool.sel()), (Some(id), (7, 7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dropped_press_keeps_the_type_in_editing() {
|
||||
let (d, id, mut tool) = editing("Hello world");
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &sel, &p);
|
||||
let t = TypeTool::text(&c, id).unwrap().clone();
|
||||
let lay = vectorcraft_text::layout(FontDb::global(), &t);
|
||||
let pt = |i: usize| {
|
||||
let (a, b) = vectorcraft_text::caret_position(&lay, i);
|
||||
t.xf * a.midpoint(b)
|
||||
};
|
||||
let (at, to) = (pt(3), pt(7));
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x + 0.1, at.y));
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, to.x + 0.1, to.y));
|
||||
assert!(tool.busy());
|
||||
assert_eq!(tool.sel(), (3, 7));
|
||||
// The press is dropped: the caret and selection are as they were before it.
|
||||
let out = tool.drop_press(&c);
|
||||
assert!(!out.iter().any(|a| matches!(a, Action::SwitchTool(_))));
|
||||
assert!(!tool.busy());
|
||||
assert_eq!((tool.editing, tool.sel()), (Some(id), (0, 0)));
|
||||
// The dropped press does not count toward a double or triple click.
|
||||
assert_eq!(tool.clicks, (None, 0));
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, at.x + 0.1, at.y));
|
||||
assert_eq!((tool.editing, tool.sel()), (Some(id), (0, 0)));
|
||||
// A Shift-press that extends the selection, dropped, leaves the caret where a click put it.
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x + 0.1, at.y));
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, at.x + 0.1, at.y));
|
||||
assert_eq!(tool.sel(), (3, 3));
|
||||
tool.pointer(&c, &PointerEvent { mods: SHIFT, ..PointerEvent::new(PointerKind::Down, to.x + 0.1, to.y) });
|
||||
assert_eq!(tool.sel(), (3, 7));
|
||||
tool.drop_press(&c);
|
||||
assert_eq!((tool.editing, tool.sel()), (Some(id), (3, 3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all_and_styled_paste() {
|
||||
let (d, _, mut tool) = editing("abc");
|
||||
|
||||
@@ -29,6 +29,20 @@ impl Tool for TypeTool {
|
||||
fn claims_key(&self, _: &ToolContext, key: ToolKey) -> bool {
|
||||
key == ToolKey::Escape && self.editing.is_some()
|
||||
}
|
||||
/// A dropped press drops only the selection drag or the type it was placing: the type being
|
||||
/// edited stays in editing, with the caret and selection it had before the press.
|
||||
fn drop_press(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
if let Some((caret, anchor)) = self.before_press.take() {
|
||||
(self.caret, self.anchor) = (caret, anchor);
|
||||
}
|
||||
self.selecting = false;
|
||||
self.clicks = (None, 0);
|
||||
self.press = None;
|
||||
self.held = None;
|
||||
self.drag = None;
|
||||
self.snap.clear();
|
||||
vec![]
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
@@ -36,6 +50,7 @@ impl Tool for TypeTool {
|
||||
let same = self.clicks.0.is_some_and(|q| (q - ev.pos).hypot() <= cx.tol(4.0));
|
||||
let n = if same { self.clicks.1 + 1 } else { 1 };
|
||||
self.clicks = (Some(ev.pos), n);
|
||||
self.before_press = Some((self.caret, self.anchor));
|
||||
let out = self.move_to(byte, ev.mods.shift);
|
||||
self.goal_x = None;
|
||||
if n >= 3 {
|
||||
@@ -51,6 +66,7 @@ impl Tool for TypeTool {
|
||||
// as it does in the type being edited.
|
||||
if let Some(out) = self.edit_at(cx, ev.pos) {
|
||||
self.selecting = true;
|
||||
self.before_press = Some((self.caret, self.anchor));
|
||||
return out;
|
||||
}
|
||||
self.press = Some((ev.pos, self.snap.press(cx, ev.pos, self.editing.as_slice(), None)));
|
||||
@@ -71,6 +87,7 @@ impl Tool for TypeTool {
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Up => {
|
||||
self.before_press = None;
|
||||
if self.selecting {
|
||||
self.selecting = false;
|
||||
return vec![];
|
||||
|
||||
@@ -274,10 +274,11 @@ pub(super) fn release(cx: &ToolContext, a: Option<&Annotator>, g: Gesture, lines
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape during a drag: the point a drag-out deselected is selected again.
|
||||
/// Escape during a drag: the point a drag-out deselected is selected again, also when the drag
|
||||
/// came back inside the object.
|
||||
pub(super) fn cancel(g: &Gesture) -> Vec<Action> {
|
||||
match g.grab {
|
||||
Grab::Point(i) if g.out => vec![Action::Cancel, select(Some(i))],
|
||||
Grab::Point(i) if g.left => vec![Action::Cancel, select(Some(i))],
|
||||
_ => vec![Action::Cancel],
|
||||
}
|
||||
}
|
||||
@@ -451,6 +452,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);
|
||||
@@ -474,6 +500,11 @@ mod tests {
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 120.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 60.0, 60.0));
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Default::default()), vec![Action::Cancel, select(Some(0))]);
|
||||
// Also after the drag came back inside.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 120.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 60.0, 60.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 110.0, 110.0));
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Default::default()), vec![Action::Cancel, select(Some(0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -230,15 +230,11 @@ pub fn mods(m: egui::Modifiers, space: bool) -> Mods {
|
||||
pub(super) fn drop_press(app: &mut VectorcraftApp, ui: &Ui, view: vectorcraft_engine::ViewInfo) {
|
||||
match ui.data(|d| d.get_temp::<Drag>(drag_id())) {
|
||||
Some(Drag::Tool | Drag::Art) => {
|
||||
// The tool's mouse-up returns it to rest. A move waiting over a panel is still live
|
||||
// and would otherwise outlive the drag that owns it (#2826).
|
||||
// Nothing to roll back without a document; the tool still gets its mouse-up.
|
||||
let _ = app.session.cancel_interaction();
|
||||
dispatch(
|
||||
app,
|
||||
&PointerEvent { kind: PointerKind::Up, pos: Point::ZERO, mods: Mods::default(), backquote: false, pressure: 1.0, pen: None },
|
||||
view,
|
||||
);
|
||||
// The tool drops the press, and a mouse-up returns it to rest with the options it had
|
||||
// when the press began. A move waiting over a panel is still live and would otherwise outlive the drag that
|
||||
// owns it (#2826).
|
||||
let r = app.session.drop_tool_press(view);
|
||||
apply_requests(app, r.map(|()| vec![]));
|
||||
egui::DragAndDrop::clear_payload(ui.ctx());
|
||||
}
|
||||
Some(_) => {}
|
||||
|
||||
@@ -6723,10 +6723,41 @@ fn long_touch_drops_a_tool_press_without_committing_art() {
|
||||
tick(&mut app, 1.5, vec![]);
|
||||
assert!(matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Gesture)), "long touch drops the tool drag");
|
||||
assert!(app.session.active().unwrap().interaction.is_none());
|
||||
assert!(app.ui.dialog.is_none(), "the dropped press is not a click that opens the size dialog");
|
||||
tick(&mut app, 1.6, pen(egui::TouchPhase::End, pos, 0.5));
|
||||
assert!(app.session.active().unwrap().doc.art_bounds().is_none(), "releasing cannot commit the cancelled press");
|
||||
}
|
||||
|
||||
/// A canceled touch drops the tool's drag: the options the drag changed (the Polygon tool's
|
||||
/// sides) are put back and not stored, and nothing is drawn.
|
||||
#[test]
|
||||
fn a_canceled_touch_drops_the_drag_without_keeping_its_options() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.prefs.touch_gestures = true;
|
||||
app.session.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
app.select_tool("polygon");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let start = xf.to_screen(Point::new(200.0, 200.0));
|
||||
frame(&mut app, &ctx, pen(egui::TouchPhase::Start, start, 0.5));
|
||||
frame(&mut app, &ctx, pen(egui::TouchPhase::Move, start + vec2(60.0, 40.0), 0.5));
|
||||
assert!(matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Tool)));
|
||||
assert!(app.session.active().unwrap().interaction.is_some(), "the drag previews the polygon");
|
||||
let view = app.view_info();
|
||||
assert_eq!(app.session.tool_options()["sides"], json!(6));
|
||||
app.session.tool_key(vectorcraft_tools::ToolKey::Up, Mods::default(), view).unwrap();
|
||||
assert_eq!(app.session.tool_options()["sides"], json!(7));
|
||||
frame(&mut app, &ctx, pen(egui::TouchPhase::Cancel, start + vec2(60.0, 40.0), 0.5));
|
||||
assert!(!app.session.tool_busy(), "the tool is at rest");
|
||||
let st = app.session.active().unwrap();
|
||||
assert!(st.interaction.is_none() && st.doc.art_bounds().is_none(), "nothing is drawn");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.session.tool_options()["sides"], json!(6), "the tool has its sides from before the press");
|
||||
let stored = app.session.prefs.tool_settings.get("polygon").and_then(|o| o.get("sides"));
|
||||
assert_eq!(stored, None, "the dropped drag's sides are not stored");
|
||||
}
|
||||
|
||||
/// #2826: a second finger drops a move waiting over a panel, so its interaction doesn't
|
||||
/// outlive the drag that began it.
|
||||
#[test]
|
||||
|
||||
@@ -840,6 +840,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
With the view rotated, a second Escape less than a second after the first resets the rotation, as Reset Rotate View does. The first Escape may end something else, such as isolation mode or a path being drawn. The second resets the view only when it has nothing else to end, and an Escape that closes a menu does not count. After a click, or a press of another key that is not a modifier (Shift, Control, Alt/Option or Command), the next Escape counts as the first. With the Rotate View tool chosen, one Escape resets the view.
|
||||
A two-finger rotation on a Mac trackpad turns the view about the pointer once it reaches 5° (Preferences › General › Trackpad Gesture to Rotate View, on by default).
|
||||
On a touch screen a second finger cancels the tool press the first finger began, in the frame it lands or later: the Pencil's stroke or a Selection move is rolled back with no undo entry, and a finger that lands while another is down presses nothing, so a pinch or two-finger pan only zooms and pans (#4198; canvas test `a_second_finger_cancels_the_tool_gesture_the_first_began`).
|
||||
A tool press dropped by a second finger, a long touch or a canceled touch cancels the tool's drag as Escape does: the tool gets back the options it had when the press began (the Polygon tool's sides, set with ↑ and ↓) and they are not stored, a press held still opens no size dialog, a Selection or Artboard tool marquee selects nothing, and the Pen goes on drawing its path without the dropped anchor (canvas tests `a_canceled_touch_drops_the_drag_without_keeping_its_options` and `long_touch_drops_a_tool_press_without_committing_art`; engine tests in `tests::toolsettings`).
|
||||
The status bar navigator's field shows the active artboard's number and name, and its drop-down lists every artboard by number and name, as the Artboards panel does. Choosing an artboard in the list makes it the active artboard and fits it in the window (`view.goToArtboard`). The list opens on the active artboard's row and lays out only the rows in view and at most one more.
|
||||
- **Geometry and effects:**
|
||||
- **Open-path anchor Delete:** Delete/Backspace removes selected anchors and their adjoining segments, leaving drawable runs as separate open subpaths of the original object. Surviving curves and appearance stay intact; one Undo restores the document and selection, and a second Delete removes the selected remainder. Explicit Remove Anchor Points still reconnects/refits. Four core regressions, one full-frame Delete/Backspace regression and three existing removal/segment controls pass, covering these distinctions, multiple deletions, transformed geometry and mixed whole-object selection. Behaviour references: the legacy public [Illustrator reference guide, Add or delete an anchor point](https://helpx.adobe.com/content/dam/help/en/pdf/illustrator_reference.pdf) distinguishes keyboard deletion from dedicated removal; the current [Add or remove anchor points](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/add-or-remove-anchor-points.html) describes the dedicated tools. Other G11 gaps and scores/hours remain unchanged.
|
||||
|
||||
Reference in New Issue
Block a user