From 13ede29a5e00a2743b99fa50657e890b696c42be Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 22:08:48 -0800 Subject: [PATCH] Type: route font wheel input before scroll smoothing --- crates/ui-egui/src/app/frame.rs | 2 +- crates/ui-egui/src/app/input.rs | 6 + crates/ui-egui/src/font_menu.rs | 233 ++++++++++++++++++++----- crates/ui-egui/src/font_menu/tests.rs | 93 ++++++++++ crates/ui-egui/src/panels/character.rs | 1 + crates/ui-egui/src/tests/font_menu.rs | 116 +++++++++++- 6 files changed, 404 insertions(+), 47 deletions(-) diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index 5fd9dd8d0..0b87eb16b 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -63,7 +63,7 @@ impl VectorcraftApp { /// Identify the document, modal dialog and focused widget that the current accepted input batch /// belongs to. - pub(super) fn input_generation(&self) -> InputGeneration { + pub(crate) fn input_generation(&self) -> InputGeneration { InputGeneration { document: self.session.active().map_or(0, |document| document.uid), dialog: self.ui.dialog.as_ref().map(|dialog| egui::Id::unique(&dialog.kind)), diff --git a/crates/ui-egui/src/app/input.rs b/crates/ui-egui/src/app/input.rs index 6bcfdfd3f..2854e11d6 100644 --- a/crates/ui-egui/src/app/input.rs +++ b/crates/ui-egui/src/app/input.rs @@ -175,6 +175,12 @@ impl VectorcraftApp { /// injected until the batch was actually applied, so a burst of hidden ticks cannot queue /// several steps of a drag or replay them together later. pub fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) { + self.fill_raw_input(ctx, raw); + crate::font_menu::sync_wheel_target(self, ctx, raw.viewport_id); + crate::font_menu::take_raw_wheel(ctx, raw); + } + + fn fill_raw_input(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) { // Both hosts call this hook once per host iteration, before deciding whether a UI pass // follows; it is the iteration boundary, not a UI-phase marker. self.begin_host_iteration(); diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs index 540951d58..f686b84c9 100644 --- a/crates/ui-egui/src/font_menu.rs +++ b/crates/ui-egui/src/font_menu.rs @@ -249,6 +249,7 @@ struct MenuState { /// kept while the pointer stays on it: each step makes its family a recent one, which would /// otherwise move it in the order. wheel_order: Option>, + wheel_target: Option, } /// The font family menu showing `current`, `width` points wide, rows sampling `sample` (the @@ -263,6 +264,12 @@ pub fn font_menu( ) -> Option { let state_id = ui.scope_id().with(&id).with("font-menu"); let mut st: MenuState = ui.data(|d| d.get_temp(state_id)).unwrap_or_default(); + let target = ui.data(|d| d.get_temp(wheel_fields_id(ui.ctx().viewport_id()).with("target"))); + let target_changed = st.wheel_target != target; + if target_changed { + st.wheel_order = None; + st.wheel_target = target; + } let height = st.header + LIST_HEIGHT; let state = &mut st; // The search field and filters stay in place over the rows, which scroll. The rows are all @@ -277,35 +284,53 @@ pub fn font_menu( ) }); let drawn = shown.is_some(); + if target_changed { + ui.data_mut(|d| d.remove::(field.id.with("wheel"))); + } let mut out = shown.flatten(); if !drawn && out.is_none() && st.previewed.take().is_some() { // Closed (Escape, a click outside) with a preview showing. out = Some(FontPick::EndPreview); } - if !look.wheel || drawn || !field.contains_pointer() { + let turns = remember_wheel_field(ui, &field, look.wheel && !drawn && field.enabled()); + if !look.wheel || !field.enabled() { st.wheel_order = None; ui.data_mut(|d| d.remove::(field.id.with("wheel"))); - } else if out.is_none() { - let notches = wheel_notches(ui, &field); - if !notches.is_empty() { + } else if out.is_none() || drawn { + if !turns.is_empty() { let mut own = vectorcraft_text::FontDb::global().canonical(current, "").0; - if st.wheel_order.is_none() { - st.wheel_order = Some(wheel_order(&st, look).into()); - } let mut steps = Vec::new(); - if let Some(order) = st.wheel_order.as_deref() { - for notch in notches { - let next = wheel_steps(order, &own, notch); - if let Some(last) = next.last() { - own.clone_from(last); + for turn in turns { + match turn { + WheelInput::Leave => st.wheel_order = None, + WheelInput::Notches(notches) => { + if st.wheel_order.is_none() { + st.wheel_order = Some(wheel_order(&st, look).into()); + } + if let Some(order) = st.wheel_order.as_deref() { + let next = wheel_steps(order, &own, notches); + if let Some(last) = next.last() { + own.clone_from(last); + } + steps.extend(next); + } } - steps.extend(next); } } if !steps.is_empty() { + if drawn { + // These turns preceded the opener. The popup cancels back to the newly + // applied family, and any preview drawn this pass has not been applied. + st.current.clone_from(&own); + st.previewed = None; + } out = Some(FontPick::Wheel(steps)); } } + if drawn || !field.contains_pointer() { + st.wheel_order = None; + ui.data_mut(|d| d.remove::(field.id.with("wheel"))); + } } if matches!(out, Some(FontPick::Chosen(..))) { st.previewed = None; @@ -355,40 +380,158 @@ fn wheel_steps(order: &[String], current: &str, notches: i32) -> Vec { if to >= at { order.iter().skip(at + 1).take(to - at).cloned().collect() } else { order.iter().take(at).skip(to).rev().cloned().collect() } } -/// Whole notches of the mouse wheel turned over closed field `resp` this frame, positive when it -/// turns down (towards the user). A wheel notch is a line, or `line_scroll_speed` points of a -/// trackpad or fine wheel; what is left of a notch carries over to the next turn. The wheel over -/// the field is the field's: the panel under it does not scroll. A turn with a modifier key is -/// left to the panel and the canvas. -fn wheel_notches(ui: &Ui, resp: &egui::Response) -> Vec { - let (line, id) = (ui.ctx().options(|o| o.input_options.line_scroll_speed), resp.id.with("wheel")); - let mut notches: f32 = ui.data(|d| d.get_temp(id)).unwrap_or(0.0); - let mut completed = Vec::new(); - let mut turned = false; - ui.input_mut(|i| { - i.events.retain(|e| match e { - egui::Event::MouseWheel { unit, delta, modifiers, .. } if modifiers.is_none() => { - notches -= match unit { - egui::MouseWheelUnit::Point => delta.y / line.max(1.0), - egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => delta.y, - }; - let whole = notches.trunc(); - if whole != 0.0 { - completed.push(whole as i32); - notches -= whole; - } - turned = true; - false - } - _ => true, - }); - if turned || i.modifiers.is_none() { - // The rest of a turn egui spreads over the next frames scrolls nothing either. - i.smooth_scroll_delta = egui::Vec2::ZERO; +#[derive(Clone)] +enum WheelInput { + Leave, + Notches(i32), +} + +#[derive(Clone, Copy)] +struct WheelField { + id: egui::Id, + rect: egui::Rect, + layer: egui::LayerId, +} + +#[derive(Clone, Default)] +struct WheelFields { + pass: u64, + pending_frame: u64, + activation_pass: u64, + activating: Vec, + pointer: Option, + fields: Vec, + pending: HashMap>, +} + +fn wheel_fields_id(viewport: egui::ViewportId) -> egui::Id { + egui::Id::unique("font-wheel-fields").with(viewport) +} + +/// Pending turns belong to the document, dialog and focus that received their input batch. +pub(crate) fn sync_wheel_target(app: &VectorcraftApp, ctx: &egui::Context, viewport: egui::ViewportId) { + let target = app.input_generation(); + let key = wheel_fields_id(viewport); + ctx.data_mut(|d| { + if d.get_temp::(key.with("target")).is_some_and(|old| old != target) { + d.remove::(key); } + d.insert_temp(key.with("target"), target); }); - ui.data_mut(|d| d.insert_temp(id, notches)); - completed +} + +/// Remember the visible closed fields for the next input batch and take this field's turns. +fn remember_wheel_field(ui: &Ui, resp: &egui::Response, enabled: bool) -> Vec { + let key = wheel_fields_id(ui.ctx().viewport_id()); + let pass = ui.ctx().cumulative_pass_nr(); + ui.data_mut(|d| { + let mut fields: WheelFields = d.get_temp(key).unwrap_or_default(); + if fields.pass != pass { + fields.pass = pass; + fields.fields.clear(); + } + fields.fields.retain(|f| f.id != resp.id); + if enabled { + fields.fields.push(WheelField { id: resp.id, rect: resp.interact_rect, layer: resp.layer_id }); + } + let turns = fields.pending.remove(&resp.id).unwrap_or_default(); + d.insert_temp(key, fields); + turns + }) +} + +/// Take the closed font fields' wheel events before egui smooths and combines them. Other +/// events stay in the input, so scrolling elsewhere keeps its own delta and event order. +pub(crate) fn take_raw_wheel(ctx: &egui::Context, raw: &mut egui::RawInput) { + let key = wheel_fields_id(raw.viewport_id); + let Some(mut fields) = ctx.data(|d| d.get_temp::(key)) else { return }; + if !ctx.input(|i| i.raw.viewports.contains_key(&raw.viewport_id)) { + ctx.data_mut(|d| d.remove::(key)); + return; + } + let frame = ctx.cumulative_frame_nr_for(raw.viewport_id); + let pass = ctx.cumulative_pass_nr_for(raw.viewport_id); + if pass < fields.pass || pass.saturating_sub(fields.pass) > 1 { + fields.fields.clear(); + fields.pointer = None; + } + if fields.pending_frame != frame { + fields.pending_frame = frame; + fields.pending.clear(); + } + let line = ctx.options(|o| o.input_options.line_scroll_speed).max(1.0); + let mut pointer = fields.pointer.or_else(|| ctx.input(|i| i.pointer.latest_pos())); + let over = + |field: &WheelField, point: Option| point.is_some_and(|p| field.rect.contains(p) && ctx.layer_id_at(p) == Some(field.layer)); + // An activation batch belongs to the popup, including a release after a prior press. + if fields.activation_pass != pass { + fields.activation_pass = pass; + fields.activating.clear(); + let pressed = ctx.input(|i| if i.pointer.button_down(egui::PointerButton::Primary) { i.pointer.press_origin() } else { None }); + if let Some(field) = fields.fields.iter().find(|f| over(f, pressed)) { + fields.activating.push(field.id); + } + } + let focused = ctx.memory(|m| m.focused()); + let mut captured = false; + raw.events.retain(|event| { + let opening = match event { + egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, .. } => { + fields.fields.iter().rev().find(|f| over(f, Some(*pos))).map(|f| f.id) + } + egui::Event::Key { key: egui::Key::Enter | egui::Key::Space, pressed: true, .. } if raw.focused => { + focused.filter(|id| fields.fields.iter().any(|f| f.id == *id)) + } + _ => None, + }; + if let Some(id) = opening + && !fields.activating.contains(&id) + { + fields.activating.push(id); + } + match event { + egui::Event::PointerMoved(p) | egui::Event::PointerButton { pos: p, .. } => { + for field in &fields.fields { + if over(field, pointer) && !over(field, Some(*p)) { + ctx.data_mut(|d| d.remove::(field.id.with("wheel"))); + fields.pending.entry(field.id).or_default().push(WheelInput::Leave); + } + } + pointer = Some(*p); + } + egui::Event::PointerGone => { + pointer = None; + for field in &fields.fields { + ctx.data_mut(|d| d.remove::(field.id.with("wheel"))); + fields.pending.entry(field.id).or_default().push(WheelInput::Leave); + } + } + egui::Event::MouseWheel { unit, delta, modifiers, .. } if modifiers.is_none() && delta.y != 0.0 => { + if let Some(field) = fields.fields.iter().rev().find(|f| !fields.activating.contains(&f.id) && over(f, pointer)).copied() { + let id = field.id.with("wheel"); + let mut notches: f32 = ctx.data(|d| d.get_temp(id)).unwrap_or(0.0); + notches -= match unit { + egui::MouseWheelUnit::Point => delta.y / line, + egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => delta.y, + }; + let whole = notches.trunc(); + ctx.data_mut(|d| d.insert_temp(id, notches - whole)); + if whole != 0.0 { + fields.pending.entry(field.id).or_default().push(WheelInput::Notches(whole as i32)); + } + captured = true; + return false; + } + } + _ => {} + } + true + }); + fields.pointer = pointer; + ctx.data_mut(|d| d.insert_temp(key, fields)); + if captured { + ctx.request_repaint(); + } } /// What the menu's header hands its rows. diff --git a/crates/ui-egui/src/font_menu/tests.rs b/crates/ui-egui/src/font_menu/tests.rs index 3643c54d2..73db46de3 100644 --- a/crates/ui-egui/src/font_menu/tests.rs +++ b/crates/ui-egui/src/font_menu/tests.rs @@ -262,6 +262,99 @@ fn the_wheel_follows_the_menus_order_and_filters() { assert_eq!(order, favorites, "the favourites filter"); } +/// A field removed by a later layout pass must not intercept the next wheel event. +#[test] +fn a_family_field_omitted_from_the_last_pass_leaves_the_wheel_to_its_owner() { + let ctx = egui::Context::default(); + let mut point = egui::Pos2::ZERO; + for _ in 0..3 { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + let resp = ui.button("Family"); + point = resp.rect.center(); + super::remember_wheel_field(ui, &resp, true); + }); + out.textures_delta.clear(); + } + let mut pass = 0; + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + if pass == 0 { + let resp = ui.button("Family"); + point = resp.rect.center(); + super::remember_wheel_field(ui, &resp, true); + ui.ctx().request_discard("the next pass removes the field"); + } + pass += 1; + }); + out.textures_delta.clear(); + assert!(pass > 1); + let mut raw = egui::RawInput { + events: vec![ + egui::Event::PointerMoved(point), + egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Line, + delta: egui::vec2(0.0, -1.0), + phase: egui::TouchPhase::Move, + modifiers: Default::default(), + source: egui::MouseWheelSource::Unknown, + }, + ], + ..Default::default() + }; + super::take_raw_wheel(&ctx, &mut raw); + assert!(raw.events.iter().any(|e| matches!(e, egui::Event::MouseWheel { .. }))); +} + +/// Activation leaves later wheel input to the popup; an earlier turn still belongs to the field. +#[test] +fn activating_the_family_popup_leaves_its_wheel_input_available() { + let activations = [None, Some((egui::Key::Enter, egui::Modifiers::NONE)), Some((egui::Key::Space, egui::Modifiers::SHIFT))]; + for activation in activations { + let ctx = egui::Context::default(); + let frame = |events| { + let mut raw = egui::RawInput { events, ..Default::default() }; + super::take_raw_wheel(&ctx, &mut raw); + let mut point = egui::Pos2::ZERO; + let mut out = ctx.run_ui(raw, |ui| { + let resp = ui.button("Family"); + point = resp.rect.center(); + if activation.is_some() { + resp.request_focus(); + } + super::remember_wheel_field(ui, &resp, true); + }); + out.textures_delta.clear(); + point + }; + let mut point = egui::Pos2::ZERO; + for _ in 0..3 { + point = frame(vec![]); + } + let button = + |pressed| egui::Event::PointerButton { pos: point, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + let opener = if let Some((key, modifiers)) = activation { + egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers } + } else { + frame(vec![egui::Event::PointerMoved(point), button(true)]); + button(false) + }; + let turn = || egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Line, + delta: egui::vec2(0.0, -1.0), + phase: egui::TouchPhase::Move, + modifiers: Default::default(), + source: egui::MouseWheelSource::Unknown, + }; + let mut events = vec![egui::Event::PointerMoved(point)]; + if activation.is_some() { + events.push(turn()); + } + events.extend([opener, turn()]); + let mut raw = egui::RawInput { events, ..Default::default() }; + super::take_raw_wheel(&ctx, &mut raw); + assert_eq!(raw.events.iter().filter(|e| matches!(e, egui::Event::MouseWheel { .. })).count(), 1); + } +} + /// The Font Technology filter's Variable is the font adjective, not the Variables panel's noun. #[test] fn the_variable_filter_is_named_as_fonts_are() { diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index b81ed8cd2..be3daf2d4 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -804,6 +804,7 @@ fn font_height_reference(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) { /// Control bar), with their `ids` and `widths`. The mouse wheel over the closed family field steps /// the text through the families. pub(crate) fn font_pickers(app: &mut VectorcraftApp, ui: &mut Ui, s: &CharStyle, ids: (&str, &str), widths: (f32, f32)) { + crate::font_menu::sync_wheel_target(app, ui.ctx(), ui.ctx().viewport_id()); let sample = crate::font_menu::sample_text(app); let look = crate::font_menu::MenuLook { wheel: true, ..crate::font_menu::MenuLook::of(app) }; if let Some(pick) = crate::font_menu::font_menu(ui, ids.0, &s.font_family, widths.0, sample.as_deref(), look) { diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index 42548244d..078286133 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -232,8 +232,9 @@ fn panel_frame( family: &str, draw: &dyn Fn(&mut VectorcraftApp, &mut egui::Ui), ) -> (Option, bool) { - let input = + let mut input = egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() }; + app.raw_input_hook(ctx, &mut input); let mut out = ctx.run_ui(input, |ui| { crate::font_menu::end_stale_preview(app, ui.ctx()); draw(app, ui); @@ -336,6 +337,119 @@ fn opposite_wheel_notches_in_one_frame_keep_their_undo_steps() { assert_eq!(first_style(&app, id).font_family, "Inter"); } +/// A turn before opening applies once; the popup keeps the later wheel and cancels to that font. +#[test] +fn a_wheel_before_opening_the_family_popup_keeps_its_committed_change() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let id = selected_inter(&mut app); + let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| crate::panels::character::show(app, ui); + let mut field = None; + for _ in 0..3 { + field = panel_frame(&ctx, &mut app, vec![], "Inter", &draw).0; + } + let field = field.unwrap(); + let before = app.session.active().unwrap().history.undo.len(); + let button = |pressed| egui::Event::PointerButton { pos: field, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + let turn = || wheel(egui::MouseWheelUnit::Line, -1.0, Default::default()); + panel_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(field), turn(), button(true), button(false), turn()], "Inter", &draw); + let applied = first_style(&app, id).font_family; + assert_ne!(applied, "Inter"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1); + for _ in 0..3 { + panel_frame(&ctx, &mut app, vec![], &applied, &draw); + } + panel_frame(&ctx, &mut app, key(egui::Key::Escape), &applied, &draw); + assert_eq!(first_style(&app, id).font_family, applied); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(first_style(&app, id).font_family, "Inter"); +} + +/// A later pointer move must not change which field owns an earlier wheel event. +#[test] +fn wheel_events_use_the_pointer_position_when_they_occurred() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let id = selected_inter(&mut app); + let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| crate::panels::character::show(app, ui); + let mut field = None; + for _ in 0..3 { + field = panel_frame(&ctx, &mut app, vec![], "Inter", &draw).0; + } + let field = field.expect("the Character panel shows the family field"); + let outside = egui::pos2(580.0, 780.0); + panel_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(outside)], "Inter", &draw); + let before = app.session.active().unwrap().history.undo.len(); + let line = egui::MouseWheelUnit::Line; + panel_frame(&ctx, &mut app, vec![wheel(line, -1.0, Default::default()), egui::Event::PointerMoved(field)], "Inter", &draw); + assert_eq!(first_style(&app, id).font_family, "Inter", "the wheel was outside the field"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before); + assert!(ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::MouseWheel { .. }))), "outside wheel input is left for its owner"); + panel_frame(&ctx, &mut app, vec![wheel(line, -1.0, Default::default()), egui::Event::PointerMoved(outside)], "Inter", &draw); + assert_ne!(first_style(&app, id).font_family, "Inter", "the wheel was over the field before the pointer left"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(first_style(&app, id).font_family, "Inter"); + let button = |pos| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }; + panel_frame(&ctx, &mut app, vec![button(field), wheel(line, -1.0, Default::default()), button(outside)], "Inter", &draw); + assert_ne!(first_style(&app, id).font_family, "Inter", "button events also carry the pointer's position"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1); + app.run("edit.undo", json!({})).unwrap(); + let end = egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::Vec2::ZERO, + modifiers: Default::default(), + phase: egui::TouchPhase::End, + source: egui::MouseWheelSource::Unknown, + }; + panel_frame(&ctx, &mut app, vec![end], "Inter", &draw); + panel_frame( + &ctx, + &mut app, + vec![ + wheel(egui::MouseWheelUnit::Point, -2.0, Default::default()), + egui::Event::PointerMoved(field), + wheel(line, -1.0, Default::default()), + egui::Event::PointerMoved(outside), + ], + "Inter", + &draw, + ); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1); + assert_eq!(ctx.input(|i| i.smooth_scroll_delta.y), -2.0, "only the outside wheel contributes to panel scrolling"); +} + +/// A turn queued for one document cannot restyle another document opened before its UI pass. +#[test] +fn a_wheel_queued_for_another_document_is_discarded() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + selected_inter(&mut app); + let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| crate::panels::character::show(app, ui); + let mut field = None; + for _ in 0..3 { + field = panel_frame(&ctx, &mut app, vec![], "Inter", &draw).0; + } + let field = field.expect("the Character panel shows the family field"); + let mut raw = egui::RawInput { + events: vec![egui::Event::PointerMoved(field), wheel(egui::MouseWheelUnit::Line, -1.0, Default::default())], + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), + ..Default::default() + }; + app.raw_input_hook(&ctx, &mut raw); + assert!(!raw.events.iter().any(|e| matches!(e, egui::Event::MouseWheel { .. }))); + let id = selected_inter(&mut app); + let before = app.session.active().unwrap().history.undo.len(); + let mut out = ctx.run_ui(raw, |ui| draw(&mut app, ui)); + out.textures_delta.clear(); + assert_eq!(first_style(&app, id).font_family, "Inter"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before); +} + /// #5253: the Properties panel and the Control bar have the same family field, and the wheel steps /// the families over it too. #[test]