Rollover highlight: a resting pointer follows the layers frozen in the active viewport

The object under the cursor and selection cycling's badge were worked out only when the pointer
moved. Since selection respects the layers frozen in the active viewport, both depend on them:
after a layer was frozen or thawed there (VPLAYER, undo), or a viewport that freezes other
layers became the active one, a pointer that had not moved kept the object the viewport no
longer draws. That object was not highlighted, but the object now on top was not either, and
the badge stayed, until the pointer moved.

`CanvasState` remembers the frozen layers both were worked out for (`hover_frozen`), and
`update_hover` works them out again when that list changes.

Tests, each failing without its line: `the_highlight_follows_the_frozen_layers_under_a_resting_pointer`
(crates/ui-egui/tests/viewport_frozen_grips.rs) and
`the_cycling_badge_follows_the_frozen_layers_under_a_resting_pointer` (canvas/tests.rs).
This commit is contained in:
V1dalTheAbidal
2026-11-13 16:36:21 +02:00
parent eaae189f5d
commit 6754b96068
3 changed files with 69 additions and 3 deletions
+14 -2
View File
@@ -176,6 +176,9 @@ pub struct CanvasState {
pub build_ms: f64,
pub draw_ms: f64,
hover_at: Option<Pos2>,
/// The layers frozen in the active viewport when the object and the cycling badge under the
/// cursor were last worked out ([`cadcraft_engine::DocState::viewport_frozen`]).
hover_frozen: Vec<String>,
/// Constraint glyphs for the parametric overlay (cached per drawing revision).
pub param: crate::parametric::Cache,
/// A selection window opened by a press-and-drag, to close where the button is let go.
@@ -695,8 +698,17 @@ fn update_hover(app: &mut CadApp, ui: &egui::Ui, inside: bool, hover_pos: Option
// The cursor picks objects (pick box, rollover highlight): "Select objects" and single-object
// prompts, or no command with PICKFIRST on; SELECTIONPREVIEW says in which of those it shows.
let selecting = app.session.previews_selection();
// What the cursor is over also depends on the layers frozen in the active viewport, and those
// can change while the pointer rests (VPLAYER, undo, another viewport made the active one).
let refrozen = match app.session.state() {
Ok(st) if st.viewport_frozen() != app.canvas.hover_frozen.as_slice() => {
app.canvas.hover_frozen = st.viewport_frozen().to_vec();
true
}
_ => false,
};
if inside && selecting && app.session.pending_window.is_none() {
if hover_pos != app.canvas.hover_at
if (hover_pos != app.canvas.hover_at || refrozen)
&& let (Some(w), Ok(st)) = (raw_world, app.session.state())
{
let filter = app.session.settings.previewfilter;
@@ -718,7 +730,7 @@ fn update_hover(app: &mut CadApp, ui: &egui::Ui, inside: bool, hover_pos: Option
app.canvas.hover_at = None;
}
shift_space_cycle(app, ui, hover_pos.filter(|_| inside), raw_world);
if inside && hover_pos != app.canvas.cycle_badge_at {
if inside && (hover_pos != app.canvas.cycle_badge_at || refrozen) {
app.canvas.cycle_badge = raw_world.is_some_and(|w| app.session.cycling_badge(w));
app.canvas.cycle_badge_at = hover_pos;
} else if !inside {
+29
View File
@@ -366,3 +366,32 @@ fn the_viewport_highlight_leaves_out_objects_the_viewport_does_not_draw() {
app.session.execute("vplayer", &serde_json::json!({ "handles": [b], "thaw": ["Hidden"] })).unwrap();
assert_eq!(outlined(&app, &mut highlight, both), both);
}
/// Selection cycling's badge beside a resting pointer follows the layers frozen in the active
/// viewport: over two overlapping lines it shows while the viewport draws both, goes when the
/// layer of one of them is frozen there, and comes back when the layer is thawed.
#[test]
fn the_cycling_badge_follows_the_frozen_layers_under_a_resting_pointer() {
let mut app = CadApp::new(cadcraft_engine::Session::new(), crate::Services::default());
for _ in 0..2 {
app.run("line", serde_json::json!({ "points": [[0, 0], [100, 0]] })).unwrap();
}
let on_top = app.session.doc().unwrap().model.handles()[1];
let (_, b) = viewports_freezing_hidden(&mut app);
app.session.execute("properties.set", &serde_json::json!({ "handles": [on_top.hex()], "layer": "Hidden" })).unwrap();
app.session.execute("setvar", &serde_json::json!({ "name": "SELECTIONCYCLING", "value": 1 })).unwrap();
app.session.execute("mspace", &serde_json::json!({ "handle": b })).unwrap();
let ctx = egui::Context::default();
/// Whether the badge shows after a frame with the pointer resting on the two lines.
fn badge(app: &mut CadApp, ctx: &egui::Context) -> bool {
let (at, on_lines) = (Some(Pos2::new(200.0, 150.0)), Some(Vec2::new(50.0, 0.0)));
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| update_hover(app, ui, true, at, on_lines, 2.0));
output.textures_delta.clear();
app.canvas.cycle_badge
}
assert!(badge(&mut app, &ctx), "this viewport draws both lines: a click would choose between them");
app.session.execute("vplayer", &serde_json::json!({ "handles": [b], "freeze": ["Hidden"] })).unwrap();
assert!(!badge(&mut app, &ctx), "the badge stayed for a line the viewport no longer draws");
app.session.execute("vplayer", &serde_json::json!({ "handles": [b], "thaw": ["Hidden"] })).unwrap();
assert!(badge(&mut app, &ctx), "thawed in the viewport: two lines are under the cursor again");
}
+26 -1
View File
@@ -1,5 +1,6 @@
//! Inside a layout viewport an object on a layer frozen in that viewport is not drawn: selected,
//! it paints no grips there, and the cursor does not highlight it.
//! it paints no grips there, and the cursor does not highlight it, also when the layer is frozen
//! while the pointer rests over the object.
use cadcraft_engine::doc::Handle;
use cadcraft_engine::geom::Vec2;
@@ -143,3 +144,27 @@ fn the_cursor_highlights_the_object_the_viewport_draws() {
assert_eq!(app.canvas.hover, Some(under_cursor));
}
}
/// The layer of the line under a resting pointer is frozen in the active viewport, then thawed:
/// the highlight goes to the line the viewport still draws and comes back, without the pointer
/// moving in between.
#[test]
fn the_highlight_follows_the_frozen_layers_under_a_resting_pointer() {
let Sheet { mut app, shown, hidden, a: _, b } = sheet();
let ctx = egui::Context::default();
painted_grips(&mut app, &ctx);
enter(&mut app, &b);
painted_grips(&mut app, &ctx);
let on_lines = app.canvas.xf.unwrap().to_screen(Vec2::new(50.0, 0.0));
assert!(app.canvas.rect.unwrap().contains(on_lines), "the viewport must show the lines");
frame(&mut app, &ctx, Some(on_lines));
frame(&mut app, &ctx, Some(on_lines));
assert_eq!(app.canvas.hover, Some(hidden), "this viewport draws both lines, the later one on top");
app.run("vplayer", json!({ "handles": [b], "freeze": ["Hidden"] })).unwrap();
frame(&mut app, &ctx, Some(on_lines));
assert_eq!(app.canvas.hover, Some(shown), "the cursor kept the line the viewport no longer draws");
app.run("vplayer", json!({ "handles": [b], "thaw": ["Hidden"] })).unwrap();
frame(&mut app, &ctx, Some(on_lines));
assert_eq!(app.canvas.hover, Some(hidden), "thawed in the viewport: the line on top is under the cursor again");
}