Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions crates/ui-egui/src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,14 +442,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let space = ui.input(|i| i.key_down(egui::Key::Space));
let panning = matches!(ui.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Pan { .. }));
let zooming = if space { m.command } else { app.session.tool_id() == "zoom" };
let cur = if panning {
egui::CursorIcon::Grabbing
// The hand and the magnifier are drawn like the tool cursors below: Windows has no
// system cursor for them (winit shows the four-headed move cursor for Grab).
let view_cursor = if panning {
Some(Cursor::HandGrab)
} else if zooming {
if m.alt { egui::CursorIcon::ZoomOut } else { egui::CursorIcon::ZoomIn }
Some(if m.alt { Cursor::ZoomOut } else { Cursor::ZoomIn })
} else if space || app.session.tool_id() == "hand" {
egui::CursorIcon::Grab
} else if let Some(p) = app.hover_doc {
let c = app.session.cursor(p, mods(m, space), view_info);
Some(Cursor::Hand)
} else {
None
};
let cur = if let Some(c) = view_cursor.or_else(|| app.hover_doc.map(|p| app.session.cursor(p, mods(m, space), view_info))) {
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("tool-cursor")));
// The loaded place cursor carries the file's thumbnail.
if app.session.tool_id() == "place"
Expand Down
64 changes: 58 additions & 6 deletions crates/ui-egui/src/cursors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,54 @@ fn shape_builder(p: &mut Ink, o: Pos2, erase: bool) {
}
}

/// The Hand tool and Space: an open hand (palm and four fingers, thumb to the left), or the same
/// hand closed while it drags the view. Windows has no system hand that isn't a pointing finger
/// (winit maps egui's Grab and Grabbing to the four-headed move cursor), so it is drawn like the
/// other tool cursors. Hotspot in the palm.
fn hand(p: &mut Ink, o: Pos2, closed: bool) {
// Thumb out to the left, four fingers up from the palm (open) or folded over it (closed),
// then the palm, a rounded block below the hotspot, over their bases.
let mut parts = vec![vec![o + vec2(-7.0, 0.0), o + vec2(-13.0, -4.0), o + vec2(-14.0, -1.0), o + vec2(-9.0, 5.0)]];
for (i, x) in [-5.5f32, -1.75, 2.0, 5.75].into_iter().enumerate() {
let top = if closed {
-5.0
} else if i == 0 || i == 3 {
-11.0
} else {
-14.0
};
parts.push(vec![o + vec2(x - 1.5, 0.0), o + vec2(x - 1.5, top), o + vec2(x + 1.5, top), o + vec2(x + 1.5, 0.0)]);
}
parts.push(vec![o + vec2(-7.0, -2.0), o + vec2(7.25, -2.0), o + vec2(8.0, 6.0), o + vec2(4.0, 11.0), o + vec2(-5.0, 11.0), o + vec2(-9.0, 5.0)]);
// Every halo first, so no part's halo covers another's outline.
for part in &parts {
p.add(Shape::closed_line(part.clone(), Stroke::new(3.0, HALO)));
}
for part in parts {
p.add(Shape::convex_polygon(part.clone(), HALO, Stroke::NONE));
p.add(Shape::closed_line(part, Stroke::new(1.0, INK)));
}
}

/// The Zoom tool: a magnifier with a plus in the lens (Alt: a minus), the lens centred on the
/// hotspot, the handle down to the right.
fn magnifier(p: &mut Ink, o: Pos2, out: bool) {
const R: f32 = 7.0;
p.circle_stroke(o, R, Stroke::new(3.0, HALO));
let d = vec2(R * 0.72, R * 0.72);
line(p, o + d, o + d + vec2(7.0, 7.0));
p.circle_filled(o, R, HALO);
p.circle_stroke(o, R, Stroke::new(1.2, INK));
// Inside the white lens, without halos: a halo would cut the plus's other bar.
let bar = Stroke::new(1.2, INK);
p.line_segment([o - vec2(3.5, 0.0), o + vec2(3.5, 0.0)], bar);
if !out {
p.line_segment([o - vec2(0.0, 3.5), o + vec2(0.0, 3.5)], bar);
}
}

/// The shapes of cursor `c` with its hotspot at `p`; `None` for cursors that stay system cursors
/// (hand, zoom, busy states).
/// (the busy states).
fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
let ink = &mut Ink::default();
match c {
Expand Down Expand Up @@ -299,6 +345,10 @@ fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
Cursor::TypeWidget => type_widget(ink, p),
Cursor::ShapeBuilder => shape_builder(ink, p, false),
Cursor::ShapeBuilderErase => shape_builder(ink, p, true),
Cursor::Hand => hand(ink, p, false),
Cursor::HandGrab => hand(ink, p, true),
Cursor::ZoomIn => magnifier(ink, p, false),
Cursor::ZoomOut => magnifier(ink, p, true),
_ => return None,
}
Some(std::mem::take(&mut ink.0))
Expand Down Expand Up @@ -437,7 +487,11 @@ mod tests {
use crate::VectorcraftApp;

/// Every cursor with a glyph.
const GLYPHS: [Cursor; 34] = [
const GLYPHS: [Cursor; 38] = [
Cursor::Hand,
Cursor::HandGrab,
Cursor::ZoomIn,
Cursor::ZoomOut,
Cursor::Arrow,
Cursor::ArrowHollow,
Cursor::Move,
Expand Down Expand Up @@ -492,9 +546,7 @@ mod tests {
assert!(img.rgba.as_chunks::<4>().0.iter().any(|p| p[3] == 255), "{c:?}: nothing opaque");
}
}
for c in [Cursor::Hand, Cursor::HandGrab, Cursor::ZoomIn, Cursor::ZoomOut, Cursor::NotAllowed] {
assert!(rasterize(c, 1.0).is_none(), "{c:?} stays a system cursor");
}
assert!(rasterize(Cursor::NotAllowed, 1.0).is_none(), "the busy states stay system cursors");
for ppp in [0.0, -1.0, f32::NAN, f32::INFINITY, 1e9] {
assert!(rasterize(Cursor::Arrow, ppp).is_none(), "{ppp}");
}
Expand Down Expand Up @@ -529,7 +581,7 @@ mod tests {
let b = images.get(Cursor::Pen, 1.5).unwrap();
assert!(Arc::ptr_eq(&a.rgba, &b.rgba));
assert!(!Arc::ptr_eq(&a.rgba, &images.get(Cursor::Pen, 2.0).unwrap().rgba));
assert!(images.get(Cursor::Hand, 1.5).is_none());
assert!(images.get(Cursor::NotAllowed, 1.5).is_none());
assert!(images.get(Cursor::Arrow, f32::NAN).is_none());
assert_eq!(images.0.len(), 2);
}
Expand Down
Loading