gpui: Add is_visible and on_visibility_changed API on Window (#64107)

Adds a `WindowVisibility` API to GPUI: `Window::visibility()` /
`Window::is_visible()`, `Window::observe_window_visibility`, and
`Context::observe_window_visibility`, backed by one platform signal per
backend — `NSWindow.occlusionState` on macOS, `WS_VISIBLE` plus the
minimized state on Windows, the xdg-shell v6 `suspended` toplevel state
on Wayland, mapped state plus `VisibilityNotify` on X11, and the Page
Visibility API on web. Only transitions are reported, the initial value
is read from the platform when the window is created, and the two
`PlatformWindow` methods have no default so a backend cannot silently
opt out. The per-platform limits (Windows can't see a fully covered
window, pre-v6 Wayland compositors never report `Hidden`) are documented
on the type.

It also adds `Platform::on_system_sleep` / `App::on_system_sleep`,
mirroring the existing wake hook, on macOS
(`NSWorkspaceWillSleepNotification`), Windows (`PBT_APMSUSPEND`), and
Linux (logind `PrepareForSleep(true)`, which the listener previously
discarded). Nothing in Zed subscribes to either yet; the hang-telemetry
journal will use visibility to stop measuring frames that could not have
been presented, and the sleep/wake pair to bracket time the process was
not running.

Release Notes:

- N/A
This commit is contained in:
Anthony Eid authored and GitHub committed 2026-09-12 03:25:52 +00:00
1 parent 56239b364e
commit 6736a09abe
22 files changed
+719 -119

No files matched your search

+33
View File
@@ -707,6 +707,7 @@ pub struct App {
pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>,
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
pub(crate) thermal_state_observers: SubscriberSet<(), Handler>,
pub(crate) system_sleep_observers: SubscriberSet<(), Handler>,
pub(crate) system_wake_observers: SubscriberSet<(), Handler>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
@@ -841,6 +842,7 @@ impl App {
keystroke_interceptors: SubscriberSet::new(),
keyboard_layout_observers: SubscriberSet::new(),
thermal_state_observers: SubscriberSet::new(),
system_sleep_observers: SubscriberSet::new(),
system_wake_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
@@ -901,6 +903,18 @@ impl App {
}
}));
platform.on_system_sleep(Box::new({
let app = Rc::downgrade(&app);
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.system_sleep_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
}
}
}));
platform.on_system_wake(Box::new({
let app = Rc::downgrade(&app);
move || {
@@ -1371,6 +1385,25 @@ impl App {
subscription
}
/// Invokes a handler when the system is about to sleep.
///
/// The platform gives the process only a short time before suspending, so
/// handlers should record state or cancel work rather than start it.
pub fn on_system_sleep<F>(&self, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut App),
{
let (subscription, activate) = self.system_sleep_observers.insert(
(),
Box::new(move |cx| {
callback(cx);
true
}),
);
activate();
subscription
}
/// Invokes a handler when the system wakes from sleep.
pub fn on_system_wake<F>(&self, mut callback: F) -> Subscription
where
+20
View File
@@ -2,6 +2,7 @@ use crate::{
AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, Effect, EntityId, EventEmitter,
FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Priority, Reservation,
SubscriberSet, Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle,
WindowVisibility,
};
use anyhow::Result;
use futures::FutureExt;
@@ -458,6 +459,25 @@ impl<'a, T: 'static> Context<'a, T> {
subscription
}
/// Registers a callback to be invoked when the window's visibility changes
/// (see [`WindowVisibility`]).
pub fn observe_window_visibility(
&self,
window: &mut Window,
mut callback: impl FnMut(&mut T, WindowVisibility, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let (subscription, activate) = window.visibility_observers.insert(
(),
Box::new(move |visibility, window, cx| {
view.update(cx, |view, cx| callback(view, visibility, window, cx))
.is_ok()
}),
);
activate();
subscription
}
/// Registers a callback to be invoked when the window appearance changes.
pub fn observe_window_appearance(
&self,
+16 -1
View File
@@ -6,7 +6,7 @@ use crate::{
Pixels, Platform, Point, Render, Result, SharedString, Size, SystemNotification,
SystemNotificationResponse, Task, TestDispatcher, TestPlatform, TestScreenCaptureSource,
TestWindow, TextSystem, VisualContext, Window, WindowBounds, WindowHandle, WindowOptions,
app::GpuiMode, window::ElementArenaScope,
WindowVisibility, app::GpuiMode, window::ElementArenaScope,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
@@ -418,6 +418,16 @@ impl TestAppContext {
self.test_window(window_handle).simulate_resize(size);
}
/// Simulates a change in whether the platform is presenting the window.
pub fn simulate_window_visibility_change(
&self,
window_handle: AnyWindowHandle,
visibility: WindowVisibility,
) {
self.test_window(window_handle)
.simulate_visibility_change(visibility);
}
/// Simulates visible viewport changes without resizing the window's layout area.
pub fn simulate_window_visual_viewport_change(
&self,
@@ -919,6 +929,11 @@ impl VisualTestContext {
self.simulate_window_resize(self.window, size)
}
/// Simulates a change in whether the platform is presenting this window.
pub fn simulate_visibility_change(&self, visibility: WindowVisibility) {
self.simulate_window_visibility_change(self.window, visibility);
}
/// Simulates the window moving to a display with a different scale factor.
pub fn simulate_scale_factor_change(&self, scale_factor: f32) {
self.simulate_window_scale_factor_change(self.window, scale_factor)
+47
View File
@@ -78,6 +78,43 @@ pub use app_menu::*;
pub use keyboard::*;
pub use keystroke::*;
/// Whether the platform is presenting a window's frames.
///
/// This is about presentation, not the window's shown/hidden state: a shown
/// window that is fully covered by other windows, minimized, on another
/// Space or virtual desktop, or on a display that is asleep is `Hidden`. A
/// window only partly covered by other windows is `Visible`.
///
/// Each platform reports from a single source, and what that source can see
/// differs:
///
/// * macOS: `NSWindow.occlusionState`. Covers all of the cases above.
/// * Windows: `WS_VISIBLE` and the minimized state. Windows keeps compositing
/// covered windows for thumbnails and Alt-Tab and offers no occlusion
/// notification, so a fully covered window stays `Visible`. Display sleep is
/// not reported either.
/// * Wayland: the `xdg_toplevel` `suspended` state (xdg-shell v6). Compositors
/// that don't support it never report `Hidden`.
/// * X11: mapped state plus `VisibilityNotify`. Compositing window managers
/// generally never report a window as fully obscured, so covering is only
/// detected without compositing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowVisibility {
/// At least part of the window is being presented; frames drawn for it
/// will be shown.
Visible,
/// No part of the window is being presented. The platform will not
/// request frames for it until it becomes visible again.
Hidden,
}
impl WindowVisibility {
/// Whether frames drawn for the window will be shown.
pub fn is_visible(self) -> bool {
self == Self::Visible
}
}
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
pub(crate) use test::*;
@@ -221,6 +258,9 @@ pub trait Platform: 'static {
fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
/// Registers the callback invoked when the system is about to sleep.
fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
/// Registers the callback invoked when the system resumes from sleep.
fn on_system_wake(&self, callback: Box<dyn FnMut()>);
// Mobile platform methods. On mobile the OS owns the application
@@ -883,6 +923,9 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
/// Requests that the operating system draw attention to this window.
fn request_attention(&self) {}
fn is_active(&self) -> bool;
/// The current [`WindowVisibility`]. Read once when the window is created;
/// afterwards changes arrive through [`Self::on_visibility_change`].
fn visibility(&self) -> WindowVisibility;
fn is_hovered(&self) -> bool;
fn background_appearance(&self) -> WindowBackgroundAppearance;
fn set_title(&mut self, title: &str);
@@ -897,6 +940,10 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
/// Registers the callback invoked when [`Self::visibility`] changes. Only
/// transitions are reported; the callback runs on the main thread outside
/// of any window update.
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
fn on_moved(&self, callback: Box<dyn FnMut()>);
@@ -584,6 +584,8 @@ impl Platform for TestPlatform {
unimplemented!()
}
fn on_system_sleep(&self, _callback: Box<dyn FnMut()>) {}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_app_identity(&self, identifier: &str, name: &str) {
+25 -1
View File
@@ -4,7 +4,7 @@ use crate::{
PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration,
TextInputStateChange, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowInsets, WindowParams,
WindowControlArea, WindowInsets, WindowParams, WindowVisibility,
};
use collections::HashMap;
use gpui_util::ResultExt as _;
@@ -34,6 +34,8 @@ pub(crate) struct TestWindowState {
hit_test_window_control_callback: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
input_callback: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
visibility: WindowVisibility,
visibility_callback: Option<Box<dyn FnMut(WindowVisibility)>>,
hover_status_change_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
visual_viewport: Option<Bounds<Pixels>>,
@@ -105,6 +107,8 @@ impl TestWindow {
hit_test_window_control_callback: None,
input_callback: None,
active_status_change_callback: None,
visibility: WindowVisibility::Visible,
visibility_callback: None,
hover_status_change_callback: None,
resize_callback: None,
visual_viewport: None,
@@ -153,6 +157,18 @@ impl TestWindow {
self.0.lock().frame_scheduled
}
pub fn simulate_visibility_change(&self, visibility: WindowVisibility) {
let callback = {
let mut state = self.0.lock();
state.visibility = visibility;
state.visibility_callback.take()
};
if let Some(mut callback) = callback {
callback(visibility);
self.0.lock().visibility_callback = Some(callback);
}
}
pub fn simulate_visual_viewport_change(&self, bounds: Bounds<Pixels>) {
let callback = {
let mut state = self.0.lock();
@@ -394,6 +410,10 @@ impl PlatformWindow for TestWindow {
false
}
fn visibility(&self) -> WindowVisibility {
self.0.lock().visibility
}
fn is_hovered(&self) -> bool {
false
}
@@ -476,6 +496,10 @@ impl PlatformWindow for TestWindow {
self.0.lock().active_status_change_callback = Some(callback)
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
self.0.lock().visibility_callback = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.lock().hover_status_change_callback = Some(callback)
}
+2
View File
@@ -176,6 +176,8 @@ impl Platform for VisualTestPlatform {
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
fn on_system_sleep(&self, _callback: Box<dyn FnMut()>) {}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
+101 -1
View File
@@ -22,7 +22,7 @@ use crate::{
TextInputStateChange, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
point, prelude::*, px, rems, size, transparent_black,
WindowVisibility, point, prelude::*, px, rems, size, transparent_black,
};
use crate::gestures::{GestureTuning, RecognizedTouchGesture, TouchGestureRecognizer};
@@ -1195,6 +1195,9 @@ pub struct Window {
pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>,
pub(crate) button_layout_observers: SubscriberSet<(), AnyObserver>,
active: Rc<Cell<bool>>,
visibility: WindowVisibility,
pub(crate) visibility_observers:
SubscriberSet<(), Box<dyn FnMut(WindowVisibility, &mut Window, &mut App) -> bool>>,
hovered: Rc<Cell<bool>>,
pub(crate) needs_present: Rc<Cell<bool>>,
/// Tracks recent input event timestamps to determine if input is arriving at a high rate.
@@ -1568,6 +1571,7 @@ impl Window {
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
let invalidator = WindowInvalidator::new(handle.window_id());
let active = Rc::new(Cell::new(platform_window.is_active()));
let visibility = platform_window.visibility();
let hovered = Rc::new(Cell::new(platform_window.is_hovered()));
let needs_present = Rc::new(Cell::new(false));
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
@@ -1895,6 +1899,23 @@ impl Window {
.log_err();
}
}));
platform_window.on_visibility_change(Box::new({
let mut cx = cx.to_async();
move |visibility| {
handle
.update(&mut cx, |_, window, cx| {
if window.visibility == visibility {
return;
}
window.visibility = visibility;
window
.visibility_observers
.clone()
.retain(&(), |callback| callback(visibility, window, cx));
})
.log_err();
}
}));
platform_window.on_hover_status_change(Box::new({
let mut cx = cx.to_async();
move |active| {
@@ -2035,6 +2056,8 @@ impl Window {
appearance_observers: SubscriberSet::new(),
button_layout_observers: SubscriberSet::new(),
active,
visibility,
visibility_observers: SubscriberSet::new(),
hovered,
needs_present,
input_rate_tracker,
@@ -2128,6 +2151,37 @@ impl Window {
}
}
/// Whether the platform is presenting this window's frames (see
/// [`WindowVisibility`]).
pub fn visibility(&self) -> WindowVisibility {
self.visibility
}
/// Whether frames drawn for this window will be shown.
///
/// This is not the window's shown/hidden state: a shown window that is
/// fully behind another window, minimized, or on a sleeping display is not
/// visible here.
pub fn is_visible(&self) -> bool {
self.visibility.is_visible()
}
/// Registers a callback to be invoked when the window's visibility changes.
pub fn observe_window_visibility(
&self,
mut callback: impl FnMut(WindowVisibility, &mut Window, &mut App) + 'static,
) -> Subscription {
let (subscription, activate) = self.visibility_observers.insert(
(),
Box::new(move |visibility, window, cx| {
callback(visibility, window, cx);
true
}),
);
activate();
subscription
}
/// Registers a callback to be invoked when the window appearance changes.
pub fn observe_window_appearance(
&self,
@@ -7442,6 +7496,52 @@ mod tests {
WindowOptions, canvas, div, point, px, size,
};
/// Visibility transitions reach observers exactly once each, with the new
/// state already stored on the window, and never wake the platform for a
/// frame: the platform requests one itself when it resumes presenting.
#[gpui::test]
fn test_window_visibility(cx: &mut TestAppContext) {
use crate::WindowVisibility;
let window = cx.add_window(|_, _| EmptyView);
let observed = Rc::new(RefCell::new(Vec::new()));
let _subscription = window
.update(cx, {
let observed = observed.clone();
move |_, window, _| {
assert_eq!(window.visibility(), WindowVisibility::Visible);
assert!(window.is_visible());
window.observe_window_visibility(move |visibility, window, _| {
assert_eq!(window.visibility(), visibility);
observed.borrow_mut().push(visibility);
})
}
})
.unwrap();
let test_window = cx.test_window(window.into());
let frame_wake_count = test_window.frame_wake_count();
test_window.simulate_visibility_change(WindowVisibility::Hidden);
assert_eq!(*observed.borrow(), [WindowVisibility::Hidden]);
window
.update(cx, |_, window, _| assert!(!window.is_visible()))
.unwrap();
// Platforms may report the same state again; observers only see changes.
test_window.simulate_visibility_change(WindowVisibility::Hidden);
assert_eq!(observed.borrow().len(), 1);
test_window.simulate_visibility_change(WindowVisibility::Visible);
assert_eq!(
*observed.borrow(),
[WindowVisibility::Hidden, WindowVisibility::Visible]
);
window
.update(cx, |_, window, _| assert!(window.is_visible()))
.unwrap();
assert_eq!(test_window.frame_wake_count(), frame_wake_count);
}
#[gpui::test]
fn test_fully_visible_bounds_preserve_layout_viewport(cx: &mut TestAppContext) {
let window = cx.add_window(|_, _| EmptyView);
@@ -25,7 +25,7 @@ impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver, wake_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
@@ -38,9 +38,9 @@ impl HeadlessClient {
.ok();
handle
.insert_source(wake_receiver, |event, _, client: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(()) = event {
client.with_common(|common| common.handle_system_wake());
.insert_source(power_receiver, |event, _, client: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(event) = event {
client.with_common(|common| common.handle_system_power_event(event));
}
})
.ok();
@@ -19,7 +19,7 @@ use gpui::{
DisplayId, GpuSpecs, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
Scene, Size, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowParams, px,
WindowControlArea, WindowParams, WindowVisibility, px,
};
#[derive(Debug)]
@@ -159,6 +159,11 @@ impl PlatformWindow for HeadlessWindow {
false
}
fn visibility(&self) -> WindowVisibility {
// There is no display server: `draw` discards every scene.
WindowVisibility::Hidden
}
fn is_hovered(&self) -> bool {
false
}
@@ -198,6 +203,8 @@ impl PlatformWindow for HeadlessWindow {
fn on_active_status_change(&self, _callback: Box<dyn FnMut(bool)>) {}
fn on_visibility_change(&self, _callback: Box<dyn FnMut(WindowVisibility)>) {}
fn on_hover_status_change(&self, _callback: Box<dyn FnMut(bool)>) {}
fn on_resize(&self, _callback: Box<dyn FnMut(Size<Pixels>, f32)>) {}
+49 -22
View File
@@ -120,9 +120,22 @@ pub(crate) struct PlatformHandlers {
pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
pub(crate) system_sleep: Option<Box<dyn FnMut()>>,
pub(crate) system_wake: Option<Box<dyn FnMut()>>,
}
/// A logind `PrepareForSleep` signal, forwarded from the D-Bus listener to
/// the client's event loop.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
allow(dead_code)
)]
pub(crate) enum SystemPowerEvent {
Sleep,
Wake,
}
pub(crate) struct LinuxCommon {
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
@@ -139,8 +152,8 @@ pub(crate) struct LinuxCommon {
not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
allow(dead_code)
)]
wake_sender: Sender<()>,
wake_listener_started: bool,
power_sender: Sender<SystemPowerEvent>,
power_listener_started: bool,
}
impl LinuxCommon {
@@ -149,10 +162,10 @@ impl LinuxCommon {
) -> (
Self,
PriorityQueueCalloopReceiver<RunnableVariant>,
calloop::channel::Channel<()>,
calloop::channel::Channel<SystemPowerEvent>,
) {
let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
let (wake_sender, wake_receiver) = calloop::channel::channel();
let (power_sender, power_receiver) = calloop::channel::channel();
#[cfg(any(feature = "wayland", feature = "x11"))]
let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
@@ -178,40 +191,45 @@ impl LinuxCommon {
app_name: None,
system_notifications: crate::linux::system_notifications::SystemNotificationState::new(
),
wake_sender,
wake_listener_started: false,
power_sender,
power_listener_started: false,
};
(common, main_receiver, wake_receiver)
(common, main_receiver, power_receiver)
}
pub(crate) fn start_wake_listener(&mut self) {
if !self.wake_listener_started {
pub(crate) fn start_power_listener(&mut self) {
if !self.power_listener_started {
#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
smol::spawn({
let wake_sender = self.wake_sender.clone();
let power_sender = self.power_sender.clone();
async move {
if let Err(error) = listen_for_system_wake(wake_sender).await {
log::debug!("failed to listen for system wake events: {error:?}");
if let Err(error) = listen_for_system_power_events(power_sender).await {
log::debug!("failed to listen for system sleep/wake events: {error:?}");
}
}
})
.detach();
self.wake_listener_started = true;
self.power_listener_started = true;
}
}
pub(crate) fn handle_system_wake(&mut self) {
if let Some(mut callback) = self.callbacks.system_wake.take() {
pub(crate) fn handle_system_power_event(&mut self, event: SystemPowerEvent) {
let callback = match event {
SystemPowerEvent::Sleep => &mut self.callbacks.system_sleep,
SystemPowerEvent::Wake => &mut self.callbacks.system_wake,
};
if let Some(callback) = callback.as_mut() {
callback();
self.callbacks.system_wake = Some(callback);
}
}
}
#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
async fn listen_for_system_wake(wake_sender: Sender<()>) -> anyhow::Result<()> {
async fn listen_for_system_power_events(
power_sender: Sender<SystemPowerEvent>,
) -> anyhow::Result<()> {
use futures::StreamExt as _;
let connection = ashpd::zbus::Connection::system().await?;
@@ -225,10 +243,12 @@ async fn listen_for_system_wake(wake_sender: Sender<()>) -> anyhow::Result<()> {
let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?;
while let Some(message) = sleep_events.next().await {
let sleeping = message.body().deserialize::<bool>()?;
if !sleeping {
wake_sender.send(()).ok();
}
let event = if message.body().deserialize::<bool>()? {
SystemPowerEvent::Sleep
} else {
SystemPowerEvent::Wake
};
power_sender.send(event).ok();
}
Ok(())
@@ -594,10 +614,17 @@ impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
});
}
fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
self.inner.with_common(|common| {
common.callbacks.system_sleep = Some(callback);
common.start_power_listener();
});
}
fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
self.inner.with_common(|common| {
common.callbacks.system_wake = Some(callback);
common.start_wake_listener();
common.start_power_listener();
});
}
+11 -5
View File
@@ -260,7 +260,9 @@ impl Globals {
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
seat,
wm_base: globals.bind(&qh, 1..=5, ()).unwrap(),
// Accept any xdg_wm_base version up to 6, which added the `suspended`
// toplevel state; older compositors bind at their own version.
wm_base: globals.bind(&qh, 1..=6, ()).unwrap(),
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
@@ -787,7 +789,7 @@ impl WaylandClient {
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver, wake_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
@@ -809,10 +811,14 @@ impl WaylandClient {
handle
.insert_source(
wake_receiver,
power_receiver,
|event, _, client: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(()) = event {
client.get_client().borrow_mut().common.handle_system_wake();
if let calloop::channel::Event::Msg(event) = event {
client
.get_client()
.borrow_mut()
.common
.handle_system_power_event(event);
}
},
)
+52 -16
View File
@@ -37,7 +37,7 @@ use gpui::{
Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size,
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
WindowControls, WindowDecorations, WindowKind, WindowParams,
WindowControls, WindowDecorations, WindowKind, WindowParams, WindowVisibility,
layer_shell::{Anchor, LayerShellNotSupportedError},
popup::PopupOptions,
px, size,
@@ -49,6 +49,7 @@ pub(crate) struct Callbacks {
request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
input: Option<Box<dyn FnMut(gpui::PlatformInput) -> gpui::DispatchEventResult>>,
active_status_change: Option<Box<dyn FnMut(bool)>>,
visibility_change: Option<Box<dyn FnMut(WindowVisibility)>>,
hover_status_change: Option<Box<dyn FnMut(bool)>>,
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved: Option<Box<dyn FnMut()>>,
@@ -91,6 +92,7 @@ struct InProgressConfigure {
fullscreen: bool,
maximized: bool,
resizing: bool,
visibility: WindowVisibility,
tiling: Tiling,
}
@@ -116,6 +118,8 @@ pub struct WaylandWindowState {
background_appearance: WindowBackgroundAppearance,
fullscreen: bool,
maximized: bool,
/// `Hidden` while the `xdg_toplevel` `suspended` state (xdg-shell v6) is set.
visibility: WindowVisibility,
tiling: Tiling,
window_bounds: Bounds<Pixels>,
client: WaylandClientStatePtr,
@@ -613,6 +617,7 @@ impl WaylandWindowState {
background_appearance: WindowBackgroundAppearance::Opaque,
fullscreen: false,
maximized: false,
visibility: WindowVisibility::Visible,
tiling: Tiling::default(),
window_bounds: options.bounds,
in_progress_configure: None,
@@ -1078,27 +1083,37 @@ impl WaylandWindowStatePtr {
state.fullscreen = configure.fullscreen;
state.maximized = configure.maximized;
state.tiling = configure.tiling;
let visibility_changed = state.visibility != configure.visibility;
state.visibility = configure.visibility;
// Limit interactive resizes to once per vblank
if configure.resizing && state.resize_throttle {
let throttled = configure.resizing && state.resize_throttle;
if throttled {
state.surface_state.ack_configure(serial);
return;
} else if configure.resizing {
state.resize_throttle = true;
}
if !configure.fullscreen && !configure.maximized {
configure.size = if got_unmaximized {
Some(state.window_bounds.size)
} else {
compute_outer_size(state.inset(), configure.size, state.tiling)
};
if let Some(size) = configure.size {
state.window_bounds = Bounds {
origin: Point::default(),
size,
} else {
if configure.resizing {
state.resize_throttle = true;
}
if !configure.fullscreen && !configure.maximized {
configure.size = if got_unmaximized {
Some(state.window_bounds.size)
} else {
compute_outer_size(state.inset(), configure.size, state.tiling)
};
if let Some(size) = configure.size {
state.window_bounds = Bounds {
origin: Point::default(),
size,
};
}
}
}
drop(state);
if visibility_changed {
self.report_visibility(configure.visibility);
}
if throttled {
return;
}
if let Some(size) = configure.size {
self.resize(size);
}
@@ -1192,6 +1207,7 @@ impl WaylandWindowStatePtr {
let mut fullscreen = false;
let mut maximized = false;
let mut resizing = false;
let mut visibility = WindowVisibility::Visible;
for state in states {
match state {
@@ -1202,6 +1218,7 @@ impl WaylandWindowStatePtr {
fullscreen = true;
}
xdg_toplevel::State::Resizing => resizing = true,
xdg_toplevel::State::Suspended => visibility = WindowVisibility::Hidden,
xdg_toplevel::State::TiledTop => {
tiling.top = true;
}
@@ -1230,6 +1247,7 @@ impl WaylandWindowStatePtr {
fullscreen,
maximized,
resizing,
visibility,
tiling,
});
@@ -1304,6 +1322,7 @@ impl WaylandWindowStatePtr {
fullscreen: false,
maximized: false,
resizing: false,
visibility: WindowVisibility::Visible,
tiling: Tiling::default(),
});
drop(state);
@@ -1338,6 +1357,7 @@ impl WaylandWindowStatePtr {
fullscreen: false,
maximized: false,
resizing: false,
visibility: WindowVisibility::Visible,
tiling: Tiling::default(),
});
@@ -1556,6 +1576,14 @@ impl WaylandWindowStatePtr {
}
}
fn report_visibility(&self, visibility: WindowVisibility) {
let callback = self.callbacks.borrow_mut().visibility_change.take();
if let Some(mut callback) = callback {
callback(visibility);
self.callbacks.borrow_mut().visibility_change = Some(callback);
}
}
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
self.state.borrow_mut().appearance = appearance;
@@ -1781,6 +1809,10 @@ impl PlatformWindow for WaylandWindow {
self.borrow().active
}
fn visibility(&self) -> WindowVisibility {
self.borrow().visibility
}
fn is_hovered(&self) -> bool {
self.borrow().hovered
}
@@ -1867,6 +1899,10 @@ impl PlatformWindow for WaylandWindow {
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
self.0.callbacks.borrow_mut().visibility_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.callbacks.borrow_mut().hover_status_change = Some(callback);
}
+40 -12
View File
@@ -12,7 +12,7 @@ use http_client::Url;
use log::Level;
use smallvec::SmallVec;
use std::{
cell::RefCell,
cell::{RefCell, RefMut},
collections::{BTreeMap, HashSet},
ops::Deref,
path::PathBuf,
@@ -63,7 +63,7 @@ use gpui::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput,
PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size,
TouchPhase, WindowButtonLayout, WindowParams, point, px,
TouchPhase, WindowButtonLayout, WindowParams, WindowVisibility, point, px,
};
use gpui_wgpu::{CompositorGpuHint, GpuContext};
@@ -90,6 +90,17 @@ impl WindowRef {
pub fn handle(&self) -> AnyWindowHandle {
self.window.state.borrow().handle
}
/// Whether the X server is presenting this window. Compositing window
/// managers rarely report `FULLY_OBSCURED`, so under them this is the
/// mapped state alone.
fn visibility(&self) -> WindowVisibility {
if self.is_mapped && !matches!(self.last_visibility, Visibility::FULLY_OBSCURED) {
WindowVisibility::Visible
} else {
WindowVisibility::Hidden
}
}
}
impl Deref for WindowRef {
@@ -309,7 +320,7 @@ impl X11Client {
pub(crate) fn new() -> anyhow::Result<Self> {
let event_loop = EventLoop::try_new()?;
let (common, main_receiver, wake_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
@@ -339,13 +350,17 @@ impl X11Client {
})?;
handle
.insert_source(wake_receiver, |event, _, client: &mut X11Client| {
if let calloop::channel::Event::Msg(()) = event {
client.0.borrow_mut().common.handle_system_wake();
.insert_source(power_receiver, |event, _, client: &mut X11Client| {
if let calloop::channel::Event::Msg(event) = event {
client
.0
.borrow_mut()
.common
.handle_system_power_event(event);
}
})
.map_err(|err| {
anyhow!("Failed to initialize event loop handling of wake events: {err:?}")
anyhow!("Failed to initialize event loop handling of sleep/wake events: {err:?}")
})?;
let (xcb_connection, x_root_index) = XCBConnection::connect(None)?;
@@ -801,21 +816,21 @@ impl X11Client {
if let Some(window_ref) = state.windows.get_mut(&event.window) {
window_ref.is_mapped = false;
}
state.update_refresh_loop(event.window);
handle_visibility_changed(state, event.window);
}
Event::MapNotify(event) => {
let mut state = self.0.borrow_mut();
if let Some(window_ref) = state.windows.get_mut(&event.window) {
window_ref.is_mapped = true;
}
state.update_refresh_loop(event.window);
handle_visibility_changed(state, event.window);
}
Event::VisibilityNotify(event) => {
let mut state = self.0.borrow_mut();
if let Some(window_ref) = state.windows.get_mut(&event.window) {
window_ref.last_visibility = event.state;
}
state.update_refresh_loop(event.window);
handle_visibility_changed(state, event.window);
}
Event::ClientMessage(event) => {
let window = self.get_window(event.window)?;
@@ -1903,8 +1918,7 @@ impl X11ClientState {
let Some(window_ref) = self.windows.get_mut(&x_window) else {
return;
};
let is_visible = window_ref.is_mapped
&& !matches!(window_ref.last_visibility, Visibility::FULLY_OBSCURED);
let is_visible = window_ref.visibility().is_visible();
match (is_visible, window_ref.refresh_state.take()) {
(false, refresh_state @ Some(RefreshState::Hidden { .. }))
| (false, refresh_state @ None)
@@ -2163,6 +2177,20 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
Duration::from_micros(micros)
}
/// Applies a mapped/obscured change to the refresh loop and reports the
/// resulting visibility to the window. Consumes the client borrow because the
/// visibility callback re-enters GPUI.
fn handle_visibility_changed(mut state: RefMut<'_, X11ClientState>, x_window: xproto::Window) {
state.update_refresh_loop(x_window);
let Some(window_ref) = state.windows.get(&x_window) else {
return;
};
let visibility = window_ref.visibility();
let window = window_ref.window.clone();
drop(state);
window.set_visibility(visibility);
}
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32
}
+27 -1
View File
@@ -7,7 +7,8 @@ use gpui::{
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size,
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
WindowDecorations, WindowKind, WindowParams, popup::PopupNotSupportedError, px,
WindowDecorations, WindowKind, WindowParams, WindowVisibility, popup::PopupNotSupportedError,
px,
};
use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig};
@@ -245,6 +246,7 @@ pub struct Callbacks {
request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
input: Option<Box<dyn FnMut(PlatformInput) -> gpui::DispatchEventResult>>,
active_status_change: Option<Box<dyn FnMut(bool)>>,
visibility_change: Option<Box<dyn FnMut(WindowVisibility)>>,
hovered_status_change: Option<Box<dyn FnMut(bool)>>,
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved: Option<Box<dyn FnMut()>>,
@@ -277,6 +279,9 @@ pub struct X11WindowState {
maximized_horizontal: bool,
hidden: bool,
active: bool,
/// Owned by the client's `WindowRef`, which combines the mapped state with
/// `VisibilityNotify`; this is the last value it reported.
visibility: WindowVisibility,
hovered: bool,
pub(crate) force_render_after_recovery: bool,
fullscreen: bool,
@@ -827,6 +832,8 @@ impl X11WindowState {
atoms: *atoms,
input_handler: None,
active: false,
// The window is not mapped until the client sees `MapNotify`.
visibility: WindowVisibility::Hidden,
hovered: false,
force_render_after_recovery: false,
fullscreen: false,
@@ -1338,6 +1345,17 @@ impl X11WindowStatePtr {
}
}
pub fn set_visibility(&self, visibility: WindowVisibility) {
if std::mem::replace(&mut self.state.borrow_mut().visibility, visibility) == visibility {
return;
}
let callback = self.callbacks.borrow_mut().visibility_change.take();
if let Some(mut fun) = callback {
fun(visibility);
self.callbacks.borrow_mut().visibility_change = Some(fun);
}
}
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
let mut state = self.state.borrow_mut();
state.appearance = appearance;
@@ -1536,6 +1554,10 @@ impl PlatformWindow for X11Window {
self.0.state.borrow().active
}
fn visibility(&self) -> WindowVisibility {
self.0.state.borrow().visibility
}
fn is_hovered(&self) -> bool {
self.0.state.borrow().hovered
}
@@ -1683,6 +1705,10 @@ impl PlatformWindow for X11Window {
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
self.0.callbacks.borrow_mut().visibility_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.callbacks.borrow_mut().hovered_status_change = Some(callback);
}
+92 -34
View File
@@ -44,8 +44,11 @@ use objc::{
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use objc2::MainThreadMarker;
use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel, NSSavePanel, NSWorkspace};
use objc2::{MainThreadMarker, runtime::AnyObject};
use objc2_app_kit::{
NSModalResponse, NSModalResponseOK, NSOpenPanel, NSSavePanel, NSWorkspace,
NSWorkspaceDidWakeNotification, NSWorkspaceWillSleepNotification,
};
use objc2_foundation::NSActivityOptions;
use parking_lot::Mutex;
use ptr::null_mut;
@@ -156,6 +159,11 @@ unsafe fn build_classes() {
on_thermal_state_change as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(onSystemSleep:),
on_system_sleep as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(onSystemWake:),
on_system_wake as extern "C" fn(&mut Object, Sel, id),
@@ -179,8 +187,9 @@ pub(crate) struct MacPlatformState {
reopen: Option<Box<dyn FnMut()>>,
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
on_thermal_state_change: Option<Box<dyn FnMut()>>,
on_system_sleep: Option<Box<dyn FnMut()>>,
on_system_wake: Option<Box<dyn FnMut()>>,
system_wake_observer_registered: bool,
system_power_observers_registered: bool,
quit: Option<Box<dyn FnMut() -> bool>>,
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
@@ -236,8 +245,9 @@ impl MacPlatform {
dock_menu: None,
on_keyboard_layout_change: None,
on_thermal_state_change: None,
on_system_sleep: None,
on_system_wake: None,
system_wake_observer_registered: false,
system_power_observers_registered: false,
menus: None,
keyboard_mapper,
cursor_visible: Arc::new(AtomicBool::new(true)),
@@ -478,6 +488,28 @@ impl MacPlatform {
version.patchVersion,
)
}
// Before the app finishes launching there is no delegate yet;
// `did_finish_launching` registers the observers then.
fn ensure_system_power_observers(&self) {
if self.0.lock().system_power_observers_registered {
return;
}
// The shared application must be created through `APP_CLASS`, or `run`
// finds a plain `NSApplication` without the `platform` ivar; only the
// delegate lookup goes through the typed binding.
// SAFETY: APP_CLASS is registered during startup and `sharedApplication`
// returns a live NSApplication instance.
let delegate = unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
(*(app as *const objc2_app_kit::NSApplication)).delegate()
};
if let Some(delegate) = delegate {
register_system_power_observers(delegate.as_ref());
self.0.lock().system_power_observers_registered = true;
}
}
}
impl Platform for MacPlatform {
@@ -974,23 +1006,14 @@ impl Platform for MacPlatform {
self.0.lock().on_thermal_state_change = Some(callback);
}
fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
let mut state = self.0.lock();
state.on_system_wake = Some(callback);
if state.system_wake_observer_registered {
return;
}
drop(state);
fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
self.0.lock().on_system_sleep = Some(callback);
self.ensure_system_power_observers();
}
// SAFETY: APP_CLASS is registered during startup and returns the shared NSApplication.
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
let delegate: id = msg_send![app, delegate];
if delegate != nil {
register_system_wake_observer(delegate);
self.0.lock().system_wake_observer_registered = true;
}
}
fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
self.0.lock().on_system_wake = Some(callback);
self.ensure_system_power_observers();
}
fn thermal_state(&self) -> ThermalState {
@@ -1313,13 +1336,16 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
object: process_info
];
let observer = this as *mut Object as id;
// SAFETY: `this` is a live Objective-C object; only the pointer's type changes.
let observer = &*(this as *mut Object as *const AnyObject);
let platform = get_mac_platform(this);
let callback = {
let mut state = platform.0.lock();
if state.on_system_wake.is_some() && !state.system_wake_observer_registered {
register_system_wake_observer(observer);
state.system_wake_observer_registered = true;
if (state.on_system_sleep.is_some() || state.on_system_wake.is_some())
&& !state.system_power_observers_registered
{
register_system_power_observers(observer);
state.system_power_observers_registered = true;
}
state.finish_launching.take()
};
@@ -1329,17 +1355,25 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
}
}
unsafe fn register_system_wake_observer(observer: id) {
// SAFETY: observer is an Objective-C object implementing onSystemWake:.
/// `observer` must be the app delegate, whose class declares `onSystemSleep:`
/// and `onSystemWake:` (see `build_classes`).
fn register_system_power_observers(observer: &AnyObject) {
let center = NSWorkspace::sharedWorkspace().notificationCenter();
// SAFETY: both selectors are declared on the delegate class with the
// notification-handler signature these observers are invoked with.
unsafe {
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
let workspace_center: *mut Object = msg_send![workspace, notificationCenter];
let wake_name = ns_string("NSWorkspaceDidWakeNotification");
let _: () = msg_send![workspace_center, addObserver: observer
selector: sel!(onSystemWake:)
name: wake_name
object: nil
];
center.addObserver_selector_name_object(
observer,
objc2::sel!(onSystemSleep:),
Some(NSWorkspaceWillSleepNotification),
None,
);
center.addObserver_selector_name_object(
observer,
objc2::sel!(onSystemWake:),
Some(NSWorkspaceDidWakeNotification),
None,
);
}
}
@@ -1406,6 +1440,30 @@ extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) {
}
}
// Deferred to the main queue like the wake and thermal notifications. The
// machine keeps running the main run loop for well over a frame after the
// observers return, so the block runs before it suspends.
extern "C" fn on_system_sleep(this: &mut Object, _: Sel, _: id) {
// SAFETY: this is the registered app delegate carrying MAC_PLATFORM_IVAR.
let platform = unsafe { get_mac_platform(this) };
let platform_ptr = platform as *const MacPlatform as *mut c_void;
// SAFETY: platform lives for the process lifetime while callbacks are registered.
unsafe {
DispatchQueue::main().exec_async_f(platform_ptr, on_system_sleep);
}
extern "C" fn on_system_sleep(context: *mut c_void) {
// SAFETY: context is the MacPlatform pointer queued above.
let platform = unsafe { &*(context as *const MacPlatform) };
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.on_system_sleep.take() {
drop(lock);
callback();
platform.0.lock().on_system_sleep.get_or_insert(callback);
}
}
}
extern "C" fn on_system_wake(this: &mut Object, _: Sel, _: id) {
// SAFETY: this is the registered app delegate carrying MAC_PLATFORM_IVAR.
let platform = unsafe { get_mac_platform(this) };
+71 -2
View File
@@ -30,7 +30,7 @@ use gpui::{
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptButton, PromptLevel, RequestFrameOptions, SharedString, Size, SystemWindowTab,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind,
WindowParams, point, px, size,
WindowParams, WindowVisibility, point, px, size,
};
#[cfg(any(test, feature = "test-support"))]
use image::RgbaImage;
@@ -671,6 +671,10 @@ struct MacWindowState {
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
event_callback: Option<Box<dyn FnMut(PlatformInput) -> gpui::DispatchEventResult>>,
activate_callback: Option<Box<dyn FnMut(bool)>>,
visibility_callback: Option<Box<dyn FnMut(WindowVisibility)>>,
// `None` until a callback is registered, so notifications during
// construction are not queued for delivery to a callback registered later.
last_visibility: Option<WindowVisibility>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
@@ -1104,6 +1108,8 @@ impl MacWindow {
request_frame_callback: None,
event_callback: None,
activate_callback: None,
visibility_callback: None,
last_visibility: None,
resize_callback: None,
moved_callback: None,
should_close_callback: None,
@@ -1381,6 +1387,9 @@ impl Drop for MacWindow {
this.native_window.setDelegate_(nil);
}
this.input_handler.take();
// A delivery task queued by `report_visibility` may still run after the
// GPUI window is gone; without a callback it has nothing to notify.
this.visibility_callback.take();
this.foreground_executor
.spawn(async move {
unsafe {
@@ -1807,6 +1816,10 @@ impl PlatformWindow for MacWindow {
unsafe { self.0.lock().native_window.isKeyWindow() == YES }
}
fn visibility(&self) -> WindowVisibility {
visibility(&self.0.lock())
}
// is_hovered is unused on macOS. See Window::is_window_hovered.
fn is_hovered(&self) -> bool {
false
@@ -2005,6 +2018,12 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().activate_callback = Some(callback);
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
let mut state = self.0.lock();
state.last_visibility = Some(visibility(&state));
state.visibility_callback = Some(callback);
}
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
@@ -2887,9 +2906,55 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
}
}
fn visibility(state: &MacWindowState) -> WindowVisibility {
let is_visible = unsafe {
state
.native_window
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
};
if is_visible {
WindowVisibility::Visible
} else {
WindowVisibility::Hidden
}
}
fn report_visibility(window_state: &Arc<Mutex<MacWindowState>>) {
let state = window_state.lock();
if state.last_visibility.is_none() {
return;
}
let executor = state.foreground_executor.clone();
drop(state);
// AppKit can notify while GPUI is updating a window. Deliver observers
// after that update completes, as activation notifications do. The state
// is read at delivery rather than captured here so a burst of
// notifications collapses to the final value.
executor
.spawn({
let window_state = window_state.clone();
async move {
let mut state = window_state.lock();
let visibility = visibility(&state);
if state.last_visibility == Some(visibility) {
return;
}
state.last_visibility = Some(visibility);
if let Some(mut callback) = state.visibility_callback.take() {
drop(state);
callback(visibility);
window_state.lock().visibility_callback = Some(callback);
}
}
})
.detach();
}
extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = &mut *window_state.lock();
let mut lock = window_state.lock();
unsafe {
if lock
.native_window
@@ -2902,6 +2967,10 @@ extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
lock.stop_display_link();
}
}
drop(lock);
// The only visibility source: AppKit posts this for covering, minimizing,
// hiding, Space switches, and display sleep alike.
report_visibility(&window_state);
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
+4
View File
@@ -493,6 +493,10 @@ impl Platform for WebPlatform {
self.callbacks.borrow_mut().reopen = Some(callback);
}
// Browsers expose no system sleep or wake signal; the nearest thing is the
// Page Visibility API, which drives `WindowVisibility` instead.
fn on_system_sleep(&self, _callback: Box<dyn FnMut()>) {}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
+39 -15
View File
@@ -14,7 +14,7 @@ use gpui::{
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Scene, Size, TextInputConfiguration, TextInputStateChange, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
WindowInsets, WindowParams, px,
WindowInsets, WindowParams, WindowVisibility, px,
};
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig, wgpu};
use wasm_bindgen::prelude::*;
@@ -24,6 +24,7 @@ pub(crate) struct WebWindowCallbacks {
pub(crate) request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
pub(crate) input: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
pub(crate) active_status_change: Option<Box<dyn FnMut(bool)>>,
pub(crate) visibility_change: Option<Box<dyn FnMut(WindowVisibility)>>,
pub(crate) hover_status_change: Option<Box<dyn FnMut(bool)>>,
pub(crate) resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
pub(crate) visual_viewport_changed: Option<Box<dyn FnMut()>>,
@@ -43,6 +44,7 @@ pub(crate) struct WebWindowMutableState {
pub(crate) input_handler: Option<PlatformInputHandler>,
pub(crate) is_fullscreen: bool,
pub(crate) is_active: bool,
pub(crate) visibility: WindowVisibility,
pub(crate) is_hovered: bool,
pub(crate) mouse_position: Point<Pixels>,
pub(crate) modifiers: Modifiers,
@@ -194,6 +196,7 @@ impl WebWindow {
input_handler: None,
is_fullscreen: false,
is_active: true,
visibility: document_visibility(&browser_window),
is_hovered: false,
mouse_position: Point::default(),
modifiers: Modifiers::default(),
@@ -514,27 +517,24 @@ impl WebWindowInner {
document.as_ref(),
"visibilitychange",
move |_event: JsValue| {
let is_visible = this
.browser_window
.document()
.map(|doc| {
let state_str: String =
js_sys::Reflect::get(&doc, &"visibilityState".into())
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
state_str == "visible"
})
.unwrap_or(true);
let visibility = document_visibility(&this.browser_window);
let is_visible = visibility.is_visible();
{
let visibility_changed = {
let mut state = this.state.borrow_mut();
state.is_active = is_visible;
}
std::mem::replace(&mut state.visibility, visibility) != visibility
};
this.with_callback(
|callbacks| &mut callbacks.active_status_change,
|callback| callback(is_visible),
);
if visibility_changed {
this.with_callback(
|callbacks| &mut callbacks.visibility_change,
|callback| callback(visibility),
);
}
},
))
}
@@ -639,6 +639,22 @@ fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance {
}
}
/// The Page Visibility API: `hidden` when the tab is in the background or the
/// browser window is minimized. A missing document reads as visible.
fn document_visibility(browser_window: &web_sys::Window) -> WindowVisibility {
let is_visible = browser_window
.document()
.and_then(|document| js_sys::Reflect::get(&document, &"visibilityState".into()).ok())
.and_then(|value| value.as_string())
.is_none_or(|state| state == "visible");
if is_visible {
WindowVisibility::Visible
} else {
WindowVisibility::Hidden
}
}
struct MqlHandle {
mql: web_sys::MediaQueryList,
_closure: Closure<dyn FnMut(JsValue)>,
@@ -851,6 +867,10 @@ impl PlatformWindow for WebWindow {
self.inner.state.borrow().is_active
}
fn visibility(&self) -> WindowVisibility {
self.inner.state.borrow().visibility
}
fn is_hovered(&self) -> bool {
self.inner.state.borrow().is_hovered
}
@@ -920,6 +940,10 @@ impl PlatformWindow for WebWindow {
self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
self.inner.callbacks.borrow_mut().visibility_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.inner.callbacks.borrow_mut().hover_status_change = Some(callback);
}
+35 -2
View File
@@ -236,7 +236,11 @@ impl WindowsWindowInner {
Some(0)
}
fn handle_size_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
fn handle_size_msg(self: &Rc<Self>, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
// Minimizing and restoring both arrive as `WM_SIZE`; the deferred report
// reads `IsIconic` at delivery, so one call covers both directions.
self.report_visibility();
// Don't resize the renderer when the window is minimized, but record that it was minimized so
// that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`.
if wparam.0 == SIZE_MINIMIZED as usize {
@@ -1261,13 +1265,42 @@ impl WindowsWindowInner {
Some(0)
}
fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option<isize> {
fn handle_window_visibility_changed(
self: &Rc<Self>,
handle: HWND,
wparam: WPARAM,
) -> Option<isize> {
self.report_visibility();
if wparam.0 == 1 {
self.draw_window(handle, false);
}
None
}
// The window procedure can run while GPUI is updating this window (e.g.
// `ShowWindow` from an action handler), so deliver observers after that
// update completes, as activation does. The state is read at delivery so
// a burst of messages collapses to the final value.
fn report_visibility(self: &Rc<Self>) {
if self.state.last_visibility.get().is_none() {
return;
}
let this = self.clone();
self.executor
.spawn(async move {
let visibility = this.visibility();
if this.state.last_visibility.get() == Some(visibility) {
return;
}
this.state.last_visibility.set(Some(visibility));
if let Some(mut callback) = this.state.callbacks.visibility_change.take() {
callback(visibility);
this.state.callbacks.visibility_change.set(Some(callback));
}
})
.detach();
}
fn handle_device_lost(&self, lparam: LPARAM) -> Option<isize> {
let devices = lparam.0 as *const DirectXDevices;
let devices = unsafe { &*devices };
+13 -2
View File
@@ -93,6 +93,7 @@ struct PlatformCallbacks {
will_open_app_menu: Cell<Option<Box<dyn FnMut()>>>,
validate_app_menu_command: Cell<Option<Box<dyn FnMut(&dyn Action) -> bool>>>,
keyboard_layout_change: Cell<Option<Box<dyn FnMut()>>>,
system_sleep: Cell<Option<Box<dyn FnMut()>>>,
system_wake: Cell<Option<Box<dyn FnMut()>>>,
}
@@ -743,6 +744,10 @@ impl Platform for WindowsPlatform {
self.inner.state.callbacks.reopen.set(Some(callback));
}
fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
self.inner.state.callbacks.system_sleep.set(Some(callback));
}
fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
self.inner.state.callbacks.system_wake.set(Some(callback));
let mut notification = self.suspend_resume_notification.borrow_mut();
@@ -1224,8 +1229,14 @@ impl WindowsPlatformInner {
}
fn handle_power_broadcast(&self, wparam: WPARAM) -> Option<isize> {
if wparam.0 as u32 == PBT_APMRESUMEAUTOMATIC {
self.with_callback(|callbacks| &callbacks.system_wake, |callback| callback());
match wparam.0 as u32 {
PBT_APMSUSPEND => {
self.with_callback(|callbacks| &callbacks.system_sleep, |callback| callback());
}
PBT_APMRESUMEAUTOMATIC => {
self.with_callback(|callbacks| &callbacks.system_wake, |callback| callback());
}
_ => {}
}
Some(1)
}
+28
View File
@@ -60,6 +60,7 @@ pub struct WindowsWindowState {
pub last_reported_modifiers: Cell<Option<Modifiers>>,
pub last_reported_capslock: Cell<Option<Capslock>>,
pub hovered: Cell<bool>,
pub last_visibility: Cell<Option<WindowVisibility>>,
pub direct_manipulation: DirectManipulationHandler,
pub renderer: RefCell<DirectXRenderer>,
@@ -172,6 +173,7 @@ impl WindowsWindowState {
last_reported_modifiers: Cell::new(last_reported_modifiers),
last_reported_capslock: Cell::new(last_reported_capslock),
hovered: Cell::new(hovered),
last_visibility: Cell::new(None),
renderer: RefCell::new(renderer),
force_render_pending: Cell::new(false),
click_state,
@@ -249,6 +251,19 @@ impl WindowsWindowState {
}
impl WindowsWindowInner {
/// Whether the window is being presented: shown and not minimized. Windows
/// has no notification for a window fully covered by other windows, so
/// that case reports `Visible`.
pub(crate) fn visibility(&self) -> WindowVisibility {
let is_visible =
unsafe { IsWindowVisible(self.hwnd).as_bool() && !IsIconic(self.hwnd).as_bool() };
if is_visible {
WindowVisibility::Visible
} else {
WindowVisibility::Hidden
}
}
fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
let state = WindowsWindowState::new(
hwnd,
@@ -380,6 +395,7 @@ pub(crate) struct Callbacks {
pub(crate) request_frame: Cell<Option<Box<dyn FnMut(RequestFrameOptions)>>>,
pub(crate) input: Cell<Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>>,
pub(crate) active_status_change: Cell<Option<Box<dyn FnMut(bool)>>>,
pub(crate) visibility_change: Cell<Option<Box<dyn FnMut(WindowVisibility)>>>,
pub(crate) hovered_status_change: Cell<Option<Box<dyn FnMut(bool)>>>,
pub(crate) resize: Cell<Option<Box<dyn FnMut(Size<Pixels>, f32)>>>,
pub(crate) moved: Cell<Option<Box<dyn FnMut()>>>,
@@ -589,6 +605,9 @@ impl rwh::HasDisplayHandle for WindowsWindow {
impl Drop for WindowsWindow {
fn drop(&mut self) {
// `DestroyWindow` below sends `WM_SHOWWINDOW`; without a callback the
// resulting visibility report has nothing to notify.
self.0.state.callbacks.visibility_change.take();
// clone this `Rc` to prevent early release of the pointer
let this = self.0.clone();
self.0
@@ -854,6 +873,10 @@ impl PlatformWindow for WindowsWindow {
self.0.hwnd == unsafe { GetActiveWindow() }
}
fn visibility(&self) -> WindowVisibility {
self.0.visibility()
}
fn is_hovered(&self) -> bool {
self.state.hovered.get()
}
@@ -943,6 +966,11 @@ impl PlatformWindow for WindowsWindow {
.set(Some(callback));
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
self.0.state.last_visibility.set(Some(self.0.visibility()));
self.0.state.callbacks.visibility_change.set(Some(callback));
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0
.state