diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 0af8835d1..46402f446 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1075b0c15..7face7863 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 508a6e9d4..627877604 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 22d47ccbd..63794ddc6 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index b6a7fb8a2..d57a42f29 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 インターフェイスのズームをリセット diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 20341b9a6..d1fa9b401 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -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 인터페이스 확대/축소 재설정 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index aeb0e0691..66f2e59a3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/zh-cn.tsv b/crates/ui-egui/src/i18n/zh-cn.tsv index 066279eef..cc20dd968 100644 --- a/crates/ui-egui/src/i18n/zh-cn.tsv +++ b/crates/ui-egui/src/i18n/zh-cn.tsv @@ -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 重置界面缩放 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e3a644ff8..576f912da 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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) { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 48b4b82c6..1d760d0c0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -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 { let shifted: Vec = 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 = 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 } diff --git a/crates/ui-egui/tests/interface_zoom_ui.rs b/crates/ui-egui/tests/interface_zoom_ui.rs new file mode 100644 index 000000000..2908a0017 --- /dev/null +++ b/crates/ui-egui/tests/interface_zoom_ui.rs @@ -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, +} + +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); +}