LR-EXP-DIALOG: export dialog no longer widens every frame
Rows with a trailing button (Folder / Choose…, watermark Graphic / Choose…, Save preset / Save) sized their text field as the available width minus a guessed button width. When the real button was wider (e.g. Linux fonts, a folder picker present), the auto-sized dialog grew a little every frame. These rows now lay out right to left: the button first, then a field taking exactly the width that's left. Test: export_dialog_keeps_its_width (grew 492 → 693 px in 60 frames before; stable now). Fixes #8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f5a4bb609d
commit
b8c2d16a06
|
|
@ -553,6 +553,41 @@ mod tests {
|
|||
|
||||
/// The Export dialog hands its batch to a worker thread: the UI keeps drawing frames, shows
|
||||
/// progress, and reports the result when the files are written.
|
||||
#[test]
|
||||
fn export_dialog_keeps_its_width() {
|
||||
// A dialog row sized as "available width − a guessed button width" made the auto-sized
|
||||
// window grow a little every frame when the real button was wider (issue #8, Linux).
|
||||
let mut h = demo([1400.0, 900.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.app.services.pick_folder = Some(Box::new(|| None));
|
||||
h.app.services.pick_files = Some(Box::new(Vec::new));
|
||||
h.request("engine.execute", json!({"command": "dialog.export", "params": {}}), t);
|
||||
// the widest variant: watermark graphic and every optional row
|
||||
if let Some(crate::state::Dialog::Export { opts, .. }) = &mut h.app.ui.dialog {
|
||||
let mut wm = lightcraft_engine::export::Watermark { image: "logo.png".into(), ..Default::default() };
|
||||
wm.text.clear();
|
||||
opts.watermark = Some(wm);
|
||||
}
|
||||
let width = |h: &mut Headless| {
|
||||
let r = h.request("ui.widgets", json!({}), t);
|
||||
r["result"]
|
||||
.as_array()
|
||||
.and_then(|a| a.iter().find(|w| w["id"] == "dialog:window"))
|
||||
.map(|w| w["rect"][2].as_f64().unwrap())
|
||||
.expect("dialog on screen")
|
||||
};
|
||||
for _ in 0..5 {
|
||||
h.step();
|
||||
}
|
||||
let w0 = width(&mut h);
|
||||
for _ in 0..60 {
|
||||
h.step();
|
||||
}
|
||||
let w1 = width(&mut h);
|
||||
assert!((w1 - w0).abs() < 0.5, "the export dialog grew from {w0} to {w1}");
|
||||
assert!(w1 < 700.0, "{w1}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_dialog_runs_in_the_background() {
|
||||
let mut h = demo([1200.0, 900.0]);
|
||||
|
|
|
|||
|
|
@ -528,15 +528,15 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
if graphic {
|
||||
field(ui, "Graphic", |ui| {
|
||||
let can_pick = app.services.pick_files.is_some();
|
||||
let w = ui.available_width() - if can_pick { 76.0 } else { 0.0 };
|
||||
ui.add(egui::TextEdit::singleline(&mut wm.image).hint_text("logo.png").desired_width(w));
|
||||
if can_pick
|
||||
&& crate::widgets::text_button(ui, "exportWmChoose", "Choose…", false).clicked()
|
||||
&& let Some(f) = app.services.pick_files.as_mut().and_then(|pick| pick().into_iter().next())
|
||||
{
|
||||
wm.image = f;
|
||||
}
|
||||
trailing_button_row(ui, |ui| {
|
||||
if app.services.pick_files.is_some()
|
||||
&& crate::widgets::text_button(ui, "exportWmChoose", "Choose…", false).clicked()
|
||||
&& let Some(f) = app.services.pick_files.as_mut().and_then(|pick| pick().into_iter().next())
|
||||
{
|
||||
wm.image = f;
|
||||
}
|
||||
ui.add(egui::TextEdit::singleline(&mut wm.image).hint_text("logo.png").desired_width(ui.available_width()));
|
||||
});
|
||||
});
|
||||
let mut width = wm.image_width as f64 * 100.0;
|
||||
if num(ui, &WM_IMAGE_WIDTH, &mut width) {
|
||||
|
|
@ -586,16 +586,16 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
field(ui, "Folder", |ui| {
|
||||
let can_pick = app.services.pick_folder.is_some();
|
||||
let w = ui.available_width() - if can_pick { 76.0 } else { 0.0 };
|
||||
ui.add(egui::TextEdit::singleline(dir).desired_width(w));
|
||||
if can_pick
|
||||
&& crate::widgets::text_button(ui, "exportChooseFolder", "Choose…", false).clicked()
|
||||
&& let Some(pick) = app.services.pick_folder.as_mut()
|
||||
&& let Some(d) = pick()
|
||||
{
|
||||
*dir = d;
|
||||
}
|
||||
trailing_button_row(ui, |ui| {
|
||||
if app.services.pick_folder.is_some()
|
||||
&& crate::widgets::text_button(ui, "exportChooseFolder", "Choose…", false).clicked()
|
||||
&& let Some(pick) = app.services.pick_folder.as_mut()
|
||||
&& let Some(d) = pick()
|
||||
{
|
||||
*dir = d;
|
||||
}
|
||||
ui.add(egui::TextEdit::singleline(dir).desired_width(ui.available_width()));
|
||||
});
|
||||
});
|
||||
field(ui, "Subfolder", |ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut opts.subfolder).hint_text("none").desired_width(f32::INFINITY))
|
||||
|
|
@ -610,19 +610,20 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
);
|
||||
ui.add_space(4.0);
|
||||
field(ui, "Save preset", |ui| {
|
||||
let w = ui.available_width() - 60.0;
|
||||
ui.add(egui::TextEdit::singleline(preset_name).hint_text("Preset name").desired_width(w));
|
||||
let named = !preset_name.trim().is_empty();
|
||||
if crate::widgets::text_button(ui, "exportSavePreset", "Save", false).clicked() && named {
|
||||
let params = export_dialog_params(opts, *full_size, resize, *limit_kb);
|
||||
match app.run("export.savePreset", json!({"name": preset_name.trim(), "params": params})) {
|
||||
Ok(_) => {
|
||||
app.toast(ui.ctx(), format!("Saved export preset “{}”", preset_name.trim()));
|
||||
preset_name.clear();
|
||||
trailing_button_row(ui, |ui| {
|
||||
let named = !preset_name.trim().is_empty();
|
||||
if crate::widgets::text_button(ui, "exportSavePreset", "Save", false).clicked() && named {
|
||||
let params = export_dialog_params(opts, *full_size, resize, *limit_kb);
|
||||
match app.run("export.savePreset", json!({"name": preset_name.trim(), "params": params})) {
|
||||
Ok(_) => {
|
||||
app.toast(ui.ctx(), format!("Saved export preset “{}”", preset_name.trim()));
|
||||
preset_name.clear();
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
ui.add(egui::TextEdit::singleline(preset_name).hint_text("Preset name").desired_width(ui.available_width()));
|
||||
});
|
||||
});
|
||||
}
|
||||
Dialog::Merge { opts } => crate::merge::body(app, ui, opts),
|
||||
|
|
@ -722,6 +723,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
if let Some(w) = shown {
|
||||
ctx.move_to_top(w.response.layer_id);
|
||||
crate::widgets::register(ctx, "dialog:window", w.response.rect);
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
close = true;
|
||||
|
|
@ -983,6 +985,14 @@ fn field<R>(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui) -> R
|
|||
.inner
|
||||
}
|
||||
|
||||
/// The rest of a [`field`] row, laid out right to left: `add` places its trailing button(s) first,
|
||||
/// then a text field taking exactly the width that's left. (Subtracting a guessed button width
|
||||
/// instead made the auto-sized dialog grow every frame when the real button was wider, #8.)
|
||||
fn trailing_button_row(ui: &mut egui::Ui, add: impl FnOnce(&mut egui::Ui)) {
|
||||
let h = ui.spacing().interact_size.y.max(24.0);
|
||||
ui.allocate_ui_with_layout(egui::vec2(ui.available_width(), h), egui::Layout::right_to_left(egui::Align::Center), add);
|
||||
}
|
||||
|
||||
/// A labelled row of mutually exclusive choice buttons (ids `button:{id}-{index}`).
|
||||
fn choices<V: PartialEq + Copy>(ui: &mut egui::Ui, label: &str, id: &str, options: &[(V, &str)], value: &mut V) {
|
||||
field(ui, label, |ui| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue