mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3551 from Taha-Mahmoodi/fix/2452-hover-while-panning
Fix #2452: panning or zooming over detailed art no longer hit-tests it every frame
This commit is contained in:
@@ -281,6 +281,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
let v = *app.view().unwrap_or(&View::default());
|
||||
let xf = Xf::new(rect, &v);
|
||||
// While the view pans, zooms or turns under the pointer, the art under it isn't hit-tested.
|
||||
let wait = hover_wait(&mut app.canvas.view_moved, ViewKey { rect, zoom: v.zoom, center: v.center, rotation: v.rotation }, ui.input(|i| i.time));
|
||||
if let Some(secs) = wait {
|
||||
ui.ctx().request_repaint_after_secs(secs as f32);
|
||||
}
|
||||
let hover_still = wait.is_none();
|
||||
let origin_preview = if app.ui.view.rulers && app.ui.screen_mode < 3 { ruler_input(app, ui, full, rect, &xf) } else { None };
|
||||
crate::document_drag::drop_on_canvas(app, &resp, &xf);
|
||||
panel_drop(app, ui, &resp, &xf);
|
||||
@@ -604,7 +610,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// Gap Options' Preview shows the gaps it finds.
|
||||
overlays.extend(crate::dialogs::live_paint_gaps::overlays(app));
|
||||
if app.ui.view.edges && !revolve_edit {
|
||||
hover_highlight(app, &painter, &xf);
|
||||
if hover_still {
|
||||
hover_highlight(app, &painter, &xf);
|
||||
}
|
||||
selection_overlay(app, &painter, &xf, overlays.iter().any(|o| matches!(o, Overlay::CurvePoint { .. })));
|
||||
mirror_axis_overlay(app, &painter, &xf);
|
||||
if app.ui.view.text_threads {
|
||||
@@ -674,8 +682,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
None
|
||||
};
|
||||
// In Presentation Mode the pointer is the default one, with no tool cursor.
|
||||
let tool_cursor =
|
||||
(app.ui.screen_mode < 3).then(|| view_cursor.or_else(|| app.hover_doc.map(|p| app.session.cursor(p, mods(m, space), view_info))));
|
||||
let tool_cursor = (app.ui.screen_mode < 3).then(|| view_cursor.or_else(|| hover_cursor(app, mods(m, space), view_info, hover_still)));
|
||||
let cur = if let Some(c) = tool_cursor.flatten() {
|
||||
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.
|
||||
@@ -717,10 +724,55 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The active tool's cursor at the pointer, as `mods` make it. While the view moves (`still`
|
||||
/// false), the cursor the tool last gave for the same tool and keys stays: asking the tool again
|
||||
/// would hit-test the art under the pointer at every frame of the move (#2452).
|
||||
fn hover_cursor(app: &mut VectorcraftApp, mods: Mods, view: vectorcraft_engine::ViewInfo, still: bool) -> Option<Cursor> {
|
||||
let id = app.session.tool_id();
|
||||
if !still
|
||||
&& let Some((was, with, c)) = app.canvas.hover_cursor
|
||||
&& (was, with) == (id, mods)
|
||||
{
|
||||
return Some(c);
|
||||
}
|
||||
let c = app.session.cursor(app.hover_doc?, mods, view);
|
||||
app.canvas.hover_cursor = Some((id, mods, c));
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// How long (seconds) the view must stay still before a canvas with
|
||||
/// [`crate::CanvasCache::waits_for_still`] set renders a heavy document again.
|
||||
pub const STILL_SECS: f64 = 0.2;
|
||||
|
||||
/// How long (seconds) the view must stay still before the canvas hit-tests the art under the
|
||||
/// pointer again, for Object Highlighting and the tool's cursor. While the view pans, zooms or
|
||||
/// turns under a resting pointer, the document point under it changes every frame, and hit-testing
|
||||
/// detailed art each time slowed every frame of the move (#2452).
|
||||
pub const HOVER_STILL_SECS: f64 = 0.15;
|
||||
|
||||
/// The view [`hover_wait`] watches: the canvas's rectangle and the view's zoom, centre and rotation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ViewKey {
|
||||
rect: egui::Rect,
|
||||
zoom: f64,
|
||||
center: Point,
|
||||
rotation: f64,
|
||||
}
|
||||
|
||||
/// Note the view `key` at time `now` (seconds, egui's input time) → how many more seconds the
|
||||
/// canvas leaves the art under the pointer alone: the view changed less than [`HOVER_STILL_SECS`]
|
||||
/// ago. The first view noted counts as still.
|
||||
fn hover_wait(moved: &mut Option<(ViewKey, f64)>, key: ViewKey, now: f64) -> Option<f64> {
|
||||
match moved {
|
||||
Some((k, at)) if *k != key => (*k, *at) = (key, now),
|
||||
Some(_) => {}
|
||||
None => *moved = Some((key, f64::NEG_INFINITY)),
|
||||
}
|
||||
let waited = moved.map_or(f64::INFINITY, |(_, at)| now - at);
|
||||
// The clock went back (a new egui context): don't wait for it to catch up.
|
||||
(0.0..HOVER_STILL_SECS).contains(&waited).then_some(HOVER_STILL_SECS - waited)
|
||||
}
|
||||
|
||||
/// Note that the canvas asks for `key` at time `now` (seconds, egui's input time) → how many more
|
||||
/// seconds it draws the frame `shown` reprojected instead of rendering `key`: while the last
|
||||
/// render was `slow`, only the view differs between the two, and the canvas has asked for `key`
|
||||
@@ -6140,6 +6192,76 @@ mod tests {
|
||||
assert!(outlines(&mut app) > 0, "Pixel Preview off again");
|
||||
}
|
||||
|
||||
/// #2452: while the view scrolls under a resting pointer, the art under it isn't hit-tested:
|
||||
/// no hover outline, and the tool's last cursor stays. Once the view has been still for
|
||||
/// [`HOVER_STILL_SECS`], the outline comes back without the pointer moving.
|
||||
#[test]
|
||||
fn a_moving_view_leaves_the_art_under_the_pointer_unhighlighted_until_it_stops() {
|
||||
fn flat(s: &Shape, out: &mut Vec<Shape>) {
|
||||
match s {
|
||||
Shape::Vec(v) => v.iter().for_each(|s| flat(s, out)),
|
||||
s => out.push(s.clone()),
|
||||
}
|
||||
}
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let id = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 400, "height": 300})).unwrap()["id"].as_u64().unwrap();
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
let layer = egui::epaint::ColorMode::Solid(c32(app.session.active().unwrap().doc.layer_color(vectorcraft_doc::NodeId(id))));
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
|
||||
let mut time = 0.0;
|
||||
let mut outlines = |app: &mut VectorcraftApp, events: Vec<egui::Event>, dt: f64| {
|
||||
time += dt;
|
||||
let mut out =
|
||||
ctx.run_ui(egui::RawInput { time: Some(time), screen_rect: Some(screen), events, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let mut shapes = vec![];
|
||||
out.shapes.iter().for_each(|c| flat(&c.shape, &mut shapes));
|
||||
shapes.iter().filter(|s| matches!(s, Shape::Path(ps) if ps.stroke.width == 1.5 && ps.stroke.color == layer)).count()
|
||||
};
|
||||
outlines(&mut app, vec![], 0.0);
|
||||
let at = app.canvas_rect.unwrap().center();
|
||||
assert!(outlines(&mut app, vec![egui::Event::PointerMoved(at)], 0.016) > 0, "the art under the resting pointer is highlighted");
|
||||
let wheel = |dy: f32| egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, dy),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
for k in 0..12 {
|
||||
let before = app.view().unwrap().center;
|
||||
let shown = outlines(&mut app, vec![wheel(if k % 2 == 0 { 4.0 } else { -4.0 })], 0.016);
|
||||
assert_ne!(app.view().unwrap().center, before, "frame {k}: the wheel scrolls the view");
|
||||
assert_eq!(shown, 0, "frame {k}: no hover outline while the view moves");
|
||||
}
|
||||
assert_eq!(outlines(&mut app, vec![], HOVER_STILL_SECS / 2.0), 0, "the view has only just stopped");
|
||||
assert!(outlines(&mut app, vec![], HOVER_STILL_SECS) > 0, "still again: the outline comes back");
|
||||
// Selected, the rectangle gives the move cursor near its top edge. Scrolled up past the
|
||||
// pointer, it stays the move cursor until the view stops, then the arrow.
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
outlines(&mut app, vec![egui::Event::PointerMoved(at - vec2(0.0, 200.0))], 0.016);
|
||||
let cursor = |app: &VectorcraftApp| app.canvas.hover_cursor.map(|(_, _, c)| c);
|
||||
assert_eq!(cursor(&app), Some(Cursor::Move));
|
||||
for k in 0..12 {
|
||||
let before = app.view().unwrap().center;
|
||||
outlines(&mut app, vec![wheel(-80.0)], 0.016);
|
||||
assert_ne!(app.view().unwrap().center, before, "frame {k}: the wheel scrolls the view");
|
||||
assert_eq!(cursor(&app), Some(Cursor::Move), "frame {k}: the cursor stays while the view moves");
|
||||
}
|
||||
// The wheel's smooth scrolling runs on for some frames; then the view rests.
|
||||
for _ in 0..200 {
|
||||
let before = app.view().unwrap().center;
|
||||
outlines(&mut app, vec![], 0.016);
|
||||
if app.view().unwrap().center == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(cursor(&app), Some(Cursor::Move), "the view has only just stopped");
|
||||
outlines(&mut app, vec![], HOVER_STILL_SECS + 0.01);
|
||||
assert_eq!(cursor(&app), Some(Cursor::Arrow), "still again: the pointer at {:?} is past the rectangle", app.hover_doc);
|
||||
}
|
||||
|
||||
/// View › Snap to Glyph toggles through the View menu command (#3124).
|
||||
#[test]
|
||||
fn snap_to_glyph_menu_command_toggles_the_view_flag() {
|
||||
|
||||
@@ -362,6 +362,12 @@ pub struct CanvasCache {
|
||||
pub content_bounds: Option<((u64, u64), Option<vectorcraft_geom::Rect>)>,
|
||||
/// The tools' cursors as OS cursor bitmaps.
|
||||
pub cursors: cursors::Images,
|
||||
/// The view the canvas last drew and when it last changed (egui's input time): while it moves,
|
||||
/// the canvas doesn't hit-test the art under the pointer ([`canvas::hover_wait`], #2452).
|
||||
pub view_moved: Option<(canvas::ViewKey, f64)>,
|
||||
/// The cursor the active tool last gave over the canvas, with the tool and the modifier keys
|
||||
/// it was for: kept while the view moves.
|
||||
pub hover_cursor: Option<(&'static str, vectorcraft_tools::Mods, vectorcraft_tools::Cursor)>,
|
||||
}
|
||||
|
||||
/// [`CanvasCache::slices`]: the layout of the slices of (document uid, revision).
|
||||
@@ -588,6 +594,8 @@ impl VectorcraftApp {
|
||||
selection_bounds: None,
|
||||
content_bounds: None,
|
||||
cursors: Default::default(),
|
||||
view_moved: None,
|
||||
hover_cursor: None,
|
||||
},
|
||||
perf: Perf::default(),
|
||||
titlebar_inset: 0.0,
|
||||
|
||||
Reference in New Issue
Block a user