From 1b9881ce1e9bbbb2a971950164592e39cc074e40 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 20:39:00 +0000 Subject: [PATCH] refactor(tools): move lib.rs items to {context,cursor,input}.rs --- crates/tools/src/context.rs | 284 +++++++++++++++ crates/tools/src/cursor.rs | 305 ++++++++++++++++ crates/tools/src/input.rs | 112 ++++++ crates/tools/src/lib.rs | 702 +----------------------------------- 4 files changed, 707 insertions(+), 696 deletions(-) create mode 100644 crates/tools/src/context.rs create mode 100644 crates/tools/src/cursor.rs create mode 100644 crates/tools/src/input.rs diff --git a/crates/tools/src/context.rs b/crates/tools/src/context.rs new file mode 100644 index 000000000..298714b50 --- /dev/null +++ b/crates/tools/src/context.rs @@ -0,0 +1,284 @@ +use super::*; + +/// Paint defaults for new art (the fill/stroke proxy) and the rest of the new-art template. +#[derive(Clone, Debug, PartialEq)] +pub struct PaintDefaults { + pub fill: Paint, + pub stroke: Paint, + pub stroke_width: f64, + /// The appearance new art takes (stroke options, more fills and strokes, effects), its top + /// fill and stroke repainted with `fill`, `stroke` and `stroke_width`; `None`: one plain fill + /// and stroke. Its placed gradients are relative to the unit box. + pub appearance: Option, + /// New art's opacity and blend mode. + pub opacity: f32, + pub blend: vectorcraft_color::BlendMode, + /// The graphic style the template came from (by name): new art is linked to it. + pub style: Option, + /// The template is the last selection's appearance (New Art Has Basic Appearance off): with + /// the option on again, new art takes only its basic fill and stroke. + pub inherited: bool, +} + +impl Default for PaintDefaults { + /// White fill, 1 pt black stroke, nothing else. + fn default() -> Self { + Self { + fill: Paint::solid(vectorcraft_color::Color::WHITE), + stroke: Paint::solid(vectorcraft_color::Color::BLACK), + stroke_width: 1.0, + appearance: None, + opacity: 1.0, + blend: Default::default(), + style: None, + inherited: false, + } + } +} + +/// The document window on screen, in document coordinates: widgets that stay put on screen (the +/// Plane Switching Widget) are placed with it. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ScreenFrame { + /// The window's top-left corner. + pub origin: Point, + /// One screen pixel to the right and one down. + pub right: Vec2, + pub down: Vec2, + /// The window's size in screen pixels. + pub size: (f64, f64), +} + +impl ScreenFrame { + /// The document point `x`, `y` screen pixels from the window's top-left corner. + pub fn at(&self, x: f64, y: f64) -> Point { + self.origin + self.right * x + self.down * y + } + /// Where the document point `p` is, in screen pixels from the window's top-left corner (the + /// inverse of [`Self::at`]); none for a degenerate frame. + pub fn to_screen(&self, p: Point) -> Option<(f64, f64)> { + let (r, d, v) = (self.right, self.down, p - self.origin); + let det = r.x * d.y - d.x * r.y; + (det.is_finite() && det.abs() > 1e-12).then(|| ((v.x * d.y - d.x * v.y) / det, (r.x * v.y - v.x * r.y) / det)) + } +} + +/// Read-only context a tool sees. +pub struct ToolContext<'a> { + pub doc: &'a Document, + /// The rulers' zero point with the view's active artboard (see + /// [`Document::ruler_origin_for`]): where ruler ticks and measured positions count from. + pub ruler_origin: Point, + /// Which open document `doc` is (its process-unique id) and its revision, which moves with + /// every change of the document or the selection: what a tool keeps between pointer events + /// for the document as it is (the snap targets of hovering) is keyed on it. + pub revision: (u64, u64), + pub selection: &'a Selection, + /// Screen pixels per document point. + pub zoom: f64, + pub isolation: Option, + pub paint: &'a PaintDefaults, + pub outline: bool, + pub smart_guides: bool, + /// View → Show Guides with Lock Guides off: the selection tools pick and drag ruler guides. + pub guides: bool, + pub snap_to_grid: bool, + /// Bounding box shown (View → Show/Hide Bounding Box). + pub show_bbox: bool, + /// View → Snap to Pixel. + pub snap_to_pixel: bool, + /// View → Snap to Point: with Smart Guides off, dragged selections, drawn points and picked + /// points (a transform's reference point) land on anchors and ruler guides within + /// [`Self::snap_tolerance`]. + pub snap_to_point: bool, + /// View → Snapping → Snap to Tangent: with Smart Guides on, a line drawn + /// from an anchor lands where it touches a nearby curve. + pub snap_tangent: bool, + /// View → Snapping → Snap to Perpendicular: likewise where it meets a nearby segment square. + pub snap_perpendicular: bool, + /// View → Snap to Glyph: drawn points snap to live-text baselines and glyph bounds (with Smart Guides). + pub snap_to_glyph: bool, + /// View → Show Corner Widget: paths show draggable Live Corners widgets in their corners. + pub corner_widgets: bool, + /// Which paint proxy is in front (true = Fill): the one the Gradient tool edits. + pub fill_active: bool, + /// The selected gradient stop (`gradient.selectStop`), marked on the gradient annotator. + pub gradient_stop: Option, + /// The Appearance panel's active fill/stroke (paint-order index into the first selected + /// object's stack); paint tools show and edit that item. + pub appearance_item: Option, + /// General → Constrain Angle (degrees): Shift constrains drags to 45° steps from it. + pub constrain_angle: f64, + /// General → Keyboard Increment: arrow-key nudges in document units (Shift multiplies by 10). + pub keyboard_increment: f64, + /// The selected freeform gradient point (`paint.freeform.selectPoint`), marked on the + /// freeform annotator. + pub freeform_point: Option, + /// Eyedropper Options' raster sample size: the pixels square averaged when the Eyedropper + /// samples an image (1, 3 or 5). + pub raster_sample: u32, + /// General → Use Preview Bounds: the bounding box measures visual bounds (strokes included). + pub preview_bounds: bool, + /// The session's memo of the selection tools' measurements for the document as it is + /// ([`select::PreviewMemo`]); without one they measure on every call. + pub preview_memo: Option<&'a select::PreviewMemo>, + /// Units ▸ General: the unit measurement labels show lengths in. + pub unit: Unit, + /// Units ▸ Stroke: the unit measurement labels show stroke widths in. + pub stroke_unit: Unit, + /// Clipboard Handling → When pasting text: Keep Plain Text. Text pasted while typing takes + /// the style at the caret, even the text the Type tool copied with its formatting. + pub paste_plain_text: bool, + /// View → Hide Slices: the Slice Selection tool can't pick hidden slices. + pub slices_hidden: bool, + /// View → Lock Slices: the Slice Selection tool leaves locked slices alone. + pub slices_locked: bool, + /// General → Disable Auto Add/Delete is off: the Pen adds an anchor on a selected path's + /// segment and deletes one of its anchors. + pub auto_add_delete: bool, + /// Selection & Anchor Display → Tolerance: how near (screen pixels) a click must be to a path + /// or an anchor to pick it. + pub selection_tolerance: f64, + /// Selection & Anchor Display → Size (1–7, default 3): how big anchors and handles are drawn. + pub anchor_size: u32, + /// Selection & Anchor Display → Object Selection by Path Only: a click inside a filled path + /// doesn't pick it, only one on its path does. + pub path_only: bool, + /// Type → Type Object Selection by Path Only: type is picked on its type path only (point + /// type's baseline, area type's frame, type on a path's path), not anywhere in its bounds. + pub type_path_only: bool, + /// General → Double Click To Isolate: a double-click on a group with the Selection tool + /// isolates it. + pub double_click_isolate: bool, + /// Selection & Anchor Display → Command Click to Select Objects Behind: Cmd/Ctrl-click with the + /// Selection tool selects the object under the selected one, the next click the one under + /// that. + pub select_behind: bool, + /// Selection & Anchor Display → Highlight anchors on mouse over: Direct Selection marks the + /// anchor under the pointer. + pub highlight_anchors: bool, + /// Selection & Anchor Display → Snap to Point (screen pixels): how near an anchor or a ruler + /// guide pulls the pointer while View → Snap to Point is on. + pub snap_tolerance: f64, + /// Selection & Anchor Display → Show handles when multiple anchors are selected: off, Direct + /// Selection shows and drags handles only while a single anchor is selected. + pub handles_multiple: bool, + /// Selection & Anchor Display → Hide Corner Widget for angles greater than (degrees): corners + /// wider than this show no Live Corners widget. + pub corner_widget_max_angle: f64, + /// Selection & Anchor Display → Move Locked and Hidden Artwork with Artboard. + pub move_locked_with_artboard: bool, + /// Selection & Anchor Display → Enable Rubber Band for Pen Tool: the Pen previews the next + /// segment to the pointer. + pub pen_rubber_band: bool, + /// Selection & Anchor Display → Enable Rubber Band for Curvature Tool. + pub curvature_rubber_band: bool, + /// Type → Fill New Type Objects With Placeholder Text: type the Type tools place starts with + /// placeholder text, selected. + pub placeholder_text: bool, + /// Smart Guides → Color: the smart guides' lines and labels (RGB). + pub smart_guide_color: [u8; 3], + /// Smart Guides → Alignment Guides: the lines along the edges and centres the art lines up + /// with show. Off, the art still snaps into line. + pub alignment_guides: bool, + /// Smart Guides → Anchor/Path Labels: the "anchor", "center", "path"… labels show. + pub anchor_path_labels: bool, + /// Smart Guides → Measurement Labels: the size and offset readouts while drawing and moving. + pub measurement_labels: bool, + /// Smart Guides → Transform Tools: the readouts while scaling, rotating and shearing. + pub transform_tools_guides: bool, + /// Smart Guides → Spacing Guides: a moved selection snaps to spacing equal to the gap between + /// two other objects in its row or column, or evenly between its two neighbours, and the + /// equal gaps show. + pub spacing_guides: bool, + /// Smart Guides → Invoke Distance Guides: with the Selection tool, Alt/Option held over another + /// object shows how far the selection is from it, and over an empty part of an artboard how + /// far it is from the artboard's edges ([`guides::distance_guides`]). + pub distance_guides: bool, + /// Smart Guides → Snapping Tolerance (screen pixels): how near a smart guide target pulls the + /// pointer, a dragged edge or a drawn point. + pub snapping_tolerance: f64, + /// Smart Guides → Construction Guides: the angles (degrees, counter-clockwise from the x axis + /// as seen on the page) of the lines through the point a drawn point leaves from, onto which a + /// point near one lands ([`guides::construction_angles`]); none with the option off. + pub construction_angles: &'static [f64], + /// The document window (none headless): screen-fixed widgets sit in it. + pub screen: Option, + /// Where the Plane Switching Widget sits (Perspective Grid Options); None while it's hidden. + pub plane_widget: Option, +} + +impl ToolContext<'_> { + /// Tolerance in document units for `px` screen pixels. + pub fn tol(&self, px: f64) -> f64 { + px / self.zoom.max(1e-9) + } + /// How far the view is turned (Rotate View), in degrees clockwise on screen; 0 without a + /// window. + pub fn view_angle(&self) -> f64 { + // `right` is one screen pixel to the right, in document units. + self.screen.map(|s| (-s.right.y).atan2(s.right.x).to_degrees()).filter(|a| a.is_finite()).unwrap_or(0.0) + } + /// How near (document units) a smart guide target pulls: Smart Guides → Snapping Tolerance. + pub fn snap_tol(&self) -> f64 { + self.tol(self.snapping_tolerance) + } + /// A length as measurement labels show it, in the General unit (`12.50 mm`). + pub fn len(&self, v: f64) -> String { + self.unit.readout(v) + } + /// A size label: `W: …` over `H: …`. + pub fn size_label(&self, w: f64, h: f64) -> String { + format!("W: {}\nH: {}", self.len(w), self.len(h)) + } + /// Star tool drag: outer (R1) and inner (R2) radii from the centre to the pointer. + pub fn star_radii_label(&self, outer: f64, inner: f64) -> String { + format!("R1: {}\nR2: {}", self.len(outer), self.len(inner)) + } + /// A move label: `dX: …` over `dY: …`. + pub fn offset_label(&self, dx: f64, dy: f64) -> String { + format!("dX: {}\ndY: {}", self.len(dx), self.len(dy)) + } + /// A segment label while drawing: distance, angle and offsets between two points. + pub fn segment_measure_label(&self, a: Point, b: Point) -> String { + let (dist, ang, dx, dy) = xform::measure(a, b); + format!("D: {}\nAngle: {ang:.1}°\n{}", self.len(dist), self.offset_label(dx, dy)) + } + /// What Snap to Grid snaps to: the grid's subdivisions (or its lines without any). + pub fn grid_step(&self) -> f64 { + self.doc.grid.spacing / self.doc.grid.subdivisions.max(1) as f64 + } + /// The selection tolerance in document units ([`Self::selection_tolerance`]). + pub fn pick_tol(&self) -> f64 { + self.tol(self.selection_tolerance) + } + /// How near (document units) a press picks an anchor or a direction handle's end: the + /// selection tolerance, but at least 2 px past the drawn point, so a point is easy to catch + /// and wins over the segments through it, which are tested after it (#593). + pub fn point_tol(&self) -> f64 { + // Anchors are drawn 5 px wide at the default Size 3, a pixel more or less per step. + let half = (5.0 + f64::from(self.anchor_size.clamp(1, 7)) - 3.0) / 2.0; + self.tol(self.selection_tolerance.max(half + 2.0)) + } + pub fn hit_options(&self) -> vectorcraft_doc::hit::HitOptions { + vectorcraft_doc::hit::HitOptions { + tol: self.pick_tol(), + outline: self.outline, + path_only: self.path_only, + type_path_only: self.type_path_only, + scope: self.isolation, + skip_guides: !self.guides, + clip_outlines: false, + } + } + + /// [`Self::hit_options`] for the selection tools (Selection, Direct Selection, Group + /// Selection): a clipping path is also hit on its outline ([`HitOptions::clip_outlines`]). + /// Only for what a press or a hover selects: a double-click, loaded text or a guide's release + /// acts on what is drawn and takes [`Self::hit_options`]. + /// + /// [`HitOptions::clip_outlines`]: vectorcraft_doc::hit::HitOptions::clip_outlines + pub fn selection_hit_options(&self) -> vectorcraft_doc::hit::HitOptions { + vectorcraft_doc::hit::HitOptions { clip_outlines: true, ..self.hit_options() } + } +} diff --git a/crates/tools/src/cursor.rs b/crates/tools/src/cursor.rs new file mode 100644 index 000000000..7661f3d72 --- /dev/null +++ b/crates/tools/src/cursor.rs @@ -0,0 +1,305 @@ +use super::*; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +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 + /// the corner it is at, so at the top right it opens toward the bottom left. Make it with + /// [`Cursor::rotate_about`]. + RotateToward(u8), + /// Over a Live Corners widget (drag to round the corners). + CornerRadius, + /// The Pen with the new-path asterisk: a click starts a new path. + Pen, + /// The Pen without a badge, while a path is being drawn: a click adds its next anchor. + PenDrawing, + PenAdd, + 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. + PenConvert, + Text, + Hand, + HandGrab, + ZoomIn, + ZoomOut, + Eyedropper, + NotAllowed, + /// A long operation (opening a file, a background save or export): the OS wait cursor. + Busy, + /// Over the gradient annotator's bar: a click adds a stop. + AddStop, + /// A gradient stop dragged off the bar: releasing deletes it. + RemoveStop, + /// The Slice tool: a crosshair with a blade. + Slice, + /// The Slice Selection tool: the arrow with a slice badge. + SliceSelect, + /// The Width tool away from strokes. + Width, + /// The Width tool over a stroke: a drag adds a width point. + WidthAdd, + /// The Width tool over a width point or a handle end: a drag moves or widens it. + WidthPoint, + /// The Blend tool away from art: a crosshair with a hollow square. + Blend, + /// The Blend tool over an object it can blend: a crosshair with a filled square. + BlendObject, + /// The Blend tool over an anchor point (the blend starts there): a crosshair with a target. + BlendAnchor, + /// The Objects on Path tool over a path the selected objects can attach to (a click attaches + /// them). + ObjectsOnPath, + /// Over a bracket of selected type on a path (a drag moves it): the arrow with a bracket. + PathBracket, + /// Over the type widget of selected type (a double-click converts point type to area type and + /// back): the arrow with a type badge. + TypeWidget, + /// The Selection tool loaded with the text of area type or type on a path by a click on its out + /// or in port (a click threads it into a path or a new frame, a drag draws the new frame). + LoadedText, + /// The Shape Builder: a crosshair with a plus (merge mode)... + ShapeBuilder, + /// ...or, with Alt held, a minus (erase mode). + ShapeBuilderErase, + /// The Lasso tool. + Lasso, + /// The Magic Wand tool. + 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 + /// its start, where releasing the mouse button closes the path. A crosshair with a ring. + PencilClose, + /// The Pencil tool before a press that draws a straight line (Shift or Alt held). A crosshair with a + /// short straight line. + PencilStraight, + /// The Pencil tool before a press that draws a straight line (Shift or Alt held), over an end of + /// a selected open path: the line continues the path from that end. A crosshair with a short + /// straight line leaving a square end point. + PencilContinue, + /// The Paintbrush tool. + Paintbrush, + /// The Paintbrush tool with Alt held: releasing the mouse button with Alt still held closes + /// the path. Also while a stroke is drawn back near its start, where releasing the mouse button + /// closes the path. A crosshair with a ring. + PaintbrushClose, + /// 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 { + /// The rotate cursor for the pointer at `p` turning art about `center`: [`Cursor::RotateToward`] + /// the pointer's side of it, or the plain [`Cursor::Rotate`] on the centre itself. + pub fn rotate_about(center: Point, p: Point) -> Self { + let d = p - center; + if !(d.x.is_finite() && d.y.is_finite()) || d.hypot() < 1e-9 { + return Cursor::Rotate; + } + // y points down the page: atan2(x, -y) is 0 straight up and grows clockwise. + let octant = (d.x.atan2(-d.y) / std::f64::consts::FRAC_PI_4).round() as i64; + Cursor::RotateToward(octant.rem_euclid(8) as u8) + } + + /// [`Cursor::rotate_about`] in a view turned `view` degrees clockwise + /// ([`ToolContext::view_angle`]): the pointer's side of `center` as it shows on screen. The + /// pointer's direction turns with the view before it rounds to the nearest 45°. + pub fn rotate_about_in_view(center: Point, p: Point, view: f64) -> Self { + let (sn, cs) = if view.is_finite() { view.to_radians().sin_cos() } else { (0.0, 1.0) }; + let d = p - center; + // y points down the page, so this turns `d` clockwise on the page. + Cursor::rotate_about(center, center + Vec2::new(d.x * cs - d.y * sn, d.x * sn + d.y * cs)) + } + + /// The cursor as it shows in a view turned `deg` degrees clockwise (Rotate View): the resize + /// cursors and [`Cursor::RotateToward`] turn with the view to the nearest 45°, and every other + /// cursor stays as it is. + pub fn turned(self, deg: f64) -> Self { + const RESIZE: [Cursor; 4] = [Cursor::ResizeH, Cursor::ResizeNeSw, Cursor::ResizeV, Cursor::ResizeNwSe]; + let k = if deg.is_finite() { (deg / 45.0).round() as i64 } else { 0 }; + match self { + Cursor::RotateToward(o) => Cursor::RotateToward((i64::from(o) + k).rem_euclid(8) as u8), + c => match RESIZE.iter().position(|r| *r == c) { + Some(i) => RESIZE.get((i as i64 - k).rem_euclid(4) as usize).copied().unwrap_or(c), + None => c, + }, + } + } + + /// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the + /// 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 + | Cursor::PenDrawing + | Cursor::PenAdd + | Cursor::PenDelete + | Cursor::PenClose + | 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::Lasso + | Cursor::MagicWand + | Cursor::Pencil + | Cursor::PencilNew + | Cursor::PencilClose + | Cursor::PencilStraight + | Cursor::PencilContinue + | Cursor::Paintbrush + | Cursor::PaintbrushClose + | 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 { + 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::ObjectsOnPath + | 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, + } + } +} diff --git a/crates/tools/src/input.rs b/crates/tools/src/input.rs new file mode 100644 index 000000000..804939840 --- /dev/null +++ b/crates/tools/src/input.rs @@ -0,0 +1,112 @@ +use super::*; + +/// Modifier keys. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Mods { + #[serde(default)] + pub shift: bool, + /// Option on macOS. + #[serde(default)] + pub alt: bool, + /// Command on macOS, Ctrl elsewhere. + #[serde(default)] + pub cmd: bool, + #[serde(default)] + pub ctrl: bool, + #[serde(default)] + pub space: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PointerKind { + /// Button pressed. + Down, + /// Moved with the button held. + Drag, + /// Button released. + Up, + /// Moved with no button (hover). + Move, + DoubleClick, +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub struct PointerEvent { + pub kind: PointerKind, + /// Document coordinates. + pub pos: Point, + #[serde(default)] + pub mods: Mods, + #[serde(default = "one")] + pub pressure: f32, +} + +fn one() -> f32 { + 1.0 +} + +impl PointerEvent { + pub fn new(kind: PointerKind, x: f64, y: f64) -> Self { + Self { kind, pos: Point::new(x, y), mods: Mods::default(), pressure: 1.0 } + } + pub fn with_mods(mut self, m: Mods) -> Self { + self.mods = m; + self + } + /// The pen `pressure` of a pointer event given as JSON (control channel, MCP): 0..1, default 1. + pub fn json_pressure(e: &Value) -> f32 { + e.get("pressure").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(1.0, |f| f.clamp(0.0, 1.0) as f32) + } + /// How long the pointer then holds still, in seconds, from a JSON pointer event's `holdMs` + /// (0..60000; default 0): the time [`Tool::tick`] gets. + pub fn json_hold(e: &Value) -> f64 { + e.get("holdMs").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(0.0, |ms| ms.clamp(0.0, 60_000.0) / 1000.0) + } +} + +/// Keys tools care about. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum ToolKey { + Enter, + Escape, + Backspace, + Delete, + Up, + Down, + Left, + Right, + /// Illustrator: ↑/↓ while drawing a polygon/star change sides/points; `[`/`]` change sizes. + BracketLeft, + BracketRight, + Tab, + Home, + End, + /// A digit key 0–9 (5 while dragging with the Perspective Selection tool moves perpendicular + /// to the plane). + Digit(u8), + /// A letter key a–z, lowercase (E while dragging a Selection or Group Selection marquee makes it + /// select only the objects entirely inside it, and E again switches it back). + Letter(char), +} + +/// What a tool asks the engine to do. +#[derive(Clone, Debug, PartialEq)] +pub enum Action { + /// Start an interaction (snapshot the document). The label becomes the undo name. + Begin(String), + /// Replace the current preview with this command, applied to the snapshot. + Preview(String, Value), + /// Finish the interaction: keep the last preview as one undo step. + Commit, + /// Abort the interaction: restore the snapshot. + Cancel, + /// Execute a command immediately (its own undo step, if it edits). + Exec(String, Value), + /// Ask the UI to open a dialog (e.g. click with the Rectangle tool → size dialog). + Dialog(String, Value), + /// Switch to another tool (e.g. after placing a text frame). + SwitchTool(String), + /// Call the tool's `notify` after the preceding actions ran (e.g. to pick up a created object). + Notify(String), +} diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index b2b2c97c8..e55bb4069 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -51,399 +51,13 @@ use vectorcraft_geom::{BezPath, Point, Rect, Vec2}; pub use catalog::{TOOL_GROUPS, ToolInfo, tool_info}; -/// Modifier keys. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Mods { - #[serde(default)] - pub shift: bool, - /// Option on macOS. - #[serde(default)] - pub alt: bool, - /// Command on macOS, Ctrl elsewhere. - #[serde(default)] - pub cmd: bool, - #[serde(default)] - pub ctrl: bool, - #[serde(default)] - pub space: bool, -} +mod context; +mod cursor; +mod input; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum PointerKind { - /// Button pressed. - Down, - /// Moved with the button held. - Drag, - /// Button released. - Up, - /// Moved with no button (hover). - Move, - DoubleClick, -} - -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub struct PointerEvent { - pub kind: PointerKind, - /// Document coordinates. - pub pos: Point, - #[serde(default)] - pub mods: Mods, - #[serde(default = "one")] - pub pressure: f32, -} - -fn one() -> f32 { - 1.0 -} - -impl PointerEvent { - pub fn new(kind: PointerKind, x: f64, y: f64) -> Self { - Self { kind, pos: Point::new(x, y), mods: Mods::default(), pressure: 1.0 } - } - pub fn with_mods(mut self, m: Mods) -> Self { - self.mods = m; - self - } - /// The pen `pressure` of a pointer event given as JSON (control channel, MCP): 0..1, default 1. - pub fn json_pressure(e: &Value) -> f32 { - e.get("pressure").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(1.0, |f| f.clamp(0.0, 1.0) as f32) - } - /// How long the pointer then holds still, in seconds, from a JSON pointer event's `holdMs` - /// (0..60000; default 0): the time [`Tool::tick`] gets. - pub fn json_hold(e: &Value) -> f64 { - e.get("holdMs").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(0.0, |ms| ms.clamp(0.0, 60_000.0) / 1000.0) - } -} - -/// Keys tools care about. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ToolKey { - Enter, - Escape, - Backspace, - Delete, - Up, - Down, - Left, - Right, - /// Illustrator: ↑/↓ while drawing a polygon/star change sides/points; `[`/`]` change sizes. - BracketLeft, - BracketRight, - Tab, - Home, - End, - /// A digit key 0–9 (5 while dragging with the Perspective Selection tool moves perpendicular - /// to the plane). - Digit(u8), - /// A letter key a–z, lowercase (E while dragging a Selection or Group Selection marquee makes it - /// select only the objects entirely inside it, and E again switches it back). - Letter(char), -} - -/// What a tool asks the engine to do. -#[derive(Clone, Debug, PartialEq)] -pub enum Action { - /// Start an interaction (snapshot the document). The label becomes the undo name. - Begin(String), - /// Replace the current preview with this command, applied to the snapshot. - Preview(String, Value), - /// Finish the interaction: keep the last preview as one undo step. - Commit, - /// Abort the interaction: restore the snapshot. - Cancel, - /// Execute a command immediately (its own undo step, if it edits). - Exec(String, Value), - /// Ask the UI to open a dialog (e.g. click with the Rectangle tool → size dialog). - Dialog(String, Value), - /// Switch to another tool (e.g. after placing a text frame). - SwitchTool(String), - /// Call the tool's `notify` after the preceding actions ran (e.g. to pick up a created object). - Notify(String), -} - -/// Paint defaults for new art (the fill/stroke proxy) and the rest of the new-art template. -#[derive(Clone, Debug, PartialEq)] -pub struct PaintDefaults { - pub fill: Paint, - pub stroke: Paint, - pub stroke_width: f64, - /// The appearance new art takes (stroke options, more fills and strokes, effects), its top - /// fill and stroke repainted with `fill`, `stroke` and `stroke_width`; `None`: one plain fill - /// and stroke. Its placed gradients are relative to the unit box. - pub appearance: Option, - /// New art's opacity and blend mode. - pub opacity: f32, - pub blend: vectorcraft_color::BlendMode, - /// The graphic style the template came from (by name): new art is linked to it. - pub style: Option, - /// The template is the last selection's appearance (New Art Has Basic Appearance off): with - /// the option on again, new art takes only its basic fill and stroke. - pub inherited: bool, -} - -impl Default for PaintDefaults { - /// White fill, 1 pt black stroke, nothing else. - fn default() -> Self { - Self { - fill: Paint::solid(vectorcraft_color::Color::WHITE), - stroke: Paint::solid(vectorcraft_color::Color::BLACK), - stroke_width: 1.0, - appearance: None, - opacity: 1.0, - blend: Default::default(), - style: None, - inherited: false, - } - } -} - -/// The document window on screen, in document coordinates: widgets that stay put on screen (the -/// Plane Switching Widget) are placed with it. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct ScreenFrame { - /// The window's top-left corner. - pub origin: Point, - /// One screen pixel to the right and one down. - pub right: Vec2, - pub down: Vec2, - /// The window's size in screen pixels. - pub size: (f64, f64), -} - -impl ScreenFrame { - /// The document point `x`, `y` screen pixels from the window's top-left corner. - pub fn at(&self, x: f64, y: f64) -> Point { - self.origin + self.right * x + self.down * y - } - /// Where the document point `p` is, in screen pixels from the window's top-left corner (the - /// inverse of [`Self::at`]); none for a degenerate frame. - pub fn to_screen(&self, p: Point) -> Option<(f64, f64)> { - let (r, d, v) = (self.right, self.down, p - self.origin); - let det = r.x * d.y - d.x * r.y; - (det.is_finite() && det.abs() > 1e-12).then(|| ((v.x * d.y - d.x * v.y) / det, (r.x * v.y - v.x * r.y) / det)) - } -} - -/// Read-only context a tool sees. -pub struct ToolContext<'a> { - pub doc: &'a Document, - /// The rulers' zero point with the view's active artboard (see - /// [`Document::ruler_origin_for`]): where ruler ticks and measured positions count from. - pub ruler_origin: Point, - /// Which open document `doc` is (its process-unique id) and its revision, which moves with - /// every change of the document or the selection: what a tool keeps between pointer events - /// for the document as it is (the snap targets of hovering) is keyed on it. - pub revision: (u64, u64), - pub selection: &'a Selection, - /// Screen pixels per document point. - pub zoom: f64, - pub isolation: Option, - pub paint: &'a PaintDefaults, - pub outline: bool, - pub smart_guides: bool, - /// View → Show Guides with Lock Guides off: the selection tools pick and drag ruler guides. - pub guides: bool, - pub snap_to_grid: bool, - /// Bounding box shown (View → Show/Hide Bounding Box). - pub show_bbox: bool, - /// View → Snap to Pixel. - pub snap_to_pixel: bool, - /// View → Snap to Point: with Smart Guides off, dragged selections, drawn points and picked - /// points (a transform's reference point) land on anchors and ruler guides within - /// [`Self::snap_tolerance`]. - pub snap_to_point: bool, - /// View → Snapping → Snap to Tangent: with Smart Guides on, a line drawn - /// from an anchor lands where it touches a nearby curve. - pub snap_tangent: bool, - /// View → Snapping → Snap to Perpendicular: likewise where it meets a nearby segment square. - pub snap_perpendicular: bool, - /// View → Snap to Glyph: drawn points snap to live-text baselines and glyph bounds (with Smart Guides). - pub snap_to_glyph: bool, - /// View → Show Corner Widget: paths show draggable Live Corners widgets in their corners. - pub corner_widgets: bool, - /// Which paint proxy is in front (true = Fill): the one the Gradient tool edits. - pub fill_active: bool, - /// The selected gradient stop (`gradient.selectStop`), marked on the gradient annotator. - pub gradient_stop: Option, - /// The Appearance panel's active fill/stroke (paint-order index into the first selected - /// object's stack); paint tools show and edit that item. - pub appearance_item: Option, - /// General → Constrain Angle (degrees): Shift constrains drags to 45° steps from it. - pub constrain_angle: f64, - /// General → Keyboard Increment: arrow-key nudges in document units (Shift multiplies by 10). - pub keyboard_increment: f64, - /// The selected freeform gradient point (`paint.freeform.selectPoint`), marked on the - /// freeform annotator. - pub freeform_point: Option, - /// Eyedropper Options' raster sample size: the pixels square averaged when the Eyedropper - /// samples an image (1, 3 or 5). - pub raster_sample: u32, - /// General → Use Preview Bounds: the bounding box measures visual bounds (strokes included). - pub preview_bounds: bool, - /// The session's memo of the selection tools' measurements for the document as it is - /// ([`select::PreviewMemo`]); without one they measure on every call. - pub preview_memo: Option<&'a select::PreviewMemo>, - /// Units ▸ General: the unit measurement labels show lengths in. - pub unit: Unit, - /// Units ▸ Stroke: the unit measurement labels show stroke widths in. - pub stroke_unit: Unit, - /// Clipboard Handling → When pasting text: Keep Plain Text. Text pasted while typing takes - /// the style at the caret, even the text the Type tool copied with its formatting. - pub paste_plain_text: bool, - /// View → Hide Slices: the Slice Selection tool can't pick hidden slices. - pub slices_hidden: bool, - /// View → Lock Slices: the Slice Selection tool leaves locked slices alone. - pub slices_locked: bool, - /// General → Disable Auto Add/Delete is off: the Pen adds an anchor on a selected path's - /// segment and deletes one of its anchors. - pub auto_add_delete: bool, - /// Selection & Anchor Display → Tolerance: how near (screen pixels) a click must be to a path - /// or an anchor to pick it. - pub selection_tolerance: f64, - /// Selection & Anchor Display → Size (1–7, default 3): how big anchors and handles are drawn. - pub anchor_size: u32, - /// Selection & Anchor Display → Object Selection by Path Only: a click inside a filled path - /// doesn't pick it, only one on its path does. - pub path_only: bool, - /// Type → Type Object Selection by Path Only: type is picked on its type path only (point - /// type's baseline, area type's frame, type on a path's path), not anywhere in its bounds. - pub type_path_only: bool, - /// General → Double Click To Isolate: a double-click on a group with the Selection tool - /// isolates it. - pub double_click_isolate: bool, - /// Selection & Anchor Display → Command Click to Select Objects Behind: Cmd/Ctrl-click with the - /// Selection tool selects the object under the selected one, the next click the one under - /// that. - pub select_behind: bool, - /// Selection & Anchor Display → Highlight anchors on mouse over: Direct Selection marks the - /// anchor under the pointer. - pub highlight_anchors: bool, - /// Selection & Anchor Display → Snap to Point (screen pixels): how near an anchor or a ruler - /// guide pulls the pointer while View → Snap to Point is on. - pub snap_tolerance: f64, - /// Selection & Anchor Display → Show handles when multiple anchors are selected: off, Direct - /// Selection shows and drags handles only while a single anchor is selected. - pub handles_multiple: bool, - /// Selection & Anchor Display → Hide Corner Widget for angles greater than (degrees): corners - /// wider than this show no Live Corners widget. - pub corner_widget_max_angle: f64, - /// Selection & Anchor Display → Move Locked and Hidden Artwork with Artboard. - pub move_locked_with_artboard: bool, - /// Selection & Anchor Display → Enable Rubber Band for Pen Tool: the Pen previews the next - /// segment to the pointer. - pub pen_rubber_band: bool, - /// Selection & Anchor Display → Enable Rubber Band for Curvature Tool. - pub curvature_rubber_band: bool, - /// Type → Fill New Type Objects With Placeholder Text: type the Type tools place starts with - /// placeholder text, selected. - pub placeholder_text: bool, - /// Smart Guides → Color: the smart guides' lines and labels (RGB). - pub smart_guide_color: [u8; 3], - /// Smart Guides → Alignment Guides: the lines along the edges and centres the art lines up - /// with show. Off, the art still snaps into line. - pub alignment_guides: bool, - /// Smart Guides → Anchor/Path Labels: the "anchor", "center", "path"… labels show. - pub anchor_path_labels: bool, - /// Smart Guides → Measurement Labels: the size and offset readouts while drawing and moving. - pub measurement_labels: bool, - /// Smart Guides → Transform Tools: the readouts while scaling, rotating and shearing. - pub transform_tools_guides: bool, - /// Smart Guides → Spacing Guides: a moved selection snaps to spacing equal to the gap between - /// two other objects in its row or column, or evenly between its two neighbours, and the - /// equal gaps show. - pub spacing_guides: bool, - /// Smart Guides → Invoke Distance Guides: with the Selection tool, Alt/Option held over another - /// object shows how far the selection is from it, and over an empty part of an artboard how - /// far it is from the artboard's edges ([`guides::distance_guides`]). - pub distance_guides: bool, - /// Smart Guides → Snapping Tolerance (screen pixels): how near a smart guide target pulls the - /// pointer, a dragged edge or a drawn point. - pub snapping_tolerance: f64, - /// Smart Guides → Construction Guides: the angles (degrees, counter-clockwise from the x axis - /// as seen on the page) of the lines through the point a drawn point leaves from, onto which a - /// point near one lands ([`guides::construction_angles`]); none with the option off. - pub construction_angles: &'static [f64], - /// The document window (none headless): screen-fixed widgets sit in it. - pub screen: Option, - /// Where the Plane Switching Widget sits (Perspective Grid Options); None while it's hidden. - pub plane_widget: Option, -} - -impl ToolContext<'_> { - /// Tolerance in document units for `px` screen pixels. - pub fn tol(&self, px: f64) -> f64 { - px / self.zoom.max(1e-9) - } - /// How far the view is turned (Rotate View), in degrees clockwise on screen; 0 without a - /// window. - pub fn view_angle(&self) -> f64 { - // `right` is one screen pixel to the right, in document units. - self.screen.map(|s| (-s.right.y).atan2(s.right.x).to_degrees()).filter(|a| a.is_finite()).unwrap_or(0.0) - } - /// How near (document units) a smart guide target pulls: Smart Guides → Snapping Tolerance. - pub fn snap_tol(&self) -> f64 { - self.tol(self.snapping_tolerance) - } - /// A length as measurement labels show it, in the General unit (`12.50 mm`). - pub fn len(&self, v: f64) -> String { - self.unit.readout(v) - } - /// A size label: `W: …` over `H: …`. - pub fn size_label(&self, w: f64, h: f64) -> String { - format!("W: {}\nH: {}", self.len(w), self.len(h)) - } - /// Star tool drag: outer (R1) and inner (R2) radii from the centre to the pointer. - pub fn star_radii_label(&self, outer: f64, inner: f64) -> String { - format!("R1: {}\nR2: {}", self.len(outer), self.len(inner)) - } - /// A move label: `dX: …` over `dY: …`. - pub fn offset_label(&self, dx: f64, dy: f64) -> String { - format!("dX: {}\ndY: {}", self.len(dx), self.len(dy)) - } - /// A segment label while drawing: distance, angle and offsets between two points. - pub fn segment_measure_label(&self, a: Point, b: Point) -> String { - let (dist, ang, dx, dy) = xform::measure(a, b); - format!("D: {}\nAngle: {ang:.1}°\n{}", self.len(dist), self.offset_label(dx, dy)) - } - /// What Snap to Grid snaps to: the grid's subdivisions (or its lines without any). - pub fn grid_step(&self) -> f64 { - self.doc.grid.spacing / self.doc.grid.subdivisions.max(1) as f64 - } - /// The selection tolerance in document units ([`Self::selection_tolerance`]). - pub fn pick_tol(&self) -> f64 { - self.tol(self.selection_tolerance) - } - /// How near (document units) a press picks an anchor or a direction handle's end: the - /// selection tolerance, but at least 2 px past the drawn point, so a point is easy to catch - /// and wins over the segments through it, which are tested after it (#593). - pub fn point_tol(&self) -> f64 { - // Anchors are drawn 5 px wide at the default Size 3, a pixel more or less per step. - let half = (5.0 + f64::from(self.anchor_size.clamp(1, 7)) - 3.0) / 2.0; - self.tol(self.selection_tolerance.max(half + 2.0)) - } - pub fn hit_options(&self) -> vectorcraft_doc::hit::HitOptions { - vectorcraft_doc::hit::HitOptions { - tol: self.pick_tol(), - outline: self.outline, - path_only: self.path_only, - type_path_only: self.type_path_only, - scope: self.isolation, - skip_guides: !self.guides, - clip_outlines: false, - } - } - - /// [`Self::hit_options`] for the selection tools (Selection, Direct Selection, Group - /// Selection): a clipping path is also hit on its outline ([`HitOptions::clip_outlines`]). - /// Only for what a press or a hover selects: a double-click, loaded text or a guide's release - /// acts on what is drawn and takes [`Self::hit_options`]. - /// - /// [`HitOptions::clip_outlines`]: vectorcraft_doc::hit::HitOptions::clip_outlines - pub fn selection_hit_options(&self) -> vectorcraft_doc::hit::HitOptions { - vectorcraft_doc::hit::HitOptions { clip_outlines: true, ..self.hit_options() } - } -} +pub use context::{PaintDefaults, ScreenFrame, ToolContext}; +pub use cursor::Cursor; +pub use input::{Action, Mods, PointerEvent, PointerKind, ToolKey}; /// Visual feedback drawn by the UI in screen space (coordinates here are document space). #[derive(Clone, Debug, PartialEq)] @@ -475,310 +89,6 @@ pub enum Overlay { GridLine { a: Point, b: Point, color: [u8; 4] }, } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -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 - /// the corner it is at, so at the top right it opens toward the bottom left. Make it with - /// [`Cursor::rotate_about`]. - RotateToward(u8), - /// Over a Live Corners widget (drag to round the corners). - CornerRadius, - /// The Pen with the new-path asterisk: a click starts a new path. - Pen, - /// The Pen without a badge, while a path is being drawn: a click adds its next anchor. - PenDrawing, - PenAdd, - 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. - PenConvert, - Text, - Hand, - HandGrab, - ZoomIn, - ZoomOut, - Eyedropper, - NotAllowed, - /// A long operation (opening a file, a background save or export): the OS wait cursor. - Busy, - /// Over the gradient annotator's bar: a click adds a stop. - AddStop, - /// A gradient stop dragged off the bar: releasing deletes it. - RemoveStop, - /// The Slice tool: a crosshair with a blade. - Slice, - /// The Slice Selection tool: the arrow with a slice badge. - SliceSelect, - /// The Width tool away from strokes. - Width, - /// The Width tool over a stroke: a drag adds a width point. - WidthAdd, - /// The Width tool over a width point or a handle end: a drag moves or widens it. - WidthPoint, - /// The Blend tool away from art: a crosshair with a hollow square. - Blend, - /// The Blend tool over an object it can blend: a crosshair with a filled square. - BlendObject, - /// The Blend tool over an anchor point (the blend starts there): a crosshair with a target. - BlendAnchor, - /// The Objects on Path tool over a path the selected objects can attach to (a click attaches - /// them). - ObjectsOnPath, - /// Over a bracket of selected type on a path (a drag moves it): the arrow with a bracket. - PathBracket, - /// Over the type widget of selected type (a double-click converts point type to area type and - /// back): the arrow with a type badge. - TypeWidget, - /// The Selection tool loaded with the text of area type or type on a path by a click on its out - /// or in port (a click threads it into a path or a new frame, a drag draws the new frame). - LoadedText, - /// The Shape Builder: a crosshair with a plus (merge mode)... - ShapeBuilder, - /// ...or, with Alt held, a minus (erase mode). - ShapeBuilderErase, - /// The Lasso tool. - Lasso, - /// The Magic Wand tool. - 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 - /// its start, where releasing the mouse button closes the path. A crosshair with a ring. - PencilClose, - /// The Pencil tool before a press that draws a straight line (Shift or Alt held). A crosshair with a - /// short straight line. - PencilStraight, - /// The Pencil tool before a press that draws a straight line (Shift or Alt held), over an end of - /// a selected open path: the line continues the path from that end. A crosshair with a short - /// straight line leaving a square end point. - PencilContinue, - /// The Paintbrush tool. - Paintbrush, - /// The Paintbrush tool with Alt held: releasing the mouse button with Alt still held closes - /// the path. Also while a stroke is drawn back near its start, where releasing the mouse button - /// closes the path. A crosshair with a ring. - PaintbrushClose, - /// 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 { - /// The rotate cursor for the pointer at `p` turning art about `center`: [`Cursor::RotateToward`] - /// the pointer's side of it, or the plain [`Cursor::Rotate`] on the centre itself. - pub fn rotate_about(center: Point, p: Point) -> Self { - let d = p - center; - if !(d.x.is_finite() && d.y.is_finite()) || d.hypot() < 1e-9 { - return Cursor::Rotate; - } - // y points down the page: atan2(x, -y) is 0 straight up and grows clockwise. - let octant = (d.x.atan2(-d.y) / std::f64::consts::FRAC_PI_4).round() as i64; - Cursor::RotateToward(octant.rem_euclid(8) as u8) - } - - /// [`Cursor::rotate_about`] in a view turned `view` degrees clockwise - /// ([`ToolContext::view_angle`]): the pointer's side of `center` as it shows on screen. The - /// pointer's direction turns with the view before it rounds to the nearest 45°. - pub fn rotate_about_in_view(center: Point, p: Point, view: f64) -> Self { - let (sn, cs) = if view.is_finite() { view.to_radians().sin_cos() } else { (0.0, 1.0) }; - let d = p - center; - // y points down the page, so this turns `d` clockwise on the page. - Cursor::rotate_about(center, center + Vec2::new(d.x * cs - d.y * sn, d.x * sn + d.y * cs)) - } - - /// The cursor as it shows in a view turned `deg` degrees clockwise (Rotate View): the resize - /// cursors and [`Cursor::RotateToward`] turn with the view to the nearest 45°, and every other - /// cursor stays as it is. - pub fn turned(self, deg: f64) -> Self { - const RESIZE: [Cursor; 4] = [Cursor::ResizeH, Cursor::ResizeNeSw, Cursor::ResizeV, Cursor::ResizeNwSe]; - let k = if deg.is_finite() { (deg / 45.0).round() as i64 } else { 0 }; - match self { - Cursor::RotateToward(o) => Cursor::RotateToward((i64::from(o) + k).rem_euclid(8) as u8), - c => match RESIZE.iter().position(|r| *r == c) { - Some(i) => RESIZE.get((i as i64 - k).rem_euclid(4) as usize).copied().unwrap_or(c), - None => c, - }, - } - } - - /// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the - /// 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 - | Cursor::PenDrawing - | Cursor::PenAdd - | Cursor::PenDelete - | Cursor::PenClose - | 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::Lasso - | Cursor::MagicWand - | Cursor::Pencil - | Cursor::PencilNew - | Cursor::PencilClose - | Cursor::PencilStraight - | Cursor::PencilContinue - | Cursor::Paintbrush - | Cursor::PaintbrushClose - | 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 { - 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::ObjectsOnPath - | 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, - } - } -} - /// What an IME's "delete the text around the cursor" counts: UTF-8 bytes (winit's raw /// `DeleteSurroundingBytes`) or Unicode scalar values (egui's `DeleteSurrounding`). #[derive(Clone, Copy, Debug, PartialEq, Eq)]