mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Cursors: 36 more tool states get cursors of their own, listed in cursors.toml
Node editing as the public manual describes it: Direct Selection's anchor squares (hollow or filled), Reshape Segment over a curved segment, Group Selection's plus (also Direct Selection with Alt), the arrow hollow while a drag snaps, and Alt-copy arrowheads. The Anchor Point caret, the Curvature pen and Free Transform's side-shear arrows (ShearH, ShearV) are drawn in code. Lasso, the freehand, symbolism and other tools return cursors of their own that borrow their old look until drawn (Cursor::stand_in). crates/tools/src/cursors.toml lists every Cursor value, when it shows and its hotspot, for whoever draws them; `cargo xtask cursors` (in ci) warns about a missing, unknown, repeated or incomplete entry and never fails. The busy cursor is listed as missing (G1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
816097c6bf
commit
2d0da1472f
@@ -705,7 +705,7 @@ impl Tool for LivePaintBucketTool {
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
Cursor::LivePaintBucket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,665 @@
|
||||
# The cursors VectorCraft needs: one [[cursor]] per `Cursor` value (crates/tools/src/lib.rs).
|
||||
#
|
||||
# This is the source of truth for what each cursor is for, written for whoever draws them:
|
||||
# designers never decide what a cursor is for, this file does. `cargo xtask cursors` warns about
|
||||
# a `Cursor` value without an entry. Adding a cursor: CLAUDE.md › Adding a cursor.
|
||||
#
|
||||
# app the app's name as designers read it
|
||||
# id the `Cursor` variant's name (PenAdd), which names its drawing
|
||||
# group the heading it's listed under for designers
|
||||
# shown_when every situation that shows it, one plain sentence each
|
||||
# hotspot where the click lands
|
||||
# look optional: only what the drawing must mean or match (a badge, a sibling cursor).
|
||||
# Never describe another app's cursors; describe what ours must say.
|
||||
# designer optional, default true: false for a cursor the operating system draws on purpose
|
||||
# (nothing to draw)
|
||||
#
|
||||
# Wording: plain sentences for a designer, behaviour only. `cargo xtask brands` rejects vendor
|
||||
# names here.
|
||||
|
||||
app = "VectorCraft"
|
||||
|
||||
# ── Selection ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Arrow"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"The Selection tool over empty canvas or over an unselected object (no Alt).",
|
||||
"Dragging with the Selection tool (a move), or with Direct Selection (an anchor, a segment or a handle).",
|
||||
"Any tool with no cursor of its own (Touch Type, Rotate View).",
|
||||
"The resting state of Live Paint Selection, Reshape (away from a path), Free Transform, Puppet Warp and Crop Image (nothing selected or outside the box), the Perspective Grid (locked or away from handles) and Perspective Selection.",
|
||||
]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "The default pointer: a solid arrow. ArrowHollow is its hollow twin."
|
||||
|
||||
[[cursor]]
|
||||
id = "ArrowHollow"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"Direct Selection at rest: away from anchors and curved segments, and over handles and fills.",
|
||||
"The pen-family tools with Cmd/Ctrl held (which lends Direct Selection).",
|
||||
"While a Selection or Direct Selection drag snaps to a point or a guide (the solid arrow turns hollow).",
|
||||
]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "The same pointer as Arrow, hollow: white with a dark outline."
|
||||
|
||||
[[cursor]]
|
||||
id = "DirectAnchor"
|
||||
group = "Selection"
|
||||
shown_when = ["Direct Selection over an anchor of a path that isn't selected."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "ArrowHollow with a small hollow square badge (an unselected anchor)."
|
||||
|
||||
[[cursor]]
|
||||
id = "DirectAnchorSelected"
|
||||
group = "Selection"
|
||||
shown_when = ["Direct Selection over an anchor of a selected path."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "ArrowHollow with a small filled square badge (a selected anchor)."
|
||||
|
||||
[[cursor]]
|
||||
id = "ReshapeSegment"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"Direct Selection over a curved segment of a path that isn't selected as a whole.",
|
||||
"The Anchor Point tool over a segment, and while it reshapes one.",
|
||||
"The Pen with Alt held over a segment of a selected path, and while it reshapes one.",
|
||||
]
|
||||
hotspot = "The arrowhead's tip."
|
||||
look = "Says \"a drag bends this segment\": an arrowhead with a small curve between two anchor marks."
|
||||
|
||||
[[cursor]]
|
||||
id = "GroupSelection"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"The Group Selection tool: each click selects the next enclosing group.",
|
||||
"Direct Selection with Alt held (Alt makes it Group Selection for the next click).",
|
||||
]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "ArrowHollow with a plus badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "SelectionCopy"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"Alt-dragging a copy with the Selection tool (the drag leaves the original behind).",
|
||||
"Alt-dragging a copy with Direct Selection, dragging a path whole (by its fill, or by an anchor of a path selected whole).",
|
||||
]
|
||||
hotspot = "The front arrowhead's tip."
|
||||
look = "Says \"a copy\": a double arrowhead, one in front of the other."
|
||||
|
||||
[[cursor]]
|
||||
id = "Move"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"The Selection tool over a selected object, or over any object with Alt held (once an Alt-drag starts, SelectionCopy shows).",
|
||||
"Reshape over a path or while dragging.",
|
||||
"Inside the Free Transform and Crop Image boxes.",
|
||||
"Puppet Warp over a pin.",
|
||||
"The Mesh tool over a mesh point or handle.",
|
||||
"The Artboard tool inside an artboard.",
|
||||
"The Gradient tool over the annotator's handles and during most gradient drags.",
|
||||
"The Perspective Grid and Perspective Selection over plane widgets and handles.",
|
||||
]
|
||||
hotspot = "The centre."
|
||||
look = "A four-way move cursor."
|
||||
|
||||
[[cursor]]
|
||||
id = "CornerRadius"
|
||||
group = "Selection"
|
||||
shown_when = ["Selection and Direct Selection (not Group Selection) over a Live Corners widget, and while dragging one (rounding the corners)."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "ArrowHollow with a rounded-corner badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PathBracket"
|
||||
group = "Selection"
|
||||
shown_when = ["Selection and Direct Selection over a bracket of selected type on a path, or dragging one (the type slides along its path)."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "Arrow with a bracket badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeWidget"
|
||||
group = "Selection"
|
||||
shown_when = ["The Selection tool over the type widget of selected type (a double-click converts point type to area type and back)."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "Arrow with a type badge (a letter T)."
|
||||
|
||||
# ── Pen family ────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Pen"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Pen tool where a click starts a new path.",
|
||||
"The Curvature tool away from its points.",
|
||||
"With Use Precise Cursors on, the crosshair shows instead.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "A pen nib. Every Pen-family cursor is this nib with a different badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenDrawing"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Pen tool while a path is being drawn, where a click adds the path's next point.",
|
||||
"The close, join and convert badges still show over the points they act on.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with no badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenAdd"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Pen over a segment of a selected path with Auto Add/Delete on (Shift off), or over a selected blend's spine.",
|
||||
"The Add Anchor Point tool everywhere (and the Delete Anchor Point tool with Alt).",
|
||||
"The Curvature tool over a segment.",
|
||||
"Stands in for MeshAdd and PuppetPin until they're drawn.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a plus badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenDelete"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Pen over an anchor of a selected path (Auto Add/Delete), or over a blend-spine point.",
|
||||
"The Delete Anchor Point tool everywhere (and the Add Anchor Point tool with Alt).",
|
||||
"The Mesh tool with Alt over a mesh point.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a minus badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenClose"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Pen while drawing, over the first anchor of the path (a click closes it).",
|
||||
"The Curvature tool over its first point (three points or more), or near the other end of the current open path.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a ring badge (closing the loop)."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenContinue"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Pen (not drawing) over an end of an open path (a click continues it).",
|
||||
"The Curvature tool over an open end.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a slash badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenJoin"
|
||||
group = "Pen"
|
||||
shown_when = ["The Pen while drawing, over an end of another open path (a click joins the two)."]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a badge showing a line running into an end point."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenConvert"
|
||||
group = "Pen"
|
||||
shown_when = ["The Pen while drawing, over the last anchor (a click retracts its outgoing handle). With Alt the Pen shows AnchorPoint and ReshapeSegment instead."]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a caret badge (like the AnchorPoint caret)."
|
||||
|
||||
[[cursor]]
|
||||
id = "AnchorPoint"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Anchor Point tool, except over a segment (ReshapeSegment shows there).",
|
||||
"The Pen with Alt held, over an anchor or a handle end of a selected path.",
|
||||
]
|
||||
hotspot = "The anchor mark at the caret's apex."
|
||||
look = "A caret (an upside-down V) with a small anchor square at its apex."
|
||||
|
||||
[[cursor]]
|
||||
id = "Curvature"
|
||||
group = "Pen"
|
||||
shown_when = [
|
||||
"The Curvature tool over empty canvas, or anywhere while drawing a path away from its points.",
|
||||
"Elsewhere the Curvature tool borrows Pen-family cursors: PenAdd over a segment, Move over an anchor, PenContinue at an open end, PenConvert with Alt, PenClose over the first point of the path being drawn.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "The Pen nib with a curve badge."
|
||||
|
||||
# ── Type ──────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Text"
|
||||
group = "Type"
|
||||
shown_when = [
|
||||
"Every horizontal type tool: Type, Area Type and Type on a Path (the Area and Path ones show the crosshair over a suitable path).",
|
||||
"Stands in for TypeVertical until it's drawn.",
|
||||
]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "An I-beam with a short crossbar marking the baseline."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeVertical"
|
||||
group = "Type"
|
||||
shown_when = ["The vertical type tools: Vertical Type, Vertical Area Type and Vertical Type on a Path (the Area and Path ones show the crosshair over a suitable path)."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "The Text I-beam turned a quarter turn."
|
||||
|
||||
# ── Transform and resize ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Crosshair"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The generic drawing cursor: all shape, line, grid, flare, cut, graph and liquify tools, and Place.",
|
||||
"The Rotate, Reflect, Scale and Shear tools with a selection (Reflect, Scale and Shear even mid-drag).",
|
||||
"The Area Type and Type on a Path tools over a suitable path.",
|
||||
"The Artboard tool outside every artboard; the Gradient tool away from the annotator; Free Transform corners in Distort or Perspective mode.",
|
||||
"With Use Precise Cursors on, it replaces the Pen, Eyedropper, Slice, Blend and Shape Builder cursors.",
|
||||
"Stands in for every drawing tool cursor not drawn yet.",
|
||||
]
|
||||
hotspot = "The centre of the open gap."
|
||||
look = "Four arms around an open centre, no dot, so the art under the hotspot stays visible."
|
||||
|
||||
[[cursor]]
|
||||
id = "ResizeH"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The left and right handles of the bounding box (or whichever handle reads as left/right once the box is rotated) with Selection, Free Transform, Artboard, Slice Selection and Crop Image.",
|
||||
"Over or dragging a vertical ruler guide.",
|
||||
"Perspective Grid extent handles and near-horizontal Perspective Selection handles.",
|
||||
"The freeform gradient's spread ring.",
|
||||
]
|
||||
hotspot = "The centre."
|
||||
look = "A horizontal double arrow. ResizeV, ResizeNwSe and ResizeNeSw are the same arrow turned."
|
||||
|
||||
[[cursor]]
|
||||
id = "ResizeV"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The top and bottom handles of the same boxes as ResizeH.",
|
||||
"Over or dragging a horizontal ruler guide.",
|
||||
"The Perspective Grid's horizon, height and cell-size handles, and near-vertical Perspective Selection handles.",
|
||||
]
|
||||
hotspot = "The centre."
|
||||
look = "The ResizeH arrow, vertical."
|
||||
|
||||
[[cursor]]
|
||||
id = "ResizeNwSe"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The top-left and bottom-right corner handles of the same boxes as ResizeH (rotation-adjusted).",
|
||||
"Perspective Selection handles at 112.5° to 157.5°.",
|
||||
]
|
||||
hotspot = "The centre."
|
||||
look = "The ResizeH arrow, diagonal from top-left to bottom-right."
|
||||
|
||||
[[cursor]]
|
||||
id = "ResizeNeSw"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The top-right and bottom-left corner handles of the same boxes as ResizeH (rotation-adjusted).",
|
||||
"Perspective Selection handles at 22.5° to 67.5°.",
|
||||
]
|
||||
hotspot = "The centre."
|
||||
look = "The ResizeH arrow, diagonal from bottom-left to top-right."
|
||||
|
||||
[[cursor]]
|
||||
id = "ShearH"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"Free Transform with Cmd/Ctrl held over the top or bottom handle of the bounding box, and while that drag shears the object sideways.",
|
||||
"Without Cmd/Ctrl the same handles show ResizeV.",
|
||||
]
|
||||
hotspot = "The centre, between the two arrows."
|
||||
look = "Two single arrows side by side, one above the other, pointing opposite ways (left and right): one side sliding past the other."
|
||||
|
||||
[[cursor]]
|
||||
id = "ShearV"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"Free Transform with Cmd/Ctrl held over the left or right handle of the bounding box, and while that drag shears the object up or down.",
|
||||
"Without Cmd/Ctrl the same handles show ResizeH.",
|
||||
]
|
||||
hotspot = "The centre, between the two arrows."
|
||||
look = "The ShearH arrows turned upright."
|
||||
|
||||
[[cursor]]
|
||||
id = "Rotate"
|
||||
group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The Rotate tool while dragging.",
|
||||
"The Gradient tool over its rotate zone, or rotating.",
|
||||
"Puppet Warp with Alt near a selected pin.",
|
||||
"The bounding-box rotate cursor when the pointer is exactly on the box's centre.",
|
||||
]
|
||||
hotspot = "Below the arc, in its empty middle."
|
||||
look = "An arc with an arrowhead at each end. RotateToward is the same arc, turned."
|
||||
|
||||
[[cursor]]
|
||||
id = "RotateToward"
|
||||
group = "Transform and resize"
|
||||
shown_when = ["Selection and Free Transform in the rotate zone just outside a bounding-box corner, and while rotating there. The Rotate, Gradient and Puppet Warp rotate cursors don't turn."]
|
||||
hotspot = "Below the arc, in its empty middle, as for Rotate."
|
||||
look = "The Rotate arc drawn for the top-right corner (direction 1). VectorCraft turns it in 45° steps so it wraps whichever corner the pointer is at."
|
||||
|
||||
# ── Navigation ────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Hand"
|
||||
group = "Navigation"
|
||||
shown_when = ["The Hand tool, or Space held with any tool (not with Cmd/Ctrl): ready to pan."]
|
||||
hotspot = "The middle of the hand."
|
||||
look = "An open hand. HandGrab is the same hand closed."
|
||||
|
||||
[[cursor]]
|
||||
id = "HandGrab"
|
||||
group = "Navigation"
|
||||
shown_when = ["While panning: a Hand tool drag, a Space drag, or a middle-button drag with any tool."]
|
||||
hotspot = "The middle of the hand, as for Hand."
|
||||
look = "The Hand, closed (grabbing)."
|
||||
|
||||
[[cursor]]
|
||||
id = "ZoomIn"
|
||||
group = "Navigation"
|
||||
shown_when = ["The Zoom tool, or Cmd/Ctrl+Space with any tool: a click zooms in."]
|
||||
hotspot = "The lens centre."
|
||||
look = "A magnifier with a plus in its lens."
|
||||
|
||||
[[cursor]]
|
||||
id = "ZoomOut"
|
||||
group = "Navigation"
|
||||
shown_when = ["The Zoom tool, or Cmd/Ctrl+Space, with Alt held: a click zooms out."]
|
||||
hotspot = "The lens centre."
|
||||
look = "The ZoomIn magnifier with a minus in its lens."
|
||||
|
||||
[[cursor]]
|
||||
id = "NotAllowed"
|
||||
group = "Navigation"
|
||||
shown_when = [
|
||||
"The Rotate, Reflect, Scale and Shear tools with nothing selected.",
|
||||
"Puppet Warp off the mesh, where no pin can go.",
|
||||
]
|
||||
hotspot = "The centre."
|
||||
designer = false
|
||||
|
||||
# ── Colour and gradient ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Eyedropper"
|
||||
group = "Colour and gradient"
|
||||
shown_when = ["The Eyedropper tool, whatever the modifier keys. With Use Precise Cursors on, the crosshair shows instead."]
|
||||
hotspot = "The pipette's tip."
|
||||
look = "A pipette."
|
||||
|
||||
[[cursor]]
|
||||
id = "AddStop"
|
||||
group = "Colour and gradient"
|
||||
shown_when = [
|
||||
"The Gradient tool over the annotator's bar (a click adds a stop).",
|
||||
"The freeform gradient over its line or the art.",
|
||||
]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "Arrow with a plus badge. RemoveStop is the same with a minus."
|
||||
|
||||
[[cursor]]
|
||||
id = "RemoveStop"
|
||||
group = "Colour and gradient"
|
||||
shown_when = [
|
||||
"The Gradient tool while a stop is dragged off the bar (releasing deletes it).",
|
||||
"The freeform gradient while a point is dragged out.",
|
||||
]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "Arrow with a minus badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "MeshAdd"
|
||||
group = "Colour and gradient"
|
||||
shown_when = [
|
||||
"The Mesh tool over a mesh or a path, off a mesh point: a click adds a mesh point (turning a path into a mesh).",
|
||||
"Elsewhere the Mesh tool shows PenDelete (Alt over a mesh point) and Move (over a point or handle).",
|
||||
]
|
||||
hotspot = "Where the mesh point goes."
|
||||
|
||||
[[cursor]]
|
||||
id = "LivePaintBucket"
|
||||
group = "Colour and gradient"
|
||||
shown_when = ["The Live Paint Bucket: a click fills a face (or an edge) of a Live Paint group."]
|
||||
hotspot = "Where the paint pours out."
|
||||
|
||||
# ── Width, blend, slice and shape builder ─────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Width"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Width tool away from strokes."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "ArrowHollow with a lens badge (a stroke that swells in the middle). WidthAdd and WidthPoint build on it."
|
||||
|
||||
[[cursor]]
|
||||
id = "WidthAdd"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Width tool over a stroke, away from its width points (a drag adds one)."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "The Width cursor with a plus."
|
||||
|
||||
[[cursor]]
|
||||
id = "WidthPoint"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Width tool over a width point or a handle end, and during any Width drag."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "The Width cursor with a mark across the swell (the width being dragged)."
|
||||
|
||||
[[cursor]]
|
||||
id = "Blend"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Blend tool away from art. With Use Precise Cursors on, the crosshair shows instead."]
|
||||
hotspot = "The crosshair's centre."
|
||||
look = "Crosshair with a hollow square badge. BlendObject and BlendAnchor change the badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "BlendObject"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Blend tool over an object it can blend (away from its anchors)."]
|
||||
hotspot = "The crosshair's centre."
|
||||
look = "Crosshair with a filled square badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "BlendAnchor"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Blend tool over an anchor point (the blend starts there)."]
|
||||
hotspot = "The crosshair's centre."
|
||||
look = "Crosshair with a target badge (a ring with a centre dot)."
|
||||
|
||||
[[cursor]]
|
||||
id = "Slice"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Slice tool, always. With Use Precise Cursors on, the plain crosshair shows instead."]
|
||||
hotspot = "The crosshair's centre."
|
||||
look = "Crosshair with a blade badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "SliceSelect"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Slice Selection tool away from the selected slice's handles."]
|
||||
hotspot = "The arrow's tip."
|
||||
look = "Arrow with a small slice badge (a rectangle cut by a line)."
|
||||
|
||||
[[cursor]]
|
||||
id = "ShapeBuilder"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Shape Builder tool (merge mode), and during a drag that started without Alt."]
|
||||
hotspot = "The crosshair's centre."
|
||||
look = "Crosshair with a plus badge. ShapeBuilderErase is the same with a minus."
|
||||
|
||||
[[cursor]]
|
||||
id = "ShapeBuilderErase"
|
||||
group = "Width, blend, slice and shape builder"
|
||||
shown_when = ["The Shape Builder with Alt held (erase mode), and during a drag that started with Alt."]
|
||||
hotspot = "The crosshair's centre."
|
||||
look = "Crosshair with a minus badge."
|
||||
|
||||
# ── Drawing and editing tools ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
id = "Pencil"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Pencil tool: draws a freeform path; starting near the end of a selected open path continues it; Alt while drawing closes the path."]
|
||||
hotspot = "The lead tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "Paintbrush"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Paintbrush tool: draws a brushed freeform path (with pen pressure), continuing a selected path from its end; Alt closes it."]
|
||||
hotspot = "The bristle tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "BlobBrush"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Blob Brush tool: paints filled shapes that merge with touching ones of the same colour. No brush-size outline shows while hovering."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "Eraser"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Eraser tool: a drag erases the area it covers. No size outline shows while hovering."]
|
||||
hotspot = "The eraser's centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "Smooth"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Smooth tool: a drag along a selected path smooths it."]
|
||||
hotspot = "The tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "PathEraser"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Path Eraser tool: a drag along a selected path erases that part of it."]
|
||||
hotspot = "The tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "Join"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Join tool: scrubbing across the gap or overlap between open path ends joins them."]
|
||||
hotspot = "The tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "Knife"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Knife tool: a freehand drag cuts objects along the line."]
|
||||
hotspot = "The blade's point."
|
||||
|
||||
[[cursor]]
|
||||
id = "Scissors"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Scissors tool over a path segment: a click splits the path there."]
|
||||
hotspot = "Where the blades cross, exact enough to cut where aimed."
|
||||
look = "Scissors. ScissorsNotAllowed is this drawing with a badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "ScissorsNotAllowed"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Scissors tool away from every path segment: a click does nothing."]
|
||||
hotspot = "The same point as Scissors."
|
||||
look = "The Scissors with a small not-allowed badge, so the cursor keeps its size and shape and only the badge says a click won't cut."
|
||||
|
||||
[[cursor]]
|
||||
id = "Shaper"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Shaper tool: a rough drawn shape becomes a clean live shape (rectangle, ellipse, polygon, line); scribbling over art deletes it."]
|
||||
hotspot = "The tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "Lasso"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Lasso tool: drag a loop round anchor points and segments to select them; Shift adds to the selection, Alt takes from it."]
|
||||
hotspot = "Where the loop is drawn from."
|
||||
|
||||
[[cursor]]
|
||||
id = "MagicWand"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Magic Wand tool: a click selects objects like the one clicked (per the Magic Wand options); Shift adds, Alt subtracts."]
|
||||
hotspot = "The wand's tip."
|
||||
|
||||
[[cursor]]
|
||||
id = "PuppetPin"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = [
|
||||
"Puppet Warp over the selection's mesh where a pin can go: a click adds a pin.",
|
||||
"Elsewhere Puppet Warp shows NotAllowed (off the mesh), Move (over a pin) and Rotate (Alt near a selected pin).",
|
||||
]
|
||||
hotspot = "Where the pin goes."
|
||||
|
||||
[[cursor]]
|
||||
id = "Measure"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Measure tool: drag to measure distance and angle."]
|
||||
hotspot = "Exact, because precision matters here (an open centre suits it)."
|
||||
|
||||
[[cursor]]
|
||||
id = "PrintTiling"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Print Tiling tool: a drag moves the printable page area over the artboard (it snaps to the artboard's edges)."]
|
||||
hotspot = "The page corner being placed."
|
||||
|
||||
# ── Symbolism tools ───────────────────────────────────────────────────────────────────────────────
|
||||
# The brush size is drawn by the app as a circle around the cursor.
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolSprayer"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Sprayer: a drag sprays instances of the current symbol; Alt reverses it."]
|
||||
hotspot = "The spray centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolShifter"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Shifter: a drag pushes symbol instances along; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolScruncher"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Scruncher: a drag pulls instances together; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolSizer"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Sizer: a drag enlarges instances; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolSpinner"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Spinner: a drag rotates instances in its direction; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolStainer"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Stainer: a drag tints instances toward the fill colour; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolScreener"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Screener: a drag makes instances more transparent; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
[[cursor]]
|
||||
id = "SymbolStyler"
|
||||
group = "Symbolism tools"
|
||||
shown_when = ["The Symbol Styler: a drag applies the chosen graphic style to instances; Alt reverses it."]
|
||||
hotspot = "The brush centre."
|
||||
|
||||
# ── Not needed ────────────────────────────────────────────────────────────────────────────────────
|
||||
# Tools that deliberately have no cursor of their own (listed for designers so nobody draws one).
|
||||
|
||||
[[not_needed]]
|
||||
what = "Liquify tools (Warp, Twirl, Pucker, Bloat, Scallop, Crystallize, Wrinkle)"
|
||||
why = "They show the Crosshair, and the app draws the brush's real size and tilt as an outline; a fixed-size cursor would disagree with it."
|
||||
|
||||
[[not_needed]]
|
||||
what = "Shape, line, grid, flare, graph, cut and gradient tools"
|
||||
why = "The generic Crosshair is the intended drawing cursor for them."
|
||||
+171
-3
@@ -33,7 +33,7 @@ use std::borrow::Cow;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::hit::{HitKind, hit_test};
|
||||
use vectorcraft_doc::{AnchorRef, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::{Anchor, PathData, Point, Rect};
|
||||
use vectorcraft_geom::{Anchor, PathData, Point, Rect, Vec2};
|
||||
|
||||
use crate::bbox::move_delta;
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
@@ -122,6 +122,16 @@ enum State {
|
||||
},
|
||||
}
|
||||
|
||||
/// How far (pt) a snap must move a dragged point for the arrow to say it snapped.
|
||||
const SNAPPED: f64 = 1e-9;
|
||||
|
||||
/// Whether a drag step snapped: the snapped move `d` left where the pointer alone put it
|
||||
/// (`unsnapped`). With Shift the constraint (General › Constrain Angle) moves it too, so only a
|
||||
/// snap's guide tells then.
|
||||
fn snapped(d: Vec2, unsnapped: Vec2, shift: bool, guides: &[Overlay]) -> bool {
|
||||
if shift { !guides.is_empty() } else { (d - unsnapped).hypot() > SNAPPED }
|
||||
}
|
||||
|
||||
pub struct DirectSelectionTool {
|
||||
group: bool,
|
||||
state: State,
|
||||
@@ -139,6 +149,8 @@ pub struct DirectSelectionTool {
|
||||
handle_snap: HandleSnap,
|
||||
/// What the objects being moved snap to, gathered when the move begins.
|
||||
move_snap: Option<MoveSnap>,
|
||||
/// The drag snapped to a point or a guide at its last step: the arrow turns hollow.
|
||||
snapped: bool,
|
||||
/// The anchor under the pointer (Highlight anchors on mouse over): its path, where it is.
|
||||
hover: Option<(NodeId, Point)>,
|
||||
guide: GuideEdit,
|
||||
@@ -161,6 +173,7 @@ impl DirectSelectionTool {
|
||||
copy_snap: None,
|
||||
handle_snap: HandleSnap::default(),
|
||||
move_snap: None,
|
||||
snapped: false,
|
||||
hover: None,
|
||||
guide: GuideEdit::default(),
|
||||
click: None,
|
||||
@@ -466,6 +479,7 @@ impl Tool for DirectSelectionTool {
|
||||
return vec![Action::Begin("Reshape Spine".into())];
|
||||
}
|
||||
self.spine = None;
|
||||
self.snapped = false;
|
||||
if let Some(g) = self.mesh.hit(cx, &cx.selection.objects, p) {
|
||||
self.state = State::Mesh;
|
||||
return self.mesh.press(g);
|
||||
@@ -574,6 +588,7 @@ impl Tool for DirectSelectionTool {
|
||||
self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection)));
|
||||
}
|
||||
let mut d = move_delta(start, p, ev.mods.shift);
|
||||
let unsnapped = d;
|
||||
// The grabbed anchor snaps and the others follow it. Shift keeps the move at its
|
||||
// angle, and the anchor slides along it onto what Smart Guides find there (#886).
|
||||
self.guides.clear();
|
||||
@@ -582,6 +597,7 @@ impl Tool for DirectSelectionTool {
|
||||
let (q, guides) = snap.snap_from(cx, grab + (p - start), shift.as_ref());
|
||||
(d, self.guides) = (q - grab, guides);
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y})));
|
||||
out
|
||||
}
|
||||
@@ -618,6 +634,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
}
|
||||
let mut d = move_delta(start, p, ev.mods.shift);
|
||||
let unsnapped = d;
|
||||
self.guides.clear();
|
||||
let snap = if ev.mods.alt { &self.copy_snap } else { &self.anchor_snap };
|
||||
if let (Some(grab), Some(snap)) = (grab, snap) {
|
||||
@@ -627,6 +644,7 @@ impl Tool for DirectSelectionTool {
|
||||
} else if let Some(snap) = &self.move_snap {
|
||||
(d, self.guides) = snap.snap(cx, start, d, ev.mods.alt);
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
out.push(Action::Preview(
|
||||
"object.transform".into(),
|
||||
json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(d)), "copy": ev.mods.alt}),
|
||||
@@ -648,6 +666,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
(PointerKind::Drag, State::Handle { id, si, ai, out }) => {
|
||||
let (q, guides) = self.handle_snap.snap(cx, (id, si, ai), p, ev.mods.shift);
|
||||
self.snapped = snapped(q.to_vec2(), p.to_vec2(), ev.mods.shift, &guides);
|
||||
self.guides = guides;
|
||||
vec![Action::Preview(
|
||||
"path.setHandle".into(),
|
||||
@@ -803,15 +822,89 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, _m: Mods) -> Cursor {
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
if !self.group && (matches!(self.state, State::Corner(_)) || over_widget(cx, p, true)) {
|
||||
return Cursor::CornerRadius;
|
||||
}
|
||||
if !self.group && (matches!(self.state, State::Bracket(_)) || over_bracket(cx, p)) {
|
||||
return Cursor::PathBracket;
|
||||
}
|
||||
let rest = if self.group { Cursor::GroupSelection } else { Cursor::ArrowHollow };
|
||||
if !self.group {
|
||||
match self.state {
|
||||
// Alt-dragging leaves a copy behind.
|
||||
State::MoveObject { began: true, .. } if m.alt => return Cursor::SelectionCopy,
|
||||
// Bending a segment keeps the cursor that offered it.
|
||||
State::Segment { began: true, .. } | State::SpineSegment { began: true, .. } => return Cursor::ReshapeSegment,
|
||||
// A drag shows the filled arrow, hollow while it snaps to a point or a guide.
|
||||
State::Handle { .. }
|
||||
| State::SpineHandle { .. }
|
||||
| State::SpinePoint { began: true, .. }
|
||||
| State::TypeArea { began: true, .. }
|
||||
| State::MoveAnchors { began: true, .. }
|
||||
| State::MoveObject { began: true, .. } => {
|
||||
return if self.snapped { Cursor::ArrowHollow } else { Cursor::Arrow };
|
||||
}
|
||||
// Alt makes it the Group Selection tool for the next press, unless the press takes
|
||||
// a handle, a mesh or spine point, a type frame's edge or a ruler guide.
|
||||
State::Idle if m.alt => {
|
||||
let other = self.takes_point(cx, p)
|
||||
|| hovered_anchor(cx, p).is_none() && (hit_frame_edge(cx, p, cx.pick_tol()).is_some() || self.guide.cursor(cx, p).is_some());
|
||||
if !other {
|
||||
return Cursor::GroupSelection;
|
||||
}
|
||||
}
|
||||
State::Idle => {
|
||||
if let Some(c) = self.part_under(cx, p, m) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// A press on the highlighted anchor picks it, not the guide.
|
||||
self.guide.cursor(cx, p).filter(|_| self.hover.is_none() || self.guide.busy()).unwrap_or(Cursor::ArrowHollow)
|
||||
self.guide.cursor(cx, p).filter(|_| self.hover.is_none() || self.guide.busy()).unwrap_or(rest)
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectSelectionTool {
|
||||
/// A press at `p` takes a mesh point or handle, a direction handle's end, the clicked spine
|
||||
/// point's handle or a spine point, before any anchor or segment.
|
||||
fn takes_point(&self, cx: &ToolContext, p: Point) -> bool {
|
||||
let point = cx.point_tol();
|
||||
self.mesh.hit(cx, &cx.selection.objects, p).is_some()
|
||||
|| hit_handle(cx, p, point).is_some()
|
||||
|| self.spine.is_some_and(|sel| hit_spine_handle(cx, sel, p, point).is_some())
|
||||
|| hit_spine_point(cx, p, point).is_some()
|
||||
}
|
||||
|
||||
/// The cursor for the path part a press at `p` takes, in the press's order: an anchor (its
|
||||
/// square filled when its path is selected), then a curved segment a drag reshapes (a path's
|
||||
/// not selected whole, or a selected blend's spine's). Handles, mesh and spine points, frame
|
||||
/// edges, ruler guides, straight segments and fills keep the plain cursor.
|
||||
fn part_under(&self, cx: &ToolContext, p: Point, m: Mods) -> Option<Cursor> {
|
||||
let tol = cx.pick_tol();
|
||||
if self.takes_point(cx, p) {
|
||||
return None;
|
||||
}
|
||||
// A selected path's anchors or the anchors of the path under the pointer, as Highlight
|
||||
// anchors on mouse over finds them: walking every path on each hover is too slow.
|
||||
if let Some((id, _)) = hovered_anchor(cx, p) {
|
||||
return Some(if cx.selection.contains(id) { Cursor::DirectAnchorSelected } else { Cursor::DirectAnchor });
|
||||
}
|
||||
if hit_frame_edge(cx, p, tol).is_some() {
|
||||
return None;
|
||||
}
|
||||
if matches!(hit_spine_segment(cx, p, tol), Some((.., false))) {
|
||||
return Some(Cursor::ReshapeSegment);
|
||||
}
|
||||
if m.shift || self.guide.cursor(cx, p).is_some() {
|
||||
return None;
|
||||
}
|
||||
let h = hit_test(cx.doc, p, cx.hit_options())?;
|
||||
let whole = cx.selection.contains(h.leaf) && cx.selection.partial(h.leaf).is_none();
|
||||
let curved = matches!(h.kind, HitKind::Stroke | HitKind::Outline) && segment_at(cx, h.leaf, p).is_some_and(|s| s.curved);
|
||||
(!whole && curved).then_some(Cursor::ReshapeSegment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,6 +1516,81 @@ mod tests {
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 102.5, 300.0)), vec![Action::Begin("Reshape".into())], "the handle");
|
||||
}
|
||||
|
||||
/// Direct Selection's cursors from Illustrator's manual: the white arrow at rest; over an anchor
|
||||
/// a square, hollow on an unselected path and filled on a selected one; the Reshape Segment
|
||||
/// cursor over a curved segment a drag reshapes, not over a straight one (which moves) or a
|
||||
/// path selected whole (which moves whole); Group Selection's plus with Alt and as its tool.
|
||||
#[test]
|
||||
fn hovering_shows_the_anchor_square_reshape_and_group_cursors() {
|
||||
let (d, arch) = arch_doc();
|
||||
let p = paint();
|
||||
let m = Mods::default();
|
||||
let pt = Point::new;
|
||||
let none = Selection::default();
|
||||
let c = cx(&d, &none, &p);
|
||||
let t = DirectSelectionTool::new(false);
|
||||
assert_eq!(t.cursor(&c, pt(20.0, 20.0), m), Cursor::ArrowHollow);
|
||||
assert_eq!(t.cursor(&c, pt(100.0, 300.0), m), Cursor::DirectAnchor);
|
||||
assert_eq!(t.cursor(&c, pt(150.0, 262.5), m), Cursor::ReshapeSegment);
|
||||
assert_eq!(t.cursor(&c, pt(150.0, 100.0), m), Cursor::ArrowHollow, "a straight segment moves");
|
||||
assert_eq!(t.cursor(&c, pt(150.0, 150.0), m), Cursor::ArrowHollow, "the fill");
|
||||
let alt = Mods { alt: true, ..m };
|
||||
assert_eq!(t.cursor(&c, pt(100.0, 300.0), alt), Cursor::GroupSelection);
|
||||
assert_eq!(DirectSelectionTool::new(true).cursor(&c, pt(100.0, 300.0), m), Cursor::GroupSelection);
|
||||
let mut whole = Selection::default();
|
||||
whole.set([arch]);
|
||||
let c = cx(&d, &whole, &p);
|
||||
assert_eq!(t.cursor(&c, pt(200.0, 300.0), m), Cursor::DirectAnchorSelected);
|
||||
assert_eq!(t.cursor(&c, pt(150.0, 262.5), m), Cursor::ArrowHollow, "a path selected whole moves whole");
|
||||
// One anchor selected: its handle end shows and keeps the plain arrow, Alt or not (a press
|
||||
// takes the handle).
|
||||
let s = anchors_of(arch, &[(0, 0)]);
|
||||
let c = cx(&d, &s, &p);
|
||||
assert_eq!(t.cursor(&c, pt(100.0, 250.0), m), Cursor::ArrowHollow, "a handle end");
|
||||
assert_eq!(t.cursor(&c, pt(100.0, 250.0), alt), Cursor::ArrowHollow, "a handle end with Alt");
|
||||
assert_eq!(t.cursor(&c, pt(200.0, 300.0), m), Cursor::DirectAnchorSelected);
|
||||
// Bending the curve keeps the Reshape Segment cursor.
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
let c = cx(&d, &none, &p);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 262.5));
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 150.0, 230.0));
|
||||
assert_eq!(t.cursor(&c, pt(150.0, 230.0), m), Cursor::ReshapeSegment);
|
||||
}
|
||||
|
||||
/// A drag shows the filled arrow, hollow while it snaps to a point; an Alt-drag copy shows the
|
||||
/// double arrowhead.
|
||||
#[test]
|
||||
fn drags_show_the_filled_arrow_hollow_on_a_snap_and_alt_the_copy_cursor() {
|
||||
let (d, _, b) = two_squares();
|
||||
let p = paint();
|
||||
let none = Mods::default();
|
||||
let at = |kind, x, y, m: Mods| PointerEvent::new(kind, x, y).with_mods(m);
|
||||
let one = anchors_of(b, &[(0, 0)]);
|
||||
let c = cx(&d, &one, &p);
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
t.pointer(&c, &at(PointerKind::Down, 300.0, 300.0, none));
|
||||
t.pointer(&c, &at(PointerKind::Drag, 250.0, 260.0, none));
|
||||
assert_eq!(t.cursor(&c, Point::new(250.0, 260.0), none), Cursor::Arrow);
|
||||
// Onto A's bottom-right corner.
|
||||
t.pointer(&c, &at(PointerKind::Drag, 203.0, 199.0, none));
|
||||
assert_eq!(t.cursor(&c, Point::new(203.0, 199.0), none), Cursor::ArrowHollow);
|
||||
t.pointer(&c, &at(PointerKind::Up, 203.0, 199.0, none));
|
||||
// Shift with a Constrain Angle of 10°: the constraint alone isn't a snap.
|
||||
let shift = Mods { shift: true, ..none };
|
||||
let tilted = ToolContext { constrain_angle: 10.0, ..cx(&d, &one, &p) };
|
||||
t.pointer(&tilted, &at(PointerKind::Down, 300.0, 300.0, shift));
|
||||
t.pointer(&tilted, &at(PointerKind::Drag, 380.0, 262.0, shift));
|
||||
assert_eq!(t.cursor(&tilted, Point::new(380.0, 262.0), shift), Cursor::Arrow);
|
||||
t.pointer(&tilted, &at(PointerKind::Up, 380.0, 262.0, shift));
|
||||
let mut whole = Selection::default();
|
||||
whole.set([b]);
|
||||
let c = cx(&d, &whole, &p);
|
||||
let alt = Mods { alt: true, ..none };
|
||||
t.pointer(&c, &at(PointerKind::Down, 300.0, 300.0, alt));
|
||||
t.pointer(&c, &at(PointerKind::Drag, 250.0, 260.0, alt));
|
||||
assert_eq!(t.cursor(&c, Point::new(250.0, 260.0), alt), Cursor::SelectionCopy);
|
||||
}
|
||||
|
||||
/// [`doc_with_rect`] (A, 100..200) and a second square B, 300..400: (A, B).
|
||||
fn two_squares() -> (vectorcraft_doc::Document, NodeId, NodeId) {
|
||||
let (mut d, a) = doc_with_rect();
|
||||
|
||||
@@ -475,7 +475,7 @@ impl Tool for PuppetWarpTool {
|
||||
} else if Self::pin_at(cx, &c.set, p).is_some() {
|
||||
Cursor::Move
|
||||
} else if c.set.rest_of(p).is_some() {
|
||||
Cursor::PenAdd
|
||||
Cursor::PuppetPin
|
||||
} else {
|
||||
// Off the mesh: no pin goes there.
|
||||
Cursor::NotAllowed
|
||||
@@ -733,6 +733,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_click_off_the_mesh_places_no_pin_and_says_so() {
|
||||
let mut r = Rig::new();
|
||||
assert_eq!(r.cursor(130.0, 170.0, Mods::default()), Cursor::PuppetPin, "on the mesh, away from the pins");
|
||||
r.click(130.0, 170.0, Mods::default());
|
||||
assert_eq!(r.cursor(400.0, 400.0, Mods::default()), Cursor::NotAllowed);
|
||||
assert!(r.click(400.0, 400.0, Mods::default()).is_empty());
|
||||
|
||||
@@ -219,22 +219,56 @@ impl Tool for AnchorTool {
|
||||
"deleteAnchor" => Cursor::PenAdd,
|
||||
"scissors" => {
|
||||
if hit_segment(cx, p, cx.pick_tol()).is_some() {
|
||||
Cursor::Crosshair
|
||||
Cursor::Scissors
|
||||
} else {
|
||||
Cursor::NotAllowed
|
||||
Cursor::ScissorsNotAllowed
|
||||
}
|
||||
}
|
||||
_ => Cursor::Pen,
|
||||
_ => match self.state {
|
||||
State::Reshape { .. } => Cursor::ReshapeSegment,
|
||||
State::Idle => press_cursor(cx, p),
|
||||
State::Convert { .. } | State::Handle { .. } => Cursor::AnchorPoint,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Anchor Point tool's cursor for a press at `p`: the Reshape Segment cursor where it takes a
|
||||
/// segment (a path's or a selected blend's spine's), else its caret. Handles and anchors win
|
||||
/// over the segments through them, as the press has it.
|
||||
pub(crate) fn press_cursor(cx: &ToolContext, p: Point) -> Cursor {
|
||||
let (point, tol) = (cx.point_tol(), cx.pick_tol());
|
||||
let segment = hit_handle(cx, p, point).is_none()
|
||||
&& hit_anchor(cx, p, point).is_none()
|
||||
&& (hit_segment(cx, p, tol).is_some() || crate::direct::hit_spine_segment(cx, p, tol).is_some());
|
||||
if segment { Cursor::ReshapeSegment } else { Cursor::AnchorPoint }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testutil::*;
|
||||
use vectorcraft_doc::Selection;
|
||||
|
||||
/// The Anchor Point tool has its own caret cursor, not the Pen's start-a-path one, and over a
|
||||
/// segment the Reshape Segment cursor, kept while the segment is dragged.
|
||||
#[test]
|
||||
fn anchor_point_tool_shows_its_caret_and_reshape_over_segments() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = AnchorTool::new("anchorPoint");
|
||||
let none = Mods::default();
|
||||
for at in [Point::new(20.0, 20.0), Point::new(200.0, 100.0)] {
|
||||
assert_eq!(t.cursor(&cx, at, none), Cursor::AnchorPoint, "{at:?}");
|
||||
}
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 101.0), none), Cursor::ReshapeSegment);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 101.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 140.0));
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 140.0), none), Cursor::ReshapeSegment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_delete_anchor_clicks() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
@@ -621,7 +621,7 @@ impl Tool for CurvatureTool {
|
||||
Some(0) if !self.closed && self.pts.len() >= 3 => Cursor::PenClose,
|
||||
Some(_) if m.alt => Cursor::PenConvert,
|
||||
Some(_) => Cursor::Move,
|
||||
None => Cursor::Pen,
|
||||
None => Cursor::Curvature,
|
||||
};
|
||||
}
|
||||
let tol = point_tol(cx);
|
||||
@@ -642,7 +642,7 @@ impl Tool for CurvatureTool {
|
||||
if end.is_none() && segment_in(cx, editable_paths(cx), p, tol).is_some() {
|
||||
return Cursor::PenAdd;
|
||||
}
|
||||
Cursor::Pen
|
||||
Cursor::Curvature
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,6 +653,16 @@ mod tests {
|
||||
use vectorcraft_doc::{Appearance, Document, Node, Selection};
|
||||
use vectorcraft_geom::{ParamCurve, PathData};
|
||||
|
||||
/// The Curvature tool has its own cursor, not the Pen's start-a-path one.
|
||||
#[test]
|
||||
fn curvature_tool_shows_its_cursor() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
assert_eq!(CurvatureTool::default().cursor(&cx, Point::new(10.0, 10.0), Mods::default()), Cursor::Curvature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_click_creates_then_notify_tracks_path() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
@@ -312,7 +312,16 @@ impl Tool for GestureTool {
|
||||
o
|
||||
}
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
match self.id {
|
||||
"paintbrush" => Cursor::Paintbrush,
|
||||
"blobBrush" => Cursor::BlobBrush,
|
||||
"eraser" => Cursor::Eraser,
|
||||
"knife" => Cursor::Knife,
|
||||
"smooth" => Cursor::Smooth,
|
||||
"pathEraser" => Cursor::PathEraser,
|
||||
"join" => Cursor::Join,
|
||||
_ => Cursor::Pencil,
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
match self.id {
|
||||
|
||||
@@ -393,7 +393,7 @@ impl Tool for ShaperTool {
|
||||
vec![Overlay::Path { path: crate::draw2::polyline(&self.points), color: BLUE, width: 1.5, dashed: false }]
|
||||
}
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _mods: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
Cursor::Shaper
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+211
-4
@@ -427,12 +427,30 @@ pub enum Cursor {
|
||||
#[default]
|
||||
Arrow,
|
||||
ArrowHollow,
|
||||
/// Direct Selection over an anchor of a path that isn't selected: the white arrow with a
|
||||
/// hollow square.
|
||||
DirectAnchor,
|
||||
/// Direct Selection over an anchor of a selected path: the white arrow with a filled square.
|
||||
DirectAnchorSelected,
|
||||
/// Over a segment a drag reshapes (Direct Selection, the Anchor Point tool, the Pen with Alt):
|
||||
/// an arrowhead with a small curve between two anchor marks.
|
||||
ReshapeSegment,
|
||||
/// The Group Selection tool, and Direct Selection with Alt held: the white arrow with a plus.
|
||||
GroupSelection,
|
||||
/// Alt-dragging a copy with the Selection or Direct Selection tool: a double arrowhead.
|
||||
SelectionCopy,
|
||||
Move,
|
||||
Crosshair,
|
||||
ResizeH,
|
||||
ResizeV,
|
||||
ResizeNwSe,
|
||||
ResizeNeSw,
|
||||
/// Free Transform with Cmd/Ctrl over the top or bottom side, where a drag shears the object
|
||||
/// sideways: two arrows side by side, pointing opposite ways along that side.
|
||||
ShearH,
|
||||
/// Free Transform with Cmd/Ctrl over the left or right side, where a drag shears the object up
|
||||
/// or down: the ShearH arrows turned upright.
|
||||
ShearV,
|
||||
Rotate,
|
||||
/// The rotate cursor beside a bounding box, bowed toward the pointer's side of the box: off
|
||||
/// its centre in direction `0..8` (0 straight up, then clockwise in 45° steps). Its arc wraps
|
||||
@@ -449,10 +467,14 @@ pub enum Cursor {
|
||||
PenDelete,
|
||||
PenClose,
|
||||
PenContinue,
|
||||
/// The Anchor Point tool, and the Pen with Alt held (which lends its gestures): a caret, its tip
|
||||
/// on the anchor it converts.
|
||||
AnchorPoint,
|
||||
/// The Curvature tool: the pen nib with a curve badge.
|
||||
Curvature,
|
||||
/// While drawing, over an end of another open path (a click joins the two).
|
||||
PenJoin,
|
||||
/// Over the last anchor of the path being drawn (a click retracts its outgoing handle), or with
|
||||
/// Alt held over a selected path's handle or anchor (the Anchor Point tool's gesture).
|
||||
/// Over the last anchor of the path being drawn: a click retracts its outgoing handle.
|
||||
PenConvert,
|
||||
Text,
|
||||
Hand,
|
||||
@@ -490,6 +512,60 @@ pub enum Cursor {
|
||||
ShapeBuilder,
|
||||
/// ...or, with Alt held, a minus (erase mode).
|
||||
ShapeBuilderErase,
|
||||
/// The Lasso tool.
|
||||
Lasso,
|
||||
/// The Magic Wand tool.
|
||||
MagicWand,
|
||||
/// The Pencil tool.
|
||||
Pencil,
|
||||
/// The Paintbrush tool.
|
||||
Paintbrush,
|
||||
/// The Blob Brush tool.
|
||||
BlobBrush,
|
||||
/// The Eraser tool.
|
||||
Eraser,
|
||||
/// The Smooth tool.
|
||||
Smooth,
|
||||
/// The Path Eraser tool.
|
||||
PathEraser,
|
||||
/// The Join tool.
|
||||
Join,
|
||||
/// The Knife tool.
|
||||
Knife,
|
||||
/// The Shaper tool.
|
||||
Shaper,
|
||||
/// The Live Paint Bucket.
|
||||
LivePaintBucket,
|
||||
/// The Measure tool.
|
||||
Measure,
|
||||
/// The Print Tiling tool.
|
||||
PrintTiling,
|
||||
/// The Symbol Sprayer tool.
|
||||
SymbolSprayer,
|
||||
/// The Symbol Shifter tool.
|
||||
SymbolShifter,
|
||||
/// The Symbol Scruncher tool.
|
||||
SymbolScruncher,
|
||||
/// The Symbol Sizer tool.
|
||||
SymbolSizer,
|
||||
/// The Symbol Spinner tool.
|
||||
SymbolSpinner,
|
||||
/// The Symbol Stainer tool.
|
||||
SymbolStainer,
|
||||
/// The Symbol Screener tool.
|
||||
SymbolScreener,
|
||||
/// The Symbol Styler tool.
|
||||
SymbolStyler,
|
||||
/// The vertical type tools: a turned I-beam.
|
||||
TypeVertical,
|
||||
/// The Scissors over a path segment (a click splits the path there)...
|
||||
Scissors,
|
||||
/// ...or away from every segment (a click does nothing).
|
||||
ScissorsNotAllowed,
|
||||
/// The Mesh tool off a mesh point: a click adds one.
|
||||
MeshAdd,
|
||||
/// Puppet Warp over the selection's mesh where a pin can go: a click adds one.
|
||||
PuppetPin,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
@@ -506,7 +582,7 @@ impl Cursor {
|
||||
}
|
||||
|
||||
/// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the
|
||||
/// Eyedropper's, the Slice and Blend tools') become a plain crosshair at the hotspot.
|
||||
/// Anchor Point and Curvature tools', the Eyedropper's, the Slice and Blend tools') become a plain crosshair at the hotspot.
|
||||
pub fn precise(self) -> Self {
|
||||
match self {
|
||||
Cursor::Pen
|
||||
@@ -517,16 +593,79 @@ impl Cursor {
|
||||
| Cursor::PenContinue
|
||||
| Cursor::PenJoin
|
||||
| Cursor::PenConvert
|
||||
| Cursor::AnchorPoint
|
||||
| Cursor::Curvature
|
||||
| Cursor::Eyedropper
|
||||
| Cursor::Slice
|
||||
| Cursor::Blend
|
||||
| Cursor::BlendObject
|
||||
| Cursor::BlendAnchor
|
||||
| Cursor::ShapeBuilder
|
||||
| Cursor::ShapeBuilderErase => Cursor::Crosshair,
|
||||
| Cursor::ShapeBuilderErase
|
||||
| Cursor::Lasso
|
||||
| Cursor::MagicWand
|
||||
| Cursor::Pencil
|
||||
| Cursor::Paintbrush
|
||||
| Cursor::BlobBrush
|
||||
| Cursor::Eraser
|
||||
| Cursor::Smooth
|
||||
| Cursor::PathEraser
|
||||
| Cursor::Join
|
||||
| Cursor::Knife
|
||||
| Cursor::Shaper
|
||||
| Cursor::LivePaintBucket
|
||||
| Cursor::Measure
|
||||
| Cursor::PrintTiling
|
||||
| Cursor::SymbolSprayer
|
||||
| Cursor::SymbolShifter
|
||||
| Cursor::SymbolScruncher
|
||||
| Cursor::SymbolSizer
|
||||
| Cursor::SymbolSpinner
|
||||
| Cursor::SymbolStainer
|
||||
| Cursor::SymbolScreener
|
||||
| Cursor::SymbolStyler
|
||||
| Cursor::Scissors
|
||||
| Cursor::MeshAdd
|
||||
| Cursor::PuppetPin => Cursor::Crosshair,
|
||||
c => c,
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor this one borrows until it's drawn: the generic crosshair for the
|
||||
/// tools, the I-beam for vertical type, the Pen's add cursor for the Mesh tool and Puppet
|
||||
/// Warp, not-allowed for the Scissors away from a path. `None` for a cursor with a look of
|
||||
/// its own.
|
||||
pub fn stand_in(self) -> Option<Self> {
|
||||
match self {
|
||||
Cursor::Lasso
|
||||
| Cursor::MagicWand
|
||||
| Cursor::Pencil
|
||||
| Cursor::Paintbrush
|
||||
| Cursor::BlobBrush
|
||||
| Cursor::Eraser
|
||||
| Cursor::Smooth
|
||||
| Cursor::PathEraser
|
||||
| Cursor::Join
|
||||
| Cursor::Knife
|
||||
| Cursor::Shaper
|
||||
| Cursor::LivePaintBucket
|
||||
| Cursor::Measure
|
||||
| Cursor::PrintTiling
|
||||
| Cursor::SymbolSprayer
|
||||
| Cursor::SymbolShifter
|
||||
| Cursor::SymbolScruncher
|
||||
| Cursor::SymbolSizer
|
||||
| Cursor::SymbolSpinner
|
||||
| Cursor::SymbolStainer
|
||||
| Cursor::SymbolScreener
|
||||
| Cursor::SymbolStyler
|
||||
| Cursor::Scissors => Some(Cursor::Crosshair),
|
||||
Cursor::TypeVertical => Some(Cursor::Text),
|
||||
Cursor::MeshAdd | Cursor::PuppetPin => Some(Cursor::PenAdd),
|
||||
Cursor::ScissorsNotAllowed => Some(Cursor::NotAllowed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A tool state machine.
|
||||
@@ -749,3 +888,71 @@ pub(crate) mod testutil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cursor_tests {
|
||||
use super::testutil::{cx, doc_with_rect, paint};
|
||||
use super::*;
|
||||
|
||||
fn cursor_at(id: &str, d: &Document, p: Point) -> Cursor {
|
||||
let (s, paint) = (Selection::default(), paint());
|
||||
create(id).cursor(&cx(d, &s, &paint), p, Mods::default())
|
||||
}
|
||||
|
||||
/// Each of these tools has a cursor of its own (borrowing the crosshair until it's drawn), and
|
||||
/// Use Precise Cursors turns it into the crosshair.
|
||||
#[test]
|
||||
fn drawing_tools_show_their_own_cursor() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let away = Point::new(400.0, 400.0);
|
||||
for (id, c) in [
|
||||
("lasso", Cursor::Lasso),
|
||||
("magicWand", Cursor::MagicWand),
|
||||
("pencil", Cursor::Pencil),
|
||||
("paintbrush", Cursor::Paintbrush),
|
||||
("blobBrush", Cursor::BlobBrush),
|
||||
("eraser", Cursor::Eraser),
|
||||
("smooth", Cursor::Smooth),
|
||||
("pathEraser", Cursor::PathEraser),
|
||||
("join", Cursor::Join),
|
||||
("knife", Cursor::Knife),
|
||||
("shaper", Cursor::Shaper),
|
||||
("livePaintBucket", Cursor::LivePaintBucket),
|
||||
("measure", Cursor::Measure),
|
||||
("printTiling", Cursor::PrintTiling),
|
||||
("symbolSprayer", Cursor::SymbolSprayer),
|
||||
("symbolShifter", Cursor::SymbolShifter),
|
||||
("symbolScruncher", Cursor::SymbolScruncher),
|
||||
("symbolSizer", Cursor::SymbolSizer),
|
||||
("symbolSpinner", Cursor::SymbolSpinner),
|
||||
("symbolStainer", Cursor::SymbolStainer),
|
||||
("symbolScreener", Cursor::SymbolScreener),
|
||||
("symbolStyler", Cursor::SymbolStyler),
|
||||
("mesh", Cursor::MeshAdd),
|
||||
] {
|
||||
assert_eq!(cursor_at(id, &d, away), c, "{id}");
|
||||
assert_eq!(c.precise(), Cursor::Crosshair, "{id}");
|
||||
assert_eq!(c.stand_in(), Some(if id == "mesh" { Cursor::PenAdd } else { Cursor::Crosshair }), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_vertical_type_tools_show_the_turned_i_beam() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let away = Point::new(400.0, 400.0);
|
||||
for id in ["verticalType", "verticalAreaType", "verticalTypeOnPath"] {
|
||||
assert_eq!(cursor_at(id, &d, away), Cursor::TypeVertical, "{id}");
|
||||
}
|
||||
assert_eq!(cursor_at("type", &d, away), Cursor::Text);
|
||||
assert_eq!(Cursor::TypeVertical.stand_in(), Some(Cursor::Text));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scissors_say_whether_a_click_cuts() {
|
||||
let (d, _) = doc_with_rect();
|
||||
assert_eq!(cursor_at("scissors", &d, Point::new(100.0, 150.0)), Cursor::Scissors, "over a segment");
|
||||
assert_eq!(cursor_at("scissors", &d, Point::new(400.0, 400.0)), Cursor::ScissorsNotAllowed, "away from paths");
|
||||
assert_eq!(Cursor::ScissorsNotAllowed.stand_in(), Some(Cursor::NotAllowed));
|
||||
assert_eq!(Cursor::ScissorsNotAllowed.precise(), Cursor::ScissorsNotAllowed, "nothing to aim at");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ impl Tool for MeshTool {
|
||||
match self.grab_at(cx, p) {
|
||||
Some(g) if mods.alt && g.handle.is_none() => Cursor::PenDelete,
|
||||
Some(_) => Cursor::Move,
|
||||
None => Cursor::PenAdd,
|
||||
None => Cursor::MeshAdd,
|
||||
}
|
||||
}
|
||||
fn options(&self) -> serde_json::Value {
|
||||
|
||||
+21
-4
@@ -152,6 +152,17 @@ fn alt_converts(cx: &ToolContext, p: Point, m: Mods) -> bool {
|
||||
|| crate::draw2::segment_in(cx, crate::draw2::shown_paths(cx), p, cx.pick_tol()).is_some())
|
||||
}
|
||||
|
||||
/// The cursor where [`alt_converts`]: Alt lends the Anchor Point tool, so its caret over a handle
|
||||
/// end or an anchor, the Reshape Segment cursor over a segment.
|
||||
fn alt_cursor(cx: &ToolContext, p: Point) -> Cursor {
|
||||
let point = cx.point_tol();
|
||||
if hit_handle(cx, p, point).is_some() || crate::draw2::anchor_in(cx, crate::draw2::shown_paths(cx), p, point).is_some() {
|
||||
Cursor::AnchorPoint
|
||||
} else {
|
||||
Cursor::ReshapeSegment
|
||||
}
|
||||
}
|
||||
|
||||
/// Preview the outgoing handle of anchor `ai` of subpath `si` at `h`, the incoming one left alone.
|
||||
fn set_out_handle(id: NodeId, si: usize, ai: usize, h: Point) -> Action {
|
||||
Action::Preview(
|
||||
@@ -390,6 +401,10 @@ impl Tool for PenTool {
|
||||
o
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
// An Alt gesture under way keeps the Anchor Point tool's cursor.
|
||||
if let Some(t) = &self.convert {
|
||||
return t.cursor(cx, p, m);
|
||||
}
|
||||
let tol = cx.tol(5.0).max(cx.point_tol());
|
||||
let active = self.active(cx);
|
||||
if let Some((id, first, last, _)) = active {
|
||||
@@ -404,7 +419,7 @@ impl Tool for PenTool {
|
||||
}
|
||||
}
|
||||
if alt_converts(cx, p, m) {
|
||||
return Cursor::PenConvert;
|
||||
return alt_cursor(cx, p);
|
||||
}
|
||||
// These show whenever no path is being drawn, also after another path was selected while
|
||||
// the Pen was drawing.
|
||||
@@ -1212,8 +1227,9 @@ mod tests {
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let t = PenTool::default();
|
||||
let cx0 = cx(&d, &s, &p);
|
||||
assert_eq!(t.cursor(&cx0, Point::new(250.0, 101.0), alt), Cursor::PenConvert);
|
||||
assert_eq!(t.cursor(&cx0, Point::new(200.0, 101.0), alt), Cursor::PenConvert);
|
||||
// Alt lends the Anchor Point tool, and its caret with it.
|
||||
assert_eq!(t.cursor(&cx0, Point::new(250.0, 101.0), alt), Cursor::AnchorPoint);
|
||||
assert_eq!(t.cursor(&cx0, Point::new(200.0, 101.0), alt), Cursor::AnchorPoint);
|
||||
assert_eq!(t.cursor(&cx0, Point::new(250.0, 101.0), Mods::default()), Cursor::Pen);
|
||||
assert_eq!(t.cursor(&cx0, Point::new(150.0, 300.0), alt), Cursor::Pen);
|
||||
// A drag of the handle moves it alone, as one gesture.
|
||||
@@ -1249,10 +1265,11 @@ mod tests {
|
||||
let cx = cx(&d, &s, &p);
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), alt), Cursor::PenConvert);
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), alt), Cursor::ReshapeSegment);
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), Mods::default()), Cursor::PenDrawing);
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 302.0).with_mods(alt)), vec![]);
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 262.0).with_mods(alt));
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 262.0), alt), Cursor::ReshapeSegment, "kept during the reshape");
|
||||
assert_eq!(a[0], Action::Begin("Reshape".into()));
|
||||
assert!(
|
||||
matches!(&a[1], Action::Preview(c, v) if c == "path.reshapeSegment" && v["id"] == id.0 && v["segment"] == 0 && v["dy"] == -40.0),
|
||||
|
||||
@@ -87,7 +87,7 @@ impl Tool for PrintTilingTool {
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
Cursor::PrintTiling
|
||||
}
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
|
||||
@@ -82,6 +82,8 @@ pub struct SelectionTool {
|
||||
/// What a bounding-box handle being dragged snaps to.
|
||||
targets: Option<Targets>,
|
||||
moving: Option<MoveSnap>,
|
||||
/// The move snapped to a point or a guide at its last step: the arrow turns hollow.
|
||||
snapped: bool,
|
||||
guide: GuideEdit,
|
||||
/// Smart Guides: the centre or anchor under the pointer, and its label ([`Self::hover_point`]).
|
||||
hover: Option<(Point, &'static str)>,
|
||||
@@ -425,9 +427,12 @@ impl Tool for SelectionTool {
|
||||
self.moving = Some(MoveSnap::new(cx));
|
||||
}
|
||||
let mut d = move_delta(start, p, m.shift);
|
||||
let unsnapped = d;
|
||||
if let Some(snap) = &self.moving {
|
||||
(d, self.guides) = snap.snap(cx, start, d, m.alt);
|
||||
}
|
||||
// Shift has already constrained `d`: any change is a snap.
|
||||
self.snapped = (d - unsnapped).hypot() > 1e-9;
|
||||
self.state = State::Moving { start, began: true, deselect: None, key: None };
|
||||
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(d.x, d.y)));
|
||||
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt })));
|
||||
@@ -553,7 +558,10 @@ impl Tool for SelectionTool {
|
||||
match self.state {
|
||||
State::Rotating { center, .. } => return Cursor::rotate_about(center, p),
|
||||
State::Scaling { handle, bx, .. } => return handle_cursor(handle, bx.angle),
|
||||
State::Moving { began: true, .. } => return Cursor::Arrow,
|
||||
// Alt-dragging leaves a copy behind; otherwise the filled arrow, hollow while the move
|
||||
// snaps to a point or a guide.
|
||||
State::Moving { began: true, .. } if m.alt => return Cursor::SelectionCopy,
|
||||
State::Moving { began: true, .. } => return if self.snapped { Cursor::ArrowHollow } else { Cursor::Arrow },
|
||||
State::Corner(_) => return Cursor::CornerRadius,
|
||||
State::Pie(_) => return Cursor::ArrowHollow,
|
||||
State::Bracket(_) => return Cursor::PathBracket,
|
||||
@@ -868,6 +876,33 @@ mod tests {
|
||||
assert_eq!(dx(&t.pointer(&c, &ev(PointerKind::Drag, 253.0, 150.0))), 103.0);
|
||||
}
|
||||
|
||||
/// A move shows the filled arrow, hollow while it snaps (here B's left side onto A's right
|
||||
/// one); Alt, which leaves a copy behind, the double arrowhead.
|
||||
#[test]
|
||||
fn a_move_shows_the_filled_arrow_hollow_on_a_snap_and_alt_the_copy_cursor() {
|
||||
use vectorcraft_doc::{Appearance, Node};
|
||||
let (mut d, _) = doc_with_rect();
|
||||
d.artboards[0].rect = Rect::new(0.0, 0.0, 1000.0, 1000.0);
|
||||
let l = d.layers[0].id;
|
||||
let b = d.alloc_id();
|
||||
let sq = vectorcraft_geom::shapes::rectangle(Rect::new(300.0, 300.0, 400.0, 400.0));
|
||||
d.insert(Some(l), 1, Node::path(b, sq, Appearance::default_art())).unwrap();
|
||||
let mut s = Selection::default();
|
||||
s.add(b);
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let none = Mods::default();
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&c, &ev(PointerKind::Down, 350.0, 350.0));
|
||||
t.pointer(&c, &ev(PointerKind::Drag, 377.0, 371.0));
|
||||
assert_eq!(t.cursor(&c, Point::new(377.0, 371.0), none), Cursor::Arrow);
|
||||
t.pointer(&c, &ev(PointerKind::Drag, 253.0, 371.0));
|
||||
assert_eq!(t.cursor(&c, Point::new(253.0, 371.0), none), Cursor::ArrowHollow, "lined up with A");
|
||||
let alt = Mods { alt: true, ..none };
|
||||
t.pointer(&c, &ev(PointerKind::Drag, 377.0, 371.0).with_mods(alt));
|
||||
assert_eq!(t.cursor(&c, Point::new(377.0, 371.0), alt), Cursor::SelectionCopy);
|
||||
}
|
||||
|
||||
/// #740: with Snap to Grid on, a moved object lands on the grid (every 9 pt by default) by
|
||||
/// whichever of its edges or its centre is nearest a gridline, on each axis.
|
||||
#[test]
|
||||
|
||||
@@ -170,7 +170,16 @@ impl Tool for SymbolismTool {
|
||||
vec![Overlay::Path { path: circle, color: FEEDBACK, width: 1.0, dashed: false }]
|
||||
}
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
match self.id {
|
||||
"symbolShifter" => Cursor::SymbolShifter,
|
||||
"symbolScruncher" => Cursor::SymbolScruncher,
|
||||
"symbolSizer" => Cursor::SymbolSizer,
|
||||
"symbolSpinner" => Cursor::SymbolSpinner,
|
||||
"symbolStainer" => Cursor::SymbolStainer,
|
||||
"symbolScreener" => Cursor::SymbolScreener,
|
||||
"symbolStyler" => Cursor::SymbolStyler,
|
||||
_ => Cursor::SymbolSprayer,
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
json!({"diameter": self.diameter, "intensity": self.intensity, "density": self.density, "symbol": self.symbol, "style": self.style})
|
||||
|
||||
@@ -814,7 +814,7 @@ impl Tool for TypeTool {
|
||||
if self.mode != Mode::Type && Self::path_at(cx, p, self.mode == Mode::Area).is_some() {
|
||||
return Cursor::Crosshair;
|
||||
}
|
||||
Cursor::Text
|
||||
if self.vertical { Cursor::TypeVertical } else { Cursor::Text }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -315,7 +315,9 @@ impl Tool for FreeTransformTool {
|
||||
match self.drag.map(|d| d.op).or_else(|| self.classify(cx, r, p, m)) {
|
||||
Some(Op::Rotate) => Cursor::rotate_about(r.center(), p),
|
||||
Some(Op::Move) => Cursor::Move,
|
||||
Some(Op::Scale(h) | Op::Shear(h)) => match h {
|
||||
Some(Op::Shear(Handle::Top | Handle::Bottom)) => Cursor::ShearH,
|
||||
Some(Op::Shear(_)) => Cursor::ShearV,
|
||||
Some(Op::Scale(h)) => match h {
|
||||
Handle::Top | Handle::Bottom => Cursor::ResizeV,
|
||||
Handle::Left | Handle::Right => Cursor::ResizeH,
|
||||
Handle::TopLeft | Handle::BottomRight => Cursor::ResizeNwSe,
|
||||
@@ -401,6 +403,27 @@ mod tests {
|
||||
assert!((m * Point::new(100.0, 100.0)).distance(Point::new(120.0, 100.0)) < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_over_a_side_shows_the_shear_cursor_along_it() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = FreeTransformTool::default();
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
let (top, left) = (Point::new(150.0, 100.0), Point::new(100.0, 150.0));
|
||||
assert_eq!(t.cursor(&cx, top, cmd), Cursor::ShearH);
|
||||
assert_eq!(t.cursor(&cx, left, cmd), Cursor::ShearV);
|
||||
// Without Cmd the sides scale; corners never shear.
|
||||
assert_eq!(t.cursor(&cx, top, Mods::default()), Cursor::ResizeV);
|
||||
assert_eq!(t.cursor(&cx, Point::new(200.0, 200.0), cmd), Cursor::Crosshair);
|
||||
// The cursor stays while the shear is dragged.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 150.0, 100.0).with_mods(cmd));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 170.0, 100.0).with_mods(cmd));
|
||||
assert_eq!(t.cursor(&cx, Point::new(170.0, 100.0), cmd), Cursor::ShearH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifiers_pressed_during_a_handle_drag_change_what_it_does() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Tool for MeasureTool {
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
Cursor::Measure
|
||||
}
|
||||
|
||||
fn options(&self) -> Value {
|
||||
|
||||
@@ -41,7 +41,7 @@ impl Tool for MagicWandTool {
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
Cursor::MagicWand
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ impl Tool for LassoTool {
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
Cursor::Lasso
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user