From fc48d84cad5ef6954081130ac47e7bdcd779002a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 21:34:46 -0800 Subject: [PATCH] Text styles: Paragraph Style Options keeps OK and Cancel on a small screen after a section switch The dialog stored the scroll area's offered height as its content height, which is taller than a short section draws. The room it gave a longer section after a switch then left too little for the rest of the window, and on a small screen OK and Cancel went below the screen. It now stores the height the scroll area shows, the smaller of the offered height and the content's height. A changed content height also lays the frame out again before it is shown, so the window is placed at its new size in the frame of the switch rather than one frame later. --- .../ui-egui/src/dialogs/para_style_options.rs | 12 ++-- .../src/dialogs/para_style_options/tests.rs | 59 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/dialogs/para_style_options.rs b/crates/ui-egui/src/dialogs/para_style_options.rs index c343e0682c..a78790d692 100644 --- a/crates/ui-egui/src/dialogs/para_style_options.rs +++ b/crates/ui-egui/src/dialogs/para_style_options.rs @@ -979,7 +979,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.separator(); let section = d.str("section"); let content_w = ui.available_width(); - egui::ScrollArea::both() + let shown = egui::ScrollArea::both() .id_salt(("para-style-sections", section)) .max_height(room) .min_scrolled_height(room) @@ -987,15 +987,17 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { .auto_shrink([false, true]) .show(ui, |ui| { ui.vertical(|ui| section_body(ui, d)); - }) - .inner_rect - .height() + }); + // `inner_rect` is the height offered before a short section shrinks the area to its content. + shown.inner_rect.height().min(shown.content_size.y) }); let content_h = drawn.inner; let was = ui.ctx().data(|m| m.get_temp::(content_id(ui))); ui.ctx().data_mut(|m| m.insert_temp(content_id(ui), content_h)); + // A new height lays the frame out again before it is shown, so the window is placed at its new + // size and OK and Cancel stay on the screen in the frame of a section switch. if was.is_none_or(|was| (was - content_h).abs() > 0.5) { - ui.ctx().request_repaint(); + ui.ctx().request_discard("paragraph style content height changed"); } reset_button(ui, d); match edited_attrs(d) { diff --git a/crates/ui-egui/src/dialogs/para_style_options/tests.rs b/crates/ui-egui/src/dialogs/para_style_options/tests.rs index 0890491606..f3528b9ce6 100644 --- a/crates/ui-egui/src/dialogs/para_style_options/tests.rs +++ b/crates/ui-egui/src/dialogs/para_style_options/tests.rs @@ -578,3 +578,62 @@ fn every_section_draws_its_fields() { } } } + +/// The texts of one headless frame at a screen `w` by `h`, with their screen rects. +fn texts_at(ctx: &egui::Context, app: &mut VectorcraftApp, w: f32, h: f32) -> Vec<(String, egui::Rect)> { + fn collect(s: &egui::Shape, out: &mut Vec<(String, egui::Rect)>) { + match s { + egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect())), + egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)), + _ => {} + } + } + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h)); + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::super::show(app, ui.ctx())); + out.textures_delta.clear(); + let mut texts = vec![]; + for c in &out.shapes { + collect(&c.shape, &mut texts); + } + texts +} + +/// Going from a shorter section to a longer one keeps OK and Cancel on a small screen in the first +/// frame after the switch, not only once the window has measured itself again. +#[test] +fn switching_to_a_long_section_keeps_the_buttons_on_a_small_screen() { + let mut app = app(); + let mut attrs: Value = serde_json::from_str(KEPT).unwrap(); + for (key, value) in [ + ("right_indent", json!(4.0)), + ("first_line_indent", json!(12.0)), + ("space_after", json!(6.0)), + ("composer", json!("singleLine")), + ("roman_hanging", json!(true)), + ("hyphenate", json!(true)), + ("justification", json!({"auto_leading": 150.0})), + ("korean_line_break_anywhere", json!(true)), + ] { + attrs[key] = value; + } + app.run("paraStyle.new", json!({"name": "Body", "attrs": attrs})).unwrap(); + app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap(); + for h in [500.0, 560.0, 640.0, 720.0] { + for to in SECTIONS { + for from in SECTIONS.into_iter().filter(|s| *s != to) { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + set(&mut app, "section", json!(from)); + for _ in 0..5 { + let _ = texts_at(&ctx, &mut app, 900.0, h); + } + set(&mut app, "section", json!(to)); + let texts = texts_at(&ctx, &mut app, 900.0, h); + for label in ["OK", "Cancel"] { + let r = texts.iter().find(|(text, _)| text == label).unwrap_or_else(|| panic!("{label} is drawn")).1; + assert!(r.bottom() <= h + 1.0, "{label} stays on a {h}-point screen going from {from} to {to}: {r:?}"); + } + } + } + } +}