Pencil: the cursor shows a star before a press that draws a new path

Draw freeform paths with the Pencil tool says a small star appears on
the Pencil when it is ready to draw a freeform path, and Reshape paths
with the Pencil tool says the star disappears over a selected path the
drag reshapes. The Pencil cursor had no star.

- New Cursor::PencilNew: the Pencil's crosshair with a small star where
  the closing cursor has its ring. It becomes the crosshair with Use
  Precise Cursors and as a system cursor.
- The Pencil shows it before a press when neither find_extend nor
  find_redraw (the lookups the press uses, within Edit Selected Paths)
  finds a selected path under the pointer. Over such a path and while a
  stroke is drawn, the cursor has no star.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-26 09:09:46 +07:00
committed by bflatastic
parent 0dc9d9abb4
commit c9fa005a54
7 changed files with 121 additions and 7 deletions
+1 -1
View File
@@ -417,7 +417,7 @@ fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
("paintbrush", Cursor::Paintbrush),
("blobBrush", Cursor::BlobBrush),
("shaper", Cursor::Shaper),
("pencil", Cursor::Pencil),
("pencil", Cursor::PencilNew),
("smooth", Cursor::Smooth),
("pathEraser", Cursor::PathEraser),
("join", Cursor::Join),
+7
View File
@@ -533,6 +533,13 @@ 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; Shift held at the press, or Alt held at the press when Alt key toggles to Smooth Tool is off, draws a straight line; Alt pressed after the drag starts, when that option is off, closes the path; when it is on, Alt held at the press gives the Smooth tool."]
hotspot = "The lead tip."
[[cursor]]
id = "PencilNew"
group = "Drawing and editing tools"
shown_when = ["The Pencil tool before a press that draws a new freehand path: the pointer is not within Edit Selected Paths of a selected path that the press would continue from an end or redraw. Over such a path, and while a stroke is drawn, the Pencil cursor has no star."]
hotspot = "The lead tip."
look = "The Pencil cursor with a small star badge, where PencilClose has its ring."
[[cursor]]
id = "PencilClose"
group = "Drawing and editing tools"
+60 -4
View File
@@ -574,6 +574,9 @@ impl Tool for GestureTool {
"smooth" => Cursor::Smooth,
"pathEraser" => Cursor::PathEraser,
"join" => Cursor::Join,
// Before a press that draws a new path the Pencil shows its star; over a selected path
// that the press continues from an end or redraws (`PointerKind::Down`), it does not.
_ if !self.active && self.find_extend(cx, p).is_none() && self.find_redraw(cx, p).is_none() => Cursor::PencilNew,
_ => Cursor::Pencil,
}
}
@@ -784,7 +787,7 @@ mod tests {
[("pencil", Cursor::Pencil, Cursor::PencilClose, "pencil"), ("paintbrush", Cursor::Paintbrush, Cursor::PaintbrushClose, "brush")]
{
let mut t = GestureTool::new(id);
assert_eq!(t.cursor(&cx, at, Mods::default()), own, "{id}");
assert_eq!(t.cursor(&cx, at, Mods::default()), if id == "pencil" { Cursor::PencilNew } else { own }, "{id}");
assert_eq!(t.cursor(&cx, at, alt), if id == "pencil" { Cursor::PencilStraight } else { closing }, "{id}");
for alt_at_release in [true, false] {
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0));
@@ -982,7 +985,7 @@ mod tests {
}
}
let mut t = GestureTool::new("pencil");
assert_eq!(t.cursor(&cx, at, Mods::default()), Cursor::Pencil);
assert_eq!(t.cursor(&cx, at, Mods::default()), Cursor::PencilNew);
assert_eq!(t.cursor(&cx, at, shift), Cursor::PencilStraight);
assert_eq!(t.cursor(&cx, at, alt), Cursor::PencilStraight, "Alt alone draws a line at any angle");
assert_eq!(t.cursor(&cx, at, shift_alt), Cursor::PencilStraight, "Alt at the release leaves a straight line open");
@@ -1000,6 +1003,59 @@ mod tests {
assert_eq!(Cursor::PencilStraight.precise(), Cursor::Crosshair, "Use Precise Cursors");
}
/// Before a press, the Pencil shows its new-path cursor (the star) exactly when the press draws
/// a new path: over an end of a selected open path, which the stroke continues, and over a
/// selected path, which it redraws, it does not. With nothing selected, with Edit Selected Paths
/// off and away from the paths, it does; while a stroke is drawn, it does not.
#[test]
fn pencil_shows_the_star_before_a_press_that_draws_a_new_path() {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
let line = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(vec![
vectorcraft_geom::PathEl::MoveTo(Point::new(300.0, 300.0)),
vectorcraft_geom::PathEl::LineTo(Point::new(400.0, 300.0)),
]));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, line, vectorcraft_doc::Appearance::default_art())).unwrap();
let mut s = Selection::default();
s.set([id]);
let p = paint();
let mut cx = cx(&d, &s, &p);
cx.smart_guides = false;
// The stroke edits the selected path when its previews continue or redraw it.
let edits = |acts: &[Vec<Action>]| {
acts.iter().flatten().any(|a| matches!(a, Action::Preview(_, v) if v.get("extend").is_some() || v.get("redraw").is_some()))
};
let (end, middle, away) = (Point::new(405.0, 303.0), Point::new(350.0, 304.0), Point::new(350.0, 360.0));
for edit_within in [12.0, 0.0] {
for at in [end, Point::new(296.0, 302.0), middle, away, Point::new(200.0, 200.0)] {
let mut t = GestureTool::new("pencil");
t.set_option("editWithin", &json!(edit_within));
let shown = t.cursor(&cx, at, Mods::default()) == Cursor::PencilNew;
let acts = drag(&mut t, &cx, &[(at.x, at.y), (360.0, 330.0), (420.0, 350.0)], Mods::default());
assert_eq!(shown, !edits(&acts), "editWithin {edit_within} at {at:?}");
}
}
let mut t = GestureTool::new("pencil");
assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::Pencil, "over an end the stroke continues");
assert_eq!(t.cursor(&cx, middle, Mods::default()), Cursor::Pencil, "over the path the stroke redraws");
assert_eq!(t.cursor(&cx, away, Mods::default()), Cursor::PencilNew, "away from the path");
// While a stroke is drawn, the Pencil's own cursor.
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, away.x, away.y));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 380.0, 380.0));
assert_eq!(t.cursor(&cx, Point::new(380.0, 380.0), Mods::default()), Cursor::Pencil, "while a stroke is drawn");
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 380.0, 380.0));
assert_eq!(t.cursor(&cx, away, Mods::default()), Cursor::PencilNew, "after the release");
// With nothing selected, every press draws a new path.
let none = Selection::default();
let cx_none = crate::testutil::cx(&d, &none, &p);
assert_eq!(GestureTool::new("pencil").cursor(&cx_none, middle, Mods::default()), Cursor::PencilNew, "nothing selected");
// Shift held: a straight line, with its own cursor; the Paintbrush keeps its cursor.
let shift = Mods { shift: true, ..Default::default() };
assert_eq!(GestureTool::new("pencil").cursor(&cx, away, shift), Cursor::PencilStraight);
assert_eq!(GestureTool::new("paintbrush").cursor(&cx, away, Mods::default()), Cursor::Paintbrush);
}
/// #3799: a Shift line started on an end of a selected open path continues the path from that
/// end, constrained from the end itself.
#[test]
@@ -1337,7 +1393,7 @@ mod tests {
let (acts, after) = at(PointerKind::Up, 318.0, 335.0);
let Some(Action::Preview(_, v)) = acts.first() else { panic!("{tool}: {acts:?}") };
assert_eq!(v["closed"], json!(within > 0.0), "{tool}: the release does what the cursor showed");
assert_eq!(after, own, "{tool} after the release");
assert_eq!(after, if tool == "pencil" { Cursor::PencilNew } else { own }, "{tool} after the release");
}
}
// A straight Pencil line (Shift or Alt held at the press) stays open, so it shows no ring
@@ -1468,7 +1524,7 @@ mod tests {
// When alt is pressed, cursor shows smooth tool.
let alt_mod = Mods { alt: true, ..Default::default() };
assert_eq!(t.cursor(&cx, Point::new(50.0, 50.0), alt_mod), Cursor::Smooth);
assert_eq!(t.cursor(&cx, Point::new(50.0, 50.0), Mods::default()), Cursor::Pencil);
assert_eq!(t.cursor(&cx, Point::new(50.0, 50.0), Mods::default()), Cursor::PencilNew);
// Alt-drag over selected path smooths it.
let acts = drag_held(&mut t, &cx, &[(50.0, 150.0), (150.0, 150.0), (250.0, 150.0)], alt_mod);
+11 -1
View File
@@ -578,6 +578,9 @@ pub enum Cursor {
MagicWand,
/// The Pencil tool.
Pencil,
/// The Pencil tool before a press that draws a new freehand path: the pointer is not over a
/// selected path that the press would continue or redraw. A crosshair with a small star.
PencilNew,
/// The Pencil tool with Alt pressed after the drag starts and Alt key toggles to Smooth Tool off:
/// releasing the mouse button with Alt still held closes the path. A drag started with Shift or
/// Alt held draws an open straight line instead. Also while a freehand stroke is drawn back near
@@ -706,6 +709,7 @@ impl Cursor {
| Cursor::Lasso
| Cursor::MagicWand
| Cursor::Pencil
| Cursor::PencilNew
| Cursor::PencilClose
| Cursor::PencilStraight
| Cursor::PencilContinue
@@ -1198,7 +1202,6 @@ mod cursor_tests {
for (id, c) in [
("lasso", Cursor::Lasso),
("magicWand", Cursor::MagicWand),
("pencil", Cursor::Pencil),
("paintbrush", Cursor::Paintbrush),
("blobBrush", Cursor::BlobBrush),
("eraser", Cursor::Eraser),
@@ -1224,6 +1227,13 @@ mod cursor_tests {
assert_eq!(c.precise(), Cursor::Crosshair, "{id}");
assert_eq!(c.stand_in(), Some(if id == "mesh" { Cursor::PenAdd } else { Cursor::Crosshair }), "{id}");
}
// Away from a selected path the Pencil shows its new-path cursor, drawn with a star; its
// own cursor, for drawing and for a selected path under the pointer, borrows the crosshair.
assert_eq!(cursor_at("pencil", &d, away), Cursor::PencilNew);
assert_eq!(Cursor::PencilNew.precise(), Cursor::Crosshair);
assert_eq!(Cursor::PencilNew.stand_in(), None);
assert_eq!(Cursor::Pencil.precise(), Cursor::Crosshair);
assert_eq!(Cursor::Pencil.stand_in(), Some(Cursor::Crosshair));
}
#[test]
+2
View File
@@ -907,6 +907,7 @@ fn cursor_icon(c: Cursor) -> egui::CursorIcon {
Cursor::SliceSelect => C::Default,
Cursor::Width | Cursor::WidthAdd => C::Crosshair,
Cursor::WidthPoint => C::Move,
Cursor::PencilNew => C::Crosshair,
Cursor::Blend | Cursor::BlendObject | Cursor::BlendAnchor | Cursor::ShapeBuilder | Cursor::ShapeBuilderErase => C::Crosshair,
Cursor::PencilClose | Cursor::PencilStraight | Cursor::PencilContinue | Cursor::PaintbrushClose => C::Crosshair,
Cursor::TypeVertical => C::VerticalText,
@@ -3919,6 +3920,7 @@ mod tests {
Cursor::BlendAnchor,
Cursor::ShapeBuilder,
Cursor::ShapeBuilderErase,
Cursor::PencilNew,
Cursor::PencilStraight,
Cursor::PencilContinue,
] {
+39 -1
View File
@@ -428,7 +428,7 @@ fn magnifier(p: &mut Ink, o: Pos2, out: bool) {
/// Every cursor with a glyph: its own, drawn here, or the one it borrows.
#[cfg(test)]
const GLYPHS: [Cursor; 80] = [
const GLYPHS: [Cursor; 81] = [
Cursor::Hand,
Cursor::HandGrab,
Cursor::ZoomIn,
@@ -482,6 +482,7 @@ const GLYPHS: [Cursor; 80] = [
Cursor::Lasso,
Cursor::MagicWand,
Cursor::Pencil,
Cursor::PencilNew,
Cursor::PencilClose,
Cursor::PencilStraight,
Cursor::PencilContinue,
@@ -533,6 +534,12 @@ fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
double_arrow(ink, p + vec2(15.0, 18.0), vec2(1.0, 0.0));
}
Cursor::Crosshair => crosshair(ink, p),
// The Pencil's crosshair with the Pen's star where the closing ring goes.
Cursor::PencilNew => {
crosshair(ink, p);
let b = p + vec2(5.0, 5.0);
strokes(ink, &[vec![b, b + vec2(6.0, 6.0)], vec![b + vec2(6.0, 0.0), b + vec2(0.0, 6.0)], vec![b + vec2(3.0, -1.0), b + vec2(3.0, 7.0)]]);
}
// The crosshair the Pencil and the Paintbrush borrow, with the PenClose ring below right.
Cursor::PencilClose | Cursor::PaintbrushClose => {
crosshair(ink, p);
@@ -952,6 +959,37 @@ mod tests {
}
}
/// The Pencil's new-path cursor is the crosshair with a small star below right, where the
/// closing cursor's ring is hollow and the straight-line cursor has a single line.
#[test]
fn the_pencil_new_path_cursor_is_the_crosshair_with_a_star() {
for ppp in [1.0, 2.0] {
let cross = rasterize(Cursor::Crosshair, ppp).unwrap();
let img = rasterize(Cursor::PencilNew, ppp).unwrap();
let ring = rasterize(Cursor::PencilClose, ppp).unwrap();
let straight = rasterize(Cursor::PencilStraight, ppp).unwrap();
assert_eq!(img.hotspot, cross.hotspot, "at {ppp}");
let at = |img: &CustomCursorImage, dx: f32, dy: f32| {
let [hx, hy] = img.hotspot.map(usize::from);
let (x, y) = ((hx as f32 + dx * ppp) as usize, (hy as f32 + dy * ppp) as usize);
if x < img.size[0] as usize && y < img.size[1] as usize { pixel(img, x, y) } else { [0; 4] }
};
let inked = |p: [u8; 4]| p[3] > 200 && p[0] < 64;
assert!(inked(at(&img, 8.5, 8.5)), "the star's middle at {ppp}");
assert_eq!(at(&ring, 8.5, 8.5)[3], 0, "the closing ring is hollow there at {ppp}");
assert_eq!(at(&cross, 8.5, 8.5)[3], 0, "the crosshair has no badge at {ppp}");
// Three strokes cross at the middle: the diagonal the straight-line cursor lacks, and the
// upright one.
assert!(inked(at(&img, 5.5, 5.5)) && !inked(at(&straight, 5.5, 5.5)), "the second diagonal at {ppp}");
// At 1 point per pixel the upright stroke covers two pixel columns in gray.
let dark = |p: [u8; 4]| p[3] > 200 && p[0] < 128;
let upright = |img: &CustomCursorImage, dy: f32| [7.5, 8.0, 8.5].iter().any(|&dx| dark(at(img, dx, dy)));
assert!(upright(&img, 4.5) && upright(&img, 11.5), "the upright stroke at {ppp}");
assert!(!upright(&straight, 4.5) && !upright(&straight, 11.5), "the straight line has none at {ppp}");
assert_eq!(at(&img, -6.0, 0.0), at(&cross, -6.0, 0.0), "the arms are the crosshair's at {ppp}");
}
}
/// The Pencil's straight-line cursor is the crosshair with a short straight line below right,
/// through the point where the closing cursor's ring is hollow.
#[test]
+1
View File
@@ -678,6 +678,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Double-clicking the Line Segment, Arc, Rectangular Grid or Polar Grid tool opens its Tool Options, as step 3 of the [Rectangular Grid tool](https://helpx.adobe.com/illustrator/using/tool-techniques/rectangular-grid-tool.html) and [Arc tool](https://helpx.adobe.com/illustrator/using/tool-techniques/arc-tool.html) pages and the [Tools](https://helpx.adobe.com/illustrator/using/tools.html) page describe (#2691): the dialog a click with the tool opens, without a place to draw. Line Segment Tool Options has Length, Angle and Fill Line (a new line takes the current fill, `shape.line` `fill`); Arc Segment Tool Options its lengths, Reference Point, Type, Base Along, Slope and Fill Arc; the grids' Tool Options the Reference Point, Default Size, dividers, skews and switches. OK draws nothing: the next drag draws with the options and a click's dialog opens at them, the Length, Angle and Default Size included; Cancel changes nothing. Tests: `toolbar::tests::double_clicking_the_line_arc_and_grid_tools_opens_their_tool_options`, `line_segment_options_and_fill_line`, `grid_default_size_is_an_option_the_drag_leaves_out`, `fill_line_gives_a_new_line_the_current_fill`.
Live curve fitting in the Pencil Tool Options is on by default; while it is off, the path shown while a stroke is drawn joins the stroke's points with straight segments, and the release fits the curve, as [Pencil Tool Options](https://web.archive.org/web/20261002104400/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/pencil-tool-options.html) describes (`path.freehand` `fit`).
In the Pencil and Paintbrush Tool Options, Edit selected paths within 0 px turns editing off, so a stroke that starts exactly on an end of a selected open path draws a new path; Close paths when ends are within 0 px turns closing off, so a stroke that ends exactly where it started stays open.
Before a press that draws a new path, the Pencil cursor shows a small star, as [Draw freeform paths with the Pencil tool](https://web.archive.org/web/20260926095638/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-freeform-paths-with-the-pencil-tool.html) describes; over a selected path within Edit selected paths' distance, which the press continues from an end or redraws, the star is not shown ([Reshape paths with the Pencil tool](https://web.archive.org/web/20260106004407/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/reshape-paths-with-the-pencil-tool.html)).
Keep selected in the Pencil and Paintbrush Tool Options is on by default; while it is off, the path a stroke draws or continues is not selected afterwards and the rest of the selection stays as it was (`path.freehand` `keepSelected`).
Double-clicking the Shape Builder tool opens Shape Builder Tool Options ([Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html)): with Consider Open Filled Paths as Closed off, a filled open path cuts regions as an unfilled one does; Pick Color From Color Swatches fills merged shapes with the current fill, the Left and Right arrow keys step it through the document's color swatches, and Cursor Swatch Preview shows the current fill and the swatches before and after it above the pointer; Selection Straight Line merges what the line from a drag's first point to its last crosses ([Setting Illustrator's Shape Builder Options](https://web.archive.org/web/20260615204453/https://creativepro.com/setting-illustrators-shape-builder-options/)); with Highlight Fill off, merge mode shades no regions (`shapeBuilder.merge` `openFilledAsClosed` and `pickColorFrom`).
With In Merge Mode, Clicking Stroke Splits the Path on in the same dialog, a click on a stroke in merge mode splits its path into the run of stroke under the pointer and the rest of the path, as the [Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html) page describes; the run goes from the anchor or crossing point before the pointer to the one after it, and the pointer shows scissors over it. Highlight Stroke When Editable draws that run in the color chosen in its Color list (Red by default), and with Pick Color From Color Swatches the run takes the current fill as its stroke (`shapeBuilder.merge` `splitStroke`).