diff --git a/crates/ui-egui/src/panels/comp_settings.rs b/crates/ui-egui/src/panels/comp_settings.rs index f06a069e8..0d07afd94 100644 --- a/crates/ui-egui/src/panels/comp_settings.rs +++ b/crates/ui-egui/src/panels/comp_settings.rs @@ -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 { - 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 { + 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| { diff --git a/crates/ui-egui/tests/it/ui_comp_presets.rs b/crates/ui-egui/tests/it/ui_comp_presets.rs index 0b0b30720..9e71f8fd7 100644 --- a/crates/ui-egui/tests/it/ui_comp_presets.rs +++ b/crates/ui-egui/tests/it/ui_comp_presets.rs @@ -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"); + } + } +}