Merge pull request #2481 from Felix-Helleckes/fix/2253-reversible-ui-zoom

#2253: Ctrl+Plus no longer scales the interface one way only; View > Reset Interface Zoom
This commit is contained in:
Brandon Thomas
2026-10-19 12:51:48 -04:00
committed by GitHub
11 changed files with 139 additions and 0 deletions
+2
View File
@@ -3610,3 +3610,5 @@
Shows the version, community links and credits in a FilmCraft window. Muestra la versión, los enlaces de la comunidad y los créditos en una ventana de FilmCraft.
Couldn't open your web browser No se pudo abrir el navegador web
Open this address in your browser yourself: Abre esta dirección tú mismo en el navegador:
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom Restablecer zoom de la interfaz
Can't render this file because it contains an unexpected character in line 3 and column 95.
+2
View File
@@ -3577,3 +3577,5 @@
Export the labels to a preset file… Exporter les étiquettes dans un fichier de paramètre prédéfini…
Show the presets folder Afficher le dossier des paramètres prédéfinis
Delete this preset Supprimer ce paramètre prédéfini
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom Réinitialiser le zoom de l'interface
Can't render this file because it contains an unexpected character in line 5 and column 83.
+2
View File
@@ -3575,3 +3575,5 @@
Appearance Settings… Pengaturan Tampilan…
Add to favorites Tambahkan ke favorit
Remove from favorites Hapus dari favorit
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom Atur Ulang Zoom Antarmuka
Can't render this file because it contains an unexpected character in line 6 and column 14.
+2
View File
@@ -3601,3 +3601,5 @@
Shows the version, community links and credits in a FilmCraft window. Mostra la versione, i link della community e i riconoscimenti in una finestra di FilmCraft.
Couldn't open your web browser Impossibile aprire il browser web
Open this address in your browser yourself: Apri tu questo indirizzo nel browser:
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom Reimposta zoom dell'interfaccia
Can't render this file because it contains an unexpected character in line 3 and column 95.
+2
View File
@@ -3609,3 +3609,5 @@
Shows the version, community links and credits in a FilmCraft window. バージョン、コミュニティへのリンク、クレジットを FilmCraft のウィンドウに表示します。
Couldn't open your web browser ウェブブラウザーを開けませんでした
Open this address in your browser yourself: このアドレスをご自身でブラウザーで開いてください:
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom インターフェイスのズームをリセット
Can't render this file because it contains an unexpected character in line 11 and column 59.
+2
View File
@@ -3596,3 +3596,5 @@
Shows the version, community links and credits in a FilmCraft window. FilmCraft 창에 버전, 커뮤니티 링크, 제작진을 보여 줍니다.
Couldn't open your web browser 웹 브라우저를 열 수 없습니다
Open this address in your browser yourself: 이 주소를 브라우저에서 직접 여세요:
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom 인터페이스 확대/축소 재설정
Can't render this file because it contains an unexpected character in line 2814 and column 7.
+2
View File
@@ -3578,3 +3578,5 @@
# The native macOS application menu (Settings…).
Settings Preferências
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom Redefinir zoom da interface
Can't render this file because it contains an unexpected character in line 6 and column 4.
+2
View File
@@ -3585,3 +3585,5 @@
Shows the version, community links and credits in a FilmCraft window. 在 FilmCraft 窗口中显示版本、社区链接和致谢。
Couldn't open your web browser 无法打开网页浏览器
Open this address in your browser yourself: 请自行在浏览器中打开此地址:
# View ▸ Reset Interface Zoom (#2253).
Reset Interface Zoom 重置界面缩放
Can't render this file because it contains an unexpected character in line 3 and column 95.
+10
View File
@@ -685,6 +685,16 @@ impl FilmcraftApp {
}
}
/// View ▸ Reset Interface Zoom: the scale Settings ▸ Appearance ▸ UI Scale asks for (the
/// system's with Auto), undoing any session zoom (#2253).
/// Returns the zoom factor set (egui reports it from the next pass on).
pub(crate) fn reset_ui_zoom(&mut self, ctx: &egui::Context) -> f32 {
let native = ctx.native_pixels_per_point().filter(|v| v.is_finite() && *v > 0.0).unwrap_or(1.0);
let zoom = self.session.prefs.appearance.ui_scale_factor().map_or(1.0, |scale| scale / native);
ctx.set_zoom_factor(zoom);
zoom
}
/// 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) {
+17
View File
@@ -53,6 +53,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[
uic!("view.zoomIn", "Zoom In", ["View"], Some("=")),
uic!("view.zoomOut", "Zoom Out", ["View"], Some("-")),
uic!("view.zoomToSequence", "Zoom to Sequence", ["View"], Some("\\")),
uic!("view.resetInterfaceZoom", "Reset Interface Zoom", ["View"], None),
uic!("view.playbackRes.full", "Full", ["View", "Playback Resolution"], None),
uic!("view.playbackRes.half", "1/2", ["View", "Playback Resolution"], None),
uic!("view.playbackRes.quarter", "1/4", ["View", "Playback Resolution"], None),
@@ -417,6 +418,12 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
app.ui.timeline.fit_pending = true;
return Ok(Value::Null);
}
// the whole interface back to Settings ▸ Appearance ▸ UI Scale, after Ctrl+Plus or a mouse
// mapped to page zoom scaled it (#2253)
"view.resetInterfaceZoom" => {
let zoom = app.reset_ui_zoom(ctx);
return Ok(json!({"zoom": zoom}));
}
"mode.import" => {
app.ui.mode = Mode::Import;
return Ok(Value::Null);
@@ -842,6 +849,16 @@ pub fn bindings(app: &FilmcraftApp) -> Vec<KeyBinding> {
let shifted: Vec<KeyBinding> =
v.iter().filter(|b| b.0.shift).filter_map(|(m, k, id, p)| shifted_key(*k).map(|k2| (*m, k2, id.clone(), p.clone()))).collect();
v.extend(shifted);
// `+` is the `=` key with Shift on US layouts and its own key on others and the keypad: a chord
// on `=` takes it too, unless something is bound to `+` itself. Otherwise Ctrl+Plus fell
// through to egui's interface zoom while Ctrl+- and Ctrl+0 were taken by FilmCraft
// shortcuts, so the interface grew and could not shrink back (#2253).
let plus: Vec<KeyBinding> = v
.iter()
.filter(|(m, k, _, p)| *k == egui::Key::Equals && !v.iter().any(|(m2, k2, _, p2)| *k2 == egui::Key::Plus && m2 == m && p2 == p))
.map(|(m, _, id, p)| (*m, egui::Key::Plus, id.clone(), p.clone()))
.collect();
v.extend(plus);
v.sort_by_key(|(m, ..)| std::cmp::Reverse(specificity(*m)));
v
}
+96
View File
@@ -0,0 +1,96 @@
//! Headless UI tests of the interface zoom (#2253).
use std::sync::mpsc::{Sender, channel};
use egui_kittest::Harness;
use filmcraft_engine::Session;
use filmcraft_ui_egui::FilmcraftApp;
use filmcraft_ui_egui::control::ControlRequest;
use serde_json::{Value, json};
struct Driver {
harness: Harness<'static, FilmcraftApp>,
tx: Sender<ControlRequest>,
}
impl Driver {
fn demo() -> Self {
let mut session = Session::default();
session.execute("file.openDemoProject", json!({})).expect("demo project");
let (tx, rx) = channel();
let app = FilmcraftApp::new(session).with_control(rx);
let harness = Harness::builder().with_size(egui::vec2(1600.0, 980.0)).with_step_dt(1.0 / 60.0).with_max_steps(10_000).build_eframe(move |_cc| app);
let mut d = Driver { harness, tx };
d.frames(4);
d
}
fn frames(&mut self, n: usize) {
for _ in 0..n {
let ctx = self.harness.ctx.clone();
let mut raw = std::mem::take(self.harness.input_mut());
eframe::App::raw_input_hook(self.harness.state_mut(), &ctx, &mut raw);
*self.harness.input_mut() = raw;
self.harness.step();
}
}
fn ok(&mut self, method: &str, params: Value) -> Value {
let (req, reply) = ControlRequest::new(method, params.clone());
self.tx.send(req).unwrap();
for _ in 0..600 {
self.frames(1);
if let Ok(v) = reply.try_recv() {
assert_eq!(v["ok"], json!(true), "{method} {params} failed: {v}");
return v["result"].clone();
}
}
panic!("no reply to {method} {params}");
}
fn click(&mut self, id: &str) {
self.ok("ui.click", json!({"id": id}));
self.frames(2);
}
fn app(&mut self) -> &mut FilmcraftApp {
self.harness.state_mut()
}
/// One key press with `modifiers`, as the window reports it.
fn press(&mut self, key: egui::Key, modifiers: egui::Modifiers) {
let events = &mut self.harness.input_mut().events;
events.push(egui::Event::ModifiersChanged(modifiers));
for pressed in [true, false] {
events.push(egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers });
}
events.push(egui::Event::ModifiersChanged(egui::Modifiers::NONE));
self.frames(3);
}
}
/// #2253: with the Timeline focused, Ctrl+= and Ctrl+- are its track-height keys, but Ctrl+Plus
/// (the `+` key, or Shift+=) fell through to egui and scaled the whole interface, which the taken
/// Ctrl+- and Ctrl+0 could not undo. `+` now goes where `=` goes, and View ▸ Reset Interface Zoom
/// gives the configured scale back.
#[test]
fn ctrl_plus_does_what_ctrl_equals_does_and_the_interface_zoom_can_be_reset() {
let mut d = Driver::demo();
let clip = d.app().session.active_sequence().unwrap().video_tracks[0].items[0].id.0;
d.click(&format!("timeline.clip.{clip}"));
assert_eq!(d.app().ui.focused, filmcraft_ui_egui::dock::PanelKind::Timeline);
let zoom = d.harness.ctx.zoom_factor();
let height = d.app().ui.timeline.video_track_h;
d.press(egui::Key::Plus, egui::Modifiers::COMMAND);
assert_eq!(d.harness.ctx.zoom_factor(), zoom, "the interface keeps its scale");
assert!(d.app().ui.timeline.video_track_h > height, "Ctrl+Plus makes the video tracks taller, as Ctrl+= does");
// however it got scaled, View ▸ Reset Interface Zoom brings it back
d.harness.ctx.set_zoom_factor(1.75);
d.frames(2);
assert_eq!(d.harness.ctx.zoom_factor(), 1.75);
let r = d.ok("ui.menu.invoke", json!({"id": "view.resetInterfaceZoom"}));
assert_eq!(r["zoom"], json!(1.0), "the reply says the zoom it set: {r}");
d.frames(2);
assert_eq!(d.harness.ctx.zoom_factor(), 1.0);
}