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:
1 parent
56239b364e
commit
6736a09abe
22 files changed
+719
-119
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)>) {}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user