mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 13:36:53 +00:00
Merge pull request #7045 from dotCooCoo/fix/char-style-options-height
Text styles: Character Style Options opens at its full height
This commit is contained in:
@@ -942,27 +942,23 @@ pub(crate) fn opentype_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The key under which the dialog keeps its footer's height for the next frame: the dialog's own
|
||||
/// id, so the stored value is found on every frame.
|
||||
fn footer_id(ui: &egui::Ui) -> egui::Id {
|
||||
ui.layer_id().id.with("char-style-footer")
|
||||
/// The key under which the dialog keeps the height its section content was drawn at, for the next
|
||||
/// frame.
|
||||
fn content_id(ui: &egui::Ui) -> egui::Id {
|
||||
ui.layer_id().id.with("char-style-content")
|
||||
}
|
||||
|
||||
/// The dialog's body: the section list beside the selected section's content, which scrolls when it
|
||||
/// is taller than the window (General lists every attribute of the style and OpenType every
|
||||
/// feature), and the footer below it - Reset Panel, the Preview checkbox and an invalid value's
|
||||
/// error. The content's height is the space the footer leaves, measured last frame, so the buttons
|
||||
/// stay on screen at any interface scale; the section list is outside the scroll area and stays put
|
||||
/// while the content scrolls. Returns whether the dialog asks to be cancelled (never: Cancel comes
|
||||
/// from the button row in `dialogs::show`).
|
||||
/// The dialog's body: the section list beside the selected section's content, and below them
|
||||
/// Reset Panel, the Preview checkbox and an invalid value's error. The content is at most 420 points
|
||||
/// tall, and scrolls when it is taller than that or than the screen less the rest of the window
|
||||
/// (as drawn last frame). The section list stays put while the content scrolls. Returns whether
|
||||
/// the dialog asks to be canceled (never: Cancel comes from the button row in `dialogs::show`).
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
// The content scrolls inside a shorter window: General lists every attribute of the style and
|
||||
// OpenType every feature, so the window stays within the screen and its buttons stay visible.
|
||||
// The footer was measured last frame, so the room left for the content keeps it on screen at any
|
||||
// interface scale; the section list stays put while the content scrolls.
|
||||
let footer = ui.ctx().data(|m| m.get_temp::<f32>(footer_id(ui)).unwrap_or(120.0));
|
||||
let room = (ui.available_height() - footer - 8.0).clamp(0.0, 420.0);
|
||||
ui.horizontal(|ui| {
|
||||
let window = ui.ctx().memory(|m| m.area_rect(ui.layer_id().id)).map_or(0.0, |r| r.height());
|
||||
let content = ui.ctx().data(|m| m.get_temp::<f32>(content_id(ui))).unwrap_or(0.0);
|
||||
let rest = (window - content).max(0.0);
|
||||
let room = (ui.ctx().content_rect().height() - rest - 2.0 * super::EDGE_GAP).clamp(0.0, 420.0);
|
||||
let drawn = ui.horizontal(|ui| {
|
||||
section_list(ui, d);
|
||||
ui.separator();
|
||||
// The section list handles a click before this frame's content is laid out, so the section
|
||||
@@ -971,9 +967,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let content_w = ui.available_width();
|
||||
// Long sections scroll both ways: at a large interface scale the window has fewer points, and
|
||||
// a wide row (a character-format grid) must stay inside it rather than widen the window.
|
||||
egui::ScrollArea::both().id_salt(("char-style-sections", section)).max_height(room).max_width(content_w).auto_shrink([false, true]).show(
|
||||
ui,
|
||||
|ui| {
|
||||
let shown = egui::ScrollArea::both()
|
||||
.id_salt(("char-style-sections", section))
|
||||
.max_height(room)
|
||||
.min_scrolled_height(room)
|
||||
.max_width(content_w)
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
if d.str("section") == "basic" {
|
||||
basic_body(ui, d);
|
||||
@@ -996,28 +996,24 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
// `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 footer_h = ui
|
||||
.scope(|ui| {
|
||||
reset_button(ui, d);
|
||||
match edited_attrs(d) {
|
||||
Ok(attrs) => {
|
||||
let params = json!({"name": d.str("__style"), "to": d.str("name"), "attrs": attrs});
|
||||
form::preview(app, ui, d, tl!("Character Style Options"), "charStyle.edit", params);
|
||||
}
|
||||
Err(why) => attrs_error(ui, &why),
|
||||
}
|
||||
})
|
||||
.response
|
||||
.rect
|
||||
.height();
|
||||
let was = ui.ctx().data(|m| m.get_temp::<f32>(footer_id(ui)));
|
||||
ui.ctx().data_mut(|m| m.insert_temp(footer_id(ui), footer_h));
|
||||
if was != Some(footer_h) {
|
||||
let content_h = drawn.inner;
|
||||
let was = ui.ctx().data(|m| m.get_temp::<f32>(content_id(ui)));
|
||||
ui.ctx().data_mut(|m| m.insert_temp(content_id(ui), content_h));
|
||||
if was.is_none_or(|was| (was - content_h).abs() > 0.5) {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
reset_button(ui, d);
|
||||
match edited_attrs(d) {
|
||||
Ok(attrs) => {
|
||||
let params = json!({"name": d.str("__style"), "to": d.str("name"), "attrs": attrs});
|
||||
form::preview(app, ui, d, tl!("Character Style Options"), "charStyle.edit", params);
|
||||
}
|
||||
Err(why) => attrs_error(ui, &why),
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1127,6 +1123,98 @@ mod tests {
|
||||
let basic = ctx.memory(|m| m.area_rect(dialog)).expect("the dialog is drawn").width();
|
||||
assert!((basic - width).abs() < 0.5, "the width does not change with the section: {width} -> {basic}");
|
||||
}
|
||||
|
||||
/// On a 1920 by 1080 screen the dialog opens with its content 420 points tall when the General
|
||||
/// list is longer than that: the first frame that draws it, with no input, already has the full
|
||||
/// height, and later frames keep it.
|
||||
#[test]
|
||||
fn the_sections_open_at_their_full_height_without_input() {
|
||||
let mut app = app();
|
||||
app.run(
|
||||
"charStyle.new",
|
||||
json!({"name": "Body", "attrs": {
|
||||
"font_family": "Source Sans 3", "font_style": "Regular", "size": 24.0, "leading": 30.0,
|
||||
"kerning": 0.0, "tracking": 5.0, "all_caps": false, "small_caps": null,
|
||||
"position": {"kind": "normal", "size": 100.0, "position": 0.0}, "underline": false,
|
||||
"strikethrough": false, "standardVerticalRoman": false, "charAlign": "romanBaseline",
|
||||
"h_scale": 100.0, "v_scale": 100.0, "baseline_shift": 0.0, "rotation": 0.0,
|
||||
"language": "English: USA", "overprint_fill": false, "overprint_stroke": false,
|
||||
"stroke_width": 0.0, "features": ["liga", "calt", "dlig", "swsh", "salt", "titl", "ordn", "frac"]
|
||||
}}),
|
||||
)
|
||||
.unwrap();
|
||||
open(&mut app, "Body").unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1920.0, 1080.0));
|
||||
let mut drawn = 0;
|
||||
for _ in 0..6 {
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::super::show(&mut app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
let texts = texts_of(&out);
|
||||
let top = |label: &str| texts.iter().find(|(text, _)| text == label).map(|r| r.1.top());
|
||||
// The first frame only measures the window and draws no text.
|
||||
let (Some(reset), Some(general)) = (top("Reset Panel"), top("General")) else { continue };
|
||||
drawn += 1;
|
||||
let content = reset - general;
|
||||
assert!(content >= 410.0, "the content is 420 points tall in drawn frame {drawn}: {content}");
|
||||
}
|
||||
assert!(drawn >= 4, "the dialog is drawn: {drawn} frames");
|
||||
// On the smallest window the buttons are on the screen in the first frame that draws them.
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 500.0));
|
||||
for _ in 0..3 {
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::super::show(&mut app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
let texts = texts_of(&out);
|
||||
let Some(ok) = texts.iter().find(|(text, _)| text == "OK").map(|r| r.1) else { continue };
|
||||
let cancel = texts.iter().find(|(text, _)| text == "Cancel").expect("Cancel is drawn with OK").1;
|
||||
assert!(ok.bottom() <= 501.0 && cancel.bottom() <= 501.0, "the buttons are on an 800 by 500 screen when first drawn: {ok:?} {cancel:?}");
|
||||
return;
|
||||
}
|
||||
panic!("the dialog is drawn on an 800 by 500 screen");
|
||||
}
|
||||
|
||||
/// Going back to the long General list from a shorter section 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();
|
||||
app.run(
|
||||
"charStyle.new",
|
||||
json!({"name": "Body", "attrs": {
|
||||
"font_family": "Source Sans 3", "font_style": "Regular", "size": 24.0, "leading": 30.0,
|
||||
"kerning": 0.0, "tracking": 5.0, "all_caps": false, "small_caps": null,
|
||||
"position": {"kind": "normal", "size": 100.0, "position": 0.0}, "underline": false,
|
||||
"strikethrough": false, "standardVerticalRoman": false, "charAlign": "romanBaseline",
|
||||
"h_scale": 100.0, "v_scale": 100.0, "baseline_shift": 0.0, "rotation": 0.0,
|
||||
"language": "English: USA", "overprint_fill": false, "overprint_stroke": false,
|
||||
"stroke_width": 0.0, "features": ["liga", "calt", "dlig", "swsh", "salt", "titl", "ordn", "frac"]
|
||||
}}),
|
||||
)
|
||||
.unwrap();
|
||||
open(&mut app, "Body").unwrap();
|
||||
for h in [560.0, 640.0, 720.0] {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, h));
|
||||
for section in ["basic", "advanced", "color", "opentype"] {
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!(section));
|
||||
let _ = frame_texts_in(&ctx, &mut app, 900.0, h);
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("general"));
|
||||
let mut out =
|
||||
ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::super::show(&mut app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
let texts = texts_of(&out);
|
||||
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 {section} to General: {r:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog stays usable in the smallest window the app allows (800 by 500 points): in every
|
||||
/// section the section list, OK and Cancel are on the screen.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user