Merge pull request #672 from vangel76/fix-457-ui-scale

#457: Settings > Appearance > UI Scale
This commit is contained in:
Brandon Thomas 2026-10-10 14:54:09 -04:00 committed by GitHub
commit 66a7239115
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 75 additions and 0 deletions

View File

@ -104,6 +104,9 @@ pub struct AppearancePrefs {
pub accessible_contrast: bool,
/// Highlight (accent) colour of selections, focus and primary buttons (`#rrggbb`).
pub highlight_color: String,
/// "UI Scale": `auto` (default: the scale the operating system reports) or a fixed
/// percentage of [`UI_SCALES`] (`100` = one point per physical pixel).
pub ui_scale: String,
}
impl Default for AppearancePrefs {
@ -115,11 +118,19 @@ impl Default for AppearancePrefs {
light_theme: "light".into(),
accessible_contrast: false,
highlight_color: DEFAULT_HIGHLIGHT.into(),
ui_scale: "auto".into(),
}
}
}
impl AppearancePrefs {
/// The fixed UI scale (physical pixels per point) "UI Scale" asks for, or `None` for `auto`
/// (and for a value outside [`UI_SCALES`]), which keeps the operating system's scale.
pub fn ui_scale_factor(&self) -> Option<f32> {
// `auto` doesn't parse; every other listed value is a positive whole percentage
UI_SCALES.iter().any(|(k, _)| *k == self.ui_scale).then(|| self.ui_scale.parse::<f32>().ok()).flatten().map(|pct| pct / 100.0)
}
/// The theme to show (`darkest` | `dark` | `light`): the light or dark theme of the mode. Auto
/// follows `system_light` (the operating system's appearance) and is dark without an answer.
pub fn shown_theme(&self, system_light: Option<bool>) -> &str {
@ -669,6 +680,17 @@ const BIN_OPEN: &[(&str, &str)] = &[("openInPlace", "Open in place"), ("openNewT
const PROJECT_OPEN: &[(&str, &str)] = &[("openNewTab", "Open new tab"), ("openNewWindow", "Open in new window")];
const THEMES: &[(&str, &str)] = &[("darkest", "Darkest"), ("dark", "Dark"), ("light", "Light")];
pub const APPEARANCE_MODES: &[(&str, &str)] = &[("auto", "Sync with system"), ("dark", "Dark"), ("light", "Light")];
pub const UI_SCALES: &[(&str, &str)] = &[
("auto", "Auto (system)"),
("75", "75%"),
("100", "100%"),
("125", "125%"),
("150", "150%"),
("175", "175%"),
("200", "200%"),
("250", "250%"),
("300", "300%"),
];
pub const DARK_THEMES: &[(&str, &str)] = &[("darkest", "Darkest"), ("dark", "Dark")];
pub const LIGHT_THEMES: &[(&str, &str)] = &[("light", "Light")];
const MIXDOWN: &[(&str, &str)] = &[("front", "Front Only"), ("frontRear", "Front + Rear"), ("frontLfe", "Front + LFE"), ("frontRearLfe", "Front + Rear + LFE")];
@ -795,6 +817,7 @@ static CATEGORIES: &[Category] = &[
f("appearance.colorTheme", "Color Theme", Kind::Choice(THEMES), true),
b("appearance.accessibleContrast", "Accessible color contrast", true),
f("appearance.highlightColor", "Highlight Color", Kind::Color, true),
f("appearance.uiScale", "UI Scale", Kind::Choice(UI_SCALES), true),
],
},
Category {

View File

@ -3504,6 +3504,8 @@
Light Theme Tema claro
Next Appearance Mode Siguiente modo de apariencia
Sync with system Sincronizar con el sistema
UI Scale Escala de la interfaz
Auto (system) Automática (sistema)
In Entrada
Out Salida
Export Markers as CSV… Exportar marcadores como CSV…

Can't render this file because it contains an unexpected character in line 3 and column 95.

View File

@ -3448,6 +3448,8 @@
Light Theme ライトテーマ
Next Appearance Mode 次の外観モード
Sync with system システムと同期
UI Scale UI のスケール
Auto (system) 自動 (システム)
Go to previous keyframe 前のキーフレームへ移動
Go to next keyframe 次のキーフレームへ移動
Add keyframe キーフレームを追加

Can't render this file because it contains an unexpected character in line 11 and column 59.

View File

@ -3490,6 +3490,8 @@
Appearance Mode 外观模式
Next Appearance Mode 下一个外观模式
Sync with system 跟随系统
UI Scale 界面缩放
Auto (system) 自动(系统)
GPU Rendering GPU 渲染
Composite on the GPU when available 可用时在 GPU 上合成
Text to Speech 文字转语音

Can't render this file because it contains an unexpected character in line 3 and column 95.

View File

@ -321,6 +321,8 @@ pub struct FilmcraftApp {
watched_render: Option<(u64, Tick)>,
/// Settings last applied to the UI (theme, tooltips, frame cache, audio device).
applied_prefs: Option<filmcraft_engine::autosave::Preferences>,
/// Settings ▸ Appearance ▸ UI Scale last applied, with the system scale it was applied for.
applied_ui_scale: Option<(Option<f32>, f32)>,
workspace_restored: bool,
/// Window ▸ Workspaces: saved layouts ([`dock::WORKSPACES_FILE`] in the data directory).
pub workspaces: dock::WorkspacePrefs,
@ -528,6 +530,7 @@ impl FilmcraftApp {
gpu: None,
watched_render: None,
applied_prefs: None,
applied_ui_scale: None,
workspace_restored: false,
workspaces,
menu_workspaces: Default::default(),
@ -587,9 +590,28 @@ impl FilmcraftApp {
ctx.global_style_mut(|s| s.interaction.tooltip_delay = delay);
}
/// Settings ▸ Appearance ▸ UI Scale: a fixed scale makes a point that many physical pixels,
/// whatever scale the system reports. It is applied only when the setting or the system scale
/// changes, so Ctrl+- / Ctrl++ still zoom the interface for the session; going back to Auto
/// restores the system's scale.
fn apply_ui_scale(&mut self, ctx: &egui::Context) {
let want = self.session.prefs.appearance.ui_scale_factor();
let native = ctx.native_pixels_per_point().filter(|v| v.is_finite() && *v > 0.0).unwrap_or(1.0);
if self.applied_ui_scale == Some((want, native)) {
return;
}
let prev = self.applied_ui_scale.replace((want, native));
match want {
Some(scale) => ctx.set_zoom_factor(scale / native),
None if prev.is_some_and(|(p, _)| p.is_some()) => ctx.set_zoom_factor(1.0),
None => {}
}
}
/// Make the UI follow the settings after they change (theme, tooltips, frame cache budget,
/// play after rendering, audio device).
pub fn apply_prefs(&mut self, ctx: &egui::Context) {
self.apply_ui_scale(ctx);
// Appearance Mode ▸ Sync with system: follow the system while running. The host reports
// changes as they happen (no polling) and wakes the UI only when the value changes.
if self.applied_prefs.is_some() {

View File

@ -785,6 +785,30 @@ mod tests {
use super::*;
use crate::theme::ThemeKind;
/// #457: on a desktop that reports 150%, UI Scale 100% makes a point one physical pixel. It is
/// applied once, so Ctrl+- still zooms for the session, and Auto gives the system's scale back.
#[test]
fn ui_scale_overrides_the_system_scale_and_auto_restores_it() {
let ctx = egui::Context::default();
let mut app = FilmcraftApp::new(filmcraft_engine::Session::default());
// a frame applying the settings, then the next one, where a new zoom takes effect
let pass = |app: &mut FilmcraftApp| {
let mut input = egui::RawInput::default();
input.viewports.entry(egui::ViewportId::ROOT).or_default().native_pixels_per_point = Some(1.5);
ctx.run_ui(input.clone(), |ui| app.apply_prefs(ui.ctx())).textures_delta.clear();
ctx.run_ui(input, |_| {}).textures_delta.clear();
ctx.zoom_factor()
};
assert_eq!(pass(&mut app), 1.0, "Auto keeps the system's scale");
app.session.execute("prefs.set", json!({"key": "appearance.uiScale", "value": "100"})).unwrap();
assert_eq!(pass(&mut app), 1.0 / 1.5, "100% is one physical pixel per point");
ctx.set_zoom_factor(0.9 / 1.5); // Ctrl+-
assert_eq!(pass(&mut app), 0.9 / 1.5, "a session zoom isn't undone every frame");
assert!(app.session.execute("prefs.set", json!({"key": "appearance.uiScale", "value": "130"})).is_err());
app.session.execute("prefs.set", json!({"key": "appearance.uiScale", "value": "auto"})).unwrap();
assert_eq!(pass(&mut app), 1.0);
}
#[test]
fn appearance_button_cycles_modes_without_losing_theme_choices() {
let ctx = egui::Context::default();