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

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:
Anthony Eid 2026-10-08 14:12:10 +00:00 • committed by GitHub
parent b0048cde74
commit f495dd0f77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 990 additions and 140 deletions

View file

@ -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, &current);
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

View file

@ -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,

View file

@ -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,

View file

@ -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();

View file

@ -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
}
}

View file

@ -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))

View file

@ -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);
}

View file

@ -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()
}

View file

@ -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);

View file

@ -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(),
)))
}

View file

@ -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()>) {}

View file

@ -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()

View file

@ -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();
}
_ => {}
}

View file

@ -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);
}
}

View file

@ -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)) = &current_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);
}

View file

@ -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)
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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.

View file

@ -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) {

View file

@ -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
}

View file

@ -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()
}

View file

@ -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);
}

View file

@ -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]> {

View file

@ -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;

View file

@ -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(

View file

@ -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));
}