mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 03:17:28 -06:00
* fix(ui): prevent options pane labels from overflowing (#1266) Clip each navigation label to its fixed row while preserving the full accessible name, click region and hover/selection backgrounds. Cover real painting bounds and row interactions in English and Russian at 100%, 125% and 150% interface size with 2x native display scaling. Co-Authored-By: Codex <noreply@openai.com> * Test: Options nav labels under the absolute interface size (#1142) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Zaxaerith <189452125+Zaxaerith@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Codex
Claude Opus 5.5
Zaxaerith
Brandon Thomas
parent
ae8048e3f1
commit
5f2e18e1cd
@@ -116,7 +116,8 @@ pub fn page(app: &mut WordApp, ui: &mut Ui) {
|
||||
ui.painter().rect_filled(r, 4.0, t.hover);
|
||||
}
|
||||
let font = if active { semibold(14.0) } else { crate::theme::regular(14.0) };
|
||||
ui.painter().text(egui::pos2(r.min.x + 12.0, r.center().y), egui::Align2::LEFT_CENTER, label, font, t.text);
|
||||
// Long translations stay inside the row; WidgetInfo above keeps the full name.
|
||||
ui.painter().with_clip_rect(r).text(egui::pos2(r.min.x + 12.0, r.center().y), egui::Align2::LEFT_CENTER, label, font, t.text);
|
||||
if resp.clicked() {
|
||||
app.ui.options_pane = id.to_string();
|
||||
}
|
||||
@@ -711,7 +712,7 @@ fn numeral_picker(app: &mut WordApp, ui: &mut Ui) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
use serde_json::json;
|
||||
use wordcraft_geom::Unit;
|
||||
|
||||
@@ -724,6 +725,115 @@ mod tests {
|
||||
WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default())
|
||||
}
|
||||
|
||||
fn nav_harness(language: &str, percent: u16) -> egui_kittest::Harness<'static, WordApp> {
|
||||
let mut a = app();
|
||||
a.run("ui.language", json!({"value": language})).unwrap();
|
||||
a.run("ui.interfaceSize", json!({"percent": percent})).unwrap();
|
||||
a.run("file.options", json!({"pane": "general"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_pixels_per_point(2.0).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
/// #1266: check the actual painted text and renderer clip, including the bold selected
|
||||
/// label. AccessKit must still expose the full name and the row must keep its fixed size.
|
||||
#[test]
|
||||
fn options_nav_labels_stay_inside_their_rows() {
|
||||
for language in ["en", "ru"] {
|
||||
for percent in [100, 125, 150] {
|
||||
let mut h = nav_harness(language, percent);
|
||||
// An interface size is absolute (#822): `percent` points per 100 screen pixels,
|
||||
// whatever the display's own scale (2.0 here).
|
||||
assert_eq!(h.ctx.pixels_per_point(), f32::from(percent) / 100.0);
|
||||
for selected in ["general", "accessibility"] {
|
||||
h.state_mut().run("file.options", json!({"pane": selected})).unwrap();
|
||||
h.run_steps(4);
|
||||
for id in wordcraft_engine::cmd::options::PANES {
|
||||
let label = crate::i18n::tr(crate::i18n::Lang::from_pref(language), super::pane_label(id));
|
||||
let node =
|
||||
h.query_all_by_label(label).find(|n| n.accesskit_node().toggled().is_some()).expect("pane has its full accessible name");
|
||||
assert_eq!(
|
||||
node.accesskit_node().toggled(),
|
||||
Some(if id == selected { egui::accesskit::Toggled::True } else { egui::accesskit::Toggled::False })
|
||||
);
|
||||
let row = node.rect();
|
||||
assert_eq!(row.size(), egui::vec2(170.0, 30.0));
|
||||
let (clip, text) = h
|
||||
.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.find_map(|s| match &s.shape {
|
||||
egui::Shape::Text(text) if text.galley.text() == label && row.contains(text.pos) => Some((s.clip_rect, text)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("navigation label is painted");
|
||||
let bounds = text.visual_bounding_rect();
|
||||
assert!(row.contains_rect(bounds.intersect(clip)), "{language} {percent}% {selected}: {label} paints outside {row:?}");
|
||||
if language == "ru" && id == "accessibility" {
|
||||
assert_eq!(label, "Специальные возможности");
|
||||
assert!(bounds.right() > row.right(), "exercise a label wider than the row");
|
||||
}
|
||||
if language == "en" && ["general", "display", "save"].contains(&id) {
|
||||
assert!(clip.contains_rect(bounds), "short labels must remain fully visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clipping only the painter must leave the entire row clickable, its hover/selection
|
||||
/// backgrounds intact, and the selected pane's settings visible at every interface size.
|
||||
#[test]
|
||||
fn options_nav_rows_keep_hover_and_selection() {
|
||||
for language in ["en", "ru"] {
|
||||
for percent in [100, 125, 150] {
|
||||
let mut h = nav_harness(language, percent);
|
||||
for (id, setting) in
|
||||
[("display", "Spaces"), ("save", "Save documents"), ("accessibility", "Visual effects"), ("general", "Interface")]
|
||||
{
|
||||
let lang = crate::i18n::Lang::from_pref(language);
|
||||
let row = h
|
||||
.query_all_by_label(crate::i18n::tr(lang, super::pane_label(id)))
|
||||
.find(|n| n.accesskit_node().toggled().is_some())
|
||||
.expect("pane has its full accessible name")
|
||||
.rect();
|
||||
// Both ends of the row, including its empty left inset, stay clickable.
|
||||
for x in [row.left() + 2.0, row.right() - 2.0] {
|
||||
h.state_mut().run("file.options", json!({"pane": "agents"})).unwrap();
|
||||
h.hover_at(egui::pos2(x, row.center().y));
|
||||
h.run_steps(4);
|
||||
let t = crate::theme::Tokens::get(&h.ctx);
|
||||
let background = |h: &egui_kittest::Harness<'_, WordApp>, color| {
|
||||
h.output().shapes.iter().any(|s| matches!(&s.shape, egui::Shape::Rect(r) if r.rect == row && r.fill == color))
|
||||
};
|
||||
assert!(background(&h, t.hover), "{language} {percent}%: hover covers the row");
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(egui::Event::PointerButton {
|
||||
pos: egui::pos2(x, row.center().y),
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed,
|
||||
modifiers: Default::default(),
|
||||
});
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().ui.options_pane, id);
|
||||
assert!(background(&h, t.checked), "{language} {percent}%: selection covers the row");
|
||||
assert!(h.query_by_label(crate::i18n::tr(lang, setting)).is_some(), "{id} settings are shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// File › Options settings come back after a restart, and a `ui.json` with a wrong value in
|
||||
/// one setting keeps every other setting (#485).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user