Files
vectorcraft/crates/ui-egui/src/tests_font_menu.rs
T
bflatastic 896f8efea8 Merge #568: Fix #563: the font menu's live preview no longer flickers
# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/tests_font_menu.rs
2026-10-09 05:48:00 +07:00

379 lines
19 KiB
Rust

//! The font menu's samples, ↑/↓ highlight with live preview, and applying or undoing the preview.
use serde_json::json;
use vectorcraft_doc::NodeKind;
use vectorcraft_engine::Session;
use vectorcraft_text::FontDb;
use crate::VectorcraftApp;
use crate::font_menu::{FontPick, MenuLook, font_menu};
fn key(k: egui::Key) -> Vec<egui::Event> {
[true, false].map(|pressed| egui::Event::Key { key: k, physical_key: None, pressed, repeat: false, modifiers: Default::default() }).to_vec()
}
#[test]
fn arrows_preview_the_next_family_and_enter_applies_it() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let frame = |events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() };
let (mut at, mut picked) = (egui::Pos2::ZERO, None);
let mut out = ctx.run_ui(input, |ui| {
at = ui.next_widget_position();
picked = font_menu(ui, "test-menu", "Inter", 260.0, Some("Gagaku"), MenuLook::default());
});
out.textures_delta.clear();
(at, picked)
};
let (at, _) = frame(vec![]);
let click = at + egui::vec2(40.0, 10.0);
let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(vec![egui::Event::PointerMoved(click), button(true)]);
frame(vec![button(false)]);
for _ in 0..5 {
frame(vec![]);
}
let families = FontDb::global().families();
let inter = families.iter().position(|f| f == "Inter").unwrap();
let next = families[inter + 1].clone();
let (_, picked) = frame(key(egui::Key::ArrowDown));
assert_eq!(picked, Some(FontPick::Preview(next.clone(), None)), "↓ previews the family below the current one");
let (_, picked) = frame(key(egui::Key::ArrowUp));
assert_eq!(picked, Some(FontPick::EndPreview), "back on the current family, the preview goes");
frame(key(egui::Key::ArrowDown));
let (_, picked) = frame(key(egui::Key::Enter));
assert_eq!(picked, Some(FontPick::Chosen(next, None)));
}
#[test]
fn escape_closes_the_menu_and_ends_the_preview() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let frame = |events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() };
let (mut at, mut picked) = (egui::Pos2::ZERO, None);
let mut out = ctx.run_ui(input, |ui| {
at = ui.next_widget_position();
picked = font_menu(ui, "test-esc", "Inter", 260.0, None, MenuLook::default());
});
out.textures_delta.clear();
(at, picked)
};
let (at, _) = frame(vec![]);
let click = at + egui::vec2(40.0, 10.0);
let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(vec![egui::Event::PointerMoved(click), button(true)]);
frame(vec![button(false)]);
for _ in 0..5 {
frame(vec![]);
}
assert!(matches!(frame(key(egui::Key::ArrowDown)).1, Some(FontPick::Preview(..))));
let mut ended = false;
for events in [key(egui::Key::Escape), vec![], vec![], vec![]] {
ended |= frame(events).1 == Some(FontPick::EndPreview);
}
assert!(ended, "closing without a choice ends the preview");
}
#[test]
fn samples_show_the_text_in_the_family_or_a_generic_sample() {
let latin = crate::font_menu::render_sample_for_test("Source Serif 4", "Gagaku", 24).unwrap();
assert_eq!(latin.size[1], 24);
assert!(latin.pixels.iter().any(|p| p.a() > 0), "ink");
// Japanese text in a Latin font: the generic sample, not glyphs from another font.
let ja = crate::font_menu::render_sample_for_test("Source Serif 4", "雅楽", 24).unwrap();
let sample = crate::font_menu::render_sample_for_test("Source Serif 4", "Sample", 24).unwrap();
assert_eq!(ja.size, sample.size);
}
#[test]
fn preview_changes_the_text_live_and_only_a_choice_is_a_step() {
let ctx = egui::Context::default();
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let font = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.first_style().font_family,
_ => String::new(),
};
let steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
assert_eq!(crate::font_menu::sample_text(&app).as_deref(), Some("Gagaku"));
let before = steps(&app);
crate::font_menu::apply(&mut app, &ctx, FontPick::Preview("Source Serif 4".into(), None));
assert_eq!(font(&app), "Source Serif 4", "previewed on the text");
crate::font_menu::apply(&mut app, &ctx, FontPick::Preview("Source Sans 3".into(), None));
assert_eq!(font(&app), "Source Sans 3");
crate::font_menu::apply(&mut app, &ctx, FontPick::EndPreview);
assert_eq!((font(&app), steps(&app)), ("Inter".to_string(), before), "the preview leaves nothing behind");
crate::font_menu::apply(&mut app, &ctx, FontPick::Preview("Source Serif 4".into(), None));
crate::font_menu::apply(&mut app, &ctx, FontPick::Chosen("Source Serif 4".into(), None));
assert_eq!((font(&app), steps(&app)), ("Source Serif 4".to_string(), before + 1), "one step");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(font(&app), "Inter");
}
/// A preview whose menu isn't drawn any more (the text was deselected, the panel closed) is undone
/// at the start of the next frame, before the canvas sees its input.
#[test]
fn a_preview_left_open_by_a_menu_that_is_gone_is_undone() {
let ctx = egui::Context::default();
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let steps = app.session.active().unwrap().history.undo.len();
crate::font_menu::apply(&mut app, &ctx, FontPick::Preview("Source Serif 4".into(), None));
assert!(app.session.in_interaction());
crate::font_menu::end_stale_preview(&mut app, &ctx);
assert!(!app.session.in_interaction(), "no menu drawn: the preview is undone");
let font = match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.first_style().font_family,
_ => String::new(),
};
assert_eq!((font.as_str(), app.session.active().unwrap().history.undo.len()), ("Inter", steps));
// With nothing previewed it does nothing.
crate::font_menu::end_stale_preview(&mut app, &ctx);
assert_eq!(app.session.active().unwrap().history.undo.len(), steps);
}
/// A press outside the open menu ends the preview in that frame, before the canvas could act on
/// it inside the preview's interaction (the menu itself closes on the release).
#[test]
fn a_press_outside_the_menu_ends_the_preview_at_once() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let mut frame = |events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() };
let mut at = egui::Pos2::ZERO;
let mut out = ctx.run_ui(input, |ui| {
crate::font_menu::end_stale_preview(&mut app, ui.ctx());
at = ui.next_widget_position();
if let Some(pick) = font_menu(ui, "test-press", "Inter", 260.0, None, MenuLook::default()) {
crate::font_menu::apply(&mut app, ui.ctx(), pick);
}
});
out.textures_delta.clear();
(at, app.session.in_interaction())
};
let (at, _) = frame(vec![]);
let click = at + egui::vec2(40.0, 10.0);
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(vec![egui::Event::PointerMoved(click), button(click, true)]);
frame(vec![button(click, false)]);
for _ in 0..5 {
frame(vec![]);
}
assert!(frame(key(egui::Key::ArrowDown)).1, "↓ previews");
assert!(frame(vec![]).1, "the preview lasts while the menu is open");
let outside = egui::pos2(580.0, 780.0);
assert!(!frame(vec![egui::Event::PointerMoved(outside), button(outside, true)]).1, "the press ends the preview");
assert!(!frame(vec![button(outside, false)]).1);
}
#[test]
fn bundled_families_are_classified() {
use vectorcraft_text::FontClass;
let db = FontDb::global();
let class = |f: &str| db.family_traits(f).class;
assert_eq!(class("JetBrains Mono"), FontClass::Monospaced);
assert_eq!(class("Source Serif 4"), FontClass::Serif);
assert_eq!(class("Source Sans 3"), FontClass::Sans);
assert!(!db.family_traits("Inter").japanese);
}
#[test]
fn right_arrow_lists_a_familys_styles_and_they_preview() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let frame = |events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() };
let (mut at, mut picked) = (egui::Pos2::ZERO, None);
let mut out = ctx.run_ui(input, |ui| {
at = ui.next_widget_position();
picked = font_menu(ui, "test-styles", "Source Sans 3", 260.0, None, MenuLook::default());
});
out.textures_delta.clear();
(at, picked)
};
let (at, _) = frame(vec![]);
let click = at + egui::vec2(40.0, 10.0);
let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(vec![egui::Event::PointerMoved(click), button(true)]);
frame(vec![button(false)]);
for _ in 0..5 {
frame(vec![]);
}
let styles = FontDb::global().styles("Source Sans 3");
assert!(styles.len() > 1);
frame(key(egui::Key::ArrowRight));
let (_, picked) = frame(key(egui::Key::ArrowDown));
assert_eq!(picked, Some(FontPick::Preview("Source Sans 3".into(), Some(styles[0].clone()))), "the first style under the family");
let (_, picked) = frame(key(egui::Key::ArrowDown));
assert_eq!(picked, Some(FontPick::Preview("Source Sans 3".into(), Some(styles[1].clone()))));
let (_, picked) = frame(key(egui::Key::Enter));
assert_eq!(picked, Some(FontPick::Chosen("Source Sans 3".into(), Some(styles[1].clone()))));
}
#[test]
fn stars_mark_favourites_and_the_preferences_size_the_rows() {
let mut app = VectorcraftApp::new(Session::new(), crate::Services::default());
let ctx = egui::Context::default();
crate::font_menu::apply(&mut app, &ctx, FontPick::Favorite("Inter".into()));
crate::font_menu::apply(&mut app, &ctx, FontPick::Favorite("Source Serif 4".into()));
assert_eq!(app.ui.favorite_fonts, ["Inter", "Source Serif 4"]);
crate::font_menu::apply(&mut app, &ctx, FontPick::Favorite("inter".into()));
assert_eq!(app.ui.favorite_fonts, ["Source Serif 4"], "a second click takes the star off (any case)");
let saved: crate::state::UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap();
assert_eq!(saved.favorite_fonts, ["Source Serif 4"], "favourites are kept with the UI state");
let look = MenuLook::of(&app);
assert!(look.samples && look.row == 24.0);
app.run("prefs.set", json!({"values": {"fontPreviewSize": "large", "fontPreview": false}})).unwrap();
let look = MenuLook::of(&app);
assert!(!look.samples && look.row > 24.0, "Font Preview Size and Enable in-menu font previews");
}
/// Find Font's Replace With and the Glyphs panel only pick a font: their menu's highlight never
/// previews on the document, a choice is just returned, and a star is still toggled.
#[test]
fn menus_that_only_pick_a_font_leave_the_document_alone() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let font = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.first_style().font_family,
_ => String::new(),
};
let steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
let before = steps(&app);
use crate::font_menu::picked;
assert_eq!(picked(&mut app, None), None);
assert_eq!(picked(&mut app, Some(FontPick::Preview("Source Serif 4".into(), None))), None);
assert_eq!(picked(&mut app, Some(FontPick::EndPreview)), None);
assert_eq!(
picked(&mut app, Some(FontPick::Chosen("Source Serif 4".into(), Some("Bold".into())))),
Some(("Source Serif 4".to_string(), Some("Bold".to_string())))
);
assert_eq!((font(&app), steps(&app)), ("Inter".to_string(), before), "nothing previewed or applied on the text");
assert_eq!(picked(&mut app, Some(FontPick::Favorite("Inter".into()))), None);
assert_eq!(app.ui.favorite_fonts, ["Inter"], "a star is still toggled");
assert_eq!(picked(&mut app, Some(FontPick::Favorite("inter".into()))), None);
assert!(app.ui.favorite_fonts.is_empty());
}
/// #555: the menu's search field and filters stay at its top while the wheel scrolls the families,
/// past the list's end too: the list is the menu's one scrolling part.
#[test]
fn the_search_field_stays_put_while_the_list_scrolls() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let frame = |events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 500.0))), ..Default::default() };
let mut at = egui::Pos2::ZERO;
let mut out = ctx.run_ui(input, |ui| {
// Low in the window, as in a panel at the bottom of the dock: the menu opens upwards.
ui.add_space(390.0);
at = ui.next_widget_position();
font_menu(ui, "test-scroll", "Inter", 260.0, None, MenuLook::default());
});
out.textures_delta.clear();
let search = out.shapes.iter().find_map(|s| match &s.shape {
egui::Shape::Text(t) if t.galley.text() == "Search" => Some(t.pos),
_ => None,
});
(at, search)
};
let (at, _) = frame(vec![]);
let click = at + egui::vec2(40.0, 10.0);
let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(vec![egui::Event::PointerMoved(click), button(true)]);
frame(vec![button(false)]);
let mut field = None;
for _ in 0..5 {
field = frame(vec![]).1;
}
let field = field.expect("the search field shows");
let over_list = egui::pos2(field.x + 40.0, field.y + 120.0);
for dy in [-400.0, -4000.0, -4000.0] {
let wheel = egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, dy),
modifiers: Default::default(),
phase: egui::TouchPhase::Move,
};
frame(vec![egui::Event::PointerMoved(over_list), wheel]);
for _ in 0..10 {
assert_eq!(frame(vec![]).1, Some(field), "the field stays where it was");
}
}
}
/// #563: a preview stays on the text while the menu stays on its family. The menu's caller passes
/// the text's font, which the preview changed; the menu keeps the font from before the preview as
/// the current one, so it doesn't take the previewed family for it and end the preview, then
/// preview it again, every frame (the text flickered).
#[test]
fn a_preview_stays_put_while_the_menu_is_still() {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let font = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.first_style().font_family,
_ => String::new(),
};
let revision = |app: &VectorcraftApp| app.session.active().unwrap().revision;
// A panel's frame: the menu shows the text's font, and what it picks is applied.
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() };
let current = font(app);
let (mut at, mut picked) = (egui::Pos2::ZERO, None);
let mut out = ctx.run_ui(input, |ui| {
crate::font_menu::end_stale_preview(app, ui.ctx());
at = ui.next_widget_position();
picked = font_menu(ui, "test-steady", &current, 260.0, None, MenuLook::default());
if let Some(p) = picked.clone() {
crate::font_menu::apply(app, ui.ctx(), p);
}
});
out.textures_delta.clear();
(at, picked)
};
let (at, _) = frame(&mut app, vec![]);
let click = at + egui::vec2(40.0, 10.0);
let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, vec![egui::Event::PointerMoved(click), button(true)]);
frame(&mut app, vec![button(false)]);
for _ in 0..5 {
frame(&mut app, vec![]);
}
let (_, picked) = frame(&mut app, key(egui::Key::ArrowDown));
assert!(matches!(picked, Some(FontPick::Preview(..))), "{picked:?}");
let previewed = font(&app);
assert_ne!(previewed, "Inter");
let rev = revision(&app);
for _ in 0..6 {
let (_, picked) = frame(&mut app, vec![]);
assert_eq!(picked, None, "nothing more to apply");
}
assert_eq!((font(&app), revision(&app)), (previewed, rev), "the preview stays, untouched");
// Back on the text's own family, the preview ends.
let (_, picked) = frame(&mut app, key(egui::Key::ArrowUp));
assert_eq!(picked, Some(FontPick::EndPreview));
assert_eq!(font(&app), "Inter");
}