From bcfb25ca93a3dd1a4916239703d7d506c3461f28 Mon Sep 17 00:00:00 2001 From: tylerjenningsw Date: Mon, 12 Oct 2026 21:24:41 -0400 Subject: [PATCH] Dialogs: share the buttons-first layout Settings uses Settings keeps its buttons in view by laying them out first, at the bottom of its body, and giving its pages the height left above them, clipped to it. That layout is now dialogs::above_footer, so other dialogs can keep their buttons in view the same way. Settings looks and works as before. --- crates/ui-egui/src/panels/dialogs.rs | 16 ++++++++ crates/ui-egui/src/panels/settings.rs | 54 ++++++++++++--------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index e95a667e..6f735e9c 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -192,6 +192,22 @@ pub(crate) fn modal_fit(ctx: &egui::Context, title: &str, size: egui::Vec2, t: & }); } +/// The buttons of a [`modal_fit`] body of `height`, kept in view: `footer` lays them out first, +/// at the bottom (bottom up: its first row is the lowest), and the rest of the body gets the room +/// above them. Returns that room (a left-to-right row, clipped to it, so a long or wide body +/// scrolls or clips under the buttons instead of pushing them out of the dialog) and its height. +pub(crate) fn above_footer(ui: &mut egui::Ui, height: f32, footer: impl FnOnce(&mut egui::Ui)) -> (egui::Ui, f32) { + let area = ui.allocate_space(vec2(ui.available_width(), height)).1; + let mut foot = ui.new_child(egui::UiBuilder::new().max_rect(area).layout(egui::Layout::bottom_up(egui::Align::Min))); + footer(&mut foot); + foot.add_space(8.0); + let body_h = (foot.min_rect().top() - area.top()).max(0.0); + let room = Rect::from_min_size(area.min, vec2(area.width(), body_h)); + let mut body = ui.new_child(egui::UiBuilder::new().max_rect(room).layout(egui::Layout::left_to_right(egui::Align::Min))); + body.set_clip_rect(room.intersect(body.clip_rect())); + (body, body_h) +} + pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context) { let Some(d) = app.dialog else { return }; let t = app.tokens; diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index f7a45feb..5f115b84 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -10,7 +10,7 @@ use crate::i18n::{tr, tr_args}; use effectcraft_engine::prefs::{APPEARANCE_MODES, DARK_THEMES, Item, Kind, LIGHT_THEMES, Page, pages}; use effectcraft_engine::segment::Task; -use egui::{Color32, Rect, RichText, vec2}; +use egui::{Color32, RichText, vec2}; use serde_json::{Value, json}; use crate::theme::{ThemeKind, Tokens}; @@ -633,37 +633,31 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let mut acts: Vec = vec![]; let (mut ok, mut cancel) = (false, false); super::dialogs::modal_fit(ctx, "Settings", vec2(820.0, 600.0), t, |ui, height| { - // The buttons go first, from the bottom up, and the pages get the height they leave: at - // a high UI Scale the pages scroll and the buttons stay in view. - let area = ui.allocate_space(vec2(ui.available_width(), height)).1; - let mut foot = ui.new_child(egui::UiBuilder::new().max_rect(area).layout(egui::Layout::bottom_up(egui::Align::Min))); - foot.horizontal(|ui| { - let r = ui.add_enabled(idx > 0, egui::Button::new(tr("Previous"))); - app.auto.add("settings.previous", r.rect, "Previous"); - if r.clicked() { - page = all[idx - 1].id.to_string(); - } - let r = ui.add_enabled(idx + 1 < all.len(), egui::Button::new(tr("Next"))); - app.auto.add("settings.next", r.rect, "Next"); - if r.clicked() { - page = all[idx + 1].id.to_string(); - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let r = ui.add(egui::Button::new(RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); - app.auto.add("settings.ok", r.rect, "OK"); - ok |= r.clicked(); - let r = ui.button(tr("Cancel")); - app.auto.add("settings.cancel", r.rect, "Cancel"); - cancel |= r.clicked(); + // The buttons go first and the pages get the height they leave: at a high UI Scale the + // pages scroll and the buttons stay in view. + let (mut body, body_h) = super::dialogs::above_footer(ui, height, |foot| { + foot.horizontal(|ui| { + let r = ui.add_enabled(idx > 0, egui::Button::new(tr("Previous"))); + app.auto.add("settings.previous", r.rect, "Previous"); + if r.clicked() { + page = all[idx - 1].id.to_string(); + } + let r = ui.add_enabled(idx + 1 < all.len(), egui::Button::new(tr("Next"))); + app.auto.add("settings.next", r.rect, "Next"); + if r.clicked() { + page = all[idx + 1].id.to_string(); + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let r = ui.add(egui::Button::new(RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); + app.auto.add("settings.ok", r.rect, "OK"); + ok |= r.clicked(); + let r = ui.button(tr("Cancel")); + app.auto.add("settings.cancel", r.rect, "Cancel"); + cancel |= r.clicked(); + }); }); + foot.separator(); }); - foot.separator(); - foot.add_space(8.0); - let body_h = (foot.min_rect().top() - area.top()).max(0.0); - // (A child, clipped to its share, so a wide page can't widen the dialog under the buttons.) - let pages = Rect::from_min_size(area.min, vec2(area.width(), body_h)); - let mut body = ui.new_child(egui::UiBuilder::new().max_rect(pages).layout(egui::Layout::left_to_right(egui::Align::Min))); - body.set_clip_rect(pages.intersect(body.clip_rect())); { let ui = &mut body; // Page list (left, like After Effects).