mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Settings: allow custom UI scale percentages (#682)
Settings > Appearance > UI Scale offered only 75, 100, 125, 150, 175 and 200 %, so a size between 75 and 100 wasn't possible. It is now the shared numeric field (Kind::Int(75, 200, "%")): type or drag any whole percentage. The engine already stored the scale as a clamped integer and the UI applies it with set_zoom_factor, so the range, the default, live preview, Cancel and saved preferences are unchanged; the preset list goes. Numeric settings now apply a typed number once it is entered (Enter or leaving the field), not at every keystroke: typing 85 used to apply the partial 8, clamped to 75, and resize the whole interface before the 5. Dragging still applies as it moves. Tests: the UI Scale test types 85 a key at a time (nothing applies and the field keeps the keyboard until Enter), Cancel, 50 and 300 clamp to 75 and 200, 90 with OK, and a new app with the saved preferences starts at 90 %. The GPU Information transaction test types 125.
This commit is contained in:
1 parent
9a7d3e7699
commit
42d8120147
6 files changed
+58
-35
No files matched your search
@@ -34,9 +34,6 @@ pub const LANGUAGES: &[(&str, &str)] = &[
|
||||
("Português (Brasil)", "pt-br"),
|
||||
];
|
||||
|
||||
/// Settings ▸ Appearance ▸ UI Scale: (label, `appearance.uiScale` percent).
|
||||
pub const UI_SCALES: &[(&str, &str)] = &[("75%", "75"), ("100%", "100"), ("125%", "125"), ("150%", "150"), ("175%", "175"), ("200%", "200")];
|
||||
|
||||
/// Settings ▸ Startup & Repair ▸ Window Graphics: (label, `startup.windowGraphics` value).
|
||||
pub const WINDOW_GRAPHICS: &[(&str, &str)] = &[("Automatic", "auto"), ("OpenGL (compatibility)", "gl")];
|
||||
|
||||
@@ -1170,7 +1167,7 @@ pub fn pages() -> Vec<Page> {
|
||||
s("appearance.lightTheme", "Light Theme", Kind::Choice(LIGHT_THEMES), true),
|
||||
s("appearance.darkTheme", "Dark Theme", Kind::Choice(DARK_THEMES), true),
|
||||
s("appearance.brightness", "Brightness", Kind::Slider(-1.0, 1.0), true),
|
||||
s("appearance.uiScale", "UI Scale", Kind::Choice(UI_SCALES), true),
|
||||
s("appearance.uiScale", "UI Scale", Kind::Int(75, 200, "%"), true),
|
||||
Section("Labels and Colors"),
|
||||
s("appearance.useLabelColorForHandles", "Use Label Color for Layer Handles and Paths", B, true),
|
||||
s("appearance.useLabelColorForTabs", "Use Label Color for Related Tabs", B, true),
|
||||
|
||||
@@ -88,7 +88,7 @@ fn prefs_set_validates_keys_types_and_ranges() {
|
||||
}
|
||||
|
||||
/// Settings ▸ Appearance ▸ UI Scale (#284): a percentage, 100 by default, kept within 75–200;
|
||||
/// the dialog's choices set it as a number.
|
||||
/// the dialog types or drags any whole percentage in that range (#682).
|
||||
#[test]
|
||||
fn ui_scale_is_a_percentage_from_75_to_200() {
|
||||
let mut p = Prefs::default();
|
||||
@@ -101,10 +101,8 @@ fn ui_scale_is_a_percentage_from_75_to_200() {
|
||||
p.set("appearance.uiScale", json!(10)).unwrap();
|
||||
assert_eq!(p.appearance.ui_scale, 75);
|
||||
let page = crate::prefs::pages().into_iter().find(|pg| pg.id == "appearance").unwrap();
|
||||
let listed = page.items.iter().any(|i| {
|
||||
matches!(i, crate::prefs::Item::Setting { key: "appearance.uiScale", kind: crate::prefs::Kind::Choice(c), live: true, .. } if c.contains(&("200%", "200")))
|
||||
});
|
||||
assert!(listed, "Settings ▸ Appearance offers it");
|
||||
let field = crate::prefs::Item::Setting { key: "appearance.uiScale", label: "UI Scale", kind: crate::prefs::Kind::Int(75, 200, "%"), live: true };
|
||||
assert!(page.items.contains(&field), "Settings ▸ Appearance offers it");
|
||||
// Older settings files without it get the default.
|
||||
assert_eq!(Prefs::from_json(r#"{"version": 2, "appearance": {"theme": "light"}}"#).appearance.ui_scale, 100);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,10 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
let r = match kind {
|
||||
Kind::Int(lo, hi, unit) => {
|
||||
let mut x = v.as_i64().unwrap_or(0);
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.25)));
|
||||
// A typed number applies once entered: typing 85 into UI Scale
|
||||
// doesn't resize the interface to 75 % at its first digit.
|
||||
let drag = egui::DragValue::new(&mut x).range(lo..=hi).speed(0.25).update_while_editing(false);
|
||||
let r = ui.add(crate::widgets::drag_value(drag));
|
||||
if !unit.is_empty() {
|
||||
ui.label(RichText::new(unit).color(t.text_dim));
|
||||
}
|
||||
@@ -152,7 +155,6 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
r
|
||||
}
|
||||
Kind::Choice(opts) => {
|
||||
// (A number with choices, UI Scale, shows as its text.)
|
||||
let curv = v.as_str().map_or_else(|| v.to_string(), str::to_string);
|
||||
let shown = opts.iter().find(|o| o.1 == curv).map(|o| o.0).unwrap_or(curv.as_str()).to_string();
|
||||
egui::ComboBox::from_id_salt(("settings", key))
|
||||
|
||||
@@ -237,46 +237,67 @@ fn leaving_the_learn_workspace_closes_its_tutorials() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #284: Settings ▸ Appearance ▸ UI Scale sizes the whole interface as soon as it is chosen,
|
||||
/// Cancel puts it back, OK keeps it; Ctrl+0 (Window ▸ Project) doesn't reset it.
|
||||
/// #284, #682: Settings ▸ Appearance ▸ UI Scale takes any typed percentage from 75 to 200 and
|
||||
/// sizes the whole interface once it is entered, Cancel puts it back, OK keeps it and later
|
||||
/// sessions start with it; Ctrl+0 (Window ▸ Project) doesn't reset it.
|
||||
#[test]
|
||||
fn ui_scale_setting_sizes_the_interface() {
|
||||
let mut h = harness();
|
||||
// Room for the whole Settings dialog at 200 %.
|
||||
h.set_size(egui::vec2(2400.0, 1600.0));
|
||||
let ctx = h.ctx.clone();
|
||||
let choose = |h: &mut Harness<'_, EffectcraftApp>, label: &str| {
|
||||
let open_settings = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "app.settings", json!({"page": "appearance"})).unwrap();
|
||||
h.run_steps(3);
|
||||
};
|
||||
let key = |h: &mut Harness<'_, EffectcraftApp>, key: egui::Key, modifiers: egui::Modifiers| {
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed, repeat: false, modifiers });
|
||||
h.step();
|
||||
}
|
||||
};
|
||||
// Types `text` into the field a key at a time: nothing applies (and the field keeps the
|
||||
// keyboard) until Enter.
|
||||
let type_scale = |h: &mut Harness<'_, EffectcraftApp>, text: &str| {
|
||||
let before = (h.state().session.prefs.appearance.ui_scale, h.ctx.zoom_factor());
|
||||
click(h, "settings.appearance.uiScale");
|
||||
let option = h.query_by_label(label).unwrap_or_else(|| panic!("the {label} choice")).rect().center();
|
||||
click_at(h, option);
|
||||
for c in text.chars() {
|
||||
h.input_mut().events.push(Event::Text(c.to_string()));
|
||||
h.run_steps(2);
|
||||
assert_eq!((h.state().session.prefs.appearance.ui_scale, h.ctx.zoom_factor()), before, "{text}: not while typing");
|
||||
assert!(h.ctx.memory(|m| m.focused().is_some()), "{text}: the field keeps the keyboard");
|
||||
}
|
||||
key(h, egui::Key::Enter, egui::Modifiers::NONE);
|
||||
h.run_steps(2);
|
||||
};
|
||||
let scale = |h: &Harness<'_, EffectcraftApp>| (h.state().session.prefs.appearance.ui_scale, h.ctx.zoom_factor());
|
||||
assert_eq!(ctx.zoom_factor(), 1.0);
|
||||
choose(&mut h, "150%");
|
||||
assert_eq!(h.state().session.prefs.appearance.ui_scale, 150);
|
||||
assert_eq!(ctx.zoom_factor(), 1.5, "live");
|
||||
open_settings(&mut h);
|
||||
type_scale(&mut h, "85");
|
||||
assert_eq!(scale(&h), (85, 0.85), "live");
|
||||
click(&mut h, "settings.cancel");
|
||||
h.run_steps(2);
|
||||
assert_eq!((h.state().session.prefs.appearance.ui_scale, ctx.zoom_factor()), (100, 1.0), "Cancel put it back");
|
||||
choose(&mut h, "125%");
|
||||
assert_eq!(scale(&h), (100, 1.0), "Cancel put it back");
|
||||
open_settings(&mut h);
|
||||
type_scale(&mut h, "50");
|
||||
assert_eq!(scale(&h), (75, 0.75), "at least 75");
|
||||
type_scale(&mut h, "300");
|
||||
assert_eq!(scale(&h), (200, 2.0), "at most 200");
|
||||
type_scale(&mut h, "90");
|
||||
click(&mut h, "settings.ok");
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().dialog, None);
|
||||
assert_eq!((h.state().session.prefs.appearance.ui_scale, ctx.zoom_factor()), (125, 1.25), "OK kept it");
|
||||
assert_eq!(scale(&h), (90, 0.9), "OK kept it");
|
||||
// Ctrl+0 opens the Project panel; egui's own Ctrl+0 zoom reset stays out of it.
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::Key { key: egui::Key::Num0, physical_key: None, pressed, repeat: false, modifiers: egui::Modifiers::COMMAND });
|
||||
h.step();
|
||||
}
|
||||
key(&mut h, egui::Key::Num0, egui::Modifiers::COMMAND);
|
||||
h.run_steps(2);
|
||||
assert_eq!(ctx.zoom_factor(), 1.25);
|
||||
assert_eq!(ctx.zoom_factor(), 0.9);
|
||||
// A saved scale applies from the first frame.
|
||||
let mut s = Session::default();
|
||||
s.prefs.set("appearance.uiScale", json!(175)).unwrap();
|
||||
let s = Session { prefs: effectcraft_engine::prefs::Prefs::from_json(&h.state().session.prefs.to_json()), ..Default::default() };
|
||||
let mut h2 = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h2.run_steps(3);
|
||||
assert_eq!(h2.ctx.zoom_factor(), 1.75);
|
||||
assert_eq!(h2.ctx.zoom_factor(), 0.9);
|
||||
}
|
||||
|
||||
/// Headless look at the panels (wgpu offscreen; needs a GPU adapter). Run with
|
||||
|
||||
@@ -47,11 +47,15 @@ fn check_info_transaction(via_escape: bool) {
|
||||
let edited_brightness = h.state().session.prefs.appearance.brightness;
|
||||
assert!((edited_brightness - original_brightness).abs() > 0.01, "real brightness slider changed");
|
||||
assert_eq!(saved(&store).prefs.appearance.brightness, original_brightness, "unconfirmed edit is not saved");
|
||||
// Use the upstream percent-valued Scale choice; keyboard zoom is deliberately disabled.
|
||||
// Type a UI Scale; keyboard zoom is deliberately disabled.
|
||||
let at = rect(&h, "settings.appearance.uiScale").center();
|
||||
click(&mut h, at);
|
||||
let at = h.get_by_label("125%").rect().center();
|
||||
click(&mut h, at);
|
||||
h.event(Event::Text("125".into()));
|
||||
for pressed in [true, false] {
|
||||
h.event(Event::Key { key: Key::Enter, physical_key: Some(Key::Enter), pressed, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().session.prefs.appearance.ui_scale, 125);
|
||||
assert_eq!(saved(&store).prefs.appearance.ui_scale, 100, "unconfirmed scale edit is not saved");
|
||||
// Let UI Scale settle the centered dialog before reading its next hit target.
|
||||
|
||||
+3
-2
@@ -59,8 +59,9 @@ Commands (CLI, MCP, control channel):
|
||||
- `appearance.lightTheme`: Light Theme (`light`), shown in light appearance. Settings ▸ Appearance shows the mode above a light and a dark theme card, each with a preview of the EffectCraft window in that theme.
|
||||
- `appearance.theme`: the single theme from before appearance modes, kept for older settings files and automation. A saved file with only `theme` loads as a fixed Dark or Light mode showing it; setting it (`prefs.set`, `ui.set {"theme": …}`, `view.theme.dark|darker|light`) selects that theme and fixes the mode to its family. While the mode is Dark or Light it mirrors the theme in use.
|
||||
- `appearance.brightness`: Brightness
|
||||
- `appearance.uiScale`: UI Scale, the size of the whole interface (75–200 %, on top of the
|
||||
display's own scaling), applied as soon as it is chosen
|
||||
- `appearance.uiScale`: UI Scale, the size of the whole interface (any whole percentage from
|
||||
75 to 200, on top of the display's own scaling), dragged or typed; it applies as soon as it is
|
||||
dragged or entered
|
||||
- `appearance.useLabelColorForHandles`: Use Label Color for Layer Handles and Paths
|
||||
- `grids.gridColor`: Color
|
||||
- `grids.gridSpacing`: Gridline Every
|
||||
|
||||
Reference in new issue
Block a user