From 0b0ccfb35a21f35c9042592579c00baf81ec11fa Mon Sep 17 00:00:00 2001 From: mateususa <150644469+mateususa@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:24:08 -0700 Subject: [PATCH] #1026: the rotate cursor wraps the corner it is at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beside every corner of the bounding box the curved rotate cursor bowed up. It now bows from the box's centre toward the pointer, in 45° steps (Cursor::RotateToward, made by Cursor::rotate_about): at the top right it opens toward the bottom left, at the bottom right toward the top left. The Selection tool shows it over the rotate zone and while rotating (it follows the pointer round), and Free Transform does the same. Co-Authored-By: Claude Opus 5.5 --- crates/tools/src/lib.rs | 17 +++++++++++++++++ crates/tools/src/select.rs | 22 ++++++++++++++++++++-- crates/tools/src/xform/free.rs | 2 +- crates/ui-egui/src/canvas.rs | 2 +- crates/ui-egui/src/cursors.rs | 18 ++++++++++++------ 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index 4febf31d..c15cce78 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -425,6 +425,11 @@ pub enum Cursor { ResizeNwSe, ResizeNeSw, 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, Pen, @@ -476,6 +481,18 @@ pub enum Cursor { } 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) + } + /// 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. pub fn precise(self) -> Self { diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 4a3a4dca..21934d78 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -444,7 +444,7 @@ impl Tool for SelectionTool { fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor { match self.state { - State::Rotating { .. } => return Cursor::Rotate, + 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, State::Corner(_) => return Cursor::CornerRadius, @@ -468,7 +468,7 @@ impl Tool for SelectionTool { { match box_hit(cx, &bx, p) { Some(BoxHit::Handle(h)) => return handle_cursor(h, bx.angle), - Some(BoxHit::Rotate) => return Cursor::Rotate, + Some(BoxHit::Rotate) => return Cursor::rotate_about(bx.center(), p), None => {} } } @@ -588,6 +588,24 @@ mod tests { assert!(t.pointer(&cx, &ev(PointerKind::DoubleClick, 160.0, 150.0)).is_empty()); } + /// Just outside each corner of the box the rotate cursor bows toward that corner, its arc + /// opening toward the box: at the top right it opens to the bottom left. + #[test] + fn the_rotate_cursor_wraps_the_corner_it_is_at() { + let (d, id) = doc_with_rect(); + let mut s = Selection::default(); + s.add(id); + let p = paint(); + let cx = cx(&d, &s, &p); + let t = SelectionTool::default(); + // The box runs from (100, 100) to (200, 200); 10 pt out from each corner, diagonally. + for ((x, y), octant) in [((93.0, 93.0), 7), ((207.0, 93.0), 1), ((207.0, 207.0), 3), ((93.0, 207.0), 5)] { + assert_eq!(t.cursor(&cx, Point::new(x, y), Mods::default()), Cursor::RotateToward(octant), "at ({x}, {y})"); + } + assert_eq!(Cursor::rotate_about(Point::new(1.0, 1.0), Point::new(1.0, 1.0)), Cursor::Rotate, "on the centre"); + assert_eq!(Cursor::rotate_about(Point::ORIGIN, Point::new(0.0, -5.0)), Cursor::RotateToward(0), "straight up"); + } + #[test] fn drag_moves_with_single_undo() { let (d, id) = doc_with_rect(); diff --git a/crates/tools/src/xform/free.rs b/crates/tools/src/xform/free.rs index 73d631f0..54099c81 100644 --- a/crates/tools/src/xform/free.rs +++ b/crates/tools/src/xform/free.rs @@ -313,7 +313,7 @@ impl Tool for FreeTransformTool { fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor { let Some(r) = selection_bounds(cx) else { return Cursor::Arrow }; match self.drag.map(|d| d.op).or_else(|| self.classify(cx, r, p, m)) { - Some(Op::Rotate) => Cursor::Rotate, + Some(Op::Rotate) => Cursor::rotate_about(r.center(), p), Some(Op::Move) => Cursor::Move, Some(Op::Scale(h) | Op::Shear(h)) => match h { Handle::Top | Handle::Bottom => Cursor::ResizeV, diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8cfcfe58..16fc26a5 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -487,7 +487,7 @@ fn cursor_icon(c: Cursor) -> egui::CursorIcon { Cursor::ResizeV => C::ResizeVertical, Cursor::ResizeNwSe => C::ResizeNwSe, Cursor::ResizeNeSw => C::ResizeNeSw, - Cursor::Rotate => C::Alias, + Cursor::Rotate | Cursor::RotateToward(_) => C::Alias, Cursor::Pen | Cursor::PenAdd | Cursor::PenDelete | Cursor::PenClose | Cursor::PenContinue | Cursor::PenJoin | Cursor::PenConvert => { C::Crosshair } diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index d98a2d2e..f273af20 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -113,17 +113,21 @@ fn double_arrow(p: &mut Ink, o: Pos2, dir: egui::Vec2) { } /// A curved double arrow over the hotspot: an arc with an arrowhead at each end, pointing on along -/// the arc (#701). -fn rotate(p: &mut Ink, o: Pos2) { +/// the arc (#701). The arc bows up, or toward `octant` (0 up, then clockwise in 45° steps): beside a +/// bounding box it wraps the corner the pointer is at. +fn rotate(p: &mut Ink, o: Pos2, octant: u8) { const R: f32 = 8.0; let (a0, a1) = (std::f32::consts::PI * 0.12, std::f32::consts::PI * 0.88); - let at = |a: f32| o + vec2(a.cos() * R, -a.sin() * R); + // Clockwise on screen (y down) by whole octants. + let (s, c) = (f32::from(octant % 8) * std::f32::consts::FRAC_PI_4).sin_cos(); + let turn = |v: Vec2| vec2(v.x * c - v.y * s, v.x * s + v.y * c); + let at = |a: f32| o + turn(vec2(a.cos() * R, -a.sin() * R)); let pts: Vec = (0..=12).map(|i| at(a0 + (a1 - a0) * i as f32 / 12.0)).collect(); p.add(Shape::line(pts.clone(), Stroke::new(3.2, HALO))); p.add(Shape::line(pts, Stroke::new(1.3, INK))); // Each head points away from the arc, along its tangent there. for (a, away) in [(a0, -1.0f32), (a1, 1.0)] { - let t = vec2(-a.sin(), -a.cos()) * away; + let t = turn(vec2(-a.sin(), -a.cos()) * away); let n = vec2(-t.y, t.x); let (end, tip) = (at(a) - t * 1.5, at(a) + t * 4.5); poly(p, vec![tip, end + n * 1.8, end - n * 1.8], INK, INK); @@ -269,7 +273,8 @@ fn glyph(c: Cursor, p: Pos2) -> Option> { Cursor::ResizeV => double_arrow(ink, p, vec2(0.0, 1.0)), Cursor::ResizeNwSe => double_arrow(ink, p, vec2(1.0, 1.0)), Cursor::ResizeNeSw => double_arrow(ink, p, vec2(1.0, -1.0)), - Cursor::Rotate => rotate(ink, p), + Cursor::Rotate => rotate(ink, p, 0), + Cursor::RotateToward(octant) => rotate(ink, p, octant), Cursor::CornerRadius => corner_radius(ink, p), Cursor::Pen => pen(ink, p, "*"), Cursor::PenAdd => pen(ink, p, "+"), @@ -432,7 +437,7 @@ mod tests { use crate::VectorcraftApp; /// Every cursor with a glyph. - const GLYPHS: [Cursor; 33] = [ + const GLYPHS: [Cursor; 34] = [ Cursor::Arrow, Cursor::ArrowHollow, Cursor::Move, @@ -442,6 +447,7 @@ mod tests { Cursor::ResizeNwSe, Cursor::ResizeNeSw, Cursor::Rotate, + Cursor::RotateToward(1), Cursor::CornerRadius, Cursor::Pen, Cursor::PenAdd,