Defer floating font wheel input until layer order settles

This commit is contained in:
dotCooCoo
2026-11-05 17:02:57 +07:00
committed by bflatastic
parent c5269bd096
commit a5ac92d056
2 changed files with 195 additions and 2 deletions
+69 -2
View File
@@ -13,7 +13,7 @@
//! selected text, one undo step per notch. The menu's order at the first turn stays fixed until the
//! pointer leaves the field or the text selection changes; steps stop at its first and last family.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use egui::{Color32, Sense, Ui, vec2};
@@ -436,6 +436,15 @@ struct WheelField {
layer: egui::LayerId,
}
#[derive(Clone)]
struct QueuedWheel {
frame: u64,
field: egui::Id,
position: egui::Pos2,
event: egui::Event,
turns: i32,
}
#[derive(Clone, Default)]
struct WheelFields {
pass: u64,
@@ -445,6 +454,8 @@ struct WheelFields {
pointer: Option<egui::Pos2>,
fields: Vec<WheelField>,
pending: HashMap<egui::Id, Vec<WheelInput>>,
queued: Vec<QueuedWheel>,
replay: Vec<egui::Event>,
}
#[cfg(not(target_arch = "wasm32"))]
@@ -461,6 +472,49 @@ impl egui::Plugin for WheelLayerSnapshot {
"font wheel viewport layers"
}
fn on_begin_pass(&mut self, ui: &mut Ui) {
let ctx = ui.ctx();
let viewport = ctx.viewport_id();
if viewport == egui::ViewportId::ROOT {
return;
}
let key = wheel_fields_id(viewport);
let Some(mut fields) = ctx.data(|d| d.get_temp::<WheelFields>(key)) else { return };
let frame = ctx.cumulative_frame_nr_for(viewport);
let mut ready = Vec::new();
fields.queued.retain(|event| {
if event.frame < frame {
ready.push(event.clone());
false
} else {
true
}
});
let invalid = ready
.iter()
.filter(|event| {
fields.fields.iter().find(|field| field.id == event.field).is_none_or(|field| ctx.layer_id_at(event.position) != Some(field.layer))
})
.map(|event| event.field)
.collect::<HashSet<_>>();
let replayed = fields.replay.len();
for event in ready {
if invalid.contains(&event.field) {
fields.replay.push(egui::Event::PointerMoved(event.position));
fields.replay.push(event.event);
} else if event.turns != 0 {
fields.pending.entry(event.field).or_default().push(WheelInput::Notches(event.turns));
}
}
for id in invalid {
ctx.data_mut(|d| d.remove::<f32>(id.with("wheel")));
}
if fields.replay.len() > replayed {
ctx.request_repaint();
}
ctx.data_mut(|d| d.insert_temp(key, fields));
}
fn on_end_pass(&mut self, ui: &mut Ui) {
let ctx = ui.ctx();
let viewport = ctx.viewport_id();
@@ -502,6 +556,12 @@ fn wheel_fields_id(viewport: egui::ViewportId) -> egui::Id {
egui::Id::unique("font-wheel-fields").with(viewport)
}
#[cfg(test)]
pub(crate) fn wheel_field_layer_for_test(ctx: &egui::Context, viewport: egui::ViewportId, point: egui::Pos2) -> Option<egui::LayerId> {
ctx.data(|data| data.get_temp::<WheelFields>(wheel_fields_id(viewport)))
.and_then(|fields| fields.fields.iter().find(|field| field.rect.contains(point)).map(|field| field.layer))
}
#[derive(Clone, PartialEq)]
struct WheelTarget {
input: crate::state::InputGeneration,
@@ -678,7 +738,11 @@ pub(crate) fn take_raw_wheel(ctx: &egui::Context, raw: &mut egui::RawInput) {
};
let whole = notches.trunc();
ctx.data_mut(|d| d.insert_temp(id, notches - whole));
if whole != 0.0 {
if cfg!(not(target_arch = "wasm32")) && ctx.viewport_id() != raw.viewport_id {
if let Some(position) = pointer {
fields.queued.push(QueuedWheel { frame, field: field.id, position, event: event.clone(), turns: whole as i32 });
}
} else if whole != 0.0 {
fields.pending.entry(field.id).or_default().push(WheelInput::Notches(whole as i32));
}
captured = true;
@@ -689,6 +753,9 @@ pub(crate) fn take_raw_wheel(ctx: &egui::Context, raw: &mut egui::RawInput) {
}
true
});
if !fields.replay.is_empty() {
raw.events.splice(0..0, std::mem::take(&mut fields.replay));
}
fields.pointer = pointer;
ctx.data_mut(|d| d.insert_temp(key, fields));
if captured {
@@ -34,6 +34,9 @@ struct Host {
child_transition_overlay: Option<(ViewportId, Rect, bool)>,
child_empty_overlay: Option<(ViewportId, Rect)>,
child_pass_through_overlay: Option<(ViewportId, Rect)>,
child_raise_overlay: Option<(ViewportId, Rect, bool)>,
child_raise_layer: Option<(ViewportId, egui::LayerId)>,
child_layer_order: HashMap<ViewportId, Vec<egui::LayerId>>,
/// The window with the keyboard.
focus: ViewportId,
time: f64,
@@ -98,8 +101,23 @@ impl Host {
host.borrow().child_transition_overlay.filter(|(viewport, _, _)| *viewport == id).map(|(_, rect, painted)| (rect, painted));
let child_empty_overlay = host.borrow().child_empty_overlay.filter(|(viewport, _)| *viewport == id).map(|(_, rect)| rect);
let child_pass_through_overlay = host.borrow().child_pass_through_overlay.filter(|(viewport, _)| *viewport == id).map(|(_, rect)| rect);
let child_raise_overlay = host.borrow().child_raise_overlay.filter(|(viewport, _, _)| *viewport == id).map(|(_, rect, raise)| (rect, raise));
let child_raise_layer = host.borrow().child_raise_layer.filter(|(viewport, _)| *viewport == id).map(|(_, layer)| layer);
let mut layer_order = Vec::new();
let mut full = ctx.run_ui(input, |ui| {
layer_order = ui.ctx().memory(|memory| memory.layer_ids().collect());
if let Some((rect, _)) = child_raise_overlay {
egui::Area::new(egui::Id::unique("Child raised overlay")).order(egui::Order::Background).fixed_pos(rect.min).show(ui.ctx(), |ui| {
ui.allocate_space(rect.size());
});
}
(viewport.viewport_ui_cb)(ui);
if let Some(layer) = child_raise_layer {
ui.ctx().move_to_top(layer);
}
if child_raise_overlay.is_some_and(|(_, raise)| raise) {
ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Background, egui::Id::unique("Child raised overlay")));
}
if let Some(rect) = child_overlay {
egui::Window::new("Child overlay").fixed_pos(rect.min).fixed_size(rect.size()).show(ui.ctx(), |ui| {
ui.label("Child overlay");
@@ -151,6 +169,7 @@ impl Host {
let mut shown = Vec::new();
full.shapes.iter().for_each(|c| texts(&c.shape, &mut shown));
let mut host = host.borrow_mut();
host.child_layer_order.insert(id, layer_order);
if let Some(window) = host.windows.get_mut(&id) {
window.texts = shown;
}
@@ -174,6 +193,9 @@ impl Harness {
child_transition_overlay: None,
child_empty_overlay: None,
child_pass_through_overlay: None,
child_raise_overlay: None,
child_raise_layer: None,
child_layer_order: HashMap::new(),
focus: ViewportId::ROOT,
time: 0.0,
commands: Vec::new(),
@@ -528,6 +550,7 @@ fn wheel_over_a_floating_character_font_field_changes_the_font() {
},
]);
h.frame(vec![]);
h.frame(vec![]);
assert_ne!(h.app.run("text.fonts", json!({})).unwrap(), covered);
h.host.borrow_mut().child_transparent_overlay = Some((viewport, field.expand(10.0)));
@@ -560,6 +583,7 @@ fn wheel_over_a_floating_character_font_field_changes_the_font() {
},
]);
h.frame(vec![]);
h.frame(vec![]);
assert_ne!(h.app.run("text.fonts", json!({})).unwrap(), covered);
}
@@ -658,6 +682,107 @@ fn wheel_stays_under_an_empty_interactive_overlay() {
assert_eq!(h.app.run("text.fonts", json!({})).unwrap(), covered);
}
#[test]
fn a_raised_overlay_blocks_the_next_font_wheel_event() {
let mut h = Harness::new();
h.app.run("text.create", json!({"x": 30, "y": 50, "text": "Hi", "font": "Source Sans 3"})).unwrap();
let viewport = h.float("character");
let field = h
.host
.borrow()
.windows
.get(&viewport)
.and_then(|w| w.texts.iter().find(|(text, _)| text == "Source Sans 3").map(|(_, rect)| *rect))
.expect("the Character panel's font family field");
let pointer = pos2(field.min.x + 4.0, field.center().y);
h.host.borrow_mut().child_raise_overlay = Some((viewport, field.expand(10.0), false));
h.frames(2);
let field_layer = crate::font_menu::wheel_field_layer_for_test(&h.ctx, viewport, pointer).expect("the font field layer");
h.host.borrow_mut().child_raise_layer = Some((viewport, field_layer));
h.frame(vec![]);
h.host.borrow_mut().child_raise_layer = None;
let overlay_layer = egui::LayerId::new(egui::Order::Background, egui::Id::unique("Child raised overlay"));
let before = h.app.run("text.fonts", json!({})).unwrap();
h.host.borrow_mut().child_raise_overlay = Some((viewport, field.expand(10.0), true));
h.frame(vec![]);
let order = h.host.borrow().child_layer_order.get(&viewport).cloned().expect("child layer order");
let field_index = order.iter().position(|layer| *layer == field_layer).expect("font field in layer order");
let overlay_index = order.iter().position(|layer| *layer == overlay_layer).unwrap_or_else(|| panic!("overlay absent from {order:?}"));
assert!(field_index > overlay_index, "the font field must start above the overlay: {order:?}");
h.host.borrow_mut().child_raise_overlay = Some((viewport, field.expand(10.0), false));
h.host.borrow_mut().windows.get_mut(&viewport).unwrap().events.extend([
Event::PointerMoved(pointer),
Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: vec2(0.0, -1.0),
phase: egui::TouchPhase::Move,
modifiers: Default::default(),
source: egui::MouseWheelSource::Unknown,
},
]);
h.frame(vec![]);
let order = h.host.borrow().child_layer_order.get(&viewport).cloned().expect("child layer order");
let field_index = order.iter().position(|layer| *layer == field_layer).expect("font field in layer order");
let overlay_index = order.iter().position(|layer| *layer == overlay_layer).unwrap_or_else(|| panic!("overlay absent from {order:?}"));
assert!(overlay_index > field_index, "the overlay must finish above the font field: {order:?}");
assert_eq!(h.app.run("text.fonts", json!({})).unwrap(), before);
h.frame(vec![]);
h.frame(vec![]);
assert!(
h.ctx.input_for(viewport, |input| input.raw.events.iter().any(|event| matches!(event, Event::MouseWheel { .. }))),
"the raised overlay must receive the wheel event"
);
assert_eq!(h.app.run("text.fonts", json!({})).unwrap(), before);
}
#[test]
fn an_overlay_raised_with_the_wheel_event_does_not_change_the_font() {
let mut h = Harness::new();
h.app.run("text.create", json!({"x": 30, "y": 50, "text": "Hi", "font": "Source Sans 3"})).unwrap();
let viewport = h.float("character");
let field = h
.host
.borrow()
.windows
.get(&viewport)
.and_then(|w| w.texts.iter().find(|(text, _)| text == "Source Sans 3").map(|(_, rect)| *rect))
.expect("the Character panel's font family field");
let pointer = pos2(field.min.x + 4.0, field.center().y);
h.host.borrow_mut().child_raise_overlay = Some((viewport, field.expand(10.0), false));
h.frames(2);
let field_layer = crate::font_menu::wheel_field_layer_for_test(&h.ctx, viewport, pointer).expect("the font field layer");
h.host.borrow_mut().child_raise_layer = Some((viewport, field_layer));
h.frame(vec![]);
h.host.borrow_mut().child_raise_layer = None;
h.frame(vec![]);
let overlay_layer = egui::LayerId::new(egui::Order::Background, egui::Id::unique("Child raised overlay"));
let order = h.host.borrow().child_layer_order.get(&viewport).cloned().expect("child layer order");
let field_index = order.iter().position(|layer| *layer == field_layer).expect("font field in layer order");
let overlay_index = order.iter().position(|layer| *layer == overlay_layer).expect("overlay in layer order");
assert!(field_index > overlay_index, "the font field must be above the overlay before the wheel event: {order:?}");
let before = h.app.run("text.fonts", json!({})).unwrap();
h.host.borrow_mut().child_raise_overlay = Some((viewport, field.expand(10.0), true));
h.host.borrow_mut().windows.get_mut(&viewport).unwrap().events.extend([
Event::PointerMoved(pointer),
Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: vec2(0.0, -1.0),
phase: egui::TouchPhase::Move,
modifiers: Default::default(),
source: egui::MouseWheelSource::Unknown,
},
]);
h.frame(vec![]);
assert_eq!(h.app.run("text.fonts", json!({})).unwrap(), before);
h.frame(vec![]);
h.frame(vec![]);
assert!(
h.ctx.input_for(viewport, |input| input.raw.events.iter().any(|event| matches!(event, Event::MouseWheel { .. }))),
"the raised overlay must receive the wheel event"
);
assert_eq!(h.app.run("text.fonts", json!({})).unwrap(), before);
}
#[test]
fn wheel_without_a_move_uses_the_floating_viewports_pointer() {
let mut h = Harness::new();
@@ -684,5 +809,6 @@ fn wheel_without_a_move_uses_the_floating_viewports_pointer() {
source: egui::MouseWheelSource::Unknown,
});
h.frame(vec![]);
h.frame(vec![]);
assert_ne!(h.app.run("text.fonts", json!({})).unwrap(), before);
}