mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-19 11:04:06 +00:00
gpui: Track connected displays and each window's display (#65059)
Some checks are pending
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_remote_server_linux_x86_64 (push) Blocked by required conditions
run_tests / check_remote_server_mac_aarch64 (push) Blocked by required conditions
run_tests / check_remote_server_windows_x86_64 (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
Some checks are pending
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_remote_server_linux_x86_64 (push) Blocked by required conditions
run_tests / check_remote_server_mac_aarch64 (push) Blocked by required conditions
run_tests / check_remote_server_windows_x86_64 (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
GPUI now tracks connected displays and the display each window is on.
`App` caches every display's bounds and refresh interval, rereading them
whenever a platform reports that displays may have changed, and reports
`Added`, `Removed`, and `Changed { id, changes }` through
`App::observe_displays`. Windows report only moves between displays;
`Window::refresh_interval(cx)` reads App's cache, and
`Window`/`Context::observe_window_display` fire when a window moves or
its display changes, with App updated first. This is implemented on
macOS, Windows (whose platform window is now a hidden top-level window
so it receives `WM_DISPLAYCHANGE` and `WM_ENDSESSION` with no windows
open), and Wayland, and was exercised at runtime on all three by
switching refresh rates; X11 and web report no refresh interval, and
headless Linux now reports no displays. Planned consumers are
refresh-rate-aware frame telemetry and the screen-share display picker.
Release Notes:
- [GPUI] Added `App::observe_displays`, `Window::refresh_interval`, and
`observe_window_display` for tracking connected displays, their refresh
rates, and each window's display
This commit is contained in:
parent
b0048cde74
commit
f495dd0f77
27 changed files with 990 additions and 140 deletions
|
|
@ -46,15 +46,16 @@ use crate::asset_cache::CachedLoad;
|
|||
use crate::{
|
||||
Action, ActionBuildError, ActionRegistry, ActivationPolicy, ActivityGuard, Any, AnyView,
|
||||
AnyWindowHandle, AppContext, Arena, ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds,
|
||||
ClipboardItem, ClipboardReadError, CursorStyle, DispatchPhase, DisplayId, EventEmitter,
|
||||
ExternalDragPayload, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point,
|
||||
Priority, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage,
|
||||
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
|
||||
Subscription, SvgRenderer, SystemNotification, SystemNotificationResponse, Task,
|
||||
TextRenderingMode, TextSystem, ThermalState, Window, WindowAppearance, WindowButtonLayout,
|
||||
WindowHandle, WindowId, WindowInvalidator, WindowingRequest,
|
||||
ClipboardItem, ClipboardReadError, CursorStyle, DispatchPhase, DisplayChanges, DisplayEvent,
|
||||
DisplayId, EventEmitter, ExternalDragPayload, FocusHandle, FocusMap, ForegroundExecutor,
|
||||
Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, MissingGlyph,
|
||||
OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, SystemNotification,
|
||||
SystemNotificationResponse, Task, TextRenderingMode, TextSystem, ThermalState, Window,
|
||||
WindowAppearance, WindowButtonLayout, WindowHandle, WindowId, WindowInvalidator,
|
||||
WindowingRequest,
|
||||
colors::{Colors, GlobalColors},
|
||||
hash, init_app_menus,
|
||||
};
|
||||
|
|
@ -253,6 +254,8 @@ impl Application {
|
|||
let platform = self.0.borrow().platform.clone();
|
||||
platform.run(Box::new(move || {
|
||||
let cx = &mut *this.borrow_mut();
|
||||
// Some platforms only connect to their display server in `run`.
|
||||
cx.displays_changed();
|
||||
on_finish_launching(cx);
|
||||
}));
|
||||
|
||||
|
|
@ -277,6 +280,8 @@ impl Application {
|
|||
let platform = self.0.borrow().platform.clone();
|
||||
platform.run(Box::new(move || {
|
||||
let cx = &mut *this.borrow_mut();
|
||||
// Some platforms only connect to their display server in `run`.
|
||||
cx.displays_changed();
|
||||
on_finish_launching(cx);
|
||||
}));
|
||||
ApplicationHandle { app: self.0 }
|
||||
|
|
@ -329,6 +334,59 @@ impl Application {
|
|||
}
|
||||
|
||||
type Handler = Box<dyn FnMut(&mut App) -> bool + 'static>;
|
||||
type DisplayHandler = Box<dyn FnMut(DisplayEvent, &mut App) -> bool + 'static>;
|
||||
|
||||
/// The properties of a display that GPUI reports changes to.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
struct DisplayState {
|
||||
bounds: Bounds<Pixels>,
|
||||
refresh_interval: Option<Duration>,
|
||||
}
|
||||
|
||||
impl DisplayState {
|
||||
fn changes_from(&self, previous: &DisplayState) -> DisplayChanges {
|
||||
let mut changes = DisplayChanges::empty();
|
||||
changes.set(DisplayChanges::BOUNDS, self.bounds != previous.bounds);
|
||||
changes.set(
|
||||
DisplayChanges::REFRESH_INTERVAL,
|
||||
self.refresh_interval != previous.refresh_interval,
|
||||
);
|
||||
changes
|
||||
}
|
||||
}
|
||||
|
||||
fn read_displays(platform: &dyn Platform) -> HashMap<DisplayId, DisplayState> {
|
||||
platform
|
||||
.displays()
|
||||
.into_iter()
|
||||
.map(|display| {
|
||||
let state = DisplayState {
|
||||
bounds: display.bounds(),
|
||||
refresh_interval: display.refresh_interval(),
|
||||
};
|
||||
(display.id(), state)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The events that turn one snapshot of the connected displays into the next.
|
||||
fn display_events(
|
||||
previous: &HashMap<DisplayId, DisplayState>,
|
||||
current: &HashMap<DisplayId, DisplayState>,
|
||||
) -> Vec<DisplayEvent> {
|
||||
let removed = previous
|
||||
.keys()
|
||||
.filter(|id| !current.contains_key(id))
|
||||
.map(|id| DisplayEvent::Removed(*id));
|
||||
let added_or_changed = current.iter().filter_map(|(id, state)| {
|
||||
let Some(previous_state) = previous.get(id) else {
|
||||
return Some(DisplayEvent::Added(*id));
|
||||
};
|
||||
let changes = state.changes_from(previous_state);
|
||||
(!changes.is_empty()).then_some(DisplayEvent::Changed { id: *id, changes })
|
||||
});
|
||||
removed.chain(added_or_changed).collect()
|
||||
}
|
||||
type Listener = Box<dyn FnMut(&dyn Any, &mut App) -> bool + 'static>;
|
||||
type MissingGlyphCallback = Box<dyn FnMut(&[MissingGlyph], &mut App) + 'static>;
|
||||
pub(crate) type KeystrokeObserver =
|
||||
|
|
@ -788,6 +846,10 @@ pub struct App {
|
|||
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
||||
missing_glyph_callback: Rc<MissingGlyphCallbackSlot>,
|
||||
pub(crate) thermal_state_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) display_observers: SubscriberSet<(), DisplayHandler>,
|
||||
/// The connected displays, as of the platform's last display change
|
||||
/// notification.
|
||||
displays: HashMap<DisplayId, DisplayState>,
|
||||
pub(crate) system_sleep_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) system_wake_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
|
|
@ -924,6 +986,8 @@ impl App {
|
|||
keyboard_layout_observers: SubscriberSet::new(),
|
||||
missing_glyph_callback: Rc::default(),
|
||||
thermal_state_observers: SubscriberSet::new(),
|
||||
display_observers: SubscriberSet::new(),
|
||||
displays: read_displays(platform.as_ref()),
|
||||
system_sleep_observers: SubscriberSet::new(),
|
||||
system_wake_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
|
|
@ -987,6 +1051,15 @@ impl App {
|
|||
}
|
||||
}));
|
||||
|
||||
platform.on_displays_changed(Box::new({
|
||||
let app = Rc::downgrade(&app);
|
||||
move || {
|
||||
if let Some(app) = app.upgrade() {
|
||||
app.borrow_mut().displays_changed();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_system_sleep(Box::new({
|
||||
let app = Rc::downgrade(&app);
|
||||
move || {
|
||||
|
|
@ -1554,6 +1627,57 @@ impl App {
|
|||
subscription
|
||||
}
|
||||
|
||||
pub(crate) fn displays_changed(&mut self) {
|
||||
let current = read_displays(self.platform.as_ref());
|
||||
let events = display_events(&self.displays, ¤t);
|
||||
self.displays = current;
|
||||
for event in &events {
|
||||
let DisplayEvent::Changed { id, .. } = *event else {
|
||||
continue;
|
||||
};
|
||||
for handle in self.windows() {
|
||||
self.update_window(handle, |_, window, cx| {
|
||||
if window.display_id == Some(id) {
|
||||
window.notify_display_observers(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
for event in events {
|
||||
self.display_observers
|
||||
.clone()
|
||||
.retain(&(), |callback| (callback)(event, self));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn knows_display(&self, id: DisplayId) -> bool {
|
||||
self.displays.contains_key(&id)
|
||||
}
|
||||
|
||||
/// The refresh interval of a connected display, as of the platform's last
|
||||
/// display change notification.
|
||||
pub(crate) fn display_refresh_interval(&self, id: DisplayId) -> Option<Duration> {
|
||||
self.displays.get(&id)?.refresh_interval
|
||||
}
|
||||
|
||||
/// Invokes a handler when a display is connected, disconnected, or its
|
||||
/// properties change.
|
||||
pub fn observe_displays<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(DisplayEvent, &mut App),
|
||||
{
|
||||
let (subscription, activate) = self.display_observers.insert(
|
||||
(),
|
||||
Box::new(move |event, cx| {
|
||||
callback(event, 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
|
||||
|
|
|
|||
|
|
@ -471,6 +471,26 @@ impl<'a, T: 'static> Context<'a, T> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Registers a callback invoked when the window moves to another display
|
||||
/// or its display's refresh interval changes. See
|
||||
/// [`Window::observe_window_display`].
|
||||
pub fn observe_window_display(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.weak_entity();
|
||||
let (subscription, activate) = window.display_observers.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
view.update(cx, |view, cx| callback(view, window, cx))
|
||||
.is_ok()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Registers a callback to be invoked when the window button layout changes.
|
||||
pub fn observe_button_layout_changed(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::{
|
||||
Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace,
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
|
||||
Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, Platform, PlatformTextSystem, Point, Render, Result, SharedString, Size,
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DisplayId, DrawPhase,
|
||||
Drawable, Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Pixels, Platform, PlatformTextSystem, Point, Render, Result, SharedString, Size,
|
||||
SystemNotification, SystemNotificationResponse, Task, TestDispatcher, TestPlatform,
|
||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, WindowVisibility, app::GpuiMode, window::ElementArenaScope,
|
||||
|
|
@ -472,6 +472,36 @@ impl TestAppContext {
|
|||
.simulate_visibility_change(visibility);
|
||||
}
|
||||
|
||||
/// Simulates connecting a display.
|
||||
pub fn simulate_display_added(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
refresh_interval: Option<std::time::Duration>,
|
||||
) {
|
||||
self.test_platform
|
||||
.simulate_display_added(display_id, refresh_interval);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_platform(&self) -> &TestPlatform {
|
||||
&self.test_platform
|
||||
}
|
||||
|
||||
/// Simulates disconnecting a display.
|
||||
pub fn simulate_display_removed(&self, display_id: DisplayId) {
|
||||
self.test_platform.simulate_display_removed(display_id);
|
||||
}
|
||||
|
||||
/// Simulates a connected display changing its refresh interval.
|
||||
pub fn simulate_display_refresh_interval_change(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
refresh_interval: Option<std::time::Duration>,
|
||||
) {
|
||||
self.test_platform
|
||||
.simulate_display_refresh_interval_change(display_id, refresh_interval);
|
||||
}
|
||||
|
||||
/// Simulates visible viewport changes without resizing the window's layout area.
|
||||
pub fn simulate_window_visual_viewport_change(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,42 @@ impl WindowVisibility {
|
|||
}
|
||||
}
|
||||
|
||||
/// A change to the connected displays, reported through
|
||||
/// [`App::observe_displays`](crate::App::observe_displays).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum DisplayEvent {
|
||||
/// A display was connected.
|
||||
Added(DisplayId),
|
||||
/// A display was disconnected. Windows on it are moved to another
|
||||
/// display by the platform.
|
||||
Removed(DisplayId),
|
||||
/// A connected display's properties changed.
|
||||
Changed {
|
||||
/// The display that changed.
|
||||
id: DisplayId,
|
||||
/// Which of its properties changed.
|
||||
changes: DisplayChanges,
|
||||
},
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// The properties of a display that a [`DisplayEvent::Changed`] reports.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DisplayChanges: u8 {
|
||||
/// [`PlatformDisplay::bounds`] changed.
|
||||
const BOUNDS = 1 << 0;
|
||||
/// [`PlatformDisplay::refresh_interval`] changed.
|
||||
const REFRESH_INTERVAL = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a refresh rate in hertz to the time between refreshes, rejecting
|
||||
/// the zero, negative, and non-finite rates platforms use to mean "unknown".
|
||||
pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
|
||||
(hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
|
||||
}
|
||||
|
||||
/// Controls whether the application participates in the system's foreground UI.
|
||||
///
|
||||
/// Only has an effect on macOS; other platforms ignore this setting. There,
|
||||
|
|
@ -351,6 +387,11 @@ pub trait Platform: 'static {
|
|||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
/// Registers the callback invoked when displays may have been added or
|
||||
/// removed, or a display's properties may have changed. GPUI rereads
|
||||
/// [`Self::displays`] to find out what changed, so calls may be spurious.
|
||||
/// The callback runs on the main thread outside of any window update.
|
||||
fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
|
||||
None
|
||||
|
|
@ -574,6 +615,14 @@ pub trait PlatformDisplay: Debug {
|
|||
/// Get the bounds for this display
|
||||
fn bounds(&self) -> Bounds<Pixels>;
|
||||
|
||||
/// The time between the display's refreshes. Variable refresh rate
|
||||
/// displays report their maximum rate. `None` when the platform doesn't
|
||||
/// report it.
|
||||
///
|
||||
/// May query the operating system; GPUI caches it per display, see
|
||||
/// [`Window::refresh_interval`](crate::Window::refresh_interval).
|
||||
fn refresh_interval(&self) -> Option<Duration>;
|
||||
|
||||
/// Get the visible bounds for this display, excluding taskbar/dock areas.
|
||||
/// This is the usable area where windows can be placed without being obscured.
|
||||
/// Defaults to the full display bounds if not overridden.
|
||||
|
|
@ -1215,6 +1264,10 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
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()>);
|
||||
/// Registers the callback invoked when the window moves to another
|
||||
/// display. Calls may be spurious. The callback runs on the main thread
|
||||
/// outside of any window update.
|
||||
fn on_display_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
|
||||
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>);
|
||||
|
|
@ -3588,6 +3641,21 @@ mod tests {
|
|||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_interval_from_hz() {
|
||||
assert_eq!(
|
||||
refresh_interval_from_hz(50.0),
|
||||
Some(Duration::from_millis(20))
|
||||
);
|
||||
assert_eq!(
|
||||
refresh_interval_from_hz(120.0),
|
||||
Some(Duration::from_secs(1) / 120)
|
||||
);
|
||||
for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
|
||||
assert_eq!(refresh_interval_from_hz(unknown), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_button_layout_parse_standard() {
|
||||
let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,26 @@
|
|||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, px};
|
||||
use anyhow::{Ok, Result};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TestDisplay {
|
||||
id: DisplayId,
|
||||
uuid: uuid::Uuid,
|
||||
bounds: Bounds<Pixels>,
|
||||
pub(crate) refresh_interval: Option<Duration>,
|
||||
}
|
||||
|
||||
impl TestDisplay {
|
||||
pub fn new() -> Self {
|
||||
Self::with_id(DisplayId(1))
|
||||
}
|
||||
|
||||
pub fn with_id(id: DisplayId) -> Self {
|
||||
TestDisplay {
|
||||
id: DisplayId(1),
|
||||
id,
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))),
|
||||
refresh_interval: Some(Duration::from_secs(1) / 60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,4 +37,8 @@ impl PlatformDisplay for TestDisplay {
|
|||
fn bounds(&self) -> crate::Bounds<crate::Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
fn refresh_interval(&self) -> Option<Duration> {
|
||||
self.refresh_interval
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ use crate::NoopTextSystem;
|
|||
use crate::PathPromptOptions;
|
||||
use crate::{
|
||||
ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform, PlatformDisplay,
|
||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString,
|
||||
SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow,
|
||||
ThermalState, WindowAppearance, WindowParams, size,
|
||||
DisplayId, DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform,
|
||||
PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SharedString, SourceMetadata, SystemNotification, SystemNotificationResponse, Task,
|
||||
TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
|
@ -32,7 +32,8 @@ pub(crate) struct TestPlatform {
|
|||
foreground_executor: ForegroundExecutor,
|
||||
|
||||
pub(crate) active_window: RefCell<Option<TestWindow>>,
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
displays: RefCell<Vec<TestDisplay>>,
|
||||
displays_changed_callback: RefCell<Option<Box<dyn FnMut()>>>,
|
||||
active_cursor: Mutex<CursorStyle>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
|
|
@ -140,6 +141,57 @@ impl TestPlatform {
|
|||
Self::with_platform(executor, foreground_executor, text_system, None)
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_display_added(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
refresh_interval: Option<Duration>,
|
||||
) {
|
||||
self.connect_display(display_id, refresh_interval);
|
||||
self.report_displays_changed();
|
||||
}
|
||||
|
||||
/// Adds a display without telling App yet, as when the platform's
|
||||
/// notifications about a window arrive before those about displays.
|
||||
pub(crate) fn connect_display(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
refresh_interval: Option<Duration>,
|
||||
) {
|
||||
let mut display = TestDisplay::with_id(display_id);
|
||||
display.refresh_interval = refresh_interval;
|
||||
self.displays.borrow_mut().push(display);
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_display_removed(&self, display_id: DisplayId) {
|
||||
self.displays
|
||||
.borrow_mut()
|
||||
.retain(|display| display.id() != display_id);
|
||||
self.report_displays_changed();
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_display_refresh_interval_change(
|
||||
&self,
|
||||
display_id: DisplayId,
|
||||
refresh_interval: Option<Duration>,
|
||||
) {
|
||||
for display in self.displays.borrow_mut().iter_mut() {
|
||||
if display.id() == display_id {
|
||||
display.refresh_interval = refresh_interval;
|
||||
}
|
||||
}
|
||||
self.report_displays_changed();
|
||||
}
|
||||
|
||||
fn report_displays_changed(&self) {
|
||||
let callback = self.displays_changed_callback.borrow_mut().take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
self.displays_changed_callback
|
||||
.borrow_mut()
|
||||
.get_or_insert(callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_platform(
|
||||
executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
|
|
@ -155,7 +207,8 @@ impl TestPlatform {
|
|||
prompts: Default::default(),
|
||||
screen_capture_sources: Default::default(),
|
||||
active_cursor: Default::default(),
|
||||
active_display: Rc::new(TestDisplay::new()),
|
||||
displays: RefCell::new(vec![TestDisplay::new()]),
|
||||
displays_changed_callback: Default::default(),
|
||||
active_window: Default::default(),
|
||||
expect_restart: Default::default(),
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
|
|
@ -458,11 +511,22 @@ impl Platform for TestPlatform {
|
|||
}
|
||||
|
||||
fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
vec![self.active_display.clone()]
|
||||
self.displays
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|display| Rc::new(display.clone()) as Rc<dyn PlatformDisplay>)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
Some(self.active_display.clone())
|
||||
self.displays
|
||||
.borrow()
|
||||
.first()
|
||||
.map(|display| Rc::new(display.clone()) as Rc<dyn PlatformDisplay>)
|
||||
}
|
||||
|
||||
fn on_displays_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
*self.displays_changed_callback.borrow_mut() = Some(callback);
|
||||
}
|
||||
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
|
|
@ -503,7 +567,8 @@ impl Platform for TestPlatform {
|
|||
handle,
|
||||
params,
|
||||
self.weak.clone(),
|
||||
self.active_display.clone(),
|
||||
self.primary_display()
|
||||
.unwrap_or_else(|| Rc::new(TestDisplay::new())),
|
||||
renderer,
|
||||
);
|
||||
Ok(Box::new(window))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, Bounds, DevicePixels, DispatchEventResult, GpuSpecs, HeadlessAtlas, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptButton, RequestFrameOptions, Scene, Size, TestPlatform,
|
||||
TextInputConfiguration, TextInputStateChange, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowControlArea, WindowInsets, WindowParams, WindowVisibility,
|
||||
AnyWindowHandle, Bounds, DevicePixels, DispatchEventResult, DisplayId, GpuSpecs, HeadlessAtlas,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput,
|
||||
PlatformInputHandler, PlatformWindow, Point, PromptButton, RequestFrameOptions, Scene, Size,
|
||||
TestDisplay, TestPlatform, TextInputConfiguration, TextInputStateChange, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowInsets, WindowParams,
|
||||
WindowVisibility,
|
||||
};
|
||||
use gpui_util::ResultExt as _;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
|
@ -43,6 +44,7 @@ pub(crate) struct TestWindowState {
|
|||
virtual_keyboard_requests: usize,
|
||||
virtual_keyboard_dismissals: usize,
|
||||
moved_callback: Option<Box<dyn FnMut()>>,
|
||||
display_changed_callback: Option<Box<dyn FnMut()>>,
|
||||
appearance_change_callback: Option<Box<dyn FnMut()>>,
|
||||
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
|
||||
frame_wake_count: Rc<Cell<usize>>,
|
||||
|
|
@ -116,6 +118,7 @@ impl TestWindow {
|
|||
virtual_keyboard_requests: 0,
|
||||
virtual_keyboard_dismissals: 0,
|
||||
moved_callback: None,
|
||||
display_changed_callback: None,
|
||||
appearance_change_callback: None,
|
||||
request_frame_callback: None,
|
||||
frame_wake_count: Rc::new(Cell::new(0)),
|
||||
|
|
@ -167,6 +170,19 @@ impl TestWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/// Moves the window to the display with the given ID.
|
||||
pub fn simulate_move_to_display(&self, display_id: DisplayId) {
|
||||
let callback = {
|
||||
let mut state = self.0.lock();
|
||||
state.display = Rc::new(TestDisplay::with_id(display_id));
|
||||
state.display_changed_callback.take()
|
||||
};
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
self.0.lock().display_changed_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_visual_viewport_change(&self, bounds: Bounds<Pixels>) {
|
||||
let callback = {
|
||||
let mut state = self.0.lock();
|
||||
|
|
@ -510,6 +526,10 @@ impl PlatformWindow for TestWindow {
|
|||
self.0.lock().moved_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_display_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().display_changed_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.0.lock().should_close_handler = Some(callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ impl Platform for VisualTestPlatform {
|
|||
self.platform.primary_display()
|
||||
}
|
||||
|
||||
fn on_displays_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.platform.on_displays_changed(callback)
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.platform.active_window()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1216,7 +1216,8 @@ pub struct Window {
|
|||
pub(crate) invalidator: WindowInvalidator,
|
||||
pub(crate) removed: bool,
|
||||
pub(crate) platform_window: Box<dyn PlatformWindow>,
|
||||
display_id: Option<DisplayId>,
|
||||
pub(crate) display_id: Option<DisplayId>,
|
||||
pub(crate) display_observers: SubscriberSet<(), AnyObserver>,
|
||||
is_resizable: bool,
|
||||
is_minimizable: bool,
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
|
|
@ -1961,6 +1962,10 @@ impl Window {
|
|||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_display_changed(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || cx.update(|cx| Window::display_changed(handle, cx))
|
||||
}));
|
||||
platform_window.on_appearance_changed(Box::new({
|
||||
let cx = cx.to_async();
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
|
@ -2116,6 +2121,7 @@ impl Window {
|
|||
removed: false,
|
||||
platform_window,
|
||||
display_id,
|
||||
display_observers: SubscriberSet::new(),
|
||||
is_resizable,
|
||||
is_minimizable,
|
||||
sprite_atlas,
|
||||
|
|
@ -2779,7 +2785,6 @@ impl Window {
|
|||
pub fn bounds_changed(&mut self, cx: &mut App) {
|
||||
self.scale_factor = self.platform_window.scale_factor();
|
||||
self.viewport_size = self.platform_window.content_size();
|
||||
self.display_id = self.platform_window.display().map(|display| display.id());
|
||||
self.mouse_position = self.platform_window.mouse_position();
|
||||
|
||||
self.refresh();
|
||||
|
|
@ -2789,6 +2794,65 @@ impl Window {
|
|||
.retain(&(), |callback| callback(self, cx));
|
||||
}
|
||||
|
||||
fn display_changed(handle: AnyWindowHandle, cx: &mut App) {
|
||||
let moved_to = handle
|
||||
.update(cx, |_, window, _| {
|
||||
let display_id = window.platform_window.display().map(|display| display.id());
|
||||
let moved = display_id != window.display_id;
|
||||
window.display_id = display_id;
|
||||
moved.then_some(display_id)
|
||||
})
|
||||
.log_err()
|
||||
.flatten();
|
||||
let Some(display_id) = moved_to else {
|
||||
return;
|
||||
};
|
||||
// The platform's notifications about the window and about displays
|
||||
// arrive in no guaranteed order, so make sure App knows the display
|
||||
// before the window's observers read its state.
|
||||
if let Some(display_id) = display_id
|
||||
&& !cx.knows_display(display_id)
|
||||
{
|
||||
cx.displays_changed();
|
||||
}
|
||||
handle
|
||||
.update(cx, |_, window, cx| window.notify_display_observers(cx))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub(crate) fn notify_display_observers(&mut self, cx: &mut App) {
|
||||
self.display_observers
|
||||
.clone()
|
||||
.retain(&(), |callback| callback(self, cx));
|
||||
}
|
||||
|
||||
/// The time between refreshes of the display the window is on, as of the
|
||||
/// platform's last display change notification. Variable refresh rate
|
||||
/// displays report their maximum rate. Returns `None` when the platform
|
||||
/// doesn't report it.
|
||||
pub fn refresh_interval(&self, cx: &App) -> Option<Duration> {
|
||||
cx.display_refresh_interval(self.display_id?)
|
||||
}
|
||||
|
||||
/// Registers a callback invoked when the window moves to another display
|
||||
/// or its display's properties change, for example its refresh
|
||||
/// interval. [`Self::refresh_interval`] already returns the new value
|
||||
/// when the callback runs.
|
||||
pub fn observe_window_display(
|
||||
&self,
|
||||
mut callback: impl FnMut(&mut Window, &mut App) + 'static,
|
||||
) -> Subscription {
|
||||
let (subscription, activate) = self.display_observers.insert(
|
||||
(),
|
||||
Box::new(move |window, cx| {
|
||||
callback(window, cx);
|
||||
true
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays.
|
||||
pub fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.platform_window.bounds()
|
||||
|
|
@ -8002,6 +8066,118 @@ mod tests {
|
|||
assert_eq!(test_window.frame_wake_count(), frame_wake_count);
|
||||
}
|
||||
|
||||
/// App learns display state from the platform's display notifications,
|
||||
/// and a window from the platform's report of which display it is on.
|
||||
#[gpui::test]
|
||||
fn test_display_tracking(cx: &mut TestAppContext) {
|
||||
use crate::{DisplayChanges, DisplayEvent, DisplayId, Subscription};
|
||||
|
||||
let sixty_hertz = Duration::from_secs(1) / 60;
|
||||
let one_hundred_twenty_hertz = Duration::from_secs(1) / 120;
|
||||
let one_hundred_forty_four_hertz = Duration::from_secs(1) / 144;
|
||||
|
||||
let display_events = Rc::new(RefCell::new(Vec::new()));
|
||||
let _display_subscription = cx.update({
|
||||
let display_events = display_events.clone();
|
||||
move |cx| cx.observe_displays(move |event, _| display_events.borrow_mut().push(event))
|
||||
});
|
||||
|
||||
let window = cx.add_window(|window, cx| DisplayTracker {
|
||||
refresh_intervals: Vec::new(),
|
||||
_subscription: cx.observe_window_display(
|
||||
window,
|
||||
|tracker: &mut DisplayTracker, window, cx| {
|
||||
tracker.refresh_intervals.push(window.refresh_interval(cx));
|
||||
},
|
||||
),
|
||||
});
|
||||
let window_notifications = Rc::new(RefCell::new(Vec::new()));
|
||||
let _window_subscription = window
|
||||
.update(cx, {
|
||||
let window_notifications = window_notifications.clone();
|
||||
move |_, window, cx| {
|
||||
assert_eq!(window.refresh_interval(cx), Some(sixty_hertz));
|
||||
window.observe_window_display(move |window, cx| {
|
||||
window_notifications
|
||||
.borrow_mut()
|
||||
.push((window.display_id, window.refresh_interval(cx)));
|
||||
})
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let test_window = cx.test_window(window.into());
|
||||
|
||||
// The window may hear that it moved before App hears the display was
|
||||
// connected. App learns about the display before the window's
|
||||
// observers run, and the platform's later notification changes
|
||||
// nothing.
|
||||
cx.test_platform()
|
||||
.connect_display(DisplayId(2), Some(one_hundred_twenty_hertz));
|
||||
test_window.simulate_move_to_display(DisplayId(2));
|
||||
assert_eq!(
|
||||
window_notifications.take(),
|
||||
[(Some(DisplayId(2)), Some(one_hundred_twenty_hertz))]
|
||||
);
|
||||
assert_eq!(display_events.take(), [DisplayEvent::Added(DisplayId(2))]);
|
||||
cx.simulate_display_refresh_interval_change(DisplayId(2), Some(one_hundred_twenty_hertz));
|
||||
assert!(window_notifications.borrow().is_empty());
|
||||
|
||||
// Platforms may report a move that isn't one.
|
||||
test_window.simulate_move_to_display(DisplayId(2));
|
||||
assert!(window_notifications.borrow().is_empty());
|
||||
|
||||
cx.simulate_display_refresh_interval_change(
|
||||
DisplayId(2),
|
||||
Some(one_hundred_forty_four_hertz),
|
||||
);
|
||||
assert_eq!(
|
||||
window_notifications.take(),
|
||||
[(Some(DisplayId(2)), Some(one_hundred_forty_four_hertz))]
|
||||
);
|
||||
|
||||
// Changes to a display the window isn't on don't notify it.
|
||||
cx.simulate_display_refresh_interval_change(DisplayId(1), Some(one_hundred_twenty_hertz));
|
||||
cx.simulate_display_removed(DisplayId(1));
|
||||
assert!(window_notifications.borrow().is_empty());
|
||||
|
||||
assert_eq!(
|
||||
display_events.take(),
|
||||
[
|
||||
DisplayEvent::Changed {
|
||||
id: DisplayId(2),
|
||||
changes: DisplayChanges::REFRESH_INTERVAL,
|
||||
},
|
||||
DisplayEvent::Changed {
|
||||
id: DisplayId(1),
|
||||
changes: DisplayChanges::REFRESH_INTERVAL,
|
||||
},
|
||||
DisplayEvent::Removed(DisplayId(1)),
|
||||
]
|
||||
);
|
||||
window
|
||||
.update(cx, |tracker, _, _| {
|
||||
assert_eq!(
|
||||
tracker.refresh_intervals,
|
||||
[
|
||||
Some(one_hundred_twenty_hertz),
|
||||
Some(one_hundred_forty_four_hertz)
|
||||
]
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
struct DisplayTracker {
|
||||
refresh_intervals: Vec<Option<Duration>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Render for DisplayTracker {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_fully_visible_bounds_preserve_layout_viewport(cx: &mut TestAppContext) {
|
||||
let window = cx.add_window(|_, _| EmptyView);
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ use std::rc::Rc;
|
|||
|
||||
use gpui::{AnyWindowHandle, PlatformDisplay, PlatformWindow, WindowParams};
|
||||
|
||||
pub(crate) use window::{HeadlessDisplay, HeadlessWindow};
|
||||
pub(crate) use window::HeadlessWindow;
|
||||
|
||||
/// The state of a `LinuxPlatform` with no display server.
|
||||
///
|
||||
/// It reports one fake display and opens [`HeadlessWindow`]s, which lay out and handle input but
|
||||
/// draw nothing.
|
||||
/// It reports no displays and opens [`HeadlessWindow`]s, which lay out and handle input but draw
|
||||
/// nothing.
|
||||
pub(crate) struct HeadlessConnection {
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
/// Cloned into every open window, so its count tells whether windows are open.
|
||||
window_lease: Rc<()>,
|
||||
}
|
||||
|
|
@ -19,7 +18,6 @@ pub(crate) struct HeadlessConnection {
|
|||
impl HeadlessConnection {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
display: Rc::new(HeadlessDisplay::new()),
|
||||
window_lease: Rc::new(()),
|
||||
}
|
||||
}
|
||||
|
|
@ -29,11 +27,11 @@ impl HeadlessConnection {
|
|||
}
|
||||
|
||||
pub(crate) fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
vec![self.display.clone()]
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub(crate) fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(self.display.clone())
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn open_window(
|
||||
|
|
@ -43,7 +41,6 @@ impl HeadlessConnection {
|
|||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
Ok(Box::new(HeadlessWindow::new(
|
||||
params,
|
||||
self.display.clone(),
|
||||
self.window_lease.clone(),
|
||||
)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,47 +10,15 @@ use std::cell::RefCell;
|
|||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use gpui::{
|
||||
Bounds, Capslock, DispatchEventResult, DisplayId, GpuSpecs, HeadlessAtlas, Modifiers, Pixels,
|
||||
Bounds, Capslock, DispatchEventResult, GpuSpecs, HeadlessAtlas, Modifiers, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptButton, PromptLevel, RequestFrameOptions, Scene, Size, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, WindowVisibility,
|
||||
px,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct HeadlessDisplay {
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl HeadlessDisplay {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDisplay for HeadlessDisplay {
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId::new(0)
|
||||
}
|
||||
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
// Stable identity: there is exactly one headless display.
|
||||
Ok(Uuid::nil())
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadlessWindowState {
|
||||
bounds: Bounds<Pixels>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
/// Held while this window exists, so the platform can tell that headless windows are open.
|
||||
_lease: Rc<()>,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
|
|
@ -78,14 +46,9 @@ impl raw_window_handle::HasDisplayHandle for HeadlessWindow {
|
|||
}
|
||||
|
||||
impl HeadlessWindow {
|
||||
pub(crate) fn new(
|
||||
params: WindowParams,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
lease: Rc<()>,
|
||||
) -> Self {
|
||||
pub(crate) fn new(params: WindowParams, lease: Rc<()>) -> Self {
|
||||
Self(Rc::new(RefCell::new(HeadlessWindowState {
|
||||
bounds: params.bounds,
|
||||
display,
|
||||
_lease: lease,
|
||||
input_handler: None,
|
||||
title: None,
|
||||
|
|
@ -124,7 +87,7 @@ impl PlatformWindow for HeadlessWindow {
|
|||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(self.0.borrow().display.clone())
|
||||
None
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
|
|
@ -216,6 +179,8 @@ impl PlatformWindow for HeadlessWindow {
|
|||
|
||||
fn on_moved(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_display_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {}
|
||||
|
||||
fn on_close(&self, _callback: Box<dyn FnOnce()>) {}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ pub(crate) struct PlatformHandlers {
|
|||
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()>>,
|
||||
pub(crate) displays_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
/// A logind `PrepareForSleep` signal, forwarded from the D-Bus listener to
|
||||
|
|
@ -427,7 +428,8 @@ impl LinuxPlatform {
|
|||
fn replace_connection(&self, connection: DisplayConnection) {
|
||||
let previous = std::mem::replace(&mut *self.connection.borrow_mut(), connection);
|
||||
drop(previous);
|
||||
// The app caches the keyboard layout, which belongs to the connection.
|
||||
// The app caches the keyboard layout and the displays, which belong to
|
||||
// the connection.
|
||||
let callback = self
|
||||
.common
|
||||
.borrow_mut()
|
||||
|
|
@ -442,6 +444,15 @@ impl LinuxPlatform {
|
|||
common.callbacks.keyboard_layout_change = Some(callback);
|
||||
}
|
||||
}
|
||||
let callback = self.common.borrow_mut().callbacks.displays_changed.take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
self.common
|
||||
.borrow_mut()
|
||||
.callbacks
|
||||
.displays_changed
|
||||
.get_or_insert(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a pending switch.
|
||||
|
|
@ -711,6 +722,10 @@ impl Platform for LinuxPlatform {
|
|||
self.connection.borrow().displays()
|
||||
}
|
||||
|
||||
fn on_displays_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.with_common(|common| common.callbacks.displays_changed = Some(callback));
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
self.connection.borrow().is_screen_capture_supported()
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
|||
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
|
||||
|
||||
use super::{
|
||||
display::WaylandDisplay,
|
||||
display::{WaylandDisplay, refresh_interval_from_millihertz},
|
||||
window::{ImeInput, WaylandWindowStatePtr},
|
||||
};
|
||||
|
||||
|
|
@ -322,6 +322,7 @@ pub struct InProgressOutput {
|
|||
position: Option<Point<DevicePixels>>,
|
||||
size: Option<Size<DevicePixels>>,
|
||||
subpixel: Option<wl_output::Subpixel>,
|
||||
refresh_interval: Option<Duration>,
|
||||
}
|
||||
|
||||
impl InProgressOutput {
|
||||
|
|
@ -333,6 +334,7 @@ impl InProgressOutput {
|
|||
scale,
|
||||
bounds: Bounds::new(position, size),
|
||||
subpixel: self.subpixel,
|
||||
refresh_interval: self.refresh_interval,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -346,6 +348,7 @@ pub struct Output {
|
|||
pub scale: i32,
|
||||
pub bounds: Bounds<DevicePixels>,
|
||||
pub subpixel: Option<wl_output::Subpixel>,
|
||||
pub refresh_interval: Option<Duration>,
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandClientState {
|
||||
|
|
@ -370,8 +373,12 @@ pub(crate) struct WaylandClientState {
|
|||
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
|
||||
// Output to scale mapping
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
// Kept after each `done`, since outputs only resend the properties that
|
||||
// changed before the next one.
|
||||
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
|
||||
wl_outputs: HashMap<ObjectId, wl_output::WlOutput>,
|
||||
// Registry global names of the outputs, for `global_remove`.
|
||||
output_globals: HashMap<u32, ObjectId>,
|
||||
keyboard_layout: LinuxKeyboardLayout,
|
||||
keymap_state: Option<xkb::State>,
|
||||
compose_state: Option<xkb::compose::State>,
|
||||
|
|
@ -571,11 +578,39 @@ impl WaylandClientStatePtr {
|
|||
.expect("The pointer should always be valid when dispatching in wayland")
|
||||
}
|
||||
|
||||
/// Reports that the outputs may have changed. The client state and
|
||||
/// `LinuxCommon` must not be borrowed, since GPUI reads the displays while
|
||||
/// handling it.
|
||||
fn report_displays_changed(&self) {
|
||||
let common = self.get_client().borrow().common.clone();
|
||||
let callback = common.borrow_mut().callbacks.displays_changed.take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
common
|
||||
.borrow_mut()
|
||||
.callbacks
|
||||
.displays_changed
|
||||
.get_or_insert(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the client, or `None` if it was dropped, for example by a switch to headless mode.
|
||||
pub fn try_get_client(&self) -> Option<Rc<RefCell<WaylandClientState>>> {
|
||||
self.0.upgrade()
|
||||
}
|
||||
|
||||
/// The only output, when exactly one is connected.
|
||||
pub fn sole_output(&self) -> Option<(ObjectId, Output)> {
|
||||
let client = self.try_get_client()?;
|
||||
let state = client.try_borrow().ok()?;
|
||||
let mut outputs = state.outputs.iter();
|
||||
let (id, output) = outputs.next()?;
|
||||
if outputs.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some((id.clone(), output.clone()))
|
||||
}
|
||||
|
||||
pub fn dispatch_scheduled_frames(&self) {
|
||||
let Some(client) = self.0.upgrade() else {
|
||||
return;
|
||||
|
|
@ -952,7 +987,7 @@ impl WaylandConnection {
|
|||
};
|
||||
let conn =
|
||||
Connection::from_socket(stream).context("failed to connect to Wayland compositor")?;
|
||||
let (globals, event_queue) = registry_queue_init::<WaylandClientStatePtr>(&conn)
|
||||
let (globals, mut event_queue) = registry_queue_init::<WaylandClientStatePtr>(&conn)
|
||||
.context("failed to initialize Wayland registry")?;
|
||||
let qh = event_queue.handle();
|
||||
|
||||
|
|
@ -961,6 +996,8 @@ impl WaylandConnection {
|
|||
let mut in_progress_outputs = HashMap::default();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut wl_outputs: HashMap<ObjectId, wl_output::WlOutput> = HashMap::default();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut output_globals: HashMap<u32, ObjectId> = HashMap::default();
|
||||
globals.contents().with_list(|list| -> anyhow::Result<()> {
|
||||
for global in list {
|
||||
match &global.interface[..] {
|
||||
|
|
@ -980,6 +1017,7 @@ impl WaylandConnection {
|
|||
(),
|
||||
);
|
||||
in_progress_outputs.insert(output.id(), InProgressOutput::default());
|
||||
output_globals.insert(global.name, output.id());
|
||||
wl_outputs.insert(output.id(), output);
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -1037,6 +1075,7 @@ impl WaylandConnection {
|
|||
last_ime_cursor_rectangle: None,
|
||||
outputs: HashMap::default(),
|
||||
in_progress_outputs,
|
||||
output_globals,
|
||||
wl_outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
|
|
@ -1155,6 +1194,14 @@ impl WaylandConnection {
|
|||
"failed to register desktop portal source",
|
||||
)?;
|
||||
|
||||
// Receive the outputs' descriptions now, so displays are known before
|
||||
// the app's first windows open.
|
||||
let mut pointer = pointer;
|
||||
event_queue
|
||||
.roundtrip(&mut pointer)
|
||||
.context("failed to receive initial Wayland state")
|
||||
.log_err();
|
||||
|
||||
client.register_source(
|
||||
handle.insert_source(WaylandSource::new(conn, event_queue), {
|
||||
let mut client = pointer;
|
||||
|
|
@ -1200,6 +1247,7 @@ impl WaylandConnection {
|
|||
id: id.clone(),
|
||||
name: output.name.clone(),
|
||||
bounds: output.bounds.to_pixels(output.scale as f32),
|
||||
refresh_interval: output.refresh_interval,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -1579,12 +1627,26 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
|||
state
|
||||
.in_progress_outputs
|
||||
.insert(output.id(), InProgressOutput::default());
|
||||
state.output_globals.insert(name, output.id());
|
||||
state.wl_outputs.insert(output.id(), output);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
wl_registry::Event::GlobalRemove { name: _ } => {
|
||||
// TODO: handle global removal
|
||||
wl_registry::Event::GlobalRemove { name } => {
|
||||
let Some(id) = state.output_globals.remove(&name) else {
|
||||
return;
|
||||
};
|
||||
let was_complete = state.outputs.remove(&id).is_some();
|
||||
state.in_progress_outputs.remove(&id);
|
||||
if let Some(output) = state.wl_outputs.remove(&id)
|
||||
&& output.version() >= wl_output::REQ_RELEASE_SINCE
|
||||
{
|
||||
output.release();
|
||||
}
|
||||
if was_complete {
|
||||
drop(state);
|
||||
this.report_displays_changed();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -1728,14 +1790,33 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
|
|||
in_progress_output.subpixel = Some(subpixel);
|
||||
}
|
||||
}
|
||||
wl_output::Event::Mode { width, height, .. } => {
|
||||
in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height)))
|
||||
wl_output::Event::Mode {
|
||||
flags,
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
} => {
|
||||
// Compositors before wl_output v4 may also list modes the
|
||||
// output isn't using.
|
||||
if let WEnum::Value(flags) = flags
|
||||
&& flags.contains(wl_output::Mode::Current)
|
||||
{
|
||||
in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height)));
|
||||
in_progress_output.refresh_interval = refresh_interval_from_millihertz(refresh);
|
||||
}
|
||||
}
|
||||
wl_output::Event::Done => {
|
||||
if let Some(complete) = in_progress_output.complete() {
|
||||
state.outputs.insert(output.id(), complete);
|
||||
let Some(complete) = in_progress_output.complete() else {
|
||||
return;
|
||||
};
|
||||
let id = output.id();
|
||||
state.outputs.insert(id.clone(), complete.clone());
|
||||
let windows = state.windows.values().cloned().collect::<Vec<_>>();
|
||||
drop(state);
|
||||
for window in windows {
|
||||
window.handle_output_changed(&id, &complete);
|
||||
}
|
||||
state.in_progress_outputs.remove(&output.id());
|
||||
this.report_displays_changed();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
use std::{
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use uuid::Uuid;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
||||
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay};
|
||||
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, refresh_interval_from_hz};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WaylandDisplay {
|
||||
|
|
@ -15,6 +16,7 @@ pub(crate) struct WaylandDisplay {
|
|||
pub id: ObjectId,
|
||||
pub name: Option<String>,
|
||||
pub bounds: Bounds<Pixels>,
|
||||
pub refresh_interval: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Hash for WaylandDisplay {
|
||||
|
|
@ -39,4 +41,37 @@ impl PlatformDisplay for WaylandDisplay {
|
|||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
fn refresh_interval(&self) -> Option<Duration> {
|
||||
self.refresh_interval
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a `wl_output.mode` refresh rate, in millihertz, to an interval.
|
||||
/// Compositors send 0 when the rate doesn't apply, e.g. for virtual outputs.
|
||||
pub(crate) fn refresh_interval_from_millihertz(millihertz: i32) -> Option<Duration> {
|
||||
refresh_interval_from_hz(f64::from(millihertz) / 1000.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_interval_from_millihertz() {
|
||||
assert_eq!(
|
||||
refresh_interval_from_millihertz(60_000).map(|interval| interval.as_micros()),
|
||||
Some(16_666)
|
||||
);
|
||||
assert_eq!(
|
||||
refresh_interval_from_millihertz(59_940).map(|interval| interval.as_micros()),
|
||||
Some(16683)
|
||||
);
|
||||
assert_eq!(
|
||||
refresh_interval_from_millihertz(143_998).map(|interval| interval.as_micros()),
|
||||
Some(6944)
|
||||
);
|
||||
assert_eq!(refresh_interval_from_millihertz(0), None);
|
||||
assert_eq!(refresh_interval_from_millihertz(-1), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ pub(crate) struct Callbacks {
|
|||
hover_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
display_changed: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
close: Option<Box<dyn FnOnce()>>,
|
||||
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
|
|
@ -661,7 +662,12 @@ impl WaylandWindowState {
|
|||
|
||||
pub fn primary_output_scale(&mut self) -> i32 {
|
||||
let mut scale = 1;
|
||||
let mut current_output = self.display.take();
|
||||
// Keep the display the surface last left only while it is on no
|
||||
// other output.
|
||||
let mut current_output = self
|
||||
.display
|
||||
.take()
|
||||
.filter(|(id, _)| self.outputs.is_empty() || self.outputs.contains_key(id));
|
||||
for (id, output) in self.outputs.iter() {
|
||||
if let Some((_, output_data)) = ¤t_output {
|
||||
if output.scale > output_data.scale {
|
||||
|
|
@ -1406,7 +1412,10 @@ impl WaylandWindowStatePtr {
|
|||
|
||||
state.outputs.insert(id, output.clone());
|
||||
|
||||
let previous_display = state.display.as_ref().map(|(id, _)| id.clone());
|
||||
let scale = state.primary_output_scale();
|
||||
let display_changed =
|
||||
state.display.as_ref().map(|(id, _)| id) != previous_display.as_ref();
|
||||
state.update_subpixel_layout();
|
||||
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
|
|
@ -1417,12 +1426,18 @@ impl WaylandWindowStatePtr {
|
|||
} else {
|
||||
drop(state);
|
||||
}
|
||||
if display_changed {
|
||||
self.report_display_changed();
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
wl_surface::Event::Leave { output } => {
|
||||
state.outputs.remove(&output.id());
|
||||
|
||||
let previous_display = state.display.as_ref().map(|(id, _)| id.clone());
|
||||
let scale = state.primary_output_scale();
|
||||
let display_changed =
|
||||
state.display.as_ref().map(|(id, _)| id) != previous_display.as_ref();
|
||||
state.update_subpixel_layout();
|
||||
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
|
|
@ -1433,6 +1448,9 @@ impl WaylandWindowStatePtr {
|
|||
} else {
|
||||
drop(state);
|
||||
}
|
||||
if display_changed {
|
||||
self.report_display_changed();
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
wl_surface::Event::PreferredBufferScale { factor } => {
|
||||
|
|
@ -1600,6 +1618,29 @@ impl WaylandWindowStatePtr {
|
|||
}
|
||||
}
|
||||
|
||||
fn report_display_changed(&self) {
|
||||
let callback = self.callbacks.borrow_mut().display_changed.take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
self.callbacks.borrow_mut().display_changed = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the window's copy of an output it is on after the compositor
|
||||
/// changes the output's properties, such as its mode.
|
||||
pub fn handle_output_changed(&self, id: &ObjectId, output: &Output) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let Some(entered) = state.outputs.get_mut(id) else {
|
||||
return;
|
||||
};
|
||||
*entered = output.clone();
|
||||
if let Some((display_id, display)) = &mut state.display
|
||||
&& display_id == id
|
||||
{
|
||||
*display = output.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn report_visibility(&self, visibility: WindowVisibility) {
|
||||
let callback = self.callbacks.borrow_mut().visibility_change.take();
|
||||
if let Some(mut callback) = callback {
|
||||
|
|
@ -1766,13 +1807,18 @@ impl PlatformWindow for WaylandWindow {
|
|||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
let state = self.borrow();
|
||||
state.display.as_ref().map(|(id, display)| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: id.clone(),
|
||||
name: display.name.clone(),
|
||||
bounds: display.bounds.to_pixels(state.scale),
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
// The compositor only names the window's output after mapping it. With
|
||||
// one output the window can only be there, so don't wait for `enter`.
|
||||
let (id, display) = state
|
||||
.display
|
||||
.clone()
|
||||
.or_else(|| state.client.sole_output())?;
|
||||
Some(Rc::new(WaylandDisplay {
|
||||
id,
|
||||
name: display.name.clone(),
|
||||
bounds: display.bounds.to_pixels(state.scale),
|
||||
refresh_interval: display.refresh_interval,
|
||||
}))
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
|
|
@ -1939,6 +1985,10 @@ impl PlatformWindow for WaylandWindow {
|
|||
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
fn on_display_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.callbacks.borrow_mut().display_changed = Some(callback);
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ impl X11Display {
|
|||
}
|
||||
|
||||
impl PlatformDisplay for X11Display {
|
||||
// An X screen spans every monitor, so it has no single refresh rate.
|
||||
fn refresh_interval(&self) -> Option<std::time::Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId::new(self.x_screen_index as u64)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1725,6 +1725,10 @@ impl PlatformWindow for X11Window {
|
|||
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
// A GPUI display on X11 is an X screen, which spans every monitor, and a
|
||||
// window never changes screen.
|
||||
fn on_display_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ use crate::ns_string;
|
|||
use anyhow::Result;
|
||||
use cocoa::{
|
||||
appkit::NSScreen,
|
||||
base::{id, nil},
|
||||
base::{BOOL, NO, YES, id, nil},
|
||||
foundation::{NSArray, NSDictionary},
|
||||
};
|
||||
use core_foundation::base::CFRelease;
|
||||
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
|
||||
use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
|
||||
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, point, px, size};
|
||||
use core_graphics::display::{
|
||||
CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGGetActiveDisplayList,
|
||||
};
|
||||
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, point, px, refresh_interval_from_hz, size};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -45,24 +48,30 @@ impl MacDisplay {
|
|||
}
|
||||
|
||||
/// Obtains an iterator over all currently active system displays.
|
||||
///
|
||||
/// Empty if the system can't list them, e.g. without a window server
|
||||
/// connection.
|
||||
pub fn all() -> impl Iterator<Item = Self> {
|
||||
unsafe {
|
||||
// We're assuming there aren't more than 32 displays connected to the system.
|
||||
let mut displays = Vec::with_capacity(32);
|
||||
let mut display_count = 0;
|
||||
let result = CGGetActiveDisplayList(
|
||||
// We're assuming there aren't more than 32 displays connected to the system.
|
||||
let mut displays = Vec::with_capacity(32);
|
||||
let mut display_count = 0;
|
||||
// SAFETY: `displays` has room for the capacity passed in, and
|
||||
// `display_count` reports how many entries were written.
|
||||
let result = unsafe {
|
||||
CGGetActiveDisplayList(
|
||||
displays.capacity() as u32,
|
||||
displays.as_mut_ptr(),
|
||||
&mut display_count,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
displays.set_len(display_count as usize);
|
||||
displays.into_iter().map(MacDisplay)
|
||||
} else {
|
||||
panic!("Failed to get active display list. Result: {result}");
|
||||
}
|
||||
)
|
||||
};
|
||||
if result == 0 {
|
||||
// SAFETY: `CGGetActiveDisplayList` initialized `display_count`
|
||||
// entries, at most the capacity it was given.
|
||||
unsafe { displays.set_len(display_count as usize) };
|
||||
} else {
|
||||
log::error!("Failed to get active display list. Result: {result}");
|
||||
}
|
||||
displays.into_iter().map(MacDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +155,30 @@ impl PlatformDisplay for MacDisplay {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_interval(&self) -> Option<Duration> {
|
||||
// `maximumFramesPerSecond` is the ProMotion maximum, where the
|
||||
// display mode's rate is 0 on built-in panels.
|
||||
let screen_hertz = unsafe {
|
||||
let screen = self.get_nsscreen();
|
||||
let supports_maximum: BOOL = if screen == nil {
|
||||
NO
|
||||
} else {
|
||||
msg_send![screen, respondsToSelector: sel!(maximumFramesPerSecond)]
|
||||
};
|
||||
if supports_maximum == YES {
|
||||
let frames_per_second: isize = msg_send![screen, maximumFramesPerSecond];
|
||||
frames_per_second as f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
refresh_interval_from_hz(screen_hertz).or_else(|| {
|
||||
CGDisplay::new(self.0)
|
||||
.display_mode()
|
||||
.and_then(|mode| refresh_interval_from_hz(mode.refresh_rate()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MacDisplay {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,11 @@ unsafe fn build_classes() {
|
|||
on_thermal_state_change as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.add_method(
|
||||
sel!(onScreenParametersChange:),
|
||||
on_screen_parameters_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),
|
||||
|
|
@ -193,6 +198,7 @@ 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_displays_changed: Option<Box<dyn FnMut()>>,
|
||||
on_system_sleep: Option<Box<dyn FnMut()>>,
|
||||
on_system_wake: Option<Box<dyn FnMut()>>,
|
||||
system_power_observers_registered: bool,
|
||||
|
|
@ -269,6 +275,7 @@ impl MacPlatform {
|
|||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
on_thermal_state_change: None,
|
||||
on_displays_changed: None,
|
||||
on_system_sleep: None,
|
||||
on_system_wake: None,
|
||||
system_power_observers_registered: false,
|
||||
|
|
@ -739,6 +746,10 @@ impl Platform for MacPlatform {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn on_displays_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().on_displays_changed = Some(callback);
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
let min_version = cocoa::foundation::NSOperatingSystemVersion::new(12, 3, 0);
|
||||
|
|
@ -1408,6 +1419,14 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
|||
object: nil
|
||||
];
|
||||
|
||||
// Posted when a display is connected, disconnected, rearranged, or
|
||||
// changes mode, which includes its refresh rate.
|
||||
let _: () = msg_send![notification_center, addObserver: this as id
|
||||
selector: sel!(onScreenParametersChange:)
|
||||
name: ns_string("NSApplicationDidChangeScreenParametersNotification")
|
||||
object: nil
|
||||
];
|
||||
|
||||
let thermal_name = ns_string("NSProcessInfoThermalStateDidChangeNotification");
|
||||
let process_info: id = msg_send![class!(NSProcessInfo), processInfo];
|
||||
let _: () = msg_send![notification_center, addObserver: this as id
|
||||
|
|
@ -1520,6 +1539,28 @@ extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) {
|
|||
}
|
||||
}
|
||||
|
||||
extern "C" fn on_screen_parameters_change(this: &mut Object, _: Sel, _: id) {
|
||||
// Deferred for the same reason as `on_thermal_state_change`.
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let platform_ptr = platform as *const MacPlatform as *mut c_void;
|
||||
unsafe {
|
||||
DispatchQueue::main().exec_async_f(platform_ptr, on_screen_parameters_change);
|
||||
}
|
||||
|
||||
extern "C" fn on_screen_parameters_change(context: *mut c_void) {
|
||||
let platform = unsafe { &*(context as *const MacPlatform) };
|
||||
let callback = platform.0.lock().on_displays_changed.take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
platform
|
||||
.0
|
||||
.lock()
|
||||
.on_displays_changed
|
||||
.get_or_insert(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@ struct MacWindowState {
|
|||
last_visibility: Option<WindowVisibility>,
|
||||
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved_callback: Option<Box<dyn FnMut()>>,
|
||||
display_changed_callback: Option<Box<dyn FnMut()>>,
|
||||
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
|
||||
close_callback: Option<Box<dyn FnOnce()>>,
|
||||
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
||||
|
|
@ -1112,6 +1113,7 @@ impl MacWindow {
|
|||
last_visibility: None,
|
||||
resize_callback: None,
|
||||
moved_callback: None,
|
||||
display_changed_callback: None,
|
||||
should_close_callback: None,
|
||||
close_callback: None,
|
||||
appearance_changed_callback: None,
|
||||
|
|
@ -2039,6 +2041,10 @@ impl PlatformWindow for MacWindow {
|
|||
self.0.as_ref().lock().resize_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_display_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.as_ref().lock().display_changed_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.as_ref().lock().moved_callback = Some(callback);
|
||||
}
|
||||
|
|
@ -3117,6 +3123,25 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
|
|||
lock.start_display_link();
|
||||
drop(lock);
|
||||
update_window_scale_factor(&window_state);
|
||||
report_display_change(&window_state);
|
||||
}
|
||||
|
||||
fn report_display_change(window_state: &Arc<Mutex<MacWindowState>>) {
|
||||
let executor = window_state.lock().foreground_executor.clone();
|
||||
// AppKit can post screen changes while GPUI is updating a window, e.g.
|
||||
// from `setFrame:`, so deliver after that update completes.
|
||||
executor
|
||||
.spawn({
|
||||
let window_state = window_state.clone();
|
||||
async move {
|
||||
let callback = window_state.lock().display_changed_callback.take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
window_state.lock().display_changed_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ impl WebDisplay {
|
|||
}
|
||||
|
||||
impl PlatformDisplay for WebDisplay {
|
||||
// Browsers don't expose the refresh rate; it could only be estimated from
|
||||
// `requestAnimationFrame` cadence.
|
||||
fn refresh_interval(&self) -> Option<std::time::Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
fn id(&self) -> DisplayId {
|
||||
self.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,6 +368,9 @@ impl Platform for WebPlatform {
|
|||
Some(self.active_display.clone())
|
||||
}
|
||||
|
||||
// The browser is the only display, and it never changes.
|
||||
fn on_displays_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
*self.active_window.borrow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -972,6 +972,9 @@ impl PlatformWindow for WebWindow {
|
|||
self.inner.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
// The browser is the only display, and it never changes.
|
||||
fn on_display_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.inner.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ use windows::{
|
|||
};
|
||||
|
||||
use crate::logical_point;
|
||||
use gpui::{Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay, point, size};
|
||||
use gpui::{
|
||||
Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay, point, refresh_interval_from_hz, size,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct WindowsDisplay {
|
||||
|
|
@ -149,6 +151,38 @@ impl PlatformDisplay for WindowsDisplay {
|
|||
fn visible_bounds(&self) -> Bounds<Pixels> {
|
||||
self.visible_bounds
|
||||
}
|
||||
|
||||
fn refresh_interval(&self) -> Option<std::time::Duration> {
|
||||
get_monitor_info(self.handle)
|
||||
.log_err()
|
||||
.and_then(|info| refresh_interval_for_device(&info.szDevice))
|
||||
}
|
||||
}
|
||||
|
||||
/// The refresh rate of the monitor's current mode. With variable refresh
|
||||
/// rate, the mode's rate is the maximum.
|
||||
fn refresh_interval_for_device(device_name: &[u16; 32]) -> Option<std::time::Duration> {
|
||||
let mut mode = DEVMODEW {
|
||||
dmSize: std::mem::size_of::<DEVMODEW>() as u16,
|
||||
..Default::default()
|
||||
};
|
||||
// SAFETY: `device_name` is the null-terminated `szDevice` from
|
||||
// `GetMonitorInfoW`, and `mode.dmSize` is initialized.
|
||||
let found = unsafe {
|
||||
EnumDisplaySettingsW(
|
||||
PCWSTR(device_name.as_ptr()),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mut mode,
|
||||
)
|
||||
};
|
||||
if !found.as_bool() {
|
||||
return None;
|
||||
}
|
||||
// 0 and 1 mean the hardware's default rate, which isn't reported.
|
||||
if mode.dmDisplayFrequency <= 1 {
|
||||
return None;
|
||||
}
|
||||
refresh_interval_from_hz(f64::from(mode.dmDisplayFrequency))
|
||||
}
|
||||
|
||||
fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
|
|||
pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6;
|
||||
pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7;
|
||||
pub(crate) const WM_GPUI_KEYDOWN: u32 = WM_USER + 8;
|
||||
pub(crate) const WM_GPUI_END_SESSION: u32 = WM_USER + 9;
|
||||
pub(crate) const WM_GPUI_DISPLAYS_CHANGED: u32 = WM_USER + 10;
|
||||
|
||||
const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
|
||||
|
||||
|
|
@ -109,8 +109,6 @@ impl WindowsWindowInner {
|
|||
WM_PAINT => self.handle_paint_msg(handle),
|
||||
WM_CLOSE => self.handle_close_msg(),
|
||||
WM_DESTROY => self.handle_destroy_msg(handle),
|
||||
WM_QUERYENDSESSION => Some(1),
|
||||
WM_ENDSESSION => self.handle_end_session_msg(wparam),
|
||||
WM_MOUSEMOVE => self.handle_mouse_move_msg(handle, lparam, wparam),
|
||||
WM_MOUSELEAVE | WM_NCMOUSELEAVE => self.handle_mouse_leave_msg(),
|
||||
WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(handle, lparam),
|
||||
|
|
@ -173,20 +171,6 @@ impl WindowsWindowInner {
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_end_session_msg(&self, wparam: WPARAM) -> Option<isize> {
|
||||
if wparam.0 != 0 {
|
||||
unsafe {
|
||||
SendMessageW(
|
||||
self.platform_window_handle,
|
||||
WM_GPUI_END_SESSION,
|
||||
Some(WPARAM(self.validation_number)),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn handle_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option<isize> {
|
||||
let origin = logical_point(
|
||||
lparam.signed_loword() as f32,
|
||||
|
|
@ -209,7 +193,7 @@ impl WindowsWindowInner {
|
|||
// monitor is invalid, we do nothing.
|
||||
if !monitor.is_invalid() && self.state.display.get().handle != monitor {
|
||||
// we will get the same monitor if we only have one
|
||||
self.state.display.set(WindowsDisplay::new(
|
||||
self.set_display(WindowsDisplay::new(
|
||||
WindowsDisplay::display_id_for_monitor(monitor),
|
||||
)?);
|
||||
}
|
||||
|
|
@ -956,10 +940,18 @@ impl WindowsWindowInner {
|
|||
return None;
|
||||
}
|
||||
let new_display = WindowsDisplay::new(WindowsDisplay::display_id_for_monitor(new_monitor))?;
|
||||
self.state.display.set(new_display);
|
||||
self.set_display(new_display);
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn set_display(&self, display: WindowsDisplay) {
|
||||
self.state.display.set(display);
|
||||
if let Some(mut callback) = self.state.callbacks.display_changed.take() {
|
||||
callback();
|
||||
self.state.callbacks.display_changed.set(Some(callback));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hit_test_msg(&self, handle: HWND, lparam: LPARAM) -> Option<isize> {
|
||||
if self.state.is_fullscreen() {
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ struct PlatformCallbacks {
|
|||
keyboard_layout_change: Cell<Option<Box<dyn FnMut()>>>,
|
||||
system_sleep: Cell<Option<Box<dyn FnMut()>>>,
|
||||
system_wake: Cell<Option<Box<dyn FnMut()>>>,
|
||||
displays_changed: Cell<Option<Box<dyn FnMut()>>>,
|
||||
}
|
||||
|
||||
impl WindowsPlatformState {
|
||||
|
|
@ -216,9 +217,12 @@ impl WindowsPlatform {
|
|||
directx_devices: None,
|
||||
dispatcher: None,
|
||||
};
|
||||
// A hidden top-level window rather than a message-only one, since only
|
||||
// top-level windows receive system broadcasts such as `WM_DISPLAYCHANGE`
|
||||
// and `WM_ENDSESSION`, which the app needs even with no windows open.
|
||||
let result = unsafe {
|
||||
CreateWindowExW(
|
||||
WINDOW_EX_STYLE(0),
|
||||
WS_EX_TOOLWINDOW,
|
||||
PLATFORM_WINDOW_CLASS_NAME,
|
||||
None,
|
||||
WINDOW_STYLE(0),
|
||||
|
|
@ -226,7 +230,7 @@ impl WindowsPlatform {
|
|||
0,
|
||||
0,
|
||||
0,
|
||||
Some(HWND_MESSAGE),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&raw const context as *const _),
|
||||
|
|
@ -731,6 +735,14 @@ impl Platform for WindowsPlatform {
|
|||
WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc<dyn PlatformDisplay>)
|
||||
}
|
||||
|
||||
fn on_displays_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner
|
||||
.state
|
||||
.callbacks
|
||||
.displays_changed
|
||||
.set(Some(callback));
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
true
|
||||
|
|
@ -1179,9 +1191,12 @@ impl WindowsPlatformInner {
|
|||
| WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
|
||||
| WM_GPUI_DOCK_MENU_ACTION
|
||||
| WM_GPUI_KEYBOARD_LAYOUT_CHANGED
|
||||
| WM_GPUI_GPU_DEVICE_LOST
|
||||
| WM_GPUI_END_SESSION => self.handle_gpui_events(msg, wparam, lparam),
|
||||
| WM_GPUI_DISPLAYS_CHANGED
|
||||
| WM_GPUI_GPU_DEVICE_LOST => self.handle_gpui_events(msg, wparam, lparam),
|
||||
WM_POWERBROADCAST => self.handle_power_broadcast(wparam),
|
||||
WM_DISPLAYCHANGE => self.handle_display_change(handle),
|
||||
WM_QUERYENDSESSION => Some(1),
|
||||
WM_ENDSESSION if wparam.0 != 0 => self.handle_end_session(),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(result) = handled {
|
||||
|
|
@ -1204,12 +1219,35 @@ impl WindowsPlatformInner {
|
|||
WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(),
|
||||
WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _),
|
||||
WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_keyboard_layout_change(),
|
||||
WM_GPUI_DISPLAYS_CHANGED => self.handle_displays_changed(),
|
||||
WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam),
|
||||
WM_GPUI_END_SESSION => self.handle_end_session(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_display_change(&self, handle: HWND) -> Option<isize> {
|
||||
// Broadcasts can be delivered while the app is mid-update, e.g. inside a
|
||||
// COM call, so report the change from the message loop instead.
|
||||
unsafe {
|
||||
PostMessageW(
|
||||
Some(handle),
|
||||
WM_GPUI_DISPLAYS_CHANGED,
|
||||
WPARAM(self.validation_number),
|
||||
LPARAM(0),
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn handle_displays_changed(&self) -> Option<isize> {
|
||||
self.with_callback(
|
||||
|callbacks| &callbacks.displays_changed,
|
||||
|callback| callback(),
|
||||
);
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn handle_end_session(&self) -> Option<isize> {
|
||||
let mut shutdown_completed = false;
|
||||
self.with_callback(
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ pub(crate) struct Callbacks {
|
|||
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()>>>,
|
||||
pub(crate) display_changed: Cell<Option<Box<dyn FnMut()>>>,
|
||||
pub(crate) should_close: Cell<Option<Box<dyn FnMut() -> bool>>>,
|
||||
pub(crate) close: Cell<Option<Box<dyn FnOnce()>>>,
|
||||
pub(crate) hit_test_window_control: Cell<Option<Box<dyn FnMut() -> Option<WindowControlArea>>>>,
|
||||
|
|
@ -1000,6 +1001,10 @@ impl PlatformWindow for WindowsWindow {
|
|||
self.state.callbacks.resize.set(Some(callback));
|
||||
}
|
||||
|
||||
fn on_display_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.state.callbacks.display_changed.set(Some(callback));
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.state.callbacks.moved.set(Some(callback));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue