mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
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:
parent
64b8491fc6
commit
ee571d3c69
11 changed files with 229 additions and 8 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue