Repository navigation
feat(desktop-gpui): resize a picked window to a ratio or size, and focus it to record it alone #3454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(desktop-gpui): resize a picked window to a ratio or size, and focus it to record it alone #3454
Changes from all commits
26d1cb8
981d474
22eead8
842bcde
8ebbe51
662b388
6e2ab57
6e3d8de
0fbf152
79492cf
cbeda16
70d4077
77c2f41
370929d
63d2dbb
db1e726
73e92c0
f220b1a
06ab8c4
d6c80f6
a8bcd6c
be4fe9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -916,6 +916,7 @@ pub(crate) fn show_main_window_after_capture_pause(cx: &mut App) { | |
| windows.main_hidden_for_picker || !windows.overlays.is_empty() | ||
| }; | ||
| if picker_open { | ||
| restore_arranged_windows(cx); | ||
| close_target_overlays(cx); | ||
| } | ||
| cx.global_mut::<AppWindows>().main_hidden_for_picker = false; | ||
|
|
@@ -1097,6 +1098,7 @@ pub fn request_close_main(cx: &mut App) { | |
| } | ||
|
|
||
| close_camera_window(cx); | ||
| restore_arranged_windows(cx); | ||
| close_target_overlays(cx); | ||
| // Closing the main window explicitly is not a picker dismissal; a stale | ||
| // flag here would reveal the window the user just closed. | ||
|
|
@@ -2146,13 +2148,38 @@ fn open_overlays_core(request: OverlayRequest, cx: &mut App) -> bool { | |
| true | ||
| } | ||
|
|
||
| /// Put back every window the picker moved or resized: the picker closed | ||
| /// without recording them. | ||
| fn restore_arranged_windows(cx: &mut App) { | ||
| let arranged = TargetSelect::global(cx).update(cx, |select, _| select.take_arranged()); | ||
| crate::foreign_window::put_back_all(arranged, cx.background_executor()); | ||
| } | ||
|
|
||
| /// A start the user turned back from (a storage or microphone prompt): the | ||
| /// windows the picker shaped go back. Never while a capture runs, which may be | ||
| /// recording them. | ||
| pub(crate) fn restore_arranged_windows_if_idle(cx: &mut App) { | ||
| if RecordingSession::global(cx).read(cx).phase == Phase::Idle { | ||
| restore_arranged_windows(cx); | ||
| } | ||
| } | ||
|
|
||
| /// A recording or screenshot is about to capture the windows the picker | ||
| /// shaped, so they stay where they are. | ||
| fn keep_arranged_windows(cx: &mut App) { | ||
| TargetSelect::global(cx).update(cx, |select, _| { | ||
| select.take_arranged(); | ||
| }); | ||
| } | ||
|
|
||
| /// Close the overlays and clear the main window's armed target -- Escape, the | ||
| /// overlay's own close button, or the main window toggling the mode off. | ||
| /// | ||
| /// A "cancelled" dismissal in the Tauri vocabulary, which is the kind that | ||
| /// reveals the main window again (`dismissalReveals`, | ||
| /// `new-main/index.tsx:2047-2058`). | ||
| pub fn dismiss_target_overlays(cx: &mut App) { | ||
| restore_arranged_windows(cx); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reopening Cap bypasses restoration of arranged windowsMedium Severity The unchanged
Comment on lines
2181
to
+2182
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Window restoration only runs through Restore moved windows on non-recording close paths too. Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop-gpui/src/app_windows.rs
Line: 2173-2174
Comment:
**Some exits leave windows changed**
Window restoration only runs through `dismiss_target_overlays`, but other user actions close the picker directly. Choosing Open Cap from the tray calls `show_main_window`, which closes the overlays without restoring moved windows. The next picker opening clears `arranged`, losing the original positions.
Restore moved windows on non-recording close paths too.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| if RecordingSession::global(cx).read(cx).phase == Phase::Idle | ||
| && RecordingSession::global(cx) | ||
| .read(cx) | ||
|
|
@@ -2388,6 +2415,14 @@ pub fn refresh_microphone_overlays(cx: &mut App) { | |
| } | ||
|
|
||
| /// The overlays' native window numbers, for capture exclusion. | ||
| fn overlay_handles(cx: &App) -> Vec<gpui::AnyWindowHandle> { | ||
| cx.global::<AppWindows>() | ||
| .overlays | ||
| .iter() | ||
| .map(|(_, handle)| (*handle).into()) | ||
| .collect() | ||
| } | ||
|
|
||
| fn overlay_window_ids(cx: &mut App) -> Vec<scap_targets::WindowId> { | ||
| let handles: Vec<_> = cx | ||
| .global::<AppWindows>() | ||
|
|
@@ -2418,7 +2453,52 @@ pub fn show_instant_sign_in(kind: crate::main_window::TargetType, cx: &mut App) | |
| show_main_window(cx); | ||
| } | ||
|
|
||
| /// How long Start waits for the picker's window moves to land. A move waits | ||
| /// on the target app; one still pending after this does not start a | ||
| /// recording of bounds that are about to change. | ||
| const ARRANGE_SETTLE_LIMIT: std::time::Duration = std::time::Duration::from_secs(5); | ||
|
|
||
| pub fn start_recording_from_overlay(target: ScreenCaptureTarget, cx: &mut App) { | ||
| if !TargetSelect::global(cx).read(cx).has_arranged() { | ||
| start_recording_from_overlay_now(target, cx); | ||
| return; | ||
| } | ||
| // A size the user just chose may still be on its way to the window, and | ||
| // the capture reads the window's bounds when it starts. The Start belongs | ||
| // to these overlays: dismissing them, or opening a new picker, drops it. | ||
| let overlays = overlay_handles(cx); | ||
| let settled = crate::foreign_window::in_order(|| ()); | ||
| cx.spawn(async move |cx| { | ||
| let limit = cx.background_executor().timer(ARRANGE_SETTLE_LIMIT); | ||
| let settled = matches!( | ||
| futures_util::future::select(std::pin::pin!(settled), std::pin::pin!(limit)).await, | ||
| futures_util::future::Either::Left(_) | ||
| ); | ||
| cx.update(|cx| { | ||
| let current = overlay_handles(cx); | ||
| if current.is_empty() || current != overlays { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred Start records the click-time target after the pick changesMedium Severity The deferred Start captured |
||
| return; | ||
| } | ||
| if !settled { | ||
| tracing::warn!("a picked window is still moving; recording not started"); | ||
| // The picker has nowhere to show the error: close it, which | ||
| // puts the windows back, and bring the main window forward. | ||
| dismiss_target_overlays(cx); | ||
| show_main_window(cx); | ||
| RecordingSession::global(cx).update(cx, |session, cx| { | ||
| session.error = | ||
| Some("The window is still being resized. Try starting again.".into()); | ||
| cx.notify(); | ||
| }); | ||
| return; | ||
|
Comment on lines
+2482
to
+2493
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a window move takes more than five seconds, Start stops and saves the retry message in Show the error in the picker, or reveal the main window when Start times out. Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop-gpui/src/app_windows.rs
Line: 2482-2489
Comment:
**Start hides its retry message**
When a window move takes more than five seconds, Start stops and saves the retry message in `session.error`. The picker stays open, but only the hidden main window shows that error. The user sees neither a recording nor an explanation until they close the picker.
Show the error in the picker, or reveal the main window when Start times out.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
| start_recording_from_overlay_now(target, cx); | ||
| }); | ||
| }) | ||
| .detach(); | ||
| } | ||
|
|
||
| fn start_recording_from_overlay_now(target: ScreenCaptureTarget, cx: &mut App) { | ||
| let main = cx.global::<AppWindows>().main; | ||
| if RecordingSession::global(cx).read(cx).phase != Phase::Idle | ||
| || main | ||
|
|
@@ -3936,6 +4016,7 @@ fn begin_recording_after_preflight(config: StartConfig, cx: &mut App) { | |
| if session.read(cx).phase != Phase::Idle { | ||
| return; | ||
| } | ||
| keep_arranged_windows(cx); | ||
|
|
||
| // `start_recording`'s first act (`src-tauri/src/recording.rs:1492-1494`): | ||
| // an armed editor recording target forces Studio, whatever mode the main | ||
|
|
@@ -6658,6 +6739,7 @@ pub fn abort_editor_recording_flow(cx: &mut App) { | |
| }) else { | ||
| return; | ||
| }; | ||
| restore_arranged_windows(cx); | ||
| close_target_overlays(cx); | ||
| cx.global_mut::<AppWindows>().editor_hidden_for_picker = None; | ||
| cx.global_mut::<AppWindows>().main_hidden_for_picker = false; | ||
|
|
@@ -6848,6 +6930,7 @@ pub fn prepare_for_screenshot_capture(cx: &mut App) -> bool { | |
| // The screenshot branch of `onRecordingStart` nulls the saved camera bounds | ||
| // just like the recording branch does, so a parked bubble stays parked. | ||
| release_camera_park(cx); | ||
| keep_arranged_windows(cx); | ||
| close_target_overlays(cx); | ||
| true | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: macOS picker smoke grants Accessibility TCC to runner processes used by fork-PR code
New loop grants Accessibility TCC to /bin/bash and runner worker processes used by untrusted fork-PR builds.
Grant Accessibility only to the built app, or prevent fork PRs from running this self-hosted smoke with the grant.
AI prompt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This grant does not widen what PR code can do on that runner. The Blacksmith macOS runner is a single-use VM, and the same job already runs with passwordless
sudo(this very step writesTCC.dbwith it), so code from a fork PR could grant itself any TCC service anyway. Accessibility is needed because the smoke moves another app's window through the AX API, and macOS checks that against the responsible process (the runner that launchescap-gpui), not the binary alone; granting it to the app only would make the window-arrangement check fail.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The developer’s explanation addresses the core concern: the Blacksmith runner is a single-use VM, so the TCC grant is not persistent, and the same job already runs with passwordless sudo—meaning untrusted code could already modify TCC.db directly. Additionally, the Accessibility grant to the runner processes is technically required because macOS applies the AX permission check to the responsible process launching the app, not the built binary alone, so restricting the grant to the app would break the smoke test. No additional exposure is introduced by this change.