mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
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:
co-authored by
Claude Opus 5.5
parent
5ed248bece
commit
edb3b5985e
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user