UI: Composition Settings keeps Cancel and OK in place between tabs

The button row followed each tab's content, so it jumped when switching
between Basic, Advanced and 3D Renderer. The tabs not on show are laid out
unseen each frame and the dialog pads to the tallest, so Cancel and OK stay
put with any font, language or scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pravdomil
2026-10-29 20:16:24 +01:00
co-authored by Claude Opus 5.5
parent 5ed248bece
commit edb3b5985e
2 changed files with 73 additions and 23 deletions
+53 -23
View File
@@ -246,36 +246,18 @@ fn matches(d: &CompDraft, w: u32, h: u32, par: f64, fps: f64) -> bool {
w == d.width && h == d.height && (par - d.pixel_aspect).abs() < 1e-3 && (fps - d.fps).abs() < 1e-3
}
/// Draw the dialog. Returns `Some(params)` when OK is pressed, `Some(Null)` on Cancel. `custom`
/// are the presets the user saved; saving or deleting one pushes its engine command to `cmds`.
pub fn show(
/// The controls of one tab.
fn tab_body(
ui: &mut egui::Ui,
tab: Tab,
d: &mut CompDraft,
t: &Tokens,
auto: &mut Registry,
custom: &[CompPreset],
cmds: &mut Vec<(&'static str, serde_json::Value)>,
) -> Option<serde_json::Value> {
let mut out = None;
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| {
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, label);
auto.add(&format!("dialog.comp.tab.{key}"), r.rect, label);
if r.clicked() {
d.tab = tab;
}
}
});
ui.separator();
) {
let rate = FrameRate::from_f64(d.fps);
match d.tab {
match tab {
Tab::Basic => {
egui::Grid::new("comp-basic").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| {
ui.label(tr("Preset:"));
@@ -485,6 +467,54 @@ pub fn show(
});
}
}
}
/// 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 {
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
}
/// Draw the dialog. Returns `Some(params)` when OK is pressed, `Some(Null)` on Cancel. `custom`
/// are the presets the user saved; saving or deleting one pushes its engine command to `cmds`.
pub fn show(
ui: &mut egui::Ui,
d: &mut CompDraft,
t: &Tokens,
auto: &mut Registry,
custom: &[CompPreset],
cmds: &mut Vec<(&'static str, serde_json::Value)>,
) -> Option<serde_json::Value> {
let mut out = None;
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| {
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, label);
auto.add(&format!("dialog.comp.tab.{key}"), r.rect, label);
if r.clicked() {
d.tab = tab;
}
}
});
ui.separator();
// Every tab takes the height of the tallest one, so Cancel and OK stay put between tabs.
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)));
ui.add_space((tallest - shown).max(0.0));
ui.add_space(14.0);
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
@@ -83,3 +83,23 @@ fn a_preset_name_cannot_be_a_built_in_one() {
assert!(h.state().auto.find("dialog.comp.presetName").is_none());
assert!(preset_names(&h).is_empty());
}
/// Cancel and OK stay where they are when switching between Basic, Advanced and 3D Renderer,
/// at any interface scale: the tabs' heights are measured, not assumed.
#[test]
fn composition_settings_buttons_stay_put_between_tabs() {
for zoom in [1.0, 1.3] {
let mut h = harness();
h.ctx.set_zoom_factor(zoom);
h.run_steps(3);
open_settings(&mut h);
let rects = |h: &Harness<'_, EffectcraftApp>| {
["dialog.comp.ok", "dialog.comp.cancel", "dialog.comp.name"].map(|id| h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).rect)
};
let basic = rects(&h);
for tab in ["advanced", "renderer", "basic"] {
click(&mut h, &format!("dialog.comp.tab.{tab}"));
assert_eq!(rects(&h), basic, "{tab} at {zoom}x");
}
}
}