mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 01:25:24 +00:00
Merge main into #2191
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! The pointer on the canvas: each engine cursor ([`figcraft_engine::tools::Cursor`]) as one of
|
||||
//! the system's cursors, or as a small glyph FigCraft draws at the pointer with the system's
|
||||
//! hidden (a rotate cursor turned to its corner, the pen's states, the eyedropper, the comment
|
||||
//! pin). The glyphs are original, drawn from lines and the code-drawn icons: no cursor images.
|
||||
//! pin, the crosshair of the shape and Text tools). The glyphs are original, drawn from lines and the code-drawn icons: no cursor images.
|
||||
|
||||
use egui::{Color32, CursorIcon, Painter, Pos2, Shape, Stroke, pos2, vec2};
|
||||
use figcraft_engine::tools::{Cursor, PenCursor};
|
||||
@@ -17,6 +17,9 @@ pub enum Glyph {
|
||||
Pen(PenCursor),
|
||||
Eyedropper,
|
||||
Comment,
|
||||
/// A plus open at the centre, for the shape and Text tools (the system's follows the
|
||||
/// system's pointer colour, and a white one is lost on a white frame).
|
||||
Crosshair,
|
||||
}
|
||||
|
||||
/// What the pointer shows.
|
||||
@@ -46,7 +49,7 @@ pub fn of(cursor: Cursor) -> Shown {
|
||||
use Shown::{Drawn, System};
|
||||
match cursor {
|
||||
Cursor::Default => System(CursorIcon::Default),
|
||||
Cursor::Crosshair => System(CursorIcon::Crosshair),
|
||||
Cursor::Crosshair => Drawn(Glyph::Crosshair),
|
||||
Cursor::Move => System(CursorIcon::Move),
|
||||
Cursor::Resize(a) if a.is_finite() => System(resize_icon(a)),
|
||||
Cursor::Resize(_) => System(CursorIcon::Default),
|
||||
@@ -85,7 +88,7 @@ impl Shown {
|
||||
|
||||
/// A name for it (`ui.inspect`): the CSS cursor names where there is one ("default",
|
||||
/// "nwse-resize", "copy"), and "rotate" (with its angle), "pen", "pen-close", "pen-add",
|
||||
/// "pen-bend", "eyedropper" and "comment" for the drawn ones.
|
||||
/// "pen-bend", "eyedropper", "comment" and "crosshair" for the drawn ones.
|
||||
pub fn name(self) -> String {
|
||||
match self {
|
||||
Shown::System(i) => match i {
|
||||
@@ -114,6 +117,7 @@ impl Shown {
|
||||
Shown::Drawn(Glyph::Pen(PenCursor::Bend)) => "pen-bend".into(),
|
||||
Shown::Drawn(Glyph::Eyedropper) => "eyedropper".into(),
|
||||
Shown::Drawn(Glyph::Comment) => "comment".into(),
|
||||
Shown::Drawn(Glyph::Crosshair) => "crosshair".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +131,7 @@ impl Shown {
|
||||
}
|
||||
|
||||
/// Dark lines over a light edge: readable on any page and artwork.
|
||||
const INK: Color32 = Color32::from_rgb(0x1E, 0x1E, 0x1E);
|
||||
pub(crate) const INK: Color32 = Color32::from_rgb(0x1E, 0x1E, 0x1E);
|
||||
const EDGE: Color32 = Color32::WHITE;
|
||||
|
||||
/// A line through `pts` with the light edge under it.
|
||||
@@ -167,6 +171,22 @@ pub fn paint(painter: &Painter, at: Pos2, glyph: Glyph) {
|
||||
line(painter, &[tip - t * 4.5 + n * 3.5, tip + t * 1.0, tip - t * 4.5 - n * 3.5]);
|
||||
}
|
||||
}
|
||||
Glyph::Crosshair => {
|
||||
// A plus centred on the pointer, open at the centre so its pixel stays visible; the
|
||||
// light edge goes under both arms of each line first, then the dark line on top.
|
||||
let arms = [
|
||||
(vec2(-8.0, 0.0), vec2(-2.0, 0.0)),
|
||||
(vec2(2.0, 0.0), vec2(8.0, 0.0)),
|
||||
(vec2(0.0, -8.0), vec2(0.0, -2.0)),
|
||||
(vec2(0.0, 2.0), vec2(0.0, 8.0)),
|
||||
];
|
||||
for (a, b) in arms {
|
||||
painter.add(Shape::line_segment([at + a, at + b], Stroke::new(3.0, EDGE)));
|
||||
}
|
||||
for (a, b) in arms {
|
||||
painter.add(Shape::line_segment([at + a, at + b], Stroke::new(1.0, INK)));
|
||||
}
|
||||
}
|
||||
Glyph::Pen(state) => {
|
||||
// A cross open at the centre (the point it lands on stays visible), and what a click
|
||||
// does at its lower right.
|
||||
|
||||
@@ -22,7 +22,7 @@ fn every_engine_cursor_has_a_system_cursor_or_a_glyph() {
|
||||
// (engine cursor, what shows, its name in `ui.inspect`)
|
||||
let rows: Vec<(Cursor, Shown, &str)> = vec![
|
||||
(Cursor::Default, System(CursorIcon::Default), "default"),
|
||||
(Cursor::Crosshair, System(CursorIcon::Crosshair), "crosshair"),
|
||||
(Cursor::Crosshair, Drawn(Glyph::Crosshair), "crosshair"),
|
||||
(Cursor::Move, System(CursorIcon::Move), "move"),
|
||||
(Cursor::Resize(0.0), System(CursorIcon::ResizeHorizontal), "ew-resize"),
|
||||
(Cursor::Resize(90.0), System(CursorIcon::ResizeVertical), "ns-resize"),
|
||||
@@ -59,6 +59,44 @@ fn every_engine_cursor_has_a_system_cursor_or_a_glyph() {
|
||||
assert_eq!(of(Cursor::Rotate(f64::INFINITY)), Drawn(Glyph::Rotate(0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_crosshair_is_drawn_dark_with_a_white_edge_so_it_shows_on_white() {
|
||||
use egui::epaint::Shape;
|
||||
let shown = of(Cursor::Crosshair);
|
||||
assert_eq!(shown, Shown::Drawn(Glyph::Crosshair));
|
||||
// The system's pointer is hidden under it, whatever its colour.
|
||||
assert_eq!(shown.icon(), CursorIcon::None);
|
||||
let at = pos2(50.0, 40.0);
|
||||
let ctx = egui::Context::default();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let ctx = ui.ctx().clone();
|
||||
let layer = egui::LayerId::new(egui::Order::Tooltip, egui::Id::unique("crosshair_test"));
|
||||
crate::cursors::paint(&ctx.layer_painter(layer), at, Glyph::Crosshair);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let mut strokes = Vec::new();
|
||||
for clipped in &out.shapes {
|
||||
if let Shape::LineSegment { points, stroke } = &clipped.shape {
|
||||
strokes.push((points.to_vec(), stroke.color, stroke.width));
|
||||
}
|
||||
}
|
||||
let dark: Vec<_> = strokes.iter().filter(|s| s.1 == crate::cursors::INK).collect();
|
||||
let white: Vec<_> = strokes.iter().filter(|s| s.1 == egui::Color32::WHITE).collect();
|
||||
assert!(!dark.is_empty() && !white.is_empty(), "{strokes:?}");
|
||||
// The white edge is wider than the dark line and is painted first.
|
||||
assert!(white.iter().all(|w| w.2 > dark[0].2), "{strokes:?}");
|
||||
assert_eq!(strokes[0].1, egui::Color32::WHITE);
|
||||
// The arms are centred on the pointer and leave its pixel clear.
|
||||
for (pts, ..) in &dark {
|
||||
for p in pts {
|
||||
let d = (p.x - at.x).abs().max((p.y - at.y).abs());
|
||||
assert!((2.0..=8.0).contains(&d), "{p:?}");
|
||||
assert!((p.x - at.x).abs() < 0.01 || (p.y - at.y).abs() < 0.01, "{p:?}");
|
||||
}
|
||||
}
|
||||
assert_eq!(dark.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_arrows_follow_the_angle_in_eighths() {
|
||||
// (degrees, the two-headed arrow): each covers 45° about its own direction, any turn.
|
||||
@@ -134,10 +172,10 @@ fn the_window_asks_for_the_cursor_of_the_tool_under_the_pointer() {
|
||||
("move", edge, &[], no, CursorIcon::ResizeHorizontal, "ew-resize"),
|
||||
("move", on, &[], alt, CursorIcon::Copy, "copy"),
|
||||
("hand", empty, &[], no, CursorIcon::Grab, "grab"),
|
||||
("rectangle", empty, &[], no, CursorIcon::Crosshair, "crosshair"),
|
||||
("frame", on, &[], no, CursorIcon::Crosshair, "crosshair"),
|
||||
("slice", empty, &[], no, CursorIcon::Crosshair, "crosshair"),
|
||||
("text", empty, &[], no, CursorIcon::Crosshair, "crosshair"),
|
||||
("rectangle", empty, &[], no, CursorIcon::None, "crosshair"),
|
||||
("frame", on, &[], no, CursorIcon::None, "crosshair"),
|
||||
("slice", empty, &[], no, CursorIcon::None, "crosshair"),
|
||||
("text", empty, &[], no, CursorIcon::None, "crosshair"),
|
||||
("pen", empty, &[], no, CursorIcon::None, "pen"),
|
||||
("comment", empty, &[], no, CursorIcon::None, "comment"),
|
||||
("move", empty, &[Key::Space], no, CursorIcon::Grab, "grab"),
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user