#1026: the rotate cursor wraps the corner it is at

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 <noreply@anthropic.com>
This commit is contained in:
mateususa 2026-10-10 13:24:08 -07:00 committed by bflatastic
parent 0abb4157c9
commit 0b0ccfb35a
5 changed files with 51 additions and 10 deletions

View File

@ -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 {

View File

@ -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();

View File

@ -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,

View File

@ -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
}

View File

@ -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<Pos2> = (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<Vec<Shape>> {
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,