Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions crates/gpui/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,31 @@ pub enum PlatformInput {
}

impl PlatformInput {
/// Convert native logical pixels to application UI coordinates at the platform boundary.
pub(crate) fn into_ui_coordinates(mut self, scale: f32) -> Self {
match &mut self {
Self::MouseDown(event) => event.position = event.position / scale,
Self::MouseUp(event) => event.position = event.position / scale,
Self::MouseMove(event) => event.position = event.position / scale,
Self::ScrollWheel(event) => {
event.position = event.position / scale;
if let ScrollDelta::Pixels(delta) = &mut event.delta {
*delta = *delta / scale;
}
}
Self::Pinch(event) => event.position = event.position / scale,
Self::Touch(event) => event.position = event.position / scale,
Self::FileDrop(event) => match event {
FileDropEvent::Entered { position, .. }
| FileDropEvent::Pending { position }
| FileDropEvent::Submit { position } => *position = *position / scale,
FileDropEvent::Exited => {}
},
_ => {}
}
self
}

pub(crate) fn mouse_event(&self) -> Option<&dyn Any> {
match self {
PlatformInput::KeyDown { .. } => None,
Expand Down
11 changes: 9 additions & 2 deletions crates/gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,11 @@ impl PlatformInputHandler {

pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
self.cx
.update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
.update(|window, cx| {
self.handler
.bounds_for_range(range_utf16, window, cx)
.map(|bounds| window.ui_to_platform_bounds(bounds))
})
.ok()
.flatten()
}
Expand Down Expand Up @@ -1555,7 +1559,10 @@ impl PlatformInputHandler {
#[allow(unused)]
pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
self.cx
.update(|window, cx| self.handler.character_index_for_point(point, window, cx))
.update(|window, cx| {
self.handler
.character_index_for_point(point / window.ui_scale(), window, cx)
})
.ok()
.flatten()
}
Expand Down
126 changes: 116 additions & 10 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,7 @@ pub struct Window {
modifiers: Modifiers,
capslock: Capslock,
scale_factor: f32,
ui_scale: f32,
pub(crate) bounds_observers: SubscriberSet<(), AnyObserver>,
appearance: WindowAppearance,
pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>,
Expand Down Expand Up @@ -1694,7 +1695,9 @@ impl Window {
let mut cx = cx.to_async();
Box::new(move |event| {
handle
.update(&mut cx, |_, window, cx| window.dispatch_event(event, cx))
.update(&mut cx, |_, window, cx| {
window.dispatch_event(event.into_ui_coordinates(window.ui_scale), cx)
})
.log_err()
.unwrap_or(DispatchEventResult::default())
})
Expand Down Expand Up @@ -1810,6 +1813,7 @@ impl Window {
modifiers,
capslock,
scale_factor,
ui_scale: 1.0,
bounds_observers: SubscriberSet::new(),
appearance,
appearance_observers: SubscriberSet::new(),
Expand Down Expand Up @@ -2362,10 +2366,10 @@ impl Window {
/// the platform window, then notifies observers. Normally called automatically
/// by the platform's resize callback, but exposed publicly for test infrastructure.
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.scale_factor = self.platform_window.scale_factor() * self.ui_scale;
self.viewport_size = self.platform_window.content_size() / self.ui_scale;
self.display_id = self.platform_window.display().map(|display| display.id());
self.mouse_position = self.platform_window.mouse_position();
self.mouse_position = self.platform_window.mouse_position() / self.ui_scale;

self.refresh();

Expand Down Expand Up @@ -2449,7 +2453,8 @@ impl Window {

/// Opens the native title bar context menu, useful when implementing client side decorations (Wayland and X11)
pub fn show_window_menu(&self, position: Point<Pixels>) {
self.platform_window.show_window_menu(position)
self.platform_window
.show_window_menu(position * self.ui_scale)
}

/// Handle window movement for Linux and macOS.
Expand All @@ -2463,7 +2468,7 @@ impl Window {
/// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11)
pub fn set_client_inset(&mut self, inset: Pixels) {
self.client_inset = Some(inset);
self.platform_window.set_client_inset(inset);
self.platform_window.set_client_inset(inset * self.ui_scale);
}

/// Returns the client_inset value by [`Self::set_client_inset`].
Expand Down Expand Up @@ -2528,13 +2533,40 @@ impl Window {
self.platform_window.show_character_palette();
}

/// The scale factor of the display associated with the window. For example, it could
/// return 2.0 for a "retina" display, indicating that each logical pixel should actually
/// be rendered as two pixels on screen.
/// The render scale: the display's pixel density multiplied by [`Self::ui_scale`].
pub fn scale_factor(&self) -> f32 {
self.scale_factor
}

/// Application zoom, independent of the display's pixel density.
pub fn ui_scale(&self) -> f32 {
self.ui_scale
}

/// Scale the entire UI, including absolute pixel dimensions and input hit testing.
/// Native window bounds remain in platform coordinates; layout and dispatched
/// events use UI coordinates. Invalid factors are ignored.
pub fn set_ui_scale(&mut self, scale: f32) {
if !scale.is_finite() || scale <= 0.0 || scale == self.ui_scale {
return;
}
let ratio = self.ui_scale / scale;
self.ui_scale = scale;
self.scale_factor = self.platform_window.scale_factor() * scale;
self.viewport_size = self.platform_window.content_size() / scale;
self.mouse_position = self.mouse_position * ratio;
if let Some(inset) = self.client_inset {
self.platform_window.set_client_inset(inset * scale);
}
self.refresh();
self.invalidate_character_coordinates();
}

/// Convert layout bounds to platform logical pixels for native child surfaces or IME.
pub fn ui_to_platform_bounds(&self, bounds: Bounds<Pixels>) -> Bounds<Pixels> {
bounds.map(|value| value * self.ui_scale)
}

/// The size of an em for the base font of the application. Adjusting this value allows the
/// UI to scale, just like zooming a web page.
pub fn rem_size(&self) -> Pixels {
Expand Down Expand Up @@ -5706,7 +5738,9 @@ impl Window {
self.on_next_frame(|window, cx| {
if let Some(mut input_handler) = window.platform_window.take_input_handler() {
if let Some(bounds) = input_handler.selected_bounds(window, cx) {
window.platform_window.update_ime_position(bounds);
window
.platform_window
.update_ime_position(window.ui_to_platform_bounds(bounds));
}
window.platform_window.set_input_handler(input_handler);
}
Expand Down Expand Up @@ -6875,6 +6909,78 @@ mod tests {
}
}

#[test]
fn ui_scale_changes_layout_without_resizing_native_window_or_fonts() {
let mut cx = TestAppContext::single();
let child_bounds = Rc::new(Cell::new(Bounds::default()));
let window = cx.open_window(size(px(1200.), px(800.)), {
let child_bounds = child_bounds.clone();
move |_, _| RootView {
explicit_size: false,
child_bounds,
}
});
cx.update_window(window.into(), |_, window, cx| {
let native_bounds = window.bounds();
let rem_size = window.rem_size();
let display_scale = window.scale_factor();
window.set_ui_scale(1.5);
window.draw(cx).clear();
assert_eq!(window.viewport_size(), size(px(800.), px(800. / 1.5)));
assert_eq!(window.scale_factor(), display_scale * 1.5);
assert_eq!(window.bounds(), native_bounds);
assert_eq!(window.rem_size(), rem_size);
assert_eq!(child_bounds.get().size, window.viewport_size());
window.bounds_changed(cx);
assert_eq!(window.ui_scale(), 1.5);
assert_eq!(window.scale_factor(), display_scale * 1.5);
assert_eq!(
window.ui_to_platform_bounds(Bounds::new(
crate::point(px(20.), px(40.)),
size(px(100.), px(50.))
)),
Bounds::new(crate::point(px(30.), px(60.)), size(px(150.), px(75.)))
);
for invalid in [0., -1., f32::NAN, f32::INFINITY] {
window.set_ui_scale(invalid);
assert_eq!(window.ui_scale(), 1.5);
}
window.set_ui_scale(1.0);
assert_eq!(window.viewport_size(), native_bounds.size);
})
.unwrap();
}

#[test]
fn ui_scale_converts_native_pointer_and_pixel_scroll_input() {
let input = crate::PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
position: crate::point(px(150.), px(90.)),
delta: crate::ScrollDelta::Pixels(crate::point(px(30.), px(60.))),
..Default::default()
})
.into_ui_coordinates(1.5);
let crate::PlatformInput::ScrollWheel(event) = input else {
unreachable!()
};
assert_eq!(event.position, crate::point(px(100.), px(60.)));
let crate::ScrollDelta::Pixels(delta) = event.delta else {
panic!("pixel scroll changed units")
};
assert_eq!(delta, crate::point(px(20.), px(40.)));
let input = crate::PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
delta: crate::ScrollDelta::Lines(crate::point(2., 3.)),
..Default::default()
})
.into_ui_coordinates(2.);
let crate::PlatformInput::ScrollWheel(event) = input else {
unreachable!()
};
let crate::ScrollDelta::Lines(delta) = event.delta else {
panic!("line scroll changed units")
};
assert_eq!(delta, crate::point(2., 3.));
}

#[test]
fn auto_sized_window_root_fills_the_window() {
let mut cx = TestAppContext::single();
Expand Down