Merge pull request #1534 from HEOJUNFO/fix/settings-text-overflow
fix(settings): keep long translations on the Preferences pages (#1517)
This commit is contained in:
commit
ba72c8944b
|
|
@ -9,7 +9,8 @@
|
|||
//! preference key, e.g. `settings.timeline.stillImageDuration`), `settings.<key>.<value>` for the
|
||||
//! items of an open dropdown, `settings.<key>.browse` (folder fields), `settings.<key>.hex`
|
||||
//! (colour fields), `settings.labels.colors.<label>.name|color`, `settings.mediaCache.clean`,
|
||||
//! `settings.help`, `settings.reset`, `settings.cancel`, `settings.ok`.
|
||||
//! `settings.page` (the category's page), `settings.help`, `settings.reset`, `settings.cancel`,
|
||||
//! `settings.ok`.
|
||||
|
||||
use egui::{Align, Align2, Color32, CornerRadius, Frame, Layout, Margin, Rect, RichText, Sense, Stroke, Ui, vec2};
|
||||
use filmcraft_engine::settings::{self, DeviceList, Field, Kind, Row};
|
||||
|
|
@ -400,12 +401,13 @@ fn group(ui: &mut Ui, t: &Tokens, title: &str, add: impl FnOnce(&mut Ui)) {
|
|||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
/// A right-aligned field label in the fixed label column.
|
||||
/// A right-aligned field label in the fixed label column. A label longer than the column (French,
|
||||
/// Spanish…) wraps inside it: unwrapped, it ran left out of the page, and the next rows followed.
|
||||
fn field_label(ui: &mut Ui, text: &str) {
|
||||
let w = LABEL_W.min(ui.available_width() * 0.45);
|
||||
ui.allocate_ui_with_layout(vec2(w, 24.0), Layout::right_to_left(Align::Center), |ui| {
|
||||
if !text.is_empty() {
|
||||
ui.label(RichText::new(format!("{}:", crate::i18n::t(text))).size(12.5));
|
||||
ui.add(egui::Label::new(RichText::new(format!("{}:", crate::i18n::t(text))).size(12.5)).wrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -430,9 +432,13 @@ fn combo(app: &mut FilmcraftApp, ui: &mut Ui, key: &str, current: &str, opts: &[
|
|||
let shown = opts.iter().find(|o| o.0 == current).map(|o| o.1.clone()).unwrap_or_else(|| current.to_string());
|
||||
let mut chosen = None;
|
||||
let mut items: Vec<(String, Rect, String)> = Vec::new();
|
||||
let r = egui::ComboBox::from_id_salt(("settings", key)).selected_text(RichText::new(crate::i18n::t(&shown)).size(12.5)).width(width).height(480.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
// the current choice is cut to the box's width (the open list shows it whole)
|
||||
let r = egui::ComboBox::from_id_salt(("settings", key))
|
||||
.selected_text(RichText::new(crate::i18n::t(&shown)).size(12.5))
|
||||
.width(width.min(ui.available_width()))
|
||||
.truncate()
|
||||
.height(480.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (v, l) in opts {
|
||||
let r = ui.selectable_label(v == current, crate::i18n::t(l));
|
||||
items.push((format!("settings.{key}.{v}"), r.rect, l.clone()));
|
||||
|
|
@ -440,8 +446,7 @@ fn combo(app: &mut FilmcraftApp, ui: &mut Ui, key: &str, current: &str, opts: &[
|
|||
chosen = Some(v.clone());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
app.auto.add(&format!("settings.{key}"), r.response.rect, &shown);
|
||||
for (id, rect, l) in items {
|
||||
app.auto.add(&id, rect, &l);
|
||||
|
|
@ -487,6 +492,8 @@ fn draw_field(app: &mut FilmcraftApp, ui: &mut Ui, d: &mut SettingsDraft, f: &Fi
|
|||
}
|
||||
match f.kind {
|
||||
Kind::Bool => {
|
||||
// a long label wraps at the page edge
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
|
||||
let mut b = cur.as_bool().unwrap_or(false);
|
||||
let r = ui.checkbox(&mut b, RichText::new(crate::i18n::t(f.label)).size(12.5));
|
||||
app.auto.add(&format!("settings.{}", f.key), r.rect, f.label);
|
||||
|
|
@ -554,13 +561,22 @@ fn draw_field(app: &mut FilmcraftApp, ui: &mut Ui, d: &mut SettingsDraft, f: &Fi
|
|||
Kind::Font => tl!("Font family"),
|
||||
_ => "",
|
||||
};
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut s).desired_width(if matches!(f.kind, Kind::Path) { 300.0 } else { 220.0 }).hint_text(hint));
|
||||
let browse = tl!("Browse…");
|
||||
let width = if matches!(f.kind, Kind::Path) {
|
||||
// leave room for Browse… on the page, however long its translation
|
||||
let text = egui::WidgetText::from(browse).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Button);
|
||||
let button = text.size().x + 2.0 * ui.spacing().button_padding.x + ui.spacing().item_spacing.x;
|
||||
(ui.available_width() - button).clamp(60.0, 300.0)
|
||||
} else {
|
||||
ui.available_width().clamp(60.0, 220.0)
|
||||
};
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut s).desired_width(width).hint_text(hint));
|
||||
app.auto.add(&format!("settings.{}", f.key), r.rect, f.label);
|
||||
if r.changed() {
|
||||
put(&mut d.values, f.key, json!(s));
|
||||
}
|
||||
if matches!(f.kind, Kind::Path) {
|
||||
let b = ui.button(tl!("Browse…"));
|
||||
let b = ui.button(browse);
|
||||
app.auto.add(&format!("settings.{}.browse", f.key), b.rect, "Browse…");
|
||||
if b.clicked() {
|
||||
let key = f.key;
|
||||
|
|
@ -855,13 +871,15 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) -> bool {
|
|||
} else if r.hovered() {
|
||||
ui.painter().rect_filled(row, 0.0, t.hover.gamma_multiply(0.6));
|
||||
}
|
||||
ui.painter().text(
|
||||
row.left_center() + vec2(8.0, 0.0),
|
||||
Align2::LEFT_CENTER,
|
||||
crate::i18n::t(c.title),
|
||||
Tokens::ui(12.5),
|
||||
if sel { t.text } else { t.text_dim },
|
||||
);
|
||||
// a name too long for the list ends in "…" and shows whole in a tooltip
|
||||
let title = crate::i18n::t(c.title);
|
||||
let color = if sel { t.text } else { t.text_dim };
|
||||
let mut job = egui::text::LayoutJob::simple_singleline(title.to_string(), Tokens::ui(12.5), color);
|
||||
job.wrap = egui::text::TextWrapping::truncate_at_width(row.width() - 16.0);
|
||||
let galley = ui.painter().layout_job(job);
|
||||
let elided = galley.elided;
|
||||
ui.painter().galley(row.left_center() + vec2(8.0, -galley.size().y / 2.0), galley, color);
|
||||
let r = if elided { r.on_hover_text(title) } else { r };
|
||||
app.auto.add(&format!("settings.category.{}", c.id), row, c.title);
|
||||
if r.clicked() {
|
||||
d.page = c.id.into();
|
||||
|
|
@ -874,7 +892,7 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) -> bool {
|
|||
ui.vertical(|ui| {
|
||||
ui.set_width(W - LIST_W - 60.0);
|
||||
ui.set_height(BODY_H);
|
||||
egui::ScrollArea::vertical().id_salt(("settings-page", cat.id)).auto_shrink([false, false]).max_height(BODY_H).show(ui, |ui| {
|
||||
let page = egui::ScrollArea::vertical().id_salt(("settings-page", cat.id)).auto_shrink([false, false]).max_height(BODY_H).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 6.0;
|
||||
rows(app, ui, &mut d, cat.rows);
|
||||
if !d.message.is_empty() {
|
||||
|
|
@ -882,6 +900,7 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) -> bool {
|
|||
note(ui, &t, &d.message);
|
||||
}
|
||||
});
|
||||
app.auto.add("settings.page", page.inner_rect, cat.title);
|
||||
});
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
|
||||
use std::sync::mpsc::{Sender, channel};
|
||||
|
||||
use egui::accesskit::Role;
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{By, NodeT, Queryable};
|
||||
use filmcraft_engine::Session;
|
||||
use filmcraft_ui_egui::FilmcraftApp;
|
||||
use filmcraft_ui_egui::control::ControlRequest;
|
||||
|
|
@ -84,6 +86,13 @@ impl Driver {
|
|||
fn draft(&mut self) -> Value {
|
||||
self.inspect()["ui"]["settings"].clone()
|
||||
}
|
||||
/// An automation element's rect.
|
||||
fn rect(&mut self, id: &str) -> egui::Rect {
|
||||
let v = self.ok("ui.elements", json!({"prefix": id}));
|
||||
let e = v.as_array().unwrap().iter().find(|e| e["id"] == id).unwrap_or_else(|| panic!("no element {id}"));
|
||||
let r: Vec<f32> = e["rect"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap() as f32).collect();
|
||||
egui::Rect::from_min_size(egui::pos2(r[0], r[1]), egui::vec2(r[2], r[3]))
|
||||
}
|
||||
fn pref(&mut self, key: &str) -> Value {
|
||||
self.exec("prefs.get", json!({"key": key}))
|
||||
}
|
||||
|
|
@ -448,6 +457,42 @@ fn hex_field_takes_typing_a_character_at_a_time() {
|
|||
assert_eq!(hex(&mut d), "#12ab35");
|
||||
}
|
||||
|
||||
/// Long translations stay on the page (#1517): field labels wrap in their column instead of running
|
||||
/// left into the category list, dropdowns cut their current choice to their width, and checkbox
|
||||
/// text wraps at the page edge.
|
||||
#[test]
|
||||
fn long_translations_stay_on_the_page() {
|
||||
let mut d = Driver::demo();
|
||||
for lang in ["en", "fr", "es", "pt-br", "de", "ru", "uk", "id"] {
|
||||
d.exec("prefs.set", json!({"key": "general.interfaceLanguage", "value": lang}));
|
||||
d.frames(3);
|
||||
for c in CATEGORIES {
|
||||
d.menu(&format!("app.settings.{c}"));
|
||||
let page = d.rect("settings.page");
|
||||
let ok = d.rect("settings.ok");
|
||||
// the dialog's widgets only: the subtree of its area, found from its OK button
|
||||
let Some(mut dialog) = d.harness.query_all(By::new().role(Role::Button)).find(|n| n.rect().contains(ok.center())) else { panic!("no OK button") };
|
||||
while let Some(p) = dialog.parent().filter(|p| p.parent().is_some()) {
|
||||
dialog = p;
|
||||
}
|
||||
let text = By::new().predicate(|n| matches!(n.role(), Role::Label | Role::CheckBox | Role::ComboBox | Role::TextInput | Role::Button));
|
||||
let outside: Vec<String> = dialog
|
||||
.query_all(text)
|
||||
.map(|n| (n.accesskit_node().label().unwrap_or_default(), n.rect()))
|
||||
.filter(|(_, r)| r.center().y > page.top() && r.center().y < page.bottom())
|
||||
.filter(|(_, r)| r.left() < page.left() - 1.5 || r.right() > page.right() + 1.5)
|
||||
.map(|(l, r)| format!("{l:?} {:.0}..{:.0}", r.left(), r.right()))
|
||||
.collect();
|
||||
assert!(outside.is_empty(), "{lang} {c}: outside the page {:.0}..{:.0}: {outside:?}", page.left(), page.right());
|
||||
if matches!(c, "mediaAnalysis" | "mediaCache" | "memory" | "timeline") {
|
||||
d.shot(&format!("{lang}-{c}"));
|
||||
}
|
||||
d.click("settings.cancel");
|
||||
}
|
||||
}
|
||||
filmcraft_ui_egui::i18n::set_current(filmcraft_ui_egui::i18n::Language::En);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_and_media_cache_pages() {
|
||||
let mut d = Driver::demo();
|
||||
|
|
|
|||
Loading…
Reference in New Issue