diff --git a/CHANGELOG.md b/CHANGELOG.md index f069f8add5..4b7fc09acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i - `Env` and `Key` gained methods for inspecting an `Env` at runtime ([#880] by [@Zarenor]) - `UpdateCtx::request_timer` and `UpdateCtx::request_anim_frame`. ([#898] by [@finnerale]) - `LifeCycleCtx::request_timer`. ([#954] by [@xStrom]) +- `scale` method to `WinHandler`. ([#904] by [@xStrom]) +- `WinHandler::scale` method to inform of scale changes. ([#904] by [@xStrom]) - `UpdateCtx::size` and `LifeCycleCtx::size`. ([#917] by [@jneem]) - `WidgetExt::debug_widget_id`, for displaying widget ids on hover. ([#876] by [@cmyr]) - `im` feature, with `Data` support for the [`im` crate](https://docs.rs/im/) collections. ([#924] by [@cmyr]) @@ -61,6 +63,8 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i - Global `Application` associated functions are instance methods instead, e.g. `Application::global().quit()` instead of the old `Application::quit()`. ([#763] by [@xStrom]) - Timer events will only be delivered to the widgets that requested them. ([#831] by [@sjoshid]) - `Event::Wheel` now contains a `MouseEvent` structure. ([#895] by [@teddemunnik]) +- The `WindowHandle::get_dpi` method got replaced by `WindowHandle::get_scale`. ([#904] by [@xStrom]) +- The `WinHandler::size` method now gets a `Size` in display points. ([#904] by [@xStrom]) - `AppDelegate::command` now receives a `Target` instead of a `&Target`. ([#909] by [@xStrom]) - `SHOW_WINDOW` and `CLOSE_WINDOW` commands now only use `Target` to determine the affected window. ([#928] by [@finnerale]) - Replaced `NEW_WINDOW`, `SET_MENU` and `SHOW_CONTEXT_MENU` commands with methods on `EventCtx` and `DelegateCtx`. ([#931] by [@finnerale]) @@ -94,6 +98,7 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i - X11: Support individual window closing. ([#900] by [@xStrom]) - X11: Support `Application::quit`. ([#900] by [@xStrom]) - GTK: Support file filters in open/save dialogs. ([#903] by [@jneem]) +- GTK: Support DPI values other than 96. ([#904] by [@xStrom]) - X11: Support key and mouse button state. ([#920] by [@jneem]) - Routing `LifeCycle::FocusChanged` to descendant widgets. ([#925] by [@yrns]) - Built-in open and save menu items now show the correct label and submit the right commands. ([#930] by [@finnerale]) @@ -125,6 +130,7 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i - GTK: Refactored `Application` to use the new structure. ([#892] by [@xStrom]) - X11: Refactored `Application` to use the new structure. ([#894] by [@xStrom]) - X11: Refactored `Window` to support some reentrancy and invalidation. ([#894] by [@xStrom]) +- Refactored DPI scaling. ([#904] by [@xStrom]) - Added docs generation testing for all features. ([#942] by [@xStrom]) ### Outside News @@ -183,6 +189,7 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i [#898]: https://github.com/xi-editor/druid/pull/898 [#900]: https://github.com/xi-editor/druid/pull/900 [#903]: https://github.com/xi-editor/druid/pull/903 +[#904]: https://github.com/xi-editor/druid/pull/904 [#905]: https://github.com/xi-editor/druid/pull/905 [#909]: https://github.com/xi-editor/druid/pull/909 [#917]: https://github.com/xi-editor/druid/pull/917 diff --git a/Cargo.lock b/Cargo.lock index d78f875c98..1e1f95d1f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,7 @@ dependencies = [ "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "druid-derive 0.4.0", "druid-shell 0.6.0", - "float-cmp 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "float-cmp 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-bundle 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-langneg 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-syntax 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -475,6 +475,11 @@ name = "float-cmp" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "float-cmp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "fluent-bundle" version = "0.11.0" @@ -1912,6 +1917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" "checksum float-cmp 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e" "checksum float-cmp 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d" +"checksum float-cmp 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" "checksum fluent-bundle 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "27ade33328521266c81cc0924523988f43ccd7359f64689a1b6e818afca3a646" "checksum fluent-langneg 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fe5815efd5542e40841cd34ef9003822352b04c67a70c595c6758597c72e1f56" "checksum fluent-syntax 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba" diff --git a/docs/src/widget.md b/docs/src/widget.md index 547f14b73c..1dc9a0da22 100644 --- a/docs/src/widget.md +++ b/docs/src/widget.md @@ -48,7 +48,7 @@ widgets]. Widgets are intended to be modular and composable, not monolithic. For instance, widgets generally do not control their own alignment or padding; if you have -a label, and you would like it to have 8px of horizontal padding and 4px of +a label, and you would like it to have 8dp of horizontal padding and 4dp of vertical padding, you can just do, ```rust,noplaypen diff --git a/druid-shell/examples/invalidate.rs b/druid-shell/examples/invalidate.rs index 573cc65629..7c6b319b14 100644 --- a/druid-shell/examples/invalidate.rs +++ b/druid-shell/examples/invalidate.rs @@ -16,14 +16,14 @@ use std::any::Any; use std::time::{Duration, Instant}; -use druid_shell::kurbo::{Point, Rect}; +use druid_shell::kurbo::{Point, Rect, Size}; use druid_shell::piet::{Color, Piet, RenderContext}; use druid_shell::{Application, TimerToken, WinHandler, WindowBuilder, WindowHandle}; struct InvalidateTest { handle: WindowHandle, - size: (f64, f64), + size: Size, start_time: Instant, color: Color, rect: Rect, @@ -39,10 +39,10 @@ impl InvalidateTest { (_, _) => Color::rgb8(r, g, b.wrapping_add(10)), }; - self.rect.x0 = (self.rect.x0 + 5.0) % self.size.0; - self.rect.x1 = (self.rect.x1 + 5.5) % self.size.0; - self.rect.y0 = (self.rect.y0 + 3.0) % self.size.1; - self.rect.y1 = (self.rect.y1 + 3.5) % self.size.1; + self.rect.x0 = (self.rect.x0 + 5.0) % self.size.width; + self.rect.x1 = (self.rect.x1 + 5.5) % self.size.width; + self.rect.y0 = (self.rect.y0 + 3.0) % self.size.height; + self.rect.y1 = (self.rect.y1 + 3.5) % self.size.height; } } @@ -63,12 +63,8 @@ impl WinHandler for InvalidateTest { false } - fn size(&mut self, width: u32, height: u32) { - let dpi = self.handle.get_dpi(); - let dpi_scale = dpi as f64 / 96.0; - let width_f = (width as f64) / dpi_scale; - let height_f = (height as f64) / dpi_scale; - self.size = (width_f, height_f); + fn size(&mut self, size: Size) { + self.size = size; } fn command(&mut self, id: u32) { @@ -91,7 +87,7 @@ fn main() { let app = Application::new().unwrap(); let mut builder = WindowBuilder::new(app.clone()); let inv_test = InvalidateTest { - size: Default::default(), + size: Size::ZERO, handle: Default::default(), start_time: Instant::now(), rect: Rect::from_origin_size(Point::ZERO, (10.0, 20.0)), diff --git a/druid-shell/examples/perftest.rs b/druid-shell/examples/perftest.rs index 6c3914fcdf..0c62965100 100644 --- a/druid-shell/examples/perftest.rs +++ b/druid-shell/examples/perftest.rs @@ -16,7 +16,7 @@ use std::any::Any; use time::Instant; -use piet_common::kurbo::{Line, Rect}; +use piet_common::kurbo::{Line, Rect, Size}; use piet_common::{Color, FontBuilder, Piet, RenderContext, Text, TextLayoutBuilder}; use druid_shell::{Application, KeyEvent, WinHandler, WindowBuilder, WindowHandle}; @@ -26,7 +26,7 @@ const FG_COLOR: Color = Color::rgb8(0xf0, 0xf0, 0xea); struct PerfTest { handle: WindowHandle, - size: (f64, f64), + size: Size, start_time: Instant, last_time: Instant, } @@ -37,11 +37,14 @@ impl WinHandler for PerfTest { } fn paint(&mut self, piet: &mut Piet, _: Rect) -> bool { - let (width, height) = self.size; - let rect = Rect::new(0.0, 0.0, width, height); + let rect = self.size.to_rect(); piet.fill(rect, &BG_COLOR); - piet.stroke(Line::new((0.0, height), (width, 0.0)), &FG_COLOR, 1.0); + piet.stroke( + Line::new((0.0, self.size.height), (self.size.width, 0.0)), + &FG_COLOR, + 1.0, + ); let current_ns = (Instant::now() - self.start_time).whole_nanoseconds(); let th = ::std::f64::consts::PI * (current_ns as f64) * 2e-9; @@ -98,12 +101,8 @@ impl WinHandler for PerfTest { false } - fn size(&mut self, width: u32, height: u32) { - let dpi = self.handle.get_dpi(); - let dpi_scale = dpi as f64 / 96.0; - let width_f = (width as f64) / dpi_scale; - let height_f = (height as f64) / dpi_scale; - self.size = (width_f, height_f); + fn size(&mut self, size: Size) { + self.size = size; } fn destroy(&mut self) { @@ -119,7 +118,7 @@ fn main() { let app = Application::new().unwrap(); let mut builder = WindowBuilder::new(app.clone()); let perf_test = PerfTest { - size: Default::default(), + size: Size::ZERO, handle: Default::default(), start_time: time::Instant::now(), last_time: time::Instant::now(), diff --git a/druid-shell/examples/shello.rs b/druid-shell/examples/shello.rs index 29f6d4c2bb..36b9b1be73 100644 --- a/druid-shell/examples/shello.rs +++ b/druid-shell/examples/shello.rs @@ -14,7 +14,7 @@ use std::any::Any; -use druid_shell::kurbo::{Line, Rect}; +use druid_shell::kurbo::{Line, Rect, Size}; use druid_shell::piet::{Color, RenderContext}; use druid_shell::{ @@ -27,7 +27,7 @@ const FG_COLOR: Color = Color::rgb8(0xf0, 0xf0, 0xea); #[derive(Default)] struct HelloState { - size: (f64, f64), + size: Size, handle: WindowHandle, } @@ -37,8 +37,7 @@ impl WinHandler for HelloState { } fn paint(&mut self, piet: &mut piet_common::Piet, _: Rect) -> bool { - let (width, height) = self.size; - let rect = Rect::new(0.0, 0.0, width, height); + let rect = self.size.to_rect(); piet.fill(rect, &BG_COLOR); piet.stroke(Line::new((10.0, 50.0), (90.0, 90.0)), &FG_COLOR, 1.0); false @@ -92,12 +91,8 @@ impl WinHandler for HelloState { println!("timer fired: {:?}", id); } - fn size(&mut self, width: u32, height: u32) { - let dpi = self.handle.get_dpi(); - let dpi_scale = dpi as f64 / 96.0; - let width_f = (width as f64) / dpi_scale; - let height_f = (height as f64) / dpi_scale; - self.size = (width_f, height_f); + fn size(&mut self, size: Size) { + self.size = size; } fn destroy(&mut self) { diff --git a/druid-shell/src/error.rs b/druid-shell/src/error.rs index 04cdb01eed..2c0c03b020 100644 --- a/druid-shell/src/error.rs +++ b/druid-shell/src/error.rs @@ -18,13 +18,19 @@ use std::fmt; use crate::platform::error as platform; -/// Error codes. At the moment, this is little more than HRESULT, but that -/// might change. +/// Shell errors. #[derive(Debug, Clone)] pub enum Error { + /// The Application instance has already been created. ApplicationAlreadyExists, - Other(&'static str), + /// The window has already been destroyed. + WindowDropped, + /// Runtime borrow failure. + BorrowError(BorrowError), + /// Platform specific error. Platform(platform::Error), + /// Other miscellaneous error. + Other(&'static str), } impl fmt::Display for Error { @@ -33,8 +39,10 @@ impl fmt::Display for Error { Error::ApplicationAlreadyExists => { write!(f, "An application instance has already been created.") } + Error::WindowDropped => write!(f, "The window has already been destroyed."), + Error::BorrowError(err) => fmt::Display::fmt(err, f), + Error::Platform(err) => fmt::Display::fmt(err, f), Error::Other(s) => write!(f, "{}", s), - Error::Platform(p) => fmt::Display::fmt(&p, f), } } } @@ -46,3 +54,49 @@ impl From for Error { Error::Platform(src) } } + +/// Runtime borrow failure. +#[derive(Debug, Clone)] +pub struct BorrowError { + location: &'static str, + target: &'static str, + mutable: bool, +} + +impl BorrowError { + pub fn new(location: &'static str, target: &'static str, mutable: bool) -> BorrowError { + BorrowError { + location, + target, + mutable, + } + } +} + +impl fmt::Display for BorrowError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + if self.mutable { + // Mutable borrow fails when any borrow exists + write!( + f, + "{} was already borrowed in {}", + self.target, self.location + ) + } else { + // Regular borrow fails when a mutable borrow exists + write!( + f, + "{} was already mutably borrowed in {}", + self.target, self.location + ) + } + } +} + +impl std::error::Error for BorrowError {} + +impl From for Error { + fn from(src: BorrowError) -> Error { + Error::BorrowError(src) + } +} diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index 2c03750e58..e205f69304 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -35,6 +35,7 @@ mod keycodes; mod menu; mod mouse; mod platform; +mod scale; mod util; mod window; @@ -48,6 +49,7 @@ pub use keyboard::{KeyEvent, KeyModifiers}; pub use keycodes::KeyCode; pub use menu::Menu; pub use mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +pub use scale::{Scalable, Scale, ScaledArea}; pub use window::{ IdleHandle, IdleToken, Text, TimerToken, WinHandler, WindowBuilder, WindowHandle, }; diff --git a/druid-shell/src/mouse.rs b/druid-shell/src/mouse.rs index 44c1c00557..9727e8fd3c 100644 --- a/druid-shell/src/mouse.rs +++ b/druid-shell/src/mouse.rs @@ -24,9 +24,9 @@ use crate::keyboard::KeyModifiers; /// receiving a move event before another mouse event. #[derive(Debug, Clone, PartialEq)] pub struct MouseEvent { - /// The location of the mouse in the current window. + /// The location of the mouse in [display points] in relation to the current window. /// - /// This is in px units not device pixels, that is, adjusted for hi-dpi. + /// [display points]: struct.Scale.html pub pos: Point, /// Mouse buttons being held down during a move or after a click event. /// Thus it will contain the `button` that triggered a mouse-down event, diff --git a/druid-shell/src/platform/gtk/window.rs b/druid-shell/src/platform/gtk/window.rs index 91d63df8c2..95902a2837 100644 --- a/druid-shell/src/platform/gtk/window.rs +++ b/druid-shell/src/platform/gtk/window.rs @@ -35,10 +35,11 @@ use crate::piet::{Piet, RenderContext}; use crate::common_util::IdleCallback; use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; +use crate::error::Error as ShellError; use crate::keyboard; use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::scale::{Scale, ScaledArea}; use crate::window::{IdleToken, Text, TimerToken, WinHandler}; -use crate::Error; use super::application::Application; use super::dialog; @@ -106,6 +107,8 @@ enum IdleKind { pub(crate) struct WindowState { window: ApplicationWindow, + scale: Cell, + area: Cell, drawing_area: DrawingArea, pub(crate) handler: RefCell>, idle_queue: Arc>>, @@ -154,7 +157,7 @@ impl WindowBuilder { self.menu = Some(menu); } - pub fn build(self) -> Result { + pub fn build(self) -> Result { let handler = self .handler .expect("Tried to build a window without setting the handler"); @@ -165,16 +168,16 @@ impl WindowBuilder { window.set_resizable(self.resizable); window.set_decorated(self.show_titlebar); - let dpi_scale = window + // Get the GTK reported DPI + let dpi = window .get_display() .map(|c| c.get_default_screen().get_resolution() as f64) - .unwrap_or(96.0) - / 96.0; + .unwrap_or(96.0); + let scale = Scale::from_dpi(dpi, dpi); + let area = ScaledArea::from_dp(self.size, &scale); + let size_px = area.size_px(); - window.set_default_size( - (self.size.width * dpi_scale) as i32, - (self.size.height * dpi_scale) as i32, - ); + window.set_default_size(size_px.width as i32, size_px.height as i32); let accel_group = AccelGroup::new(); window.add_accel_group(&accel_group); @@ -185,6 +188,8 @@ impl WindowBuilder { let win_state = Arc::new(WindowState { window, + scale: Cell::new(scale), + area: Cell::new(area), drawing_area, handler: RefCell::new(handler), idle_queue: Arc::new(Mutex::new(vec![])), @@ -233,40 +238,58 @@ impl WindowBuilder { Inhibit(true) }); - if let Some(min_size) = self.min_size { - win_state.drawing_area.set_size_request( - (min_size.width * dpi_scale) as i32, - (min_size.height * dpi_scale) as i32, - ); + // Set the minimum size + if let Some(min_size_dp) = self.min_size { + let min_area = ScaledArea::from_dp(min_size_dp, &scale); + let min_size_px = min_area.size_px(); + win_state + .drawing_area + .set_size_request(min_size_px.width as i32, min_size_px.height as i32); } - let last_size = Cell::new((0, 0)); - win_state.drawing_area.connect_draw(clone!(handle => move |widget, context| { if let Some(state) = handle.state.upgrade() { + let mut scale = state.scale.get(); + let mut scale_changed = false; + // Check if the GTK reported DPI has changed, + // so that we can change our scale factor without restarting the application. + if let Some(dpi) = state.window.get_window() + .map(|w| w.get_display().get_default_screen().get_resolution()) { + let reported_scale = Scale::from_dpi(dpi, dpi); + if scale != reported_scale { + scale = reported_scale; + state.scale.set(scale); + scale_changed = true; + if let Ok(mut handler_borrow) = state.handler.try_borrow_mut() { + handler_borrow.scale(scale); + } else { + log::warn!("Failed to inform the handler of scale change because it was already borrowed"); + } + } + } + // Check if the size of the window has changed let extents = widget.get_allocation(); - let dpi_scale = state.window.get_window() - .map(|w| w.get_display().get_default_screen().get_resolution()) - .unwrap_or(96.0) / 96.0; - let size = ((extents.width as f64 * dpi_scale) as u32, (extents.height as f64 * dpi_scale) as u32); - - if last_size.get() != size { + let size_px = Size::new(extents.width as f64, extents.height as f64); + if scale_changed || state.area.get().size_px() != size_px { + let area = ScaledArea::from_px(size_px, &scale); + let size_dp = area.size_dp(); + state.area.set(area); if let Ok(mut handler_borrow) = state.handler.try_borrow_mut() { - last_size.set(size); - handler_borrow.size(size.0, size.1); + handler_borrow.size(size_dp); } else { - log::warn!("Resizing was skipped because the handler was already borrowed"); + log::warn!("Failed to inform the handler of a resize because it was already borrowed"); } } - // For some reason piet needs a mutable context, so give it one I guess. - let mut context = context.clone(); - let (x0, y0, x1, y1) = context.clip_extents(); - let mut piet_context = Piet::new(&mut context); - if let Ok(mut handler_borrow) = state.handler.try_borrow_mut() { - let invalid_rect = Rect::new(x0 * dpi_scale, y0 * dpi_scale, x1 * dpi_scale, y1 * dpi_scale); + // For some reason piet needs a mutable context, so give it one I guess. + let mut context = context.clone(); + context.scale(scale.scale_x(), scale.scale_y()); + let (x0, y0, x1, y1) = context.clip_extents(); + let invalid_rect = Rect::new(x0, y0, x1, y1); + + let mut piet_context = Piet::new(&mut context); let anim = handler_borrow .paint(&mut piet_context, invalid_rect); if let Err(e) = piet_context.finish() { @@ -279,7 +302,6 @@ impl WindowBuilder { } else { log::warn!("Drawing was skipped because the handler was already borrowed"); } - } Inhibit(false) @@ -289,10 +311,11 @@ impl WindowBuilder { if let Some(state) = handle.state.upgrade() { if let Ok(mut handler) = state.handler.try_borrow_mut() { if let Some(button) = get_mouse_button(event.get_button()) { + let scale = state.scale.get(); let button_state = event.get_state(); handler.mouse_down( &MouseEvent { - pos: Point::from(event.get_position()), + pos: scale.to_dp(&Point::from(event.get_position())), buttons: get_mouse_buttons_from_modifiers(button_state).with(button), mods: get_modifiers(button_state), count: get_mouse_click_count(event.get_event_type()), @@ -303,7 +326,7 @@ impl WindowBuilder { ); } } else { - log::info!("GTK event was dropped because the handler was already borrowed"); + log::warn!("GTK event was dropped because the handler was already borrowed"); } } @@ -314,10 +337,11 @@ impl WindowBuilder { if let Some(state) = handle.state.upgrade() { if let Ok(mut handler) = state.handler.try_borrow_mut() { if let Some(button) = get_mouse_button(event.get_button()) { + let scale = state.scale.get(); let button_state = event.get_state(); handler.mouse_up( &MouseEvent { - pos: Point::from(event.get_position()), + pos: scale.to_dp(&Point::from(event.get_position())), buttons: get_mouse_buttons_from_modifiers(button_state).without(button), mods: get_modifiers(button_state), count: 0, @@ -328,7 +352,7 @@ impl WindowBuilder { ); } } else { - log::info!("GTK event was dropped because the handler was already borrowed"); + log::warn!("GTK event was dropped because the handler was already borrowed"); } } @@ -337,9 +361,10 @@ impl WindowBuilder { win_state.drawing_area.connect_motion_notify_event(clone!(handle => move |_widget, motion| { if let Some(state) = handle.state.upgrade() { + let scale = state.scale.get(); let motion_state = motion.get_state(); let mouse_event = MouseEvent { - pos: Point::from(motion.get_position()), + pos: scale.to_dp(&Point::from(motion.get_position())), buttons: get_mouse_buttons_from_modifiers(motion_state), mods: get_modifiers(motion_state), count: 0, @@ -351,7 +376,7 @@ impl WindowBuilder { if let Ok(mut handler) = state.handler.try_borrow_mut() { handler.mouse_move(&mouse_event); } else { - log::info!("GTK event was dropped because the handler was already borrowed"); + log::warn!("GTK event was dropped because the handler was already borrowed"); } } @@ -360,9 +385,10 @@ impl WindowBuilder { win_state.drawing_area.connect_leave_notify_event(clone!(handle => move |_widget, crossing| { if let Some(state) = handle.state.upgrade() { + let scale = state.scale.get(); let crossing_state = crossing.get_state(); let mouse_event = MouseEvent { - pos: Point::from(crossing.get_position()), + pos: scale.to_dp(&Point::from(crossing.get_position())), buttons: get_mouse_buttons_from_modifiers(crossing_state), mods: get_modifiers(crossing_state), count: 0, @@ -374,7 +400,7 @@ impl WindowBuilder { if let Ok(mut handler) = state.handler.try_borrow_mut() { handler.mouse_move(&mouse_event); } else { - log::info!("GTK event was dropped because the handler was already borrowed"); + log::warn!("GTK event was dropped because the handler was already borrowed"); } } @@ -383,54 +409,54 @@ impl WindowBuilder { win_state.drawing_area.connect_scroll_event(clone!(handle => move |_widget, scroll| { if let Some(state) = handle.state.upgrade() { - if let Ok(mut handler) = state.handler.try_borrow_mut() { - - let mods = get_modifiers(scroll.get_state()); - - // The magic "120"s are from Microsoft's documentation for WM_MOUSEWHEEL. - // They claim that one "tick" on a scroll wheel should be 120 units. - let wheel_delta = match scroll.get_direction() { - ScrollDirection::Up if mods.shift => Some(Vec2::new(-120.0, 0.0)), - ScrollDirection::Up => Some(Vec2::new(0.0, -120.0)), - ScrollDirection::Down if mods.shift => Some(Vec2::new(120.0, 0.0)), - ScrollDirection::Down => Some(Vec2::new(0.0, 120.0)), - ScrollDirection::Left => Some(Vec2::new(-120.0, 0.0)), - ScrollDirection::Right => Some(Vec2::new(120.0, 0.0)), - ScrollDirection::Smooth => { - //TODO: Look at how gtk's scroll containers implements it - let (mut delta_x, mut delta_y) = scroll.get_delta(); - delta_x *= 120.; - delta_y *= 120.; - if mods.shift { - delta_x += delta_y; - delta_y = 0.; - } - Some(Vec2::new(delta_x, delta_y)) + let scale = state.scale.get(); + let mods = get_modifiers(scroll.get_state()); + + // The magic "120"s are from Microsoft's documentation for WM_MOUSEWHEEL. + // They claim that one "tick" on a scroll wheel should be 120 units. + let wheel_delta = match scroll.get_direction() { + ScrollDirection::Up if mods.shift => Some(Vec2::new(-120.0, 0.0)), + ScrollDirection::Up => Some(Vec2::new(0.0, -120.0)), + ScrollDirection::Down if mods.shift => Some(Vec2::new(120.0, 0.0)), + ScrollDirection::Down => Some(Vec2::new(0.0, 120.0)), + ScrollDirection::Left => Some(Vec2::new(-120.0, 0.0)), + ScrollDirection::Right => Some(Vec2::new(120.0, 0.0)), + ScrollDirection::Smooth => { + //TODO: Look at how gtk's scroll containers implements it + let (mut delta_x, mut delta_y) = scroll.get_delta(); + delta_x *= 120.; + delta_y *= 120.; + if mods.shift { + delta_x += delta_y; + delta_y = 0.; } - e => { - eprintln!( - "Warning: the Druid widget got some whacky scroll direction {:?}", - e - ); - None - } - }; + Some(Vec2::new(delta_x, delta_y)) + } + e => { + eprintln!( + "Warning: the Druid widget got some whacky scroll direction {:?}", + e + ); + None + } + }; - if let Some(wheel_delta) = wheel_delta { - let mouse_event = MouseEvent { - pos: Point::from(scroll.get_position()), - buttons: get_mouse_buttons_from_modifiers(scroll.get_state()), - mods, - count: 0, - focus: false, - button: MouseButton::None, - wheel_delta - }; + if let Some(wheel_delta) = wheel_delta { + let mouse_event = MouseEvent { + pos: scale.to_dp(&Point::from(scroll.get_position())), + buttons: get_mouse_buttons_from_modifiers(scroll.get_state()), + mods, + count: 0, + focus: false, + button: MouseButton::None, + wheel_delta + }; + if let Ok(mut handler) = state.handler.try_borrow_mut() { handler.wheel(&mouse_event); + } else { + log::info!("GTK event was dropped because the handler was already borrowed"); } - } else { - log::info!("GTK event was dropped because the handler was already borrowed"); } } @@ -486,10 +512,10 @@ impl WindowBuilder { .expect("realize didn't create window") .set_event_compression(false); - win_state - .handler - .borrow_mut() - .connect(&handle.clone().into()); + let mut handler = win_state.handler.borrow_mut(); + handler.connect(&handle.clone().into()); + handler.scale(scale); + handler.size(self.size); Ok(handle) } @@ -536,16 +562,9 @@ impl WindowHandle { /// Request invalidation of one rectangle, which is given relative to the drawing area. pub fn invalidate_rect(&self, rect: Rect) { - let dpi_scale = self.get_dpi() as f64 / 96.0; - let rect = Rect::from_origin_size( - (rect.x0 * dpi_scale, rect.y0 * dpi_scale), - rect.size() * dpi_scale, - ); - - // GTK+ takes rects with integer coordinates, and non-negative width/height. - let r = rect.abs().expand(); - if let Some(state) = self.state.upgrade() { + // GTK takes rects with non-negative integer width/height. + let r = state.scale.get().to_px(&rect.abs()).expand(); let origin = state.drawing_area.get_allocation(); state.window.queue_draw_area( r.x0 as i32 + origin.x, @@ -615,42 +634,14 @@ impl WindowHandle { }) } - /// Get the dpi of the window. - /// - /// TODO: we want to migrate this from dpi (with 96 as nominal) to a scale - /// factor (with 1 as nominal). - pub fn get_dpi(&self) -> f32 { - self.state + /// Get the `Scale` of the window. + pub fn get_scale(&self) -> Result { + Ok(self + .state .upgrade() - .and_then(|s| s.window.get_window()) - .map(|w| w.get_display().get_default_screen().get_resolution() as f32) - .unwrap_or(96.0) - } - - // TODO: the following methods are cut'n'paste code. A good way to DRY - // would be to have a platform-independent trait with these as methods with - // default implementations. - - /// Convert a dimension in px units to physical pixels (rounding). - pub fn px_to_pixels(&self, x: f32) -> i32 { - (x * self.get_dpi() * (1.0 / 96.0)).round() as i32 - } - - /// Convert a point in px units to physical pixels (rounding). - pub fn px_to_pixels_xy(&self, x: f32, y: f32) -> (i32, i32) { - let scale = self.get_dpi() * (1.0 / 96.0); - ((x * scale).round() as i32, (y * scale).round() as i32) - } - - /// Convert a dimension in physical pixels to px units. - pub fn pixels_to_px>(&self, x: T) -> f32 { - (x.into() as f32) * 96.0 / self.get_dpi() - } - - /// Convert a point in physical pixels to px units. - pub fn pixels_to_px_xy>(&self, x: T, y: T) -> (f32, f32) { - let scale = 96.0 / self.get_dpi(); - ((x.into() as f32) * scale, (y.into() as f32) * scale) + .ok_or(ShellError::WindowDropped)? + .scale + .get()) } pub fn set_menu(&self, menu: Menu) { @@ -699,11 +690,11 @@ impl WindowHandle { &self, ty: FileDialogType, options: FileDialogOptions, - ) -> Result { + ) -> Result { if let Some(state) = self.state.upgrade() { dialog::get_file_dialog_path(state.window.upcast_ref(), ty, options) } else { - Err(Error::Other( + Err(ShellError::Other( "Cannot upgrade state from weak pointer to arc", )) } diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 7bc774a55e..3a6e8f00c6 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -52,6 +52,7 @@ use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; use crate::keyboard::{KeyEvent, KeyModifiers}; use crate::keycodes::KeyCode; use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::scale::Scale; use crate::window::{IdleToken, Text, TimerToken, WinHandler}; use crate::Error; @@ -202,9 +203,10 @@ impl WindowBuilder { idle_queue, }; (*view_state).handler.connect(&handle.clone().into()); + (*view_state).handler.scale(Scale::default()); (*view_state) .handler - .size(frame.size.width as u32, frame.size.height as u32); + .size(Size::new(frame.size.width, frame.size.height)); Ok(handle) } @@ -370,7 +372,7 @@ extern "C" fn set_frame_size(this: &mut Object, _: Sel, size: NSSize) { let view_state = &mut *(view_state as *mut ViewState); (*view_state) .handler - .size(size.width as u32, size.height as u32); + .size(Size::new(size.width, size.height)); let superclass = msg_send![this, superclass]; let () = msg_send![super(this, superclass), setFrameSize: size]; } @@ -825,13 +827,10 @@ impl WindowHandle { } } - /// Get the dpi of the window. - /// - /// TODO: we want to migrate this from dpi (with 96 as nominal) to a scale - /// factor (with 1 as nominal). - pub fn get_dpi(&self) -> f32 { - // TODO: get actual dpi - 96.0 + /// Get the `Scale` of the window. + pub fn get_scale(&self) -> Result { + // TODO: Get actual Scale + Ok(Scale::from_dpi(96.0, 96.0)) } } diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index c6613e794b..6b02375695 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -35,6 +35,8 @@ use super::keycodes::key_to_text; use super::menu::Menu; use crate::common_util::IdleCallback; use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; +use crate::error::Error as ShellError; +use crate::scale::{Scale, ScaledArea}; use crate::keyboard; use crate::keycodes::KeyCode; @@ -55,10 +57,6 @@ macro_rules! get_modifiers { }; } -type Result = std::result::Result; - -const NOMINAL_DPI: f32 = 96.0; - /// Builder abstraction for creating new windows. pub(crate) struct WindowBuilder { handler: Option>, @@ -84,7 +82,8 @@ enum IdleKind { } struct WindowState { - dpr: Cell, + scale: Cell, + area: Cell, idle_queue: Arc>>, handler: RefCell>, window: web_sys::Window, @@ -120,15 +119,7 @@ impl WindowState { } } - fn get_width(&self) -> u32 { - self.canvas.offset_width() as u32 - } - - fn get_height(&self) -> u32 { - self.canvas.offset_height() as u32 - } - - fn request_animation_frame(&self, f: impl FnOnce() + 'static) -> Result { + fn request_animation_frame(&self, f: impl FnOnce() + 'static) -> Result { Ok(self .window .request_animation_frame(Closure::once_into_js(f).as_ref().unchecked_ref())?) @@ -142,6 +133,20 @@ impl WindowState { let dpr = w.device_pixel_ratio(); (width, height, dpr) } + + /// Updates the canvas size and scale factor and returns `Scale` and `ScaledArea`. + fn update_scale_and_area(&self) -> (Scale, ScaledArea) { + let (css_width, css_height, dpr) = self.get_window_size_and_dpr(); + let scale = Scale::from_scale(dpr, dpr); + let area = ScaledArea::from_dp(Size::new(css_width, css_height), &scale); + let size_px = area.size_px(); + self.canvas.set_width(size_px.width as u32); + self.canvas.set_height(size_px.height as u32); + let _ = self.context.scale(scale.scale_x(), scale.scale_y()); + self.scale.set(scale); + self.area.set(area); + (scale, area) + } } fn setup_mouse_down_callback(ws: &Rc) { @@ -206,14 +211,15 @@ fn setup_scroll_callback(ws: &Rc) { let dx = event.delta_x(); let dy = event.delta_y(); - let height = state.canvas.height() as f64; - let width = state.canvas.width() as f64; // The value 35.0 was manually picked to produce similar behavior to mac/linux. let wheel_delta = match delta_mode { web_sys::WheelEvent::DOM_DELTA_PIXEL => Vec2::new(dx, dy), web_sys::WheelEvent::DOM_DELTA_LINE => Vec2::new(35.0 * dx, 35.0 * dy), - web_sys::WheelEvent::DOM_DELTA_PAGE => Vec2::new(width * dx, height * dy), + web_sys::WheelEvent::DOM_DELTA_PAGE => { + let size_dp = state.area.get().size_dp(); + Vec2::new(size_dp.width * dx, size_dp.height * dy) + } _ => { log::warn!("Invalid deltaMode in WheelEvent: {}", delta_mode); return; @@ -236,17 +242,10 @@ fn setup_scroll_callback(ws: &Rc) { fn setup_resize_callback(ws: &Rc) { let state = ws.clone(); register_window_event_listener(ws, "resize", move |_: web_sys::UiEvent| { - let (css_width, css_height, dpr) = state.get_window_size_and_dpr(); - let physical_width = (dpr * css_width) as u32; - let physical_height = (dpr * css_height) as u32; - state.dpr.replace(dpr); - state.canvas.set_width(physical_width); - state.canvas.set_height(physical_height); - let _ = state.context.scale(dpr, dpr); - state - .handler - .borrow_mut() - .size(physical_width, physical_height); + let (scale, area) = state.update_scale_and_area(); + // TODO: For performance, only call the handler when these values actually changed. + state.handler.borrow_mut().scale(scale); + state.handler.borrow_mut().size(area.size_dp()); }); } @@ -357,7 +356,7 @@ impl WindowBuilder { self.menu = Some(menu); } - pub fn build(self) -> Result { + pub fn build(self) -> Result { let window = web_sys::window().ok_or_else(|| Error::NoWindow)?; let canvas = window .document() @@ -371,23 +370,29 @@ impl WindowBuilder { .ok_or(Error::NoContext)? .dyn_into::() .map_err(|_| Error::JsCast)?; - - let dpr = window.device_pixel_ratio(); - let old_w = canvas.offset_width(); - let old_h = canvas.offset_height(); - let new_w = (old_w as f64 * dpr) as u32; - let new_h = (old_h as f64 * dpr) as u32; - - canvas.set_width(new_w as u32); - canvas.set_height(new_h as u32); - let _ = context.scale(dpr, dpr); + // Create the Scale for resolution scaling + let scale = { + let dpr = window.device_pixel_ratio(); + Scale::from_scale(dpr, dpr) + }; + let area = { + // The initial size in display points isn't necessarily the final size in display points + let size_dp = Size::new(canvas.offset_width() as f64, canvas.offset_height() as f64); + ScaledArea::from_dp(size_dp, &scale) + }; + let size_px = area.size_px(); + canvas.set_width(size_px.width as u32); + canvas.set_height(size_px.height as u32); + let _ = context.scale(scale.scale_x(), scale.scale_y()); + let size_dp = area.size_dp(); set_cursor(&canvas, &self.cursor); let handler = self.handler.unwrap(); let window = Rc::new(WindowState { - dpr: Cell::new(dpr), + scale: Cell::new(scale), + area: Cell::new(area), idle_queue: Default::default(), handler: RefCell::new(handler), window, @@ -398,11 +403,12 @@ impl WindowBuilder { setup_web_callbacks(&window); - // Register the size with the window handler. + // Register the scale & size with the window handler. let wh = window.clone(); window .request_animation_frame(move || { - wh.handler.borrow_mut().size(new_w, new_h); + wh.handler.borrow_mut().scale(scale); + wh.handler.borrow_mut().size(size_dp); }) .expect("Failed to request animation frame"); @@ -449,12 +455,7 @@ impl WindowHandle { pub fn invalidate(&self) { if let Some(s) = self.0.upgrade() { - let rect = Rect::from_origin_size( - Point::ORIGIN, - // FIXME: does this need scaling? Not sure exactly where dpr enters... - (s.get_width() as f64, s.get_height() as f64), - ); - s.invalid_rect.set(rect); + s.invalid_rect.set(s.area.get().size_dp().to_rect()); } self.render_soon(); } @@ -539,8 +540,8 @@ impl WindowHandle { &self, _ty: FileDialogType, _options: FileDialogOptions, - ) -> std::result::Result { - Err(crate::Error::Platform(Error::Unimplemented)) + ) -> Result { + Err(ShellError::Platform(Error::Unimplemented)) } /// Get a handle that can be used to schedule an idle task. @@ -551,34 +552,14 @@ impl WindowHandle { }) } - /// Get the dpi of the window. - pub fn get_dpi(&self) -> f32 { - self.0 + /// Get the `Scale` of the window. + pub fn get_scale(&self) -> Result { + Ok(self + .0 .upgrade() - .map(|w| NOMINAL_DPI * w.dpr.get() as f32) - .unwrap_or(NOMINAL_DPI) - } - - /// Convert a dimension in px units to physical pixels (rounding). - pub fn px_to_pixels(&self, x: f32) -> i32 { - (x * self.get_dpi() / NOMINAL_DPI).round() as i32 - } - - /// Convert a point in px units to physical pixels (rounding). - pub fn px_to_pixels_xy(&self, x: f32, y: f32) -> (i32, i32) { - let scale = self.get_dpi() / NOMINAL_DPI; - ((x * scale).round() as i32, (y * scale).round() as i32) - } - - /// Convert a dimension in physical pixels to px units. - pub fn pixels_to_px>(&self, x: T) -> f32 { - (x.into() as f32) * NOMINAL_DPI / self.get_dpi() - } - - /// Convert a point in physical pixels to px units. - pub fn pixels_to_px_xy>(&self, x: T, y: T) -> (f32, f32) { - let scale = NOMINAL_DPI / self.get_dpi(); - ((x.into() as f32) * scale, (y.into() as f32) * scale) + .ok_or(ShellError::WindowDropped)? + .scale + .get()) } pub fn set_menu(&self, _menu: Menu) { diff --git a/druid-shell/src/platform/windows/error.rs b/druid-shell/src/platform/windows/error.rs index 7668a00752..0b400bb5af 100644 --- a/druid-shell/src/platform/windows/error.rs +++ b/druid-shell/src/platform/windows/error.rs @@ -27,10 +27,10 @@ use winapi::um::winbase::{ use super::util::FromWide; -/// Error codes. At the moment, this is little more than HRESULT, but that -/// might change. +/// Windows platform error. #[derive(Debug, Clone)] pub enum Error { + /// Windows error code. Hr(HRESULT), // Maybe include the full error from the direct2d crate. D2Error, @@ -67,10 +67,10 @@ fn hresult_description(hr: HRESULT) -> Option { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - match *self { + match self { Error::Hr(hr) => { write!(f, "HRESULT 0x{:x}", hr)?; - if let Some(description) = hresult_description(hr) { + if let Some(description) = hresult_description(*hr) { write!(f, ": {}", description)?; } Ok(()) diff --git a/druid-shell/src/platform/windows/paint.rs b/druid-shell/src/platform/windows/paint.rs index fdfb2b969f..b8a3bad171 100644 --- a/druid-shell/src/platform/windows/paint.rs +++ b/druid-shell/src/platform/windows/paint.rs @@ -22,8 +22,6 @@ use std::mem; use std::ptr::null_mut; -use log::{error, warn}; - use winapi::ctypes::c_void; use winapi::shared::dxgi::*; use winapi::shared::dxgi1_2::*; @@ -38,6 +36,7 @@ use winapi::Interface; use piet_common::d2d::D2DFactory; use crate::platform::windows::{DeviceContext, DxgiSurfaceRenderTarget, HwndRenderTarget}; +use crate::scale::Scale; use super::error::Error; use super::util::as_result; @@ -48,7 +47,7 @@ pub(crate) unsafe fn create_render_target( ) -> Result { let mut rect: RECT = mem::zeroed(); if GetClientRect(hwnd, &mut rect) == 0 { - warn!("GetClientRect failed."); + log::warn!("GetClientRect failed."); Err(Error::D2Error) } else { let width = (rect.right - rect.left) as u32; @@ -56,7 +55,7 @@ pub(crate) unsafe fn create_render_target( let res = HwndRenderTarget::create(d2d_factory, hwnd, width, height); if let Err(ref e) = res { - error!("Creating hwnd render target failed: {:?}", e); + log::error!("Creating hwnd render target failed: {:?}", e); } res.map(|hrt| cast_to_device_context(&hrt).expect("removethis")) .map_err(|_| Error::D2Error) @@ -69,7 +68,7 @@ pub(crate) unsafe fn create_render_target( pub(crate) unsafe fn create_render_target_dxgi( d2d_factory: &D2DFactory, swap_chain: *mut IDXGISwapChain1, - dpi: f32, + scale: &Scale, ) -> Result { let mut buffer: *mut IDXGISurface = null_mut(); as_result((*swap_chain).GetBuffer( @@ -83,8 +82,8 @@ pub(crate) unsafe fn create_render_target_dxgi( format: DXGI_FORMAT_B8G8R8A8_UNORM, alphaMode: D2D1_ALPHA_MODE_IGNORE, }, - dpiX: dpi, - dpiY: dpi, + dpiX: scale.dpi_x() as f32, + dpiY: scale.dpi_y() as f32, usage: D2D1_RENDER_TARGET_USAGE_NONE, minLevel: D2D1_FEATURE_LEVEL_DEFAULT, }; diff --git a/druid-shell/src/platform/windows/util.rs b/druid-shell/src/platform/windows/util.rs index b231a8d390..8ae673a32c 100644 --- a/druid-shell/src/platform/windows/util.rs +++ b/druid-shell/src/platform/windows/util.rs @@ -28,7 +28,7 @@ use winapi::ctypes::c_void; use winapi::shared::guiddef::REFIID; use winapi::shared::minwindef::{HMODULE, UINT}; use winapi::shared::ntdef::{HRESULT, LPWSTR}; -use winapi::shared::windef::HMONITOR; +use winapi::shared::windef::{HMONITOR, RECT}; use winapi::shared::winerror::SUCCEEDED; use winapi::um::fileapi::{CreateFileA, GetFileType, OPEN_EXISTING}; use winapi::um::handleapi::INVALID_HANDLE_VALUE; @@ -40,6 +40,8 @@ use winapi::um::winbase::{FILE_TYPE_UNKNOWN, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS}; use winapi::um::winnt::{FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}; +use crate::kurbo::Rect; + use super::error::Error; pub fn as_result(hr: HRESULT) -> Result<(), Error> { @@ -103,6 +105,28 @@ impl FromWide for [u16] { } } +/// Converts a `Rect` to a winapi `RECT`. +#[inline] +pub(crate) fn rect_to_recti(rect: Rect) -> RECT { + RECT { + left: rect.x0 as i32, + top: rect.y0 as i32, + right: rect.x1 as i32, + bottom: rect.y1 as i32, + } +} + +/// Converts a winapi `RECT` to a `Rect`. +#[inline] +pub(crate) fn recti_to_rect(rect: RECT) -> Rect { + Rect::new( + rect.left as f64, + rect.top as f64, + rect.right as f64, + rect.bottom as f64, + ) +} + // Types for functions we want to load, which are only supported on newer windows versions // from shcore.dll type GetDpiForSystem = unsafe extern "system" fn() -> UINT; diff --git a/druid-shell/src/platform/windows/window.rs b/druid-shell/src/platform/windows/window.rs index a100b2d7b5..23854ef19c 100644 --- a/druid-shell/src/platform/windows/window.rs +++ b/druid-shell/src/platform/windows/window.rs @@ -54,13 +54,15 @@ use super::error::Error; use super::menu::Menu; use super::paint; use super::timers::TimerSlots; -use super::util::{as_result, FromWide, ToWide, OPTIONAL_FUNCTIONS}; +use super::util::{self, as_result, FromWide, ToWide, OPTIONAL_FUNCTIONS}; use crate::common_util::IdleCallback; use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; +use crate::error::Error as ShellError; use crate::keyboard::{KeyEvent, KeyModifiers}; use crate::keycodes::KeyCode; use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::scale::{Scale, ScaledArea}; use crate::window::{IdleToken, Text, TimerToken, WinHandler}; extern "system" { @@ -134,7 +136,8 @@ enum IdleKind { /// by interior mutability, so we can handle reentrant calls. struct WindowState { hwnd: Cell, - dpi: Cell, + scale: Cell, + area: Cell, wndproc: Box, idle_queue: Arc>>, timers: Arc>, @@ -166,7 +169,6 @@ struct WndState { handler: Box, render_target: Option, dcomp_state: Option, - dpi: f32, min_size: Option, /// The `KeyCode` of the last `WM_KEYDOWN` event. We stash this so we can /// include it when handling `WM_CHAR` events. @@ -286,10 +288,10 @@ fn is_point_in_client_rect(hwnd: HWND, x: i32, y: i32) -> bool { } impl WndState { - fn rebuild_render_target(&mut self, d2d: &D2DFactory) { + fn rebuild_render_target(&mut self, d2d: &D2DFactory, scale: &Scale) { unsafe { let swap_chain = self.dcomp_state.as_ref().unwrap().swap_chain; - let rt = paint::create_render_target_dxgi(d2d, swap_chain, self.dpi) + let rt = paint::create_render_target_dxgi(d2d, swap_chain, scale) .map(|rt| rt.as_device_context().expect("TODO remove this expect")); self.render_target = rt.ok(); } @@ -355,12 +357,54 @@ impl MyWndProc { msg, hwnd, wparam, lparam ); } + + fn scale(&self) -> Scale { + self.handle + // Right now there aren't any mutable borrows to this. + // TODO: Attempt to guarantee this by making mutable handle borrows useless. + .borrow() + .state + .upgrade() + .unwrap() // WindowState drops after WM_NCDESTROY, so it's always here. + .scale + .get() + } + + fn area(&self) -> ScaledArea { + self.handle + // Right now there aren't any mutable borrows to this. + // TODO: Attempt to guarantee this by making mutable handle borrows useless. + .borrow() + .state + .upgrade() + .unwrap() // WindowState drops after WM_NCDESTROY, so it's always here. + .area + .get() + } + + fn set_area(&self, area: ScaledArea) { + self.handle + // Right now there aren't any mutable borrows to this. + // TODO: Attempt to guarantee this by making mutable handle borrows useless. + .borrow() + .state + .upgrade() + .unwrap() // WindowState drops after WM_NCDESTROY, so it's always here. + .area + .set(area) + } } impl WndProc for MyWndProc { fn connect(&self, handle: &WindowHandle, state: WndState) { *self.handle.borrow_mut() = handle.clone(); *self.state.borrow_mut() = Some(state); + self.state + .borrow_mut() + .as_mut() + .unwrap() + .handler + .scale(self.scale()); } fn cleanup(&self, hwnd: HWND) { @@ -416,11 +460,12 @@ impl WndProc for MyWndProc { s.render_target = rt.ok(); } s.handler.rebuild_resources(); + let rect_dp = self.scale().to_dp(&util::recti_to_rect(rect)); s.render( &self.d2d_factory, &self.dwrite_factory, &self.handle, - self.handle.borrow().rect_to_px(rect), + rect_dp, ); if let Some(ref mut ds) = s.dcomp_state { let params = DXGI_PRESENT_PARAMETERS { @@ -444,23 +489,16 @@ impl WndProc for MyWndProc { if let Ok(mut s) = self.state.try_borrow_mut() { let s = s.as_mut().unwrap(); if s.dcomp_state.is_some() { - let mut rect: RECT = mem::zeroed(); - if GetClientRect(hwnd, &mut rect) == FALSE { - log::warn!( - "GetClientRect failed: {}", - Error::Hr(HRESULT_FROM_WIN32(GetLastError())) - ); - return None; - } let rt = paint::create_render_target(&self.d2d_factory, hwnd); s.render_target = rt.ok(); { + let rect_dp = self.area().size_dp().to_rect(); s.handler.rebuild_resources(); s.render( &self.d2d_factory, &self.dwrite_factory, &self.handle, - self.handle.borrow().rect_to_px(rect), + rect_dp, ); } @@ -479,31 +517,23 @@ impl WndProc for MyWndProc { if let Ok(mut s) = self.state.try_borrow_mut() { let s = s.as_mut().unwrap(); if s.dcomp_state.is_some() { - let mut rect: RECT = mem::zeroed(); - if GetClientRect(hwnd, &mut rect) == FALSE { - log::warn!( - "GetClientRect failed: {}", - Error::Hr(HRESULT_FROM_WIN32(GetLastError())) - ); - return None; - } - let width = (rect.right - rect.left) as u32; - let height = (rect.bottom - rect.top) as u32; + let area = self.area(); + let size_px = area.size_px(); let res = (*s.dcomp_state.as_mut().unwrap().swap_chain).ResizeBuffers( 2, - width, - height, + size_px.width as u32, + size_px.height as u32, DXGI_FORMAT_UNKNOWN, 0, ); if SUCCEEDED(res) { s.handler.rebuild_resources(); - s.rebuild_render_target(&self.d2d_factory); + s.rebuild_render_target(&self.d2d_factory, &self.scale()); s.render( &self.d2d_factory, &self.dwrite_factory, &self.handle, - self.handle.borrow().rect_to_px(rect), + area.size_dp().to_rect(), ); (*s.dcomp_state.as_ref().unwrap().swap_chain).Present(0, 0); } else { @@ -530,7 +560,11 @@ impl WndProc for MyWndProc { let s = s.as_mut().unwrap(); let width = LOWORD(lparam as u32) as u32; let height = HIWORD(lparam as u32) as u32; - s.handler.size(width, height); + let scale = self.scale(); + let area = ScaledArea::from_px((width as f64, height as f64), &scale); + let size_dp = area.size_dp(); + self.set_area(area); + s.handler.size(size_dp); let use_hwnd = if let Some(ref dcomp_state) = s.dcomp_state { dcomp_state.sizing } else { @@ -543,7 +577,12 @@ impl WndProc for MyWndProc { let _ = hrt.ptr.Resize(&size); } } - InvalidateRect(hwnd, null_mut(), FALSE); + if InvalidateRect(hwnd, null(), FALSE) == FALSE { + log::warn!( + "InvalidateRect failed: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } } else { let res; { @@ -557,10 +596,13 @@ impl WndProc for MyWndProc { ); } if SUCCEEDED(res) { - let (w, h) = self.handle.borrow().pixels_to_px_xy(width, height); - let rect = Rect::from_origin_size(Point::ORIGIN, (w as f64, h as f64)); - s.rebuild_render_target(&self.d2d_factory); - s.render(&self.d2d_factory, &self.dwrite_factory, &self.handle, rect); + s.rebuild_render_target(&self.d2d_factory, &scale); + s.render( + &self.d2d_factory, + &self.dwrite_factory, + &self.handle, + size_dp.to_rect(), + ); if let Some(ref mut dcomp_state) = s.dcomp_state { (*dcomp_state.swap_chain).Present(0, 0); let _ = dcomp_state.dcomp_device.commit(); @@ -689,8 +731,7 @@ impl WndProc for MyWndProc { } } - let (px, py) = self.handle.borrow().pixels_to_px_xy(p.x, p.y); - let pos = Point::new(px as f64, py as f64); + let pos = self.scale().to_dp(&(p.x as f64, p.y as f64).into()); let buttons = get_buttons(down_state); let event = MouseEvent { pos, @@ -736,8 +777,7 @@ impl WndProc for MyWndProc { } } - let (px, py) = self.handle.borrow().pixels_to_px_xy(x, y); - let pos = Point::new(px as f64, py as f64); + let pos = self.scale().to_dp(&(x as f64, y as f64).into()); let mods = KeyModifiers { shift: wparam & MK_SHIFT != 0, alt: get_mod_state_alt(), @@ -804,8 +844,7 @@ impl WndProc for MyWndProc { }; let x = LOWORD(lparam as u32) as i16 as i32; let y = HIWORD(lparam as u32) as i16 as i32; - let (px, py) = self.handle.borrow().pixels_to_px_xy(x, y); - let pos = Point::new(px as f64, py as f64); + let pos = self.scale().to_dp(&(x as f64, y as f64).into()); let mods = KeyModifiers { shift: wparam & MK_SHIFT != 0, alt: get_mod_state_alt(), @@ -891,11 +930,11 @@ impl WndProc for MyWndProc { let min_max_info = unsafe { &mut *(lparam as *mut MINMAXINFO) }; if let Ok(s) = self.state.try_borrow() { let s = s.as_ref().unwrap(); - if let Some(min_size) = s.min_size { - min_max_info.ptMinTrackSize.x = - (min_size.width * (f64::from(s.dpi) / 96.0)) as i32; - min_max_info.ptMinTrackSize.y = - (min_size.height * (f64::from(s.dpi) / 96.0)) as i32; + if let Some(min_size_dp) = s.min_size { + let min_area = ScaledArea::from_dp(min_size_dp, &self.scale()); + let min_size_px = min_area.size_px(); + min_max_info.ptMinTrackSize.x = min_size_px.width as i32; + min_max_info.ptMinTrackSize.y = min_size_px.height as i32; } } else { self.log_dropped_msg(hwnd, msg, wparam, lparam); @@ -981,9 +1020,23 @@ impl WindowBuilder { present_strategy: self.present_strategy, }; + // Simple scaling based on System DPI (96 is equivalent to 100%) + let dpi = if let Some(func) = OPTIONAL_FUNCTIONS.GetDpiForSystem { + // Only supported on Windows 10 + func() as f64 + } else { + // TODO GetDpiForMonitor is supported on Windows 8.1, try falling back to that here + // Probably GetDeviceCaps(..., LOGPIXELSX) is the best to do pre-10 + 96.0 + }; + let scale = Scale::from_dpi(dpi, dpi); + let area = ScaledArea::from_dp(self.size, &scale); + let size_px = area.size_px(); + let window = WindowState { hwnd: Cell::new(0 as HWND), - dpi: Cell::new(0.0), + scale: Cell::new(scale), + area: Cell::new(area), wndproc: Box::new(wndproc), idle_queue: Default::default(), timers: Arc::new(Mutex::new(TimerSlots::new(1))), @@ -994,22 +1047,10 @@ impl WindowBuilder { state: Rc::downgrade(&win), }; - // Simple scaling based on System Dpi (96 is equivalent to 100%) - let dpi = if let Some(func) = OPTIONAL_FUNCTIONS.GetDpiForSystem { - // Only supported on windows 10 - func() as f32 - } else { - // TODO GetDpiForMonitor is supported on windows 8.1, try falling back to that here - // Probably GetDeviceCaps(..., LOGPIXELSX) is the best to do pre-10 - 96.0 - }; - win.dpi.set(dpi); - let state = WndState { handler: self.handler.unwrap(), render_target: None, dcomp_state: None, - dpi, min_size: self.min_size, stashed_key_code: KeyCode::Unknown(0), stashed_char: None, @@ -1018,9 +1059,6 @@ impl WindowBuilder { }; win.wndproc.connect(&handle, state); - let width = (self.size.width * (f64::from(dpi) / 96.0)) as i32; - let height = (self.size.height * (f64::from(dpi) / 96.0)) as i32; - let (hmenu, accels) = match self.menu { Some(menu) => { let accels = menu.accels(); @@ -1045,8 +1083,8 @@ impl WindowBuilder { dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, - width, - height, + size_px.width as i32, + size_px.height as i32, 0 as HWND, hmenu, 0 as HINSTANCE, @@ -1284,14 +1322,19 @@ impl WindowHandle { if let Some(w) = self.state.upgrade() { let hwnd = w.hwnd.get(); unsafe { - InvalidateRect(hwnd, null(), FALSE); + if InvalidateRect(hwnd, null(), FALSE) == FALSE { + log::warn!( + "InvalidateRect failed: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } } } } pub fn invalidate_rect(&self, rect: Rect) { - let rect = self.px_to_rect(rect); if let Some(w) = self.state.upgrade() { + let rect = util::rect_to_recti(w.scale.get().to_px(&rect).expand()); let hwnd = w.hwnd.get(); unsafe { if InvalidateRect(hwnd, &rect, FALSE) == FALSE { @@ -1371,9 +1414,12 @@ impl WindowHandle { let hmenu = menu.into_hmenu(); if let Some(w) = self.state.upgrade() { let hwnd = w.hwnd.get(); - let (x, y) = self.px_to_pixels_xy(pos.x as f32, pos.y as f32); + let pos = w.scale.get().to_px(&pos).round(); unsafe { - let mut point = POINT { x, y }; + let mut point = POINT { + x: pos.x as i32, + y: pos.y as i32, + }; ClientToScreen(hwnd, &mut point); if TrackPopupMenu(hmenu, TPM_LEFTALIGN, point.x, point.y, 0, hwnd, null()) == FALSE { @@ -1458,52 +1504,14 @@ impl WindowHandle { } } - /// Get the dpi of the window. - pub fn get_dpi(&self) -> f32 { - if let Some(w) = self.state.upgrade() { - w.dpi.get() - } else { - 96.0 - } - } - - /// Convert a dimension in px units to physical pixels (rounding). - pub fn px_to_pixels(&self, x: f32) -> i32 { - (x * self.get_dpi() * (1.0 / 96.0)).round() as i32 - } - - /// Convert a point in px units to physical pixels (rounding). - pub fn px_to_pixels_xy(&self, x: f32, y: f32) -> (i32, i32) { - let scale = self.get_dpi() * (1.0 / 96.0); - ((x * scale).round() as i32, (y * scale).round() as i32) - } - - /// Convert a dimension in physical pixels to px units. - pub fn pixels_to_px>(&self, x: T) -> f32 { - (x.into() as f32) * 96.0 / self.get_dpi() - } - - /// Convert a point in physical pixels to px units. - pub fn pixels_to_px_xy>(&self, x: T, y: T) -> (f32, f32) { - let scale = 96.0 / self.get_dpi(); - ((x.into() as f32) * scale, (y.into() as f32) * scale) - } - - /// Convert a rectangle from physical pixels to px units. - pub fn rect_to_px(&self, rect: RECT) -> Rect { - let (x0, y0) = self.pixels_to_px_xy(rect.left, rect.top); - let (x1, y1) = self.pixels_to_px_xy(rect.right, rect.bottom); - Rect::new(x0 as f64, y0 as f64, x1 as f64, y1 as f64) - } - - pub fn px_to_rect(&self, rect: Rect) -> RECT { - let scale = self.get_dpi() as f64 / 96.0; - RECT { - left: (rect.x0 * scale).floor() as i32, - top: (rect.y0 * scale).floor() as i32, - right: (rect.x1 * scale).ceil() as i32, - bottom: (rect.y1 * scale).ceil() as i32, - } + /// Get the `Scale` of the window. + pub fn get_scale(&self) -> Result { + Ok(self + .state + .upgrade() + .ok_or(ShellError::WindowDropped)? + .scale + .get()) } /// Allocate a timer slot. @@ -1559,7 +1567,12 @@ impl IdleHandle { fn invalidate(&self) { unsafe { - InvalidateRect(self.hwnd, null(), FALSE); + if InvalidateRect(self.hwnd, null(), FALSE) == FALSE { + log::warn!( + "InvalidateRect failed: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } } } } diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index af53817339..f77b6e25d1 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -29,11 +29,13 @@ use xcb::{ }; use crate::dialog::{FileDialogOptions, FileInfo}; +use crate::error::Error as ShellError; use crate::keyboard::{KeyEvent, KeyModifiers}; use crate::keycodes::KeyCode; use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; use crate::piet::{Piet, RenderContext}; +use crate::scale::Scale; use crate::window::{IdleToken, Text, TimerToken, WinHandler}; use super::application::Application; @@ -292,7 +294,8 @@ impl Window { Ok(mut handler) => { let size = self.size()?; handler.connect(&handle.into()); - handler.size(size.width as u32, size.height as u32); + handler.scale(Scale::default()); + handler.size(size); Ok(()) } Err(err) => Err(Error::BorrowError(format!( @@ -361,7 +364,7 @@ impl Window { ))); } match self.handler.try_borrow_mut() { - Ok(mut handler) => handler.size(size.width as u32, size.height as u32), + Ok(mut handler) => handler.size(size), Err(err) => { return Err(Error::BorrowError(format!( "Window::set_size handler: {}", @@ -507,9 +510,9 @@ impl Window { // TODO(x11/menus): implement Window::set_menu (currently a no-op) } - fn get_dpi(&self) -> f32 { + fn get_scale(&self) -> Result { // TODO(x11/dpi_scaling): figure out DPI scaling - 96.0 + Ok(Scale::from_dpi(96.0, 96.0)) } pub fn handle_expose(&self, expose: &xcb::ExposeEvent) -> Result<(), Error> { @@ -860,12 +863,12 @@ impl WindowHandle { Some(IdleHandle) } - pub fn get_dpi(&self) -> f32 { + pub fn get_scale(&self) -> Result { if let Some(w) = self.window.upgrade() { - w.get_dpi() + w.get_scale().map_err(ShellError::Platform) } else { log::error!("Window {} has already been dropped", self.id); - 96.0 + Ok(Scale::from_dpi(96.0, 96.0)) } } } diff --git a/druid-shell/src/scale.rs b/druid-shell/src/scale.rs new file mode 100644 index 0000000000..7c7987044b --- /dev/null +++ b/druid-shell/src/scale.rs @@ -0,0 +1,360 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Resolution scale related helpers. + +use crate::kurbo::{Insets, Line, Point, Rect, Size, Vec2}; + +const SCALE_TARGET_DPI: f64 = 96.0; + +/// Coordinate scaling between pixels and display points. +/// +/// This holds the platform DPI and the equivalent scale factors. +/// +/// ## Pixels and Display Points +/// +/// A pixel (**px**) represents the smallest controllable area of color on the platform. +/// A display point (**dp**) is a resolution independent logical unit. +/// When developing your application you should primarily be thinking in display points. +/// These display points will be automatically converted into pixels under the hood. +/// One pixel is equal to one display point when the platform scale factor is `1.0`. +/// +/// Read more about pixels and display points [in the druid book]. +/// +/// ## Converting with `Scale` +/// +/// To translate coordinates between pixels and display points you should use one of the +/// helper conversion methods of `Scale` or for manual conversion [`scale_x`] / [`scale_y`]. +/// +/// `Scale` is designed for responsive applications, including responding to platform DPI changes. +/// The platform DPI can change quickly, e.g. when moving a window from one monitor to another. +/// +/// A copy of `Scale` will be stale as soon as the platform DPI changes. +/// +/// [`scale_x`]: #method.scale_x +/// [`scale_y`]: #method.scale_y +/// [in the druid book]: https://xi-editor.io/druid/resolution_independence.html +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Scale { + /// The platform reported DPI on the x axis. + dpi_x: f64, + /// The platform reported DPI on the y axis. + dpi_y: f64, + /// The scale factor on the x axis. + scale_x: f64, + /// The scale factor on the y axis. + scale_y: f64, +} + +/// A specific area scaling state. +/// +/// This holds the platform area size in pixels and the logical area size in display points. +/// +/// The platform area size in pixels tends to be limited to integers and `ScaledArea` works +/// under that assumption. +/// +/// The logical area size in display points is an unrounded conversion, which means that it is +/// often not limited to integers. This allows for accurate calculations of +/// the platform area pixel boundaries from the logical area using a [`Scale`]. +/// +/// Even though the logical area size can be fractional, the integer boundaries of that logical area +/// will still match up with the platform area pixel boundaries as often as the scale factor allows. +/// +/// A copy of `ScaledArea` will be stale as soon as the platform area size changes. +/// +/// [`Scale`]: struct.Scale.html +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct ScaledArea { + /// The size of the scaled area in display points. + size_dp: Size, + /// The size of the scaled area in pixels. + size_px: Size, +} + +/// The `Scalable` trait describes how coordinates should be translated +/// from display points into pixels and vice versa using a [`Scale`]. +/// +/// [`Scale`]: struct.Scale.html +pub trait Scalable { + /// Converts the scalable item from display points into pixels, + /// using the x axis scale factor for coordinates on the x axis + /// and the y axis scale factor for coordinates on the y axis. + fn to_px(&self, scale: &Scale) -> Self; + + /// Converts the scalable item from pixels into display points, + /// using the x axis scale factor for coordinates on the x axis + /// and the y axis scale factor for coordinates on the y axis. + fn to_dp(&self, scale: &Scale) -> Self; +} + +impl Default for Scale { + fn default() -> Scale { + Scale { + dpi_x: SCALE_TARGET_DPI, + dpi_y: SCALE_TARGET_DPI, + scale_x: 1.0, + scale_y: 1.0, + } + } +} + +impl Scale { + /// Create a new `Scale` state based on the specified DPIs. + /// + /// Use this constructor if the platform provided DPI is the most accurate number. + pub fn from_dpi(dpi_x: f64, dpi_y: f64) -> Scale { + Scale { + dpi_x, + dpi_y, + scale_x: dpi_x / SCALE_TARGET_DPI, + scale_y: dpi_y / SCALE_TARGET_DPI, + } + } + + /// Create a new `Scale` state based on the specified scale factors. + /// + /// Use this constructor if the platform provided scale factor is the most accurate number. + pub fn from_scale(scale_x: f64, scale_y: f64) -> Scale { + Scale { + dpi_x: SCALE_TARGET_DPI * scale_x, + dpi_y: SCALE_TARGET_DPI * scale_y, + scale_x, + scale_y, + } + } + + /// Returns the x axis platform DPI associated with this `Scale`. + #[inline] + pub fn dpi_x(&self) -> f64 { + self.dpi_x + } + + /// Returns the y axis platform DPI associated with this `Scale`. + #[inline] + pub fn dpi_y(&self) -> f64 { + self.dpi_y + } + + /// Returns the x axis scale factor. + #[inline] + pub fn scale_x(&self) -> f64 { + self.scale_x + } + + /// Returns the y axis scale factor. + #[inline] + pub fn scale_y(&self) -> f64 { + self.scale_y + } + + /// Converts the `item` from display points into pixels, + /// using the x axis scale factor for coordinates on the x axis + /// and the y axis scale factor for coordinates on the y axis. + #[inline] + pub fn to_px(&self, item: &T) -> T { + item.to_px(self) + } + + /// Converts from pixels into display points, using the x axis scale factor. + #[inline] + pub fn px_to_dp_x>(&self, x: T) -> f64 { + x.into() / self.scale_x + } + + /// Converts from pixels into display points, using the y axis scale factor. + #[inline] + pub fn px_to_dp_y>(&self, y: T) -> f64 { + y.into() / self.scale_y + } + + /// Converts from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + pub fn px_to_dp_xy>(&self, x: T, y: T) -> (f64, f64) { + (x.into() / self.scale_x, y.into() / self.scale_y) + } + + /// Converts the `item` from pixels into display points, + /// using the x axis scale factor for coordinates on the x axis + /// and the y axis scale factor for coordinates on the y axis. + #[inline] + pub fn to_dp(&self, item: &T) -> T { + item.to_dp(self) + } +} + +impl Scalable for Vec2 { + /// Converts a `Vec2` from display points into pixels, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_px(&self, scale: &Scale) -> Vec2 { + Vec2::new(self.x * scale.scale_x, self.y * scale.scale_y) + } + + /// Converts a `Vec2` from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_dp(&self, scale: &Scale) -> Vec2 { + Vec2::new(self.x / scale.scale_x, self.y / scale.scale_y) + } +} + +impl Scalable for Point { + /// Converts a `Point` from display points into pixels, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_px(&self, scale: &Scale) -> Point { + Point::new(self.x * scale.scale_x, self.y * scale.scale_y) + } + + /// Converts a `Point` from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_dp(&self, scale: &Scale) -> Point { + Point::new(self.x / scale.scale_x, self.y / scale.scale_y) + } +} + +impl Scalable for Line { + /// Converts a `Line` from display points into pixels, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_px(&self, scale: &Scale) -> Line { + Line::new(self.p0.to_px(scale), self.p1.to_px(scale)) + } + + /// Converts a `Line` from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_dp(&self, scale: &Scale) -> Line { + Line::new(self.p0.to_dp(scale), self.p1.to_dp(scale)) + } +} + +impl Scalable for Size { + /// Converts a `Size` from display points into pixels, + /// using the x axis scale factor for `width` + /// and the y axis scale factor for `height`. + #[inline] + fn to_px(&self, scale: &Scale) -> Size { + Size::new(self.width * scale.scale_x, self.height * scale.scale_y) + } + + /// Converts a `Size` from pixels into points, + /// using the x axis scale factor for `width` + /// and the y axis scale factor for `height`. + #[inline] + fn to_dp(&self, scale: &Scale) -> Size { + Size::new(self.width / scale.scale_x, self.height / scale.scale_y) + } +} + +impl Scalable for Rect { + /// Converts a `Rect` from display points into pixels, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_px(&self, scale: &Scale) -> Rect { + Rect::new( + self.x0 * scale.scale_x, + self.y0 * scale.scale_y, + self.x1 * scale.scale_x, + self.y1 * scale.scale_y, + ) + } + + /// Converts a `Rect` from pixels into display points, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_dp(&self, scale: &Scale) -> Rect { + Rect::new( + self.x0 / scale.scale_x, + self.y0 / scale.scale_y, + self.x1 / scale.scale_x, + self.y1 / scale.scale_y, + ) + } +} + +impl Scalable for Insets { + /// Converts `Insets` from display points into pixels, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_px(&self, scale: &Scale) -> Insets { + Insets::new( + self.x0 * scale.scale_x, + self.y0 * scale.scale_y, + self.x1 * scale.scale_x, + self.y1 * scale.scale_y, + ) + } + + /// Converts `Insets` from pixels into display points, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_dp(&self, scale: &Scale) -> Insets { + Insets::new( + self.x0 / scale.scale_x, + self.y0 / scale.scale_y, + self.x1 / scale.scale_x, + self.y1 / scale.scale_y, + ) + } +} + +impl Default for ScaledArea { + fn default() -> ScaledArea { + ScaledArea { + size_dp: Size::ZERO, + size_px: Size::ZERO, + } + } +} + +impl ScaledArea { + /// Create a new scaled area from pixels. + pub fn from_px>(size: T, scale: &Scale) -> ScaledArea { + let size_px = size.into(); + let size_dp = size_px.to_dp(scale); + ScaledArea { size_dp, size_px } + } + + /// Create a new scaled area from display points. + /// + /// The calculated size in pixels is rounded away from zero to integers. + /// That means that the scaled area size in display points isn't always the same + /// as the `size` given to this function. To find out the new size in points use [`size_dp`]. + /// + /// [`size_dp`]: #method.size_dp + pub fn from_dp>(size: T, scale: &Scale) -> ScaledArea { + let size_px = size.into().to_px(scale).expand(); + let size_dp = size_px.to_dp(scale); + ScaledArea { size_dp, size_px } + } + + /// Returns the scaled area size in display points. + #[inline] + pub fn size_dp(&self) -> Size { + self.size_dp + } + + /// Returns the scaled area size in pixels. + #[inline] + pub fn size_px(&self) -> Size { + self.size_px + } +} diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 3e566d3938..b8a33a9397 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -26,6 +26,7 @@ use crate::kurbo::{Point, Rect, Size}; use crate::menu::Menu; use crate::mouse::{Cursor, MouseEvent}; use crate::platform::window as platform; +use crate::scale::Scale; // It's possible we'll want to make this type alias at a lower level, // see https://github.com/linebender/piet/pull/37 for more discussion. @@ -199,12 +200,15 @@ impl WindowHandle { self.0.get_idle_handle().map(IdleHandle) } - /// Get the dpi of the window. + /// Get the [`Scale`] information of the window. /// - /// TODO: we want to migrate this from dpi (with 96 as nominal) to a scale - /// factor (with 1 as nominal). - pub fn get_dpi(&self) -> f32 { - self.0.get_dpi() + /// The returned [`Scale`] is a copy and thus its information will be stale after + /// the platform DPI changes. A correctly behaving application should consider + /// the lifetime of this [`Scale`] brief, limited to approximately a single event cycle. + /// + /// [`Scale`]: struct.Scale.html + pub fn get_scale(&self) -> Result { + self.0.get_scale().map_err(Into::into) } } @@ -229,12 +233,28 @@ impl WindowBuilder { self.0.set_handler(handler) } - /// Set the window's initial size. + /// Set the window's initial drawing area size in [display points]. + /// + /// The actual window size in pixels will depend on the platform DPI settings. + /// + /// This should be considered a request to the platform to set the size of the window. + /// The platform might increase the size a tiny bit due to DPI. + /// To know the actual size of the window you should handle the [`WinHandler::size`] method. + /// + /// [`WinHandler::size`]: trait.WinHandler.html#method.size + /// [display points]: struct.Scale.html pub fn set_size(&mut self, size: Size) { self.0.set_size(size) } - /// Set the window's initial size. + /// Set the window's minimum drawing area size in [display points]. + /// + /// The actual minimum window size in pixels will depend on the platform DPI settings. + /// + /// This should be considered a request to the platform to set the minimum size of the window. + /// The platform might increase the size a tiny bit due to DPI. + /// + /// [display points]: struct.Scale.html pub fn set_min_size(&mut self, size: Size) { self.0.set_min_size(size) } @@ -281,15 +301,29 @@ pub trait WinHandler { /// wish to stash it. fn connect(&mut self, handle: &WindowHandle); - /// Called when the size of the window is changed. Note that size - /// is in physical pixels. + /// Called when the size of the window has changed. + /// + /// The `size` parameter is the new size in [display points]. + /// + /// [display points]: struct.Scale.html #[allow(unused_variables)] - fn size(&mut self, width: u32, height: u32) {} + fn size(&mut self, size: Size) {} + + /// Called when the [`Scale`] of the window has changed. + /// + /// This is always called before the accompanying [`size`]. + /// + /// [`Scale`]: struct.Scale.html + /// [`size`]: #method.size + #[allow(unused_variables)] + fn scale(&mut self, scale: Scale) {} /// Request the handler to paint the window contents. Return value /// indicates whether window is animating, i.e. whether another paint /// should be scheduled for the next animation frame. `invalid_rect` is the - /// rectangle that needs to be repainted. + /// rectangle in [display points] that needs to be repainted. + /// + /// [display points]: struct.Scale.html fn paint(&mut self, piet: &mut piet_common::Piet, invalid_rect: Rect) -> bool; /// Called when the resources need to be rebuilt. diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 1e419547ae..094267fc4e 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -46,6 +46,6 @@ image = { version = "0.23.2", optional = true } console_log = "0.1.2" [dev-dependencies] -float-cmp = { version = "0.6.0", default-features = false } +float-cmp = { version = "0.8.0", features = ["std"], default-features = false } tempfile = "3.1.0" piet-common = { version = "0.1.0", features = ["png"] } diff --git a/druid/src/app.rs b/druid/src/app.rs index 7867c7c819..8750e325ca 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -187,23 +187,38 @@ impl WindowDesc { self } - /// Set the initial window size. + /// Set the window's initial drawing area size in [display points]. + /// + /// You can pass in a tuple `(width, height)` or a [`Size`], + /// e.g. to create a window with a drawing area 1000dp wide and 500dp high: /// - /// You can pass in a tuple `(width, height)` or `kurbo::Size` e.g. - /// to create a window 1000px wide and 500px high /// ```ignore /// window.window_size((1000.0, 500.0)); /// ``` + /// + /// The actual window size in pixels will depend on the platform DPI settings. + /// + /// This should be considered a request to the platform to set the size of the window. + /// The platform might increase the size a tiny bit due to DPI. + /// + /// [`Size`]: struct.Size.html + /// [display points]: struct.Scale.html pub fn window_size(mut self, size: impl Into) -> Self { self.size = Some(size.into()); self } - /// Set the minimum window size. + /// Set the window's minimum drawing area size in [display points]. + /// + /// The actual minimum window size in pixels will depend on the platform DPI settings. + /// + /// This should be considered a request to the platform to set the minimum size of the window. + /// The platform might increase the size a tiny bit due to DPI. /// - /// To set the initial window size, see [`window_size`]. + /// To set the window's initial drawing area size use [`window_size`]. /// - /// [`window_size`]: struct.WindowDesc.html#method.window_size + /// [`window_size`]: #method.window_size + /// [display points]: struct.Scale.html pub fn with_min_size(mut self, size: impl Into) -> Self { self.min_size = Some(size.into()); self diff --git a/druid/src/data.rs b/druid/src/data.rs index b4d17d77d2..a4d0ca9a4e 100644 --- a/druid/src/data.rs +++ b/druid/src/data.rs @@ -19,6 +19,7 @@ use std::sync::Arc; use crate::kurbo::{self, ParamCurve}; use crate::piet; +use crate::shell::Scale; pub use druid_derive::Data; @@ -244,6 +245,12 @@ impl Data for (T0, T } } +impl Data for Scale { + fn same(&self, other: &Self) -> bool { + self == other + } +} + impl Data for kurbo::Point { fn same(&self, other: &Self) -> bool { self.x.same(&other.x) && self.y.same(&other.y) diff --git a/druid/src/env.rs b/druid/src/env.rs index 9e265f27ba..1838da06a6 100644 --- a/druid/src/env.rs +++ b/druid/src/env.rs @@ -88,7 +88,7 @@ struct EnvImpl { /// [`Env`]: struct.Env.html pub struct Key { key: &'static str, - value_type: PhantomData, + value_type: PhantomData<*const T>, } // we could do some serious deriving here: the set of types that can be stored diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 864d7dc263..25498fb33e 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -146,7 +146,7 @@ pub use piet::{Color, LinearGradient, RadialGradient, RenderContext, UnitPoint}; pub use shell::{ Application, Clipboard, ClipboardFormat, Cursor, Error as PlatformError, FileDialogOptions, FileInfo, FileSpec, FormatId, HotKey, KeyCode, KeyEvent, KeyModifiers, MouseButton, - MouseButtons, RawMods, SysMods, Text, TimerToken, WindowHandle, + MouseButtons, RawMods, Scale, SysMods, Text, TimerToken, WindowHandle, }; pub use crate::core::WidgetPod; diff --git a/druid/src/widget/flex.rs b/druid/src/widget/flex.rs index 44382fc24e..50c02753b3 100644 --- a/druid/src/widget/flex.rs +++ b/druid/src/widget/flex.rs @@ -47,15 +47,15 @@ use crate::{ /// /// When should your children be flexible? With other things being equal, /// a flexible child has lower layout priority than a non-flexible child. -/// Imagine, for instance, we have a row that is 30px wide, and we have -/// two children, both of which want to be 20px wide. If child #1 is non-flex -/// and child #2 is flex, the first widget will take up its 20px, and the second -/// widget will be constrained to 10px. +/// Imagine, for instance, we have a row that is 30dp wide, and we have +/// two children, both of which want to be 20dp wide. If child #1 is non-flex +/// and child #2 is flex, the first widget will take up its 20dp, and the second +/// widget will be constrained to 10dp. /// /// If, instead, both widgets are flex, they will each be given equal space, -/// and both will end up taking up 15px. +/// and both will end up taking up 15dp. /// -/// If both are non-flex they will both take up 20px, and will overflow the +/// If both are non-flex they will both take up 20dp, and will overflow the /// container. /// /// ```no_compile diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index c3248ba967..6205d79fb5 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -22,7 +22,7 @@ use std::rc::Rc; use crate::kurbo::{Rect, Size}; use crate::piet::Piet; use crate::shell::{ - Application, FileDialogOptions, IdleToken, MouseEvent, WinHandler, WindowHandle, + Application, FileDialogOptions, IdleToken, MouseEvent, Scale, WinHandler, WindowHandle, }; use crate::app_delegate::{AppDelegate, DelegateCtx}; @@ -648,11 +648,15 @@ impl WinHandler for DruidHandler { self.app_state.paint_window(self.window_id, piet, rect) } - fn size(&mut self, width: u32, height: u32) { - let event = Event::WindowSize(Size::new(f64::from(width), f64::from(height))); + fn size(&mut self, size: Size) { + let event = Event::WindowSize(size); self.app_state.do_window_event(event, self.window_id); } + fn scale(&mut self, _scale: Scale) { + // TODO: Do something with the scale + } + fn command(&mut self, id: u32) { self.app_state.handle_system_cmd(id, Some(self.window_id)); } diff --git a/druid/src/window.rs b/druid/src/window.rs index 4db972a544..70fc639c2c 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -156,6 +156,7 @@ impl Window { env: &Env, ) -> bool { match &event { + Event::WindowSize(size) => self.size = *size, Event::MouseDown(e) | Event::MouseUp(e) | Event::MouseMove(e) | Event::Wheel(e) => { self.last_mouse_pos = Some(e.pos) } @@ -169,12 +170,6 @@ impl Window { }; let event = match event { - Event::WindowSize(size) => { - let dpi = f64::from(self.handle.get_dpi()); - let scale = 96.0 / dpi; - self.size = Size::new(size.width * scale, size.height * scale); - Event::WindowSize(self.size) - } Event::Timer(token) => { if let Some(widget_id) = self.timers.get(&token) { Event::Internal(InternalEvent::RouteTimer(token, *widget_id))