UI: Composition Settings widens to fit a longer translation (#2841)

The dialog was a fixed 600 points wide inside a scroll area, so a row
wider than that in another language (French "Conserver la fréquence
d'images en cas d'imbrication ou dans la file d'attente de rendu", the
French and Spanish Lock Aspect Ratio row) pushed OK past the right edge,
reachable only by scrolling sideways.

The dialog now measures the widest row of its three tabs (as it already
measured their heights) and takes that width, or 600 points if that is
more, capped by the window as before. English keeps 600 points.
This commit is contained in:
ljkx committed 2026-11-02 19:40:51 +01:00
1 parent 9d7baea5cc
commit cab353eb5b
3 files changed
+60 -11

No files matched your search

+24 -10
View File
@@ -469,13 +469,24 @@ fn tab_body(
}
}
/// The height `tab` would take at `top`, from laying it out unseen: it follows the font, the
/// language and the scale. Nothing it registers or changes is kept.
fn tab_height(ui: &mut egui::Ui, tab: Tab, d: &CompDraft, t: &Tokens, custom: &[CompPreset], top: f32) -> f32 {
/// The width and height `tab` would take at `top`, from laying it out unseen: they follow the
/// font, the language and the scale. Nothing it registers or changes is kept.
fn tab_size(ui: &mut egui::Ui, tab: Tab, d: &CompDraft, t: &Tokens, custom: &[CompPreset], top: f32) -> egui::Vec2 {
let rect = egui::Rect::from_min_size(egui::pos2(ui.max_rect().left(), top), vec2(ui.max_rect().width(), 0.0));
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect).id_salt("comp-tab-measure").invisible());
tab_body(&mut child, tab, &mut d.clone(), t, &mut Registry::default(), custom, &mut Vec::new());
child.cursor().top() - top
vec2(child.min_rect().width(), child.cursor().top() - top)
}
fn content_width_id() -> egui::Id {
egui::Id::unique("comp-settings-content-width")
}
/// The width the dialog's content took when it was last shown: its widest row on any tab. A
/// translation longer than the English (French, #2841) is wider than the 600-point dialog, so the
/// dialog grows to it rather than hiding OK past its right edge.
pub fn content_width(ctx: &egui::Context) -> f32 {
ctx.data(|d| d.get_temp::<f32>(content_width_id())).unwrap_or(0.0)
}
/// Draw the dialog. Returns `Some(params)` when OK is pressed, `Some(Null)` on Cancel. `custom`
@@ -490,14 +501,16 @@ pub fn show(
preview: Option<&mut bool>,
) -> Option<serde_json::Value> {
let mut out = None;
ui.horizontal(|ui| {
// The dialog's visible area, which Cancel and OK stay inside.
auto.add("dialog.comp.view", ui.clip_rect().intersect(ui.max_rect()), "Composition Settings");
let name_row = ui.horizontal(|ui| {
ui.label(tr("Composition Name:"));
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(300.0));
crate::widgets::focus_on_open(ui.ctx(), &r, &d.name);
auto.add("dialog.comp.name", r.rect, "Composition Name");
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let tab_row = ui.horizontal(|ui| {
for (tab, label, key) in [(Tab::Basic, "Basic", "basic"), (Tab::Advanced, "Advanced", "advanced"), (Tab::Renderer, "3D Renderer", "renderer")] {
let r = ui.selectable_label(d.tab == tab, tr(label));
auto.add(&format!("dialog.comp.tab.{key}"), r.rect, label);
@@ -511,11 +524,12 @@ pub fn show(
let body_top = ui.cursor().top();
tab_body(ui, d.tab, d, t, auto, custom, cmds);
let shown = ui.cursor().top() - body_top;
let tallest = [Tab::Basic, Tab::Advanced, Tab::Renderer]
.into_iter()
.filter(|tab| *tab != d.tab)
.fold(shown, |h, tab| h.max(tab_height(ui, tab, d, t, custom, body_top)));
let sizes: Vec<egui::Vec2> = [Tab::Basic, Tab::Advanced, Tab::Renderer].into_iter().map(|tab| tab_size(ui, tab, d, t, custom, body_top)).collect();
let tallest = sizes.iter().fold(shown, |h, s| h.max(s.y));
ui.add_space((tallest - shown).max(0.0));
// Every tab takes the width of the widest one too, so the dialog doesn't change size with them.
let widest = sizes.iter().fold(name_row.response.rect.width().max(tab_row.response.rect.width()), |w, s| w.max(s.x));
ui.ctx().data_mut(|m| m.insert_temp(content_width_id(), widest.ceil()));
ui.add_space(14.0);
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+3 -1
View File
@@ -446,7 +446,9 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
let live = existing && super::live_preview::active(app);
let place = super::live_preview::place(app);
let mut preview = app.dialog_state.live.on;
modal_placed(ctx, "Composition Settings", vec2(600.0, 470.0), t, place, |ui| {
// 600 points wide, or as wide as the content took last time (with the 18-point margins).
let width = (super::comp_settings::content_width(ctx) + 36.0).max(600.0);
modal_placed(ctx, "Composition Settings", vec2(width, 470.0), t, place, |ui| {
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds, live.then_some(&mut preview));
});
app.dialog_state.live.on = preview;
@@ -103,3 +103,36 @@ fn composition_settings_buttons_stay_put_between_tabs() {
}
}
}
/// Composition Settings keeps Cancel and OK in view in every interface language: a translation
/// wider than the English (French, #2841) widens the dialog instead of pushing OK past its right
/// edge, where it could be reached only by scrolling sideways.
#[test]
fn composition_settings_buttons_fit_every_language() {
// English, then French (#2841) and the other catalogs whose rows overflowed the 600 points.
for language in ["en", "fr", "es", "ka", "cs", "pt-br"] {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Edit", "width": 2560, "height": 1440, "frameRate": 60, "duration": 4})).unwrap();
s.prefs.general.language = language.into();
let mut h =
Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).allow_missing_glyphs().build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
open_settings(&mut h);
let mut oks = vec![];
for tab in ["basic", "advanced", "renderer"] {
click(&mut h, &format!("dialog.comp.tab.{tab}"));
let rect = |id: &str| h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).rect;
let (view, ok, cancel) = (rect("dialog.comp.view"), rect("dialog.comp.ok"), rect("dialog.comp.cancel"));
for (name, r) in [("OK", ok), ("Cancel", cancel)] {
assert!(r[0] >= view[0] && r[0] + r[2] <= view[0] + view[2] + 0.5, "{language} {tab}: {name} {r:?} is outside the dialog {view:?}");
}
if language == "en" {
// English still fits the 600-point dialog (18-point margins).
assert_eq!(view[2], 600.0 - 36.0, "{tab}");
}
oks.push(ok);
}
// The dialog takes the width of its widest tab, so OK stays put between tabs.
assert!(oks.windows(2).all(|w| w[0] == w[1]), "{language}: OK moves between tabs: {oks:?}");
}
}