gpui: Add system wake callback (#59831)

# Objective

GPUI apps currently don't have a cross-platform way to react when the
system wakes from sleep.

## Solution

Add `Application::on_system_wake`, backed by platform hooks for macOS,
Windows, and Linux. The platform listeners are registered lazily once an
app installs a wake callback.

## Testing

- N/A

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga 2026-06-24 16:59:34 -03:00 committed by GitHub
parent 64b8491fc6
commit ee571d3c69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 229 additions and 8 deletions

View file

@ -234,6 +234,23 @@ impl Application {
self
}
/// Invokes a handler when the system wakes from sleep.
pub fn on_system_wake<F>(&self, mut callback: F) -> &Self
where
F: 'static + FnMut(&mut App),
{
let this = Rc::downgrade(&self.0);
self.0
.borrow_mut()
.platform
.on_system_wake(Box::new(move || {
if let Some(app) = this.upgrade() {
callback(&mut app.borrow_mut());
}
}));
self
}
/// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background.
pub fn background_executor(&self) -> BackgroundExecutor {
self.0.borrow().background_executor.clone()

View file

@ -188,6 +188,7 @@ pub trait Platform: 'static {
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_system_wake(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {

View file

@ -414,6 +414,8 @@ impl Platform for TestPlatform {
unimplemented!()
}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}

View file

@ -176,6 +176,8 @@ impl Platform for VisualTestPlatform {
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {

View file

@ -23,7 +23,7 @@ impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver, wake_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
@ -35,6 +35,14 @@ 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());
}
})
.ok();
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
event_loop: Some(event_loop),
_loop_handle: handle,

View file

@ -14,7 +14,7 @@ use std::{
};
use anyhow::{Context as _, anyhow};
use calloop::LoopSignal;
use calloop::{LoopSignal, channel::Sender};
use futures::channel::oneshot;
use gpui_util::{ResultExt as _, new_std_command};
#[cfg(any(feature = "wayland", feature = "x11"))]
@ -110,6 +110,7 @@ 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_wake: Option<Box<dyn FnMut()>>,
}
pub(crate) struct LinuxCommon {
@ -122,11 +123,20 @@ pub(crate) struct LinuxCommon {
pub(crate) callbacks: PlatformHandlers,
pub(crate) signal: LoopSignal,
pub(crate) menus: Vec<OwnedMenu>,
wake_sender: Sender<()>,
wake_listener_started: bool,
}
impl LinuxCommon {
pub fn new(signal: LoopSignal) -> (Self, PriorityQueueCalloopReceiver<RunnableVariant>) {
pub fn new(
signal: LoopSignal,
) -> (
Self,
PriorityQueueCalloopReceiver<RunnableVariant>,
calloop::channel::Channel<()>,
) {
let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
let (wake_sender, wake_receiver) = calloop::channel::channel();
#[cfg(any(feature = "wayland", feature = "x11"))]
let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
@ -149,10 +159,60 @@ impl LinuxCommon {
callbacks,
signal,
menus: Vec::new(),
wake_sender,
wake_listener_started: false,
};
(common, main_receiver)
(common, main_receiver, wake_receiver)
}
pub(crate) fn start_wake_listener(&mut self) {
if !self.wake_listener_started {
#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
smol::spawn({
let wake_sender = self.wake_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:?}");
}
}
})
.detach();
self.wake_listener_started = true;
}
}
pub(crate) fn handle_system_wake(&mut self) {
if let Some(mut callback) = self.callbacks.system_wake.take() {
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<()> {
use futures::StreamExt as _;
let connection = ashpd::zbus::Connection::system().await?;
let proxy = ashpd::zbus::Proxy::new(
&connection,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
)
.await?;
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();
}
}
Ok(())
}
pub(crate) struct LinuxPlatform<P> {
@ -485,6 +545,13 @@ impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
});
}
fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
self.inner.with_common(|common| {
common.callbacks.system_wake = Some(callback);
common.start_wake_listener();
});
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.inner.with_common(|common| {
common.callbacks.app_menu_action = Some(callback);

View file

@ -575,7 +575,7 @@ impl WaylandClient {
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver, wake_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
@ -595,6 +595,17 @@ impl WaylandClient {
})
.unwrap();
handle
.insert_source(
wake_receiver,
|event, _, client: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(()) = event {
client.get_client().borrow_mut().common.handle_system_wake();
}
},
)
.unwrap();
let compositor_gpu = detect_compositor_gpu();
let gpu_context = Rc::new(RefCell::new(None));

View file

@ -309,7 +309,7 @@ impl X11Client {
pub(crate) fn new() -> anyhow::Result<Self> {
let event_loop = EventLoop::try_new()?;
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver, wake_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
@ -335,6 +335,16 @@ impl X11Client {
anyhow!("Failed to initialize event loop handling of foreground tasks: {err:?}")
})?;
handle
.insert_source(wake_receiver, |event, _, client: &mut X11Client| {
if let calloop::channel::Event::Msg(()) = event {
client.0.borrow_mut().common.handle_system_wake();
}
})
.map_err(|err| {
anyhow!("Failed to initialize event loop handling of wake events: {err:?}")
})?;
let (xcb_connection, x_root_index) = XCBConnection::connect(None)?;
xcb_connection.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?;
xcb_connection.prefetch_extension_information(randr::X11_EXTENSION_NAME)?;

View file

@ -151,6 +151,11 @@ unsafe fn build_classes() {
on_thermal_state_change as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(onSystemWake:),
on_system_wake as extern "C" fn(&mut Object, Sel, id),
);
decl.register()
}
}
@ -169,6 +174,8 @@ 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_wake: Option<Box<dyn FnMut()>>,
system_wake_observer_registered: bool,
quit: Option<Box<dyn FnMut()>>,
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
@ -222,6 +229,8 @@ impl MacPlatform {
dock_menu: None,
on_keyboard_layout_change: None,
on_thermal_state_change: None,
on_system_wake: None,
system_wake_observer_registered: false,
menus: None,
keyboard_mapper,
cursor_visible: Arc::new(AtomicBool::new(true)),
@ -919,6 +928,25 @@ 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);
// 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 thermal_state(&self) -> ThermalState {
unsafe {
let process_info: id = msg_send![class!(NSProcessInfo), processInfo];
@ -1211,14 +1239,36 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
object: process_info
];
let observer = this as *mut Object as id;
let platform = get_mac_platform(this);
let callback = platform.0.lock().finish_launching.take();
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;
}
state.finish_launching.take()
};
if let Some(callback) = callback {
callback();
}
}
}
unsafe fn register_system_wake_observer(observer: id) {
// SAFETY: observer is an Objective-C object implementing onSystemWake:.
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
];
}
}
extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
if !has_open_windows {
let platform = unsafe { get_mac_platform(this) };
@ -1282,6 +1332,27 @@ extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) {
}
}
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) };
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_wake);
}
extern "C" fn on_system_wake(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_wake.take() {
drop(lock);
callback();
platform.0.lock().on_system_wake.get_or_insert(callback);
}
}
}
extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
let urls = unsafe {
(0..urls.count())

View file

@ -246,6 +246,8 @@ impl Platform for WebPlatform {
self.callbacks.borrow_mut().reopen = Some(callback);
}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}

View file

@ -21,7 +21,7 @@ use windows::{
Foundation::*,
Graphics::{Direct3D11::ID3D11Device, Gdi::*},
Security::Credentials::*,
System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*},
System::{Com::*, LibraryLoader::*, Ole::*, Power::*, SystemInformation::*},
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
},
core::*,
@ -45,6 +45,7 @@ pub struct WindowsPlatform {
/// as resizing them has failed, causing us to have lost at least the render target.
invalidate_devices: Arc<AtomicBool>,
handle: HWND,
suspend_resume_notification: RefCell<Option<HPOWERNOTIFY>>,
disable_direct_composition: bool,
}
@ -77,6 +78,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_wake: Cell<Option<Box<dyn FnMut()>>>,
}
impl WindowsPlatformState {
@ -193,6 +195,7 @@ impl WindowsPlatform {
foreground_executor,
text_system,
direct_write_text_system,
suspend_resume_notification: RefCell::new(None),
disable_direct_composition,
drop_target_helper,
invalidate_devices: Arc::new(AtomicBool::new(false)),
@ -628,6 +631,21 @@ impl Platform for WindowsPlatform {
self.inner.state.callbacks.reopen.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();
if notification.is_none() {
*notification = unsafe {
// SAFETY: self.handle is the platform window receiving WM_POWERBROADCAST.
RegisterSuspendResumeNotification(
HANDLE(self.handle.0),
DEVICE_NOTIFY_WINDOW_HANDLE,
)
.log_err()
};
}
}
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
*self.inner.state.menus.borrow_mut() = menus.into_iter().map(|menu| menu.owned()).collect();
}
@ -882,6 +900,7 @@ impl WindowsPlatformInner {
| WM_GPUI_DOCK_MENU_ACTION
| WM_GPUI_KEYBOARD_LAYOUT_CHANGED
| WM_GPUI_GPU_DEVICE_LOST => self.handle_gpui_events(msg, wparam, lparam),
WM_POWERBROADCAST => self.handle_power_broadcast(wparam),
_ => None,
};
if let Some(result) = handled {
@ -1018,6 +1037,13 @@ impl WindowsPlatformInner {
Some(0)
}
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());
}
Some(1)
}
fn handle_device_lost(&self, lparam: LPARAM) -> Option<isize> {
let directx_devices = lparam.0 as *const DirectXDevices;
let directx_devices = unsafe { &*directx_devices };
@ -1031,6 +1057,10 @@ impl WindowsPlatformInner {
impl Drop for WindowsPlatform {
fn drop(&mut self) {
unsafe {
if let Some(notification) = self.suspend_resume_notification.borrow_mut().take() {
// SAFETY: notification was returned by RegisterSuspendResumeNotification.
UnregisterSuspendResumeNotification(notification).log_err();
}
DestroyWindow(self.handle)
.context("Destroying platform window")
.log_err();