From 81c96bf22768c2874b0182210287a6be95f8e7da Mon Sep 17 00:00:00 2001 From: cocvn157 <120394358+cocvn157@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:33:26 +0700 Subject: [PATCH] feat(desktop-gpui): make auto-zoom hold duration and click-merge gap configurable --- apps/desktop-gpui/locales/de.toml | 8 + apps/desktop-gpui/locales/en.toml | 8 + apps/desktop-gpui/locales/es.toml | 8 + apps/desktop-gpui/locales/fr.toml | 8 + apps/desktop-gpui/locales/hi.toml | 8 + apps/desktop-gpui/locales/ja.toml | 8 + apps/desktop-gpui/locales/zh-CN.toml | 8 + apps/desktop-gpui/src/editor_edits.rs | 81 ++++++- apps/desktop-gpui/src/editor_window.rs | 11 +- apps/desktop-gpui/src/i18n/tests.rs | 18 ++ apps/desktop-gpui/src/settings_window.rs | 214 ++++++++++++++++-- .../settings_window/recording_action_tests.rs | 3 + apps/desktop-gpui/src/store.rs | 22 ++ 13 files changed, 375 insertions(+), 30 deletions(-) diff --git a/apps/desktop-gpui/locales/de.toml b/apps/desktop-gpui/locales/de.toml index 5ab36438519..e904db12bbe 100644 --- a/apps/desktop-gpui/locales/de.toml +++ b/apps/desktop-gpui/locales/de.toml @@ -188,6 +188,14 @@ label = "Automatisch auf Klicks zoomen" description = "Zoomstufe für neu erstellte und automatisch erzeugte Zoom-Segmente." label = "Standard-Zoomstufe" +[settings.general.recording.auto_zoom_hold] +description = "Wie lange automatisch erzeugte Zoom-Segmente nach jedem Klick gezoomt bleiben." +label = "Zoom-Haltedauer nach Klick" + +[settings.general.recording.auto_zoom_merge_gap] +description = "Aufeinanderfolgende Klick-Zooms mit kleinerem Abstand zusammenführen. Auf 0s setzen, um Lücken nie zusammenzuführen." +label = "Abstand zum Zusammenführen naher Klicks" + [settings.general.recording.capture_keyboard] description = "Tastenanschläge aufzeichnen, damit Sie im Editor Tastatur-Overlays hinzufügen können." label = "Tastenanschläge aufzeichnen" diff --git a/apps/desktop-gpui/locales/en.toml b/apps/desktop-gpui/locales/en.toml index 91776cebf1b..c1af30c1fbe 100644 --- a/apps/desktop-gpui/locales/en.toml +++ b/apps/desktop-gpui/locales/en.toml @@ -190,6 +190,14 @@ label = "Auto zoom on clicks" description = "Zoom level for newly created and auto-generated zoom segments." label = "Default zoom amount" +[settings.general.recording.auto_zoom_hold] +description = "How long auto-generated zoom segments stay zoomed in after each click." +label = "Zoom hold after click" + +[settings.general.recording.auto_zoom_merge_gap] +description = "Merge consecutive click zooms separated by less than this gap. Set to 0s to never merge gaps." +label = "Merge nearby clicks gap" + [settings.general.recording.capture_keyboard] description = "Record key presses so you can add keyboard overlays in the editor." label = "Capture keyboard presses" diff --git a/apps/desktop-gpui/locales/es.toml b/apps/desktop-gpui/locales/es.toml index 5df166fe605..6c910d2b0c6 100644 --- a/apps/desktop-gpui/locales/es.toml +++ b/apps/desktop-gpui/locales/es.toml @@ -188,6 +188,14 @@ label = "Zoom automático en los clics" description = "Nivel de zoom de los segmentos de zoom nuevos y generados automáticamente." label = "Nivel de zoom predeterminado" +[settings.general.recording.auto_zoom_hold] +description = "Cuánto tiempo permanecen acercados los segmentos de zoom automáticos después de cada clic." +label = "Duración del zoom tras clic" + +[settings.general.recording.auto_zoom_merge_gap] +description = "Combina zooms de clics consecutivos separados por menos de este intervalo. Ajusta a 0s para no combinar huecos." +label = "Intervalo para combinar clics cercanos" + [settings.general.recording.capture_keyboard] description = "Registra las pulsaciones de teclas para poder añadir superposiciones de teclado en el editor." label = "Capturar pulsaciones de teclas" diff --git a/apps/desktop-gpui/locales/fr.toml b/apps/desktop-gpui/locales/fr.toml index d138e1e7452..4ac798a21e8 100644 --- a/apps/desktop-gpui/locales/fr.toml +++ b/apps/desktop-gpui/locales/fr.toml @@ -188,6 +188,14 @@ label = "Zoom automatique sur les clics" description = "Niveau de zoom des segments de zoom créés manuellement ou automatiquement." label = "Niveau de zoom par défaut" +[settings.general.recording.auto_zoom_hold] +description = "Durée pendant laquelle les segments de zoom automatiques restent zoomés après chaque clic." +label = "Maintien du zoom après un clic" + +[settings.general.recording.auto_zoom_merge_gap] +description = "Fusionner les zooms de clics consécutifs séparés par moins que cet écart. Régler sur 0s pour ne jamais fusionner les écarts." +label = "Écart de fusion des clics proches" + [settings.general.recording.capture_keyboard] description = "Enregistrer les frappes au clavier pour pouvoir ajouter des incrustations de touches dans l'éditeur." label = "Capturer les frappes au clavier" diff --git a/apps/desktop-gpui/locales/hi.toml b/apps/desktop-gpui/locales/hi.toml index 3ee5bc5421f..f7d6210beff 100644 --- a/apps/desktop-gpui/locales/hi.toml +++ b/apps/desktop-gpui/locales/hi.toml @@ -188,6 +188,14 @@ label = "क्लिक पर ऑटो ज़ूम" description = "नए बनाए गए और अपने-आप बने ज़ूम सेगमेंट का ज़ूम स्तर।" label = "डिफ़ॉल्ट ज़ूम स्तर" +[settings.general.recording.auto_zoom_hold] +description = "हर क्लिक के बाद अपने-आप बने ज़ूम सेगमेंट कितनी देर तक ज़ूम रहते हैं।" +label = "क्लिक के बाद ज़ूम होल्ड" + +[settings.general.recording.auto_zoom_merge_gap] +description = "इस अंतर से कम दूरी वाले लगातार क्लिक ज़ूम को मिलाएँ। अंतर कभी न मिलाने के लिए 0s पर सेट करें।" +label = "पास के क्लिक मिलाने का अंतर" + [settings.general.recording.capture_keyboard] description = "की प्रेस रिकॉर्ड करें, ताकि आप एडिटर में कीबोर्ड ओवरले जोड़ सकें।" label = "कीबोर्ड प्रेस कैप्चर करें" diff --git a/apps/desktop-gpui/locales/ja.toml b/apps/desktop-gpui/locales/ja.toml index 809fbe9fe2b..d05024bdd74 100644 --- a/apps/desktop-gpui/locales/ja.toml +++ b/apps/desktop-gpui/locales/ja.toml @@ -188,6 +188,14 @@ label = "クリック時に自動ズーム" description = "新しく作成したズームセグメントと自動生成されたズームセグメントの倍率。" label = "デフォルトのズーム倍率" +[settings.general.recording.auto_zoom_hold] +description = "各クリック後に自動生成されたズームセグメントが維持される時間。" +label = "クリック後のズーム維持時間" + +[settings.general.recording.auto_zoom_merge_gap] +description = "この間隔より短い連続したクリックズームを結合します。0s に設定すると間隔を結合しません。" +label = "近接クリックの結合間隔" + [settings.general.recording.capture_keyboard] description = "キー入力を記録し、エディタでキーボードオーバーレイを追加できるようにします。" label = "キー入力を記録" diff --git a/apps/desktop-gpui/locales/zh-CN.toml b/apps/desktop-gpui/locales/zh-CN.toml index 895124489ef..a812a83964c 100644 --- a/apps/desktop-gpui/locales/zh-CN.toml +++ b/apps/desktop-gpui/locales/zh-CN.toml @@ -188,6 +188,14 @@ label = "点击时自动缩放" description = "新建和自动生成的缩放片段所用的缩放倍数。" label = "默认缩放倍数" +[settings.general.recording.auto_zoom_hold] +description = "每次点击后自动生成的缩放片段保持放大的时长。" +label = "点击后缩放保持时长" + +[settings.general.recording.auto_zoom_merge_gap] +description = "合并间隔小于此值的连续点击缩放。设为 0s 则不合并间隔。" +label = "合并相近点击间隔" + [settings.general.recording.capture_keyboard] description = "记录按键,以便在编辑器中添加键盘按键显示。" label = "记录键盘按键" diff --git a/apps/desktop-gpui/src/editor_edits.rs b/apps/desktop-gpui/src/editor_edits.rs index ef0ac92acf5..fb5bf2faf35 100644 --- a/apps/desktop-gpui/src/editor_edits.rs +++ b/apps/desktop-gpui/src/editor_edits.rs @@ -2323,10 +2323,50 @@ pub fn clip_trim_end( // Auto zoom // --------------------------------------------------------------------------- +pub const DEFAULT_AUTO_ZOOM_HOLD_SECONDS: f64 = 2.5; +pub const DEFAULT_AUTO_ZOOM_MERGE_GAP_SECONDS: f64 = 2.5; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AutoZoomTiming { + pub post_padding_ms: f64, + pub merge_gap_ms: f64, +} + +impl Default for AutoZoomTiming { + fn default() -> Self { + Self { + post_padding_ms: DEFAULT_AUTO_ZOOM_HOLD_SECONDS * 1000.0, + merge_gap_ms: DEFAULT_AUTO_ZOOM_MERGE_GAP_SECONDS * 1000.0, + } + } +} + +impl AutoZoomTiming { + pub fn from_seconds(hold_seconds: f64, merge_gap_seconds: f64) -> Self { + Self { + post_padding_ms: hold_seconds.max(0.1) * 1000.0, + merge_gap_ms: merge_gap_seconds.max(0.0) * 1000.0, + } + } +} pub fn generate_zoom_segments_for_timeline( clicks_by_clip: &[Vec], timeline: &TimelineConfiguration, zoom_amount: f64, +) -> Vec { + generate_zoom_segments_for_timeline_with_timing( + clicks_by_clip, + timeline, + zoom_amount, + AutoZoomTiming::default(), + ) +} + +pub fn generate_zoom_segments_for_timeline_with_timing( + clicks_by_clip: &[Vec], + timeline: &TimelineConfiguration, + zoom_amount: f64, + timing: AutoZoomTiming, ) -> Vec { let ordered_clicks: Vec<_> = clicks_by_clip .iter() @@ -2364,7 +2404,12 @@ pub fn generate_zoom_segments_for_timeline( mapped.push(click); } } - generate_zoom_segments_from_clicks(mapped, timeline.duration(), zoom_amount) + generate_zoom_segments_from_clicks_with_timing( + mapped, + timeline.duration(), + zoom_amount, + timing, + ) } /// `generate_zoom_segments_from_clicks_impl` @@ -2375,17 +2420,31 @@ pub fn generate_zoom_segments_for_timeline( /// clamped inside the recording. The Tauri signature also takes the cursor /// *moves* and ignores them; this one does not pretend to want them. pub fn generate_zoom_segments_from_clicks( + clicks: Vec, + max_duration: f64, + zoom_amount: f64, +) -> Vec { + generate_zoom_segments_from_clicks_with_timing( + clicks, + max_duration, + zoom_amount, + AutoZoomTiming::default(), + ) +} + +pub fn generate_zoom_segments_from_clicks_with_timing( mut clicks: Vec, max_duration: f64, zoom_amount: f64, + timing: AutoZoomTiming, ) -> Vec { const MS_PER_SECOND: f64 = 1000.0; const START_MIN_MS: f64 = 1.0; const CLICK_PRE_PADDING_MS: f64 = 300.0; - const CLICK_POST_PADDING_MS: f64 = 2500.0; const CLICK_END_CLAMP_PADDING_MS: f64 = 800.0; const TRAILING_CLICK_IGNORE_MS: f64 = 1000.0; - const MERGE_GAP_MS: f64 = 2500.0; + let post_padding_ms = timing.post_padding_ms.max(0.0); + let merge_gap_ms = timing.merge_gap_ms.max(0.0); if max_duration <= 0.0 { return Vec::new(); @@ -2412,7 +2471,7 @@ pub fn generate_zoom_segments_from_clicks( } let start = (time_ms - CLICK_PRE_PADDING_MS).max(START_MIN_MS); - let end = (time_ms + CLICK_POST_PADDING_MS).min(end_limit_ms); + let end = (time_ms + post_padding_ms).min(end_limit_ms); if end > start { intervals.push((start, end)); @@ -2428,7 +2487,7 @@ pub fn generate_zoom_segments_from_clicks( let mut merged: Vec<(f64, f64)> = Vec::new(); for interval in intervals { if let Some(last) = merged.last_mut() - && interval.0 <= last.1 + MERGE_GAP_MS + && interval.0 <= last.1 + merge_gap_ms { last.1 = last.1.max(interval.1); continue; @@ -3842,6 +3901,18 @@ mod tests { assert_eq!(segments[1].end, 12.5); } + #[test] + fn custom_auto_zoom_timing_respects_hold_duration_and_merge_gap() { + let segments = generate_zoom_segments_from_clicks_with_timing( + vec![click(1_000.), click(2_500.), click(4_200.)], + 20.0, + 2.0, + AutoZoomTiming::from_seconds(1.0, 0.2), + ); + let bounds: Vec<_> = segments.iter().map(|s| (s.start, s.end)).collect(); + assert_eq!(bounds, vec![(0.7, 3.5), (3.9, 5.2)]); + } + #[test] fn no_clicks_or_no_duration_generate_no_zoom_segments() { assert!(generate_zoom_segments_from_clicks(Vec::new(), 20.0, 2.0).is_empty()); diff --git a/apps/desktop-gpui/src/editor_window.rs b/apps/desktop-gpui/src/editor_window.rs index 75e54af6ce3..4fe89f870ca 100644 --- a/apps/desktop-gpui/src/editor_window.rs +++ b/apps/desktop-gpui/src/editor_window.rs @@ -6283,7 +6283,12 @@ impl EditorWindow { self.auto_zoom_message = None; cx.notify(); let path = self.project_path.clone(); - let amount = new_zoom_amount(); + let settings = crate::store::GeneralSettings::load(); + let amount = f64::from(settings.new_zoom_amount()); + let timing = edits::AutoZoomTiming::from_seconds( + f64::from(settings.auto_zoom_hold_seconds()), + f64::from(settings.auto_zoom_merge_gap_seconds()), + ); cx.spawn_in(window, async move |this, cx| { let clicks = cx .background_executor() @@ -6298,7 +6303,9 @@ impl EditorWindow { .timeline .as_ref() .map_or_else(Vec::new, |timeline| { - edits::generate_zoom_segments_for_timeline(&clicks, timeline, amount) + edits::generate_zoom_segments_for_timeline_with_timing( + &clicks, timeline, amount, timing, + ) }); if segments.is_empty() { tracing::info!("auto zoom produced no segments"); diff --git a/apps/desktop-gpui/src/i18n/tests.rs b/apps/desktop-gpui/src/i18n/tests.rs index 94539acfa56..4dffc4cebc8 100644 --- a/apps/desktop-gpui/src/i18n/tests.rs +++ b/apps/desktop-gpui/src/i18n/tests.rs @@ -273,6 +273,24 @@ fn english_matches_the_literals_it_replaced() { tr::settings::general::recording::default_zoom::label(), "Default zoom amount".into(), ), + ( + tr::settings::general::recording::auto_zoom_hold::description(), + "How long auto-generated zoom segments stay zoomed in after each click.".into(), + ), + ( + tr::settings::general::recording::auto_zoom_hold::label(), + "Zoom hold after click".into(), + ), + ( + tr::settings::general::recording::auto_zoom_merge_gap::description(), + "Merge consecutive click zooms separated by less than this gap. \ + Set to 0s to never merge gaps." + .into(), + ), + ( + tr::settings::general::recording::auto_zoom_merge_gap::label(), + "Merge nearby clicks gap".into(), + ), ( tr::settings::general::recording::capture_keyboard::description(), "Record key presses so you can add keyboard overlays in the editor.".into(), diff --git a/apps/desktop-gpui/src/settings_window.rs b/apps/desktop-gpui/src/settings_window.rs index 855590dc699..5b3d6abd493 100644 --- a/apps/desktop-gpui/src/settings_window.rs +++ b/apps/desktop-gpui/src/settings_window.rs @@ -711,6 +711,13 @@ async fn fetch_profile_image() -> Result, crate::auth::AuthApiE Ok(library::rgba_to_render_image(image.into_rgba8())) } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RecordingSlider { + ZoomAmount, + ZoomHoldDuration, + ZoomMergeGap, +} + pub struct SettingsWindow { pub(crate) theme: Theme, pub(crate) page: Page, @@ -751,7 +758,10 @@ pub struct SettingsWindow { /// The zoom slider's track rect, captured during prepaint -- the row is /// inside a resizable pane, so it cannot be computed. slider_track: Rc>>>, + zoom_hold_slider_track: Rc>>>, + zoom_merge_gap_slider_track: Rc>>>, slider_dragging: bool, + active_slider: Option, /// `commands.listCaptureWindows()`, for the excluded-windows menu. windows: Vec, /// Key handling for Cmd-W lives on the root, which needs a focus handle of @@ -894,7 +904,10 @@ impl SettingsWindow { placeholders: ui::CollapsibleState::new(false), placeholders_task: None, slider_track: Rc::new(Cell::new(None)), + zoom_hold_slider_track: Rc::new(Cell::new(None)), + zoom_merge_gap_slider_track: Rc::new(Cell::new(None)), slider_dragging: false, + active_slider: None, windows: Vec::new(), focus: cx.focus_handle(), _sign_in_subscriptions: sign_in_subscriptions, @@ -3699,6 +3712,8 @@ impl SettingsWindow { .map(|(_, label)| label().to_string()) .unwrap_or_else(|| settings.max_fps.to_string()); let zoom = settings.new_zoom_amount(); + let zoom_hold = settings.auto_zoom_hold_seconds(); + let zoom_merge_gap = settings.auto_zoom_merge_gap_seconds(); self.section( tr::settings::general::recording::title(), @@ -3881,6 +3896,60 @@ impl SettingsWindow { ) .into_any_element(), ), + self.setting_row( + tr::settings::general::recording::auto_zoom_hold::label(), + Some(tr::settings::general::recording::auto_zoom_hold::description()), + div() + .flex() + .flex_row() + .items_center() + .gap(px(8.)) + .w(px(208.)) + .child(self.render_recording_slider( + "auto-zoom-hold-slider", + RecordingSlider::ZoomHoldDuration, + zoom_hold, + ZOOM_HOLD_MIN, + ZOOM_HOLD_MAX, + self.zoom_hold_slider_track.clone(), + cx, + )) + .child( + div() + .w(px(36.)) + .text_size(px(12.)) + .text_color(theme.settings_muted()) + .child(format!("{zoom_hold:.1}s")), + ) + .into_any_element(), + ), + self.setting_row( + tr::settings::general::recording::auto_zoom_merge_gap::label(), + Some(tr::settings::general::recording::auto_zoom_merge_gap::description()), + div() + .flex() + .flex_row() + .items_center() + .gap(px(8.)) + .w(px(208.)) + .child(self.render_recording_slider( + "auto-zoom-merge-gap-slider", + RecordingSlider::ZoomMergeGap, + zoom_merge_gap, + ZOOM_MERGE_GAP_MIN, + ZOOM_MERGE_GAP_MAX, + self.zoom_merge_gap_slider_track.clone(), + cx, + )) + .child( + div() + .w(px(36.)) + .text_size(px(12.)) + .text_color(theme.settings_muted()) + .child(format!("{zoom_merge_gap:.1}s")), + ) + .into_any_element(), + ), self.setting_row( tr::settings::general::recording::capture_keyboard::label(), Some(tr::settings::general::recording::capture_keyboard::description()), @@ -4942,10 +5011,31 @@ impl SettingsWindow { /// ``: a `h-[0.3rem]` /// `bg-gray-4` track with a `bg-blue-9` fill and a `size-4` thumb. fn render_zoom_slider(&self, value: f32, cx: &mut Context) -> impl IntoElement { + self.render_recording_slider( + "zoom-slider", + RecordingSlider::ZoomAmount, + value, + ZOOM_MIN, + ZOOM_MAX, + self.slider_track.clone(), + cx, + ) + } + + fn render_recording_slider( + &self, + id: &'static str, + slider: RecordingSlider, + value: f32, + min: f32, + max: f32, + track: Rc>>>, + cx: &mut Context, + ) -> impl IntoElement { let theme = self.theme; - let fraction = ((value - ZOOM_MIN) / (ZOOM_MAX - ZOOM_MIN)).clamp(0., 1.); + let fraction = ((value - min) / (max - min)).clamp(0., 1.); - ui::Slider::new("zoom-slider", fraction, self.slider_track.clone()) + ui::Slider::new(id, fraction, track) .flex() .track(px(5.), theme.settings_fill()) .fill(Hsla::from(theme.blue_9)) @@ -4962,9 +5052,10 @@ impl SettingsWindow { // centre over the 5px track. .thumb_top(px(-6.)) .on_drag_start( - cx.listener(|this, event: &gpui::MouseDownEvent, _window, cx| { + cx.listener(move |this, event: &gpui::MouseDownEvent, _window, cx| { this.slider_dragging = true; - this.set_zoom_from(event.position, cx); + this.active_slider = Some(slider); + this.set_slider_from(slider, event.position, cx); }), ) } @@ -4976,38 +5067,111 @@ impl SettingsWindow { if !self.slider_dragging { return None; } + let slider = self.active_slider.unwrap_or(RecordingSlider::ZoomAmount); Some( ui::Slider::drag_layer( "zoom-slider-drag", - cx.listener(|this, event: &gpui::MouseMoveEvent, _window, cx| { - this.set_zoom_from(event.position, cx); + cx.listener(move |this, event: &gpui::MouseMoveEvent, _window, cx| { + this.set_slider_from(slider, event.position, cx); }), - cx.listener(|this, _, _window, cx| { + cx.listener(move |this, _, _window, cx| { this.slider_dragging = false; + this.active_slider = None; // `onChangeEnd` -- the store write happens once, at the // end of the drag. - let value = this.settings.new_zoom_amount(); - this.write( - "defaultZoomAmount", - Value::from(f64::from(ui::snap_to_step(value, ZOOM_MIN, ZOOM_MAX, 0.1))), - cx, - ); + match slider { + RecordingSlider::ZoomAmount => { + let value = this.settings.new_zoom_amount(); + this.write( + "defaultZoomAmount", + Value::from(f64::from(ui::snap_to_step( + value, ZOOM_MIN, ZOOM_MAX, 0.1, + ))), + cx, + ); + } + RecordingSlider::ZoomHoldDuration => { + let value = this.settings.auto_zoom_hold_seconds(); + this.write( + "autoZoomHoldDuration", + Value::from(f64::from(ui::snap_to_step( + value, + ZOOM_HOLD_MIN, + ZOOM_HOLD_MAX, + 0.1, + ))), + cx, + ); + } + RecordingSlider::ZoomMergeGap => { + let value = this.settings.auto_zoom_merge_gap_seconds(); + this.write( + "autoZoomMergeGap", + Value::from(f64::from(ui::snap_to_step( + value, + ZOOM_MERGE_GAP_MIN, + ZOOM_MERGE_GAP_MAX, + 0.1, + ))), + cx, + ); + } + } }), ) .into_any_element(), ) } - fn set_zoom_from(&mut self, position: Point, cx: &mut Context) { - // `step={0.1}` - let Some(value) = - ui::slider_value_at(&self.slider_track, position, ZOOM_MIN, ZOOM_MAX, 0.1) - else { - return; - }; - if self.settings.default_zoom_amount != Some(value) { - self.settings.default_zoom_amount = Some(value); - cx.notify(); + fn set_slider_from( + &mut self, + slider: RecordingSlider, + position: Point, + cx: &mut Context, + ) { + match slider { + RecordingSlider::ZoomAmount => { + // `step={0.1}` + let Some(value) = + ui::slider_value_at(&self.slider_track, position, ZOOM_MIN, ZOOM_MAX, 0.1) + else { + return; + }; + if self.settings.default_zoom_amount != Some(value) { + self.settings.default_zoom_amount = Some(value); + cx.notify(); + } + } + RecordingSlider::ZoomHoldDuration => { + let Some(value) = ui::slider_value_at( + &self.zoom_hold_slider_track, + position, + ZOOM_HOLD_MIN, + ZOOM_HOLD_MAX, + 0.1, + ) else { + return; + }; + if self.settings.auto_zoom_hold_duration != Some(value) { + self.settings.auto_zoom_hold_duration = Some(value); + cx.notify(); + } + } + RecordingSlider::ZoomMergeGap => { + let Some(value) = ui::slider_value_at( + &self.zoom_merge_gap_slider_track, + position, + ZOOM_MERGE_GAP_MIN, + ZOOM_MERGE_GAP_MAX, + 0.1, + ) else { + return; + }; + if self.settings.auto_zoom_merge_gap != Some(value) { + self.settings.auto_zoom_merge_gap = Some(value); + cx.notify(); + } + } } } @@ -5035,6 +5199,10 @@ impl SettingsWindow { /// `minValue={1} maxValue={4.5}` on the zoom slider. const ZOOM_MIN: f32 = 1.; const ZOOM_MAX: f32 = 4.5; +const ZOOM_HOLD_MIN: f32 = 0.5; +const ZOOM_HOLD_MAX: f32 = 5.0; +const ZOOM_MERGE_GAP_MIN: f32 = 0.0; +const ZOOM_MERGE_GAP_MAX: f32 = 5.0; /// `new URL(v).origin` -- the Tauri handler normalises the typed URL to its /// origin before storing it, and throws (leaving the setting alone) when it diff --git a/apps/desktop-gpui/src/settings_window/recording_action_tests.rs b/apps/desktop-gpui/src/settings_window/recording_action_tests.rs index 7ddeb9ff730..82487e4a7d1 100644 --- a/apps/desktop-gpui/src/settings_window/recording_action_tests.rs +++ b/apps/desktop-gpui/src/settings_window/recording_action_tests.rs @@ -41,7 +41,10 @@ fn settings_fixture(window: &mut Window, cx: &mut Context) -> Se placeholders: ui::CollapsibleState::new(false), placeholders_task: None, slider_track: Rc::new(Cell::new(None)), + zoom_hold_slider_track: Rc::new(Cell::new(None)), + zoom_merge_gap_slider_track: Rc::new(Cell::new(None)), slider_dragging: false, + active_slider: None, windows: Vec::new(), focus: cx.focus_handle(), _sign_in_subscriptions: std::array::from_fn(|_| gpui::Subscription::new(|| {})), diff --git a/apps/desktop-gpui/src/store.rs b/apps/desktop-gpui/src/store.rs index d2ac2be411b..3fbf6b2a748 100644 --- a/apps/desktop-gpui/src/store.rs +++ b/apps/desktop-gpui/src/store.rs @@ -984,6 +984,8 @@ pub struct GeneralSettings { pub custom_cursor_capture: bool, pub auto_zoom_on_clicks: bool, pub default_zoom_amount: Option, + pub auto_zoom_hold_duration: Option, + pub auto_zoom_merge_gap: Option, pub capture_keyboard_events: bool, pub macbook_notch_overlay: bool, /// GPUI-only `recordingMarkerSound`: a short cue when a marker is added @@ -1381,6 +1383,18 @@ impl GeneralSettings { self.default_zoom_amount.unwrap_or(1.5) } + pub fn auto_zoom_hold_seconds(&self) -> f32 { + self.auto_zoom_hold_duration + .unwrap_or(crate::editor_edits::DEFAULT_AUTO_ZOOM_HOLD_SECONDS as f32) + .clamp(0.5, 5.0) + } + + pub fn auto_zoom_merge_gap_seconds(&self) -> f32 { + self.auto_zoom_merge_gap + .unwrap_or(crate::editor_edits::DEFAULT_AUTO_ZOOM_MERGE_GAP_SECONDS as f32) + .clamp(0.0, 5.0) + } + pub fn load() -> Self { let store = cached_store(&tauri_store_path()).unwrap_or_default(); let empty = Map::new(); @@ -1432,6 +1446,8 @@ impl GeneralSettings { custom_cursor_capture: bool_at(general, "custom_cursor_capture2", true), auto_zoom_on_clicks: bool_at(general, "autoZoomOnClicks", false), default_zoom_amount: opt_f32_at(general, "defaultZoomAmount"), + auto_zoom_hold_duration: opt_f32_at(general, "autoZoomHoldDuration"), + auto_zoom_merge_gap: opt_f32_at(general, "autoZoomMergeGap"), capture_keyboard_events: bool_at(general, "captureKeyboardEvents", true), macbook_notch_overlay: bool_at(general, "macbookNotchOverlay", false), recording_marker_sound: bool_at(general, RECORDING_MARKER_SOUND_KEY, false), @@ -3099,6 +3115,8 @@ mod tests { "recordingCountdown": 5, "custom_cursor_capture2": false, "defaultZoomAmount": 2.5, + "autoZoomHoldDuration": 1.2, + "autoZoomMergeGap": 0.4, "maxFps": 120, "serverUrl": "https://cap.example.com", "excludedWindows": [{ "bundleIdentifier": null, "ownerName": null, "windowTitle": "Cap" }] @@ -3119,6 +3137,10 @@ mod tests { assert!(!settings.custom_cursor_capture); assert_eq!(settings.default_zoom_amount, Some(2.5)); assert_eq!(settings.new_zoom_amount(), 2.5); + assert_eq!(settings.auto_zoom_hold_duration, Some(1.2)); + assert_eq!(settings.auto_zoom_hold_seconds(), 1.2); + assert_eq!(settings.auto_zoom_merge_gap, Some(0.4)); + assert_eq!(settings.auto_zoom_merge_gap_seconds(), 0.4); assert_eq!(settings.max_fps, 120); assert_eq!(settings.server_url, "https://cap.example.com"); assert_eq!(settings.excluded_windows.len(), 1);