diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4ed7b5b6..1bed987721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ You can find its changes [documented below](#060---2020-06-01). ### Highlights ### Added + +- Windows: Added Screen module to get information about monitors and the screen. ([#1037] by [@rhzk]) +- Added documentation to resizable() and show_titlebar() in WindowDesc. ([#1037] by [@rhzk]) +- Windows: Added internal functions to handle Re-entrancy. ([#1037] by [@rhzk]) +- Windows: WindowDesc: Create window with disabled titlebar, maximized or minimized state, and with position. ([#1037] by [@rhzk]) +- Windows: WindowHandle: Change window state. Toggle titlebar. Change size and position of window. ([#1037] by [@rhzk]) +- Windows: WindowHandle: Added handle_titlebar(), Allowing a custom titlebar to behave like the OS one. ([#1037] by [@rhzk]) - `OPEN_PANEL_CANCELLED` and `SAVE_PANEL_CANCELLED` commands. ([#1061] by @cmyr) - Export `Image` and `ImageData` by default. ([#1011] by [@covercash2]) - Re-export `druid_shell::Scalable` under `druid` namespace. ([#1075] by [@ForLoveOfCats]) @@ -29,6 +36,8 @@ You can find its changes [documented below](#060---2020-06-01). ### Changed +- Windows: Improved DPI handling. Druid should now redraw correctly when dpi changes. ([#1037] by [@rhzk]) +- windows: Window created with OS default size if not set. ([#1037] by [@rhzk]) - `Scale::from_scale` to `Scale::new`, and `Scale` methods `scale_x` / `scale_y` to `x` / `y`. ([#1042] by [@xStrom]) - Major rework of keyboard event handling. ([#1049] by [@raphlinus]) - `Container::rounded` takes `KeyOrValue` instead of `f64`. ([#1054] by [@binomial0]) @@ -288,6 +297,7 @@ Last release without a changelog :( [@sysint64]: https://github.com/sysint64 [@justinmoon]: https://github.com/justinmoon [@rjwittams]: https://github.com/rjwittams +[@rhzk]: https://github.com/rhzk [@koutoftimer]: https://github.com/koutoftimer [#599]: https://github.com/linebender/druid/pull/599 @@ -390,6 +400,7 @@ Last release without a changelog :( [#1018]: https://github.com/linebender/druid/pull/1018 [#1025]: https://github.com/linebender/druid/pull/1025 [#1028]: https://github.com/linebender/druid/pull/1028 +[#1037]: https://github.com/linebender/druid/pull/1037 [#1042]: https://github.com/linebender/druid/pull/1042 [#1043]: https://github.com/linebender/druid/pull/1043 [#1049]: https://github.com/linebender/druid/pull/1049 diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index fd8917c0fe..0e18bd3cc7 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -48,6 +48,7 @@ mod mouse; mod platform; mod region; mod scale; +mod screen; mod window; pub use application::{AppHandler, Application}; @@ -61,6 +62,9 @@ pub use menu::Menu; pub use mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; pub use region::Region; pub use scale::{Scalable, Scale, ScaledArea}; -pub use window::{IdleHandle, IdleToken, TimerToken, WinHandler, WindowBuilder, WindowHandle}; +pub use screen::{Monitor, Screen}; +pub use window::{ + IdleHandle, IdleToken, TimerToken, WinHandler, WindowBuilder, WindowHandle, WindowState, +}; pub use keyboard_types; diff --git a/druid-shell/src/platform/gtk/mod.rs b/druid-shell/src/platform/gtk/mod.rs index 60e5a46e73..1b66bb8bf6 100644 --- a/druid-shell/src/platform/gtk/mod.rs +++ b/druid-shell/src/platform/gtk/mod.rs @@ -22,3 +22,4 @@ pub mod keycodes; pub mod menu; pub mod util; pub mod window; +pub mod screen; diff --git a/druid-shell/src/platform/gtk/screen.rs b/druid-shell/src/platform/gtk/screen.rs new file mode 100644 index 0000000000..8f97d4f48a --- /dev/null +++ b/druid-shell/src/platform/gtk/screen.rs @@ -0,0 +1,22 @@ +// Copyright 2020 The Druid 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. + +//! GTK Monitors and Screen information. + +use crate::screen::Monitor; + +pub(crate) fn get_monitors() -> Vec { + log::warn!("Screen::get_monitors() is currently unimplemented for gtk."); + Vec::new() +} diff --git a/druid-shell/src/platform/gtk/window.rs b/druid-shell/src/platform/gtk/window.rs index c834120568..54bf9bce8d 100644 --- a/druid-shell/src/platform/gtk/window.rs +++ b/druid-shell/src/platform/gtk/window.rs @@ -43,6 +43,7 @@ use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::scale::{Scalable, Scale, ScaledArea}; use crate::window::{IdleToken, TimerToken, WinHandler}; +use crate::window; use super::application::Application; use super::dialog; @@ -176,6 +177,14 @@ impl WindowBuilder { self.show_titlebar = show_titlebar; } + pub fn set_position(&mut self, _position: Point) { + log::warn!("WindowBuilder::set_position is currently unimplemented for gtk."); + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowBuilder::set_window_state is currently unimplemented for gtk."); + } + pub fn set_title(&mut self, title: impl Into) { self.title = title.into(); } @@ -666,6 +675,37 @@ impl WindowHandle { } } + pub fn set_position(&self, _position: Point) { + log::warn!("WindowHandle::set_position is currently unimplemented for gtk."); + } + + pub fn get_position(&self) -> Point { + log::warn!("WindowHandle::get_position is currently unimplemented for gtk."); + Point::new(0.0, 0.0) + } + + pub fn set_size(&self, _size: Size) { + log::warn!("WindowHandle::set_size is currently unimplemented for gtk."); + } + + pub fn get_size(&self) -> Size { + log::warn!("WindowHandle::get_size is currently unimplemented for gtk."); + Size::new(0.0, 0.0) + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowHandle::set_window_state is currently unimplemented for gtk."); + } + + pub fn get_window_state(&self) -> window::WindowState { + log::warn!("WindowHandle::get_window_state is currently unimplemented for gtk."); + window::WindowState::RESTORED + } + + pub fn handle_titlebar(&self, _val: bool) { + log::warn!("WindowHandle::handle_titlebar is currently unimplemented for gtk."); + } + /// Close the window. pub fn close(&self) { if let Some(state) = self.state.upgrade() { diff --git a/druid-shell/src/platform/mac/mod.rs b/druid-shell/src/platform/mac/mod.rs index c041746b0c..be29bf7db4 100644 --- a/druid-shell/src/platform/mac/mod.rs +++ b/druid-shell/src/platform/mac/mod.rs @@ -25,3 +25,4 @@ mod keyboard; pub mod menu; pub mod util; pub mod window; +pub mod screen; diff --git a/druid-shell/src/platform/mac/screen.rs b/druid-shell/src/platform/mac/screen.rs new file mode 100644 index 0000000000..c293c6bfc3 --- /dev/null +++ b/druid-shell/src/platform/mac/screen.rs @@ -0,0 +1,22 @@ +// Copyright 2020 The Druid 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. + +//! macOS Monitors and Screen information. + +use crate::screen::Monitor; + +pub(crate) fn get_monitors() -> Vec { + log::warn!("Screen::get_monitors() is currently unimplemented for Mac."); + Vec::new() +} diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 6bac65b211..671199f3a3 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -57,8 +57,10 @@ use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::scale::Scale; use crate::window::{IdleToken, TimerToken, WinHandler}; +use crate::window; use crate::Error; + #[allow(non_upper_case_globals)] const NSWindowDidBecomeKeyNotification: &str = "NSWindowDidBecomeKeyNotification"; @@ -149,6 +151,14 @@ impl WindowBuilder { self.show_titlebar = show_titlebar; } + pub fn set_position(&mut self, _position: Point) { + log::warn!("WindowBuilder::set_position is currently unimplemented for mac platforms."); + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowBuilder::set_window_state is currently unimplemented for mac platforms."); + } + pub fn set_title(&mut self, title: impl Into) { self.title = title.into(); } @@ -866,6 +876,37 @@ impl WindowHandle { // TODO: Implement this pub fn show_titlebar(&self, _show_titlebar: bool) {} + pub fn set_position(&self, _position: Point) { + log::warn!("WindowHandle::set_position is currently unimplemented for Mac."); + } + + pub fn get_position(&self) -> Point { + log::warn!("WindowHandle::get_position is currently unimplemented for Mac."); + Point::new(0.0, 0.0) + } + + pub fn set_size(&self, _size: Size) { + log::warn!("WindowHandle::set_size is currently unimplemented for Mac."); + } + + pub fn get_size(&self) -> Size { + log::warn!("WindowHandle::get_size is currently unimplemented for Mac."); + Size::new(0.0, 0.0) + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowHandle::set_window_state is currently unimplemented for Mac."); + } + + pub fn get_window_state(&self) -> window::WindowState { + log::warn!("WindowHandle::get_window_state is currently unimplemented for Mac."); + window::WindowState::RESTORED + } + + pub fn handle_titlebar(&self, _val: bool) { + log::warn!("WindowHandle::handle_titlebar is currently unimplemented for Mac."); + } + pub fn resizable(&self, resizable: bool) { unsafe { let window: id = msg_send![*self.nsview.load(), window]; diff --git a/druid-shell/src/platform/web/mod.rs b/druid-shell/src/platform/web/mod.rs index e0fbea1115..2d44f4d844 100644 --- a/druid-shell/src/platform/web/mod.rs +++ b/druid-shell/src/platform/web/mod.rs @@ -20,3 +20,4 @@ pub mod error; pub mod keycodes; pub mod menu; pub mod window; +pub mod screen; diff --git a/druid-shell/src/platform/web/screen.rs b/druid-shell/src/platform/web/screen.rs new file mode 100644 index 0000000000..6240153c7b --- /dev/null +++ b/druid-shell/src/platform/web/screen.rs @@ -0,0 +1,22 @@ +// Copyright 2020 The Druid 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. + +//! Monitor and Screen information ignored for web. + +use crate::screen::Monitor; + +pub(crate) fn get_monitors() -> Vec { + log::warn!("Screen::get_monitors() is not implemented for web."); + Vec::new() +} diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index 7cedec774d..5858ac7e60 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -42,6 +42,7 @@ use crate::keyboard::{KbKey, KeyState, Modifiers}; use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::window::{IdleToken, TimerToken, WinHandler}; +use crate::window; // This is a macro instead of a function since KeyboardEvent and MouseEvent has identical functions // to query modifier key states. @@ -348,6 +349,14 @@ impl WindowBuilder { // Ignored } + pub fn set_position(&mut self, _position: Point) { + // Ignored + } + + pub fn set_window_state(&self, _state: window::WindowState) { + // Ignored + } + pub fn set_title>(&mut self, title: S) { self.title = title.into(); } @@ -433,6 +442,37 @@ impl WindowHandle { log::warn!("show_titlebar unimplemented for web"); } + pub fn set_position(&self, _position: Point) { + log::warn!("WindowHandle::set_position unimplemented for web"); + } + + pub fn get_position(&self) -> Point { + log::warn!("WindowHandle::get_position unimplemented for web."); + Point::new(0.0, 0.0) + } + + pub fn set_size(&self, _size: Size) { + log::warn!("WindowHandle::set_size unimplemented for web."); + } + + pub fn get_size(&self) -> Size { + log::warn!("WindowHandle::get_size unimplemented for web."); + Size::new(0.0, 0.0) + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowHandle::set_window_state unimplemented for web."); + } + + pub fn get_window_state(&self) -> window::WindowState { + log::warn!("WindowHandle::get_window_state unimplemented for web."); + window::WindowState::RESTORED + } + + pub fn handle_titlebar(&self, _val: bool) { + log::warn!("WindowHandle::handle_titlebar unimplemented for web."); + } + pub fn close(&self) { // TODO } diff --git a/druid-shell/src/platform/windows/application.rs b/druid-shell/src/platform/windows/application.rs index 14d7c8ca09..39cddbf7c8 100644 --- a/druid-shell/src/platform/windows/application.rs +++ b/druid-shell/src/platform/windows/application.rs @@ -22,14 +22,14 @@ use std::rc::Rc; use winapi::shared::minwindef::{FALSE, HINSTANCE}; use winapi::shared::ntdef::LPCWSTR; -use winapi::shared::windef::{HCURSOR, HWND}; +use winapi::shared::windef::{HCURSOR, HWND, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2}; use winapi::shared::winerror::HRESULT_FROM_WIN32; use winapi::um::errhandlingapi::GetLastError; -use winapi::um::shellscalingapi::PROCESS_SYSTEM_DPI_AWARE; +use winapi::um::shellscalingapi::PROCESS_PER_MONITOR_DPI_AWARE; use winapi::um::winuser::{ DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostMessageW, PostQuitMessage, RegisterClassW, TranslateAcceleratorW, TranslateMessage, GA_ROOT, IDI_APPLICATION, MSG, - WNDCLASSW, + WNDCLASSW, SendMessageW }; use crate::application::AppHandler; @@ -38,7 +38,7 @@ use super::accels; use super::clipboard::Clipboard; use super::error::Error; use super::util::{self, ToWide, CLASS_NAME, OPTIONAL_FUNCTIONS}; -use super::window::{self, DS_REQUEST_DESTROY}; +use super::window::{self, DS_REQUEST_DESTROY, DS_HANDLE_DEFERRED}; #[derive(Clone)] pub(crate) struct Application { @@ -50,6 +50,8 @@ struct State { windows: HashSet, } + + impl Application { pub fn new() -> Result { Application::init()?; @@ -64,10 +66,15 @@ impl Application { fn init() -> Result<(), Error> { // TODO: Report back an error instead of panicking util::attach_console(); - if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwareness { + if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwarenessContext { // This function is only supported on windows 10 unsafe { - func(PROCESS_SYSTEM_DPI_AWARE); // TODO: per monitor (much harder) + func(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + } + else if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwareness { + unsafe { + func(PROCESS_PER_MONITOR_DPI_AWARE); } } unsafe { @@ -105,6 +112,11 @@ impl Application { unsafe { // Handle windows messages loop { + if let Ok(state) = self.state.try_borrow() { + for hwnd in &state.windows { + SendMessageW(*hwnd, DS_HANDLE_DEFERRED, 0,0); + } + } let mut msg = mem::MaybeUninit::uninit(); let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0); if res <= 0 { diff --git a/druid-shell/src/platform/windows/mod.rs b/druid-shell/src/platform/windows/mod.rs index e5b330ba01..bb361ad061 100644 --- a/druid-shell/src/platform/windows/mod.rs +++ b/druid-shell/src/platform/windows/mod.rs @@ -26,6 +26,7 @@ pub mod paint; mod timers; pub mod util; pub mod window; +pub mod screen; // https://docs.microsoft.com/en-us/windows/win32/direct2d/render-targets-overview // ID2D1RenderTarget is the interface. The other resources inherit from it. @@ -53,7 +54,6 @@ use winapi::um::d2d1::{ ID2D1HwndRenderTarget, ID2D1RenderTarget, D2D1_HWND_RENDER_TARGET_PROPERTIES, D2D1_RENDER_TARGET_PROPERTIES, D2D1_SIZE_U, }; -use winapi::um::dcommon::D2D1_PIXEL_FORMAT; use wio::com::ComPtr; #[derive(Clone)] @@ -67,11 +67,8 @@ impl HwndRenderTarget { hwnd: HWND, width: u32, height: u32, + rt_props: D2D1_RENDER_TARGET_PROPERTIES, ) -> Result { - // hardcode - // - RenderTargetType::Default - // - AlphaMode::Unknown - let rt_props = DEFAULT_PROPS; let mut hwnd_props = DEFAULT_HWND_PROPS; hwnd_props.hwnd = hwnd; @@ -112,19 +109,6 @@ impl HwndRenderTarget { } } -// props for creating hwnd render target -const DEFAULT_PROPS: D2D1_RENDER_TARGET_PROPERTIES = D2D1_RENDER_TARGET_PROPERTIES { - _type: 0u32, //RenderTargetType::Default - pixelFormat: D2D1_PIXEL_FORMAT { - format: 87u32, //Format::B8G8R8A8Unorm, see https://docs.rs/dxgi/0.3.0-alpha4/src/dxgi/enums/format.rs.html#631 - alphaMode: 0u32, //AlphaMode::Unknown - }, - dpiX: 0.0, - dpiY: 0.0, - usage: 0, - minLevel: 0, -}; - const DEFAULT_HWND_PROPS: D2D1_HWND_RENDER_TARGET_PROPERTIES = D2D1_HWND_RENDER_TARGET_PROPERTIES { hwnd: std::ptr::null_mut(), pixelSize: D2D1_SIZE_U { diff --git a/druid-shell/src/platform/windows/paint.rs b/druid-shell/src/platform/windows/paint.rs index 6fc165c7c1..66234df524 100644 --- a/druid-shell/src/platform/windows/paint.rs +++ b/druid-shell/src/platform/windows/paint.rs @@ -45,6 +45,7 @@ use super::window::SCALE_TARGET_DPI; pub(crate) unsafe fn create_render_target( d2d_factory: &D2DFactory, hwnd: HWND, + scale: Scale, ) -> Result { let mut rect: RECT = mem::zeroed(); if GetClientRect(hwnd, &mut rect) == 0 { @@ -53,7 +54,20 @@ pub(crate) unsafe fn create_render_target( } else { let width = (rect.right - rect.left) as u32; let height = (rect.bottom - rect.top) as u32; - let res = HwndRenderTarget::create(d2d_factory, hwnd, width, height); + + let props = D2D1_RENDER_TARGET_PROPERTIES { + _type: D2D1_RENDER_TARGET_TYPE_DEFAULT, + pixelFormat: D2D1_PIXEL_FORMAT { + format: DXGI_FORMAT_B8G8R8A8_UNORM, + alphaMode: D2D1_ALPHA_MODE_IGNORE, + }, + dpiX: (scale.x() * SCALE_TARGET_DPI) as f32, + dpiY: (scale.y() * SCALE_TARGET_DPI) as f32, + usage: D2D1_RENDER_TARGET_USAGE_NONE, + minLevel: D2D1_FEATURE_LEVEL_DEFAULT, + }; + + let res = HwndRenderTarget::create(d2d_factory, hwnd, width, height, props); if let Err(ref e) = res { log::error!("Creating hwnd render target failed: {:?}", e); diff --git a/druid-shell/src/platform/windows/screen.rs b/druid-shell/src/platform/windows/screen.rs new file mode 100644 index 0000000000..7a6ef01f01 --- /dev/null +++ b/druid-shell/src/platform/windows/screen.rs @@ -0,0 +1,59 @@ +// Copyright 2020 The Druid 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. + +//! Windows Monitors and Screen information. + +use log::warn; +use super::error::Error; +use winapi::shared::winerror::*; +use winapi::um::errhandlingapi::GetLastError; +use winapi::shared::minwindef::*; +use winapi::shared::windef::*; +use winapi::um::winuser::*; +use std::ptr::null_mut; +use std::mem::size_of; + +use crate::screen::Monitor; +use crate::kurbo::Rect; + +unsafe extern "system" fn monitorenumproc(hmonitor : HMONITOR, _hdc : HDC, _lprect : LPRECT, _lparam : LPARAM) -> BOOL { + let rect = RECT { left: 0, top: 0, right: 0, bottom: 0}; + let mut info = MONITORINFO { cbSize : size_of::() as u32, rcMonitor : rect, rcWork : rect, dwFlags : 0}; + if GetMonitorInfoW(hmonitor,&mut info) == 0 { + warn!( + "failed to get Monitor Info: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + let primary = info.dwFlags == MONITORINFOF_PRIMARY; + let rect = Rect::new(info.rcMonitor.left as f64, info.rcMonitor.top as f64, info.rcMonitor.right as f64, info.rcMonitor.bottom as f64); + let work_rect = Rect::new(info.rcWork.left as f64, info.rcWork.top as f64, info.rcWork.right as f64, info.rcWork.bottom as f64); + let monitors = _lparam as *mut Vec::; + (*monitors).push(Monitor::new(primary, rect, work_rect)); + TRUE +} + +pub(crate) fn get_monitors() -> Vec { + unsafe { + let monitors = Vec::::new(); + let ptr = &monitors as *const Vec::; + if EnumDisplayMonitors(null_mut(), null_mut(), Some(monitorenumproc), ptr as isize) == 0{ + warn!( + "Failed to Enumerate Display Monitors: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + monitors + } +} diff --git a/druid-shell/src/platform/windows/util.rs b/druid-shell/src/platform/windows/util.rs index 4269ba19bb..f8e4061700 100644 --- a/druid-shell/src/platform/windows/util.rs +++ b/druid-shell/src/platform/windows/util.rs @@ -26,9 +26,9 @@ use std::slice; use lazy_static::lazy_static; use winapi::ctypes::c_void; use winapi::shared::guiddef::REFIID; -use winapi::shared::minwindef::{HMODULE, UINT}; +use winapi::shared::minwindef::{HMODULE, UINT, BOOL}; use winapi::shared::ntdef::{HRESULT, LPWSTR}; -use winapi::shared::windef::{HMONITOR, RECT}; +use winapi::shared::windef::{HMONITOR, RECT, HWND}; use winapi::shared::winerror::SUCCEEDED; use winapi::um::fileapi::{CreateFileA, GetFileType, OPEN_EXISTING}; use winapi::um::handleapi::INVALID_HANDLE_VALUE; @@ -140,10 +140,13 @@ pub(crate) fn region_to_rectis(region: &Region, scale: Scale) -> Vec { } // Types for functions we want to load, which are only supported on newer windows versions -// from shcore.dll +// from user32.dll type GetDpiForSystem = unsafe extern "system" fn() -> UINT; +type GetDpiForWindow = unsafe extern "system" fn(HWND) -> UINT; +type SetProcessDpiAwarenessContext = unsafe extern "system" fn(winapi::shared::windef::DPI_AWARENESS_CONTEXT) -> BOOL; +type GetSystemMetricsForDpi = unsafe extern "system" fn(winapi::ctypes::c_int, UINT) -> winapi::ctypes::c_int; +// from shcore.dll type GetDpiForMonitor = unsafe extern "system" fn(HMONITOR, MONITOR_DPI_TYPE, *mut UINT, *mut UINT); -// from user32.dll type SetProcessDpiAwareness = unsafe extern "system" fn(PROCESS_DPI_AWARENESS) -> HRESULT; type DCompositionCreateDevice2 = unsafe extern "system" fn( renderingDevice: *const IUnknown, @@ -156,8 +159,11 @@ type CreateDXGIFactory2 = #[allow(non_snake_case)] // For member fields pub struct OptionalFunctions { pub GetDpiForSystem: Option, + pub GetDpiForWindow: Option, + pub SetProcessDpiAwarenessContext: Option, pub GetDpiForMonitor: Option, pub SetProcessDpiAwareness: Option, + pub GetSystemMetricsForDpi: Option, pub DCompositionCreateDevice2: Option, pub CreateDXGIFactory2: Option, } @@ -210,7 +216,10 @@ fn load_optional_functions() -> OptionalFunctions { let mut GetDpiForSystem = None; let mut GetDpiForMonitor = None; + let mut GetDpiForWindow = None; + let mut SetProcessDpiAwarenessContext = None; let mut SetProcessDpiAwareness = None; + let mut GetSystemMetricsForDpi = None; let mut DCompositionCreateDevice2 = None; let mut CreateDXGIFactory2 = None; @@ -225,6 +234,9 @@ fn load_optional_functions() -> OptionalFunctions { log::info!("No user32.dll"); } else { load_function!(user32, GetDpiForSystem, "10"); + load_function!(user32, GetDpiForWindow, "10"); + load_function!(user32, SetProcessDpiAwarenessContext, "10"); + load_function!(user32, GetSystemMetricsForDpi, "10"); } if !dcomp.is_null() { @@ -237,8 +249,11 @@ fn load_optional_functions() -> OptionalFunctions { OptionalFunctions { GetDpiForSystem, + GetDpiForWindow, + SetProcessDpiAwarenessContext, GetDpiForMonitor, SetProcessDpiAwareness, + GetSystemMetricsForDpi, DCompositionCreateDevice2, CreateDXGIFactory2, } diff --git a/druid-shell/src/platform/windows/window.rs b/druid-shell/src/platform/windows/window.rs index 2ce3e9aae1..9275038e60 100644 --- a/druid-shell/src/platform/windows/window.rs +++ b/druid-shell/src/platform/windows/window.rs @@ -38,6 +38,7 @@ use winapi::um::errhandlingapi::GetLastError; use winapi::um::unknwnbase::*; use winapi::um::winnt::*; use winapi::um::winuser::*; +use winapi::um::shellscalingapi::MDT_EFFECTIVE_DPI; use piet_common::d2d::{D2DFactory, DeviceContext}; use piet_common::dwrite::DwriteFactory; @@ -66,6 +67,7 @@ use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::scale::{Scalable, Scale, ScaledArea}; use crate::window::{IdleToken, TimerToken, WinHandler}; +use crate::window; /// The platform target DPI. /// @@ -87,6 +89,8 @@ pub(crate) struct WindowBuilder { show_titlebar: bool, size: Size, min_size: Option, + position: Point, + state: window::WindowState, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -123,6 +127,136 @@ pub struct WindowHandle { state: Weak, } +#[derive(Clone)] +enum DeferredOp { + SetPosition(Point), + SetSize(Size), + DecorationChanged(), + // Needs a better name + SetWindowSizeState(window::WindowState), +} + +struct DeferredQueue { + queue: Vec, +} + +impl DeferredQueue { + pub fn new() -> Self { + DeferredQueue { + queue: Vec::new() + } + } + + /// Adds a DeferredOp message to the queue. + /// The message will be run at a later time. + pub fn add(state: Weak, message: DeferredOp) { + if let Some(w) = state.upgrade() { + if let Ok(mut q) = w.deferred_queue.try_borrow_mut() { + q.queue.push(message) + } else { + warn!( + "failed to borrow deferred queue" + ); + } + } + } + + /// Empties the current message queue, returning a queue with the messages. + pub fn empty(&mut self) -> Vec { + let ret = self.queue.clone(); + self.queue = Vec::new(); + ret + } + + + // Here we handle messages generated by WindowHandle + // that needs to run after borrow is released + fn handle_deferred(proc: &MyWndProc, op : DeferredOp) { + if let Some(hwnd) = proc.handle.borrow().get_hwnd() { + match op { + DeferredOp::SetSize(size) => { + unsafe { + if SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, (size.width * proc.scale().x()) as i32, (size.height * proc.scale().y()) as i32, SWP_NOMOVE | SWP_NOZORDER) == 0 { + warn!( + "failed to resize window: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + } + }, + DeferredOp::SetPosition(position) => { + unsafe { + if SetWindowPos(hwnd, HWND_TOPMOST, position.x as i32, position.y as i32, 0, 0, SWP_NOSIZE | SWP_NOZORDER) == 0 { + warn!( + "failed to move window: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + } + }, + DeferredOp::DecorationChanged() => { + unsafe { + let resizable = proc.resizable(); + let titlebar = proc.has_titlebar(); + + let mut style = GetWindowLongPtrW(hwnd, GWL_STYLE) as u32; + if style == 0 { + warn!( + "failed to get window style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + return; + } + + if !resizable { + style &= !(WS_THICKFRAME | WS_MAXIMIZEBOX); + } else { + style |= WS_THICKFRAME | WS_MAXIMIZEBOX; + } + if !titlebar { + style &= !(WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPED); + } else { + style |= WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPED; + } + if SetWindowLongPtrW(hwnd, GWL_STYLE, style as isize) == 0 { + warn!( + "failed to set the window style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } + if SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOSIZE) == 0 { + warn!( + "failed to update window style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + } + }, + DeferredOp::SetWindowSizeState(val) => { + unsafe { + let s = match val { + window::WindowState::MAXIMIZED => { + SW_MAXIMIZE + }, + window::WindowState::MINIMIZED => { + SW_MINIMIZE + }, + window::WindowState::RESTORED => { + SW_RESTORE + } + }; + ShowWindow(hwnd,s); + } + }, + } + } else { + warn!("Could not get HWND"); + } + } + + +} + /// A handle that can get used to schedule an idle handler. Note that /// this handle is thread safe. If the handle is used after the hwnd /// has been destroyed, probably not much will go wrong (the DS_RUN_IDLE @@ -150,6 +284,11 @@ struct WindowState { wndproc: Box, idle_queue: Arc>>, timers: Arc>, + deferred_queue: RefCell, + has_titlebar: Cell, + // For resizable borders, window can still be resized with code. + is_resizable: Cell, + handle_titlebar: Cell, } /// Generic handler trait for the winapi window procedure entry point. @@ -217,6 +356,21 @@ const DS_RUN_IDLE: UINT = WM_USER; /// time it is handled, we can successfully borrow the handler. pub(crate) const DS_REQUEST_DESTROY: UINT = WM_USER + 1; +/// A message which must be delivered deferred due to reentrancy. +/// +/// Due to the architecture of Windows, it sometimes delivers messages +/// reentrantly. This is problematic when the mutable state for the window +/// is borrowed, as it's not possible to safely dispatch the message. The two +/// choices are to drop the message, or put it in a queue for deferred +/// processing. +/// +/// This mechanism should be used very sparingly, as delivery of these +/// deferred messages can not be guaranteed in a timely and reliable +/// fashion. In particular, we do not run our own runloop when handling +/// live resize, when a modal dialog is open, or when the application is a +/// a guest (such as a VST plugin). +pub(crate) const DS_HANDLE_DEFERRED: UINT = WM_USER + 2; + impl Default for PresentStrategy { fn default() -> PresentStrategy { // We probably want to change this, but we need GDI to work. Too bad about @@ -338,6 +492,10 @@ impl MyWndProc { self.with_window_state(|state| state.scale.get()) } + fn set_scale(&self, scale: Scale) { + self.with_window_state(move |state| state.scale.set(scale)) + } + fn area(&self) -> ScaledArea { self.with_window_state(|state| state.area.get()) } @@ -364,6 +522,21 @@ impl MyWndProc { fn has_menu(&self) -> bool { self.with_window_state(|state| state.has_menu.get()) } + + fn has_titlebar(&self) -> bool { + self.with_window_state(|state| state.has_titlebar.get()) + } + + fn resizable(&self) -> bool { + self.with_window_state(|state| state.is_resizable.get()) + } + + fn handle_deferred_queue(&self) { + let q = self.with_window_state(move |state| state.deferred_queue.borrow_mut().empty()); + for op in q { + DeferredQueue::handle_deferred(self, op); + } + } } impl WndProc for MyWndProc { @@ -393,6 +566,28 @@ impl WndProc for MyWndProc { //println!("wndproc msg: {}", msg); match msg { WM_CREATE => { + // Only supported on Windows 10, Could remove this as the 8.1 version below also works on 10.. + let scale_factor = if let Some(func) = OPTIONAL_FUNCTIONS.GetDpiForWindow { + unsafe { + func(hwnd) as f64 / SCALE_TARGET_DPI + } + } + // Windows 8.1 Support + else if let Some(func) = OPTIONAL_FUNCTIONS.GetDpiForMonitor{ + unsafe { + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + let mut dpiX = 0; + let mut dpiY = 0; + func(monitor, MDT_EFFECTIVE_DPI, &mut dpiX, &mut dpiY); + dpiX as f64 / SCALE_TARGET_DPI + } + } else { + 1.0 + }; + + let scale = Scale::new(scale_factor, scale_factor); + self.set_scale(scale); + if let Some(state) = self.handle.borrow().state.upgrade() { state.hwnd.set(hwnd); } @@ -404,8 +599,9 @@ impl WndProc for MyWndProc { }) }; if dcomp_state.is_none() { + let scale = self.scale(); unsafe { - let rt = paint::create_render_target(&self.d2d_factory, hwnd); + let rt = paint::create_render_target(&self.d2d_factory, hwnd, scale); state.render_target = rt.ok(); } } @@ -416,6 +612,19 @@ impl WndProc for MyWndProc { } Some(0) } + WM_ACTIVATE => { + if LOWORD(wparam as u32) as u32 != 0 { + unsafe { + if SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOSIZE) == 0 { + warn!( + "failed to update window style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + } + } + Some(0) + } WM_ERASEBKGND => Some(0), WM_SETFOCUS => { if let Ok(mut s) = self.state.try_borrow_mut() { @@ -444,7 +653,7 @@ impl WndProc for MyWndProc { let invalid = self.take_invalid(); if !invalid.rects().is_empty() { if s.render_target.is_none() { - let rt = paint::create_render_target(&self.d2d_factory, hwnd); + let rt = paint::create_render_target(&self.d2d_factory, hwnd, self.scale()); s.render_target = rt.ok(); } s.handler.rebuild_resources(); @@ -468,12 +677,122 @@ impl WndProc for MyWndProc { } Some(0) }, + WM_DPICHANGED => unsafe { + let x = HIWORD(wparam as u32) as f64 / SCALE_TARGET_DPI; + let y = LOWORD(wparam as u32) as f64 / SCALE_TARGET_DPI; + let scale = Scale::new(x, y); + self.set_scale(scale); + let rect: *mut RECT = lparam as *mut RECT; + SetWindowPos(hwnd, HWND_TOPMOST, (*rect).left, (*rect).top, (*rect).right - (*rect).left, (*rect).bottom - (*rect).top, SWP_NOZORDER | SWP_FRAMECHANGED | SWP_DRAWFRAME); + if let Ok(mut s) = self.state.try_borrow_mut() { + let s = s.as_mut().unwrap(); + if s.dcomp_state.is_some() { + let scale = self.scale(); + let rt = paint::create_render_target(&self.d2d_factory, hwnd, scale); + 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, &rect_dp.into()); + self.clear_invalid(); + } + + if let Some(ref mut ds) = s.dcomp_state { + let _ = ds.dcomp_target.clear_root(); + let _ = ds.dcomp_device.commit(); + ds.sizing = true; + } + } + } else { + self.log_dropped_msg(hwnd, msg, wparam, lparam); + } + Some(0) + }, + WM_NCCALCSIZE => unsafe { + // Workaround to get rid of caption but keeping the borders created by it. + let style = GetWindowLongPtrW(hwnd, GWL_STYLE) as u32; + if style == 0 { + warn!( + "failed to get window style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + return Some(0); + } + + if !self.has_titlebar() && (style & WS_CAPTION) != 0 { + let s: *mut NCCALCSIZE_PARAMS = lparam as *mut NCCALCSIZE_PARAMS; + if let Some(mut s) = s.as_mut() { + if let Some(func) = OPTIONAL_FUNCTIONS.GetSystemMetricsForDpi { + // This function is only supported on windows 10 + let dpi = self.scale().x() * SCALE_TARGET_DPI; + // Height of the different parts that make the titlebar + let border = func(SM_CXPADDEDBORDER, dpi as u32); + let frame = func(SM_CYSIZEFRAME, dpi as u32); + let caption = func(SM_CYCAPTION, dpi as u32); + // Maximized window titlebar height is just the caption + if (style & WS_MAXIMIZE) != 0 { + s.rgrc[0].top -= (caption) as i32; + } + // Normal window titlebar height is a combination of border frame and caption + else { + s.rgrc[0].top -= (border+frame+caption) as i32; + } + + } + // Support for windows 8.1 + else { + // Note: With SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) that we use on 8.1, + // Windows will not scale the titlebar area when DPI changes. + // Instead it is "stuck" at the DPI setting the window was created with. + // GetSystemMetrics() also reports values with the DPI the window was created with. + let border = GetSystemMetrics(SM_CXPADDEDBORDER); + let frame = GetSystemMetrics(SM_CYSIZEFRAME); + let caption = GetSystemMetrics(SM_CYCAPTION); + if (style & WS_MAXIMIZE) != 0 { + s.rgrc[0].top -= (caption) as i32; + } else { + s.rgrc[0].top -= (border+frame+caption) as i32; + } + } + } + } + None + }, + WM_NCHITTEST => unsafe { + let mut hit = DefWindowProcW(hwnd, msg, wparam, lparam); + if !self.has_titlebar() { + let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0}; + if GetWindowRect(hwnd, &mut rect) == 0 { + warn!( + "failed to get window rect: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + let a = HIWORD(lparam as u32) as i16 as i32 - rect.top; + if (a == 0) && (hit != HTTOPLEFT) && (hit != HTTOPRIGHT) && self.resizable() { + hit = HTTOP; + } + } + if hit != HTTOP { + let mouseDown = GetAsyncKeyState(VK_LBUTTON) < 0; + + if self.with_window_state(|state| state.handle_titlebar.get()) && !mouseDown { + self.with_window_state(move |state| state.handle_titlebar.set(false)); + }; + + if self.with_window_state(|state| state.handle_titlebar.get()) && hit == HTCLIENT { + hit = HTCAPTION; + } + } + Some(hit) + }, WM_ENTERSIZEMOVE => unsafe { if let Ok(mut s) = self.state.try_borrow_mut() { let s = s.as_mut().unwrap(); s.handler.prepare_paint(); if s.dcomp_state.is_some() { - let rt = paint::create_render_target(&self.d2d_factory, hwnd); + let scale = self.scale(); + let rt = paint::create_render_target(&self.d2d_factory, hwnd, scale); s.render_target = rt.ok(); { let rect_dp = self.area().size_dp().to_rect(); @@ -537,10 +856,13 @@ impl WndProc for MyWndProc { None }, WM_SIZE => unsafe { + let width = LOWORD(lparam as u32) as u32; + let height = HIWORD(lparam as u32) as u32; + if width == 0 || height == 0 { + return Some(0); + } if let Ok(mut s) = self.state.try_borrow_mut() { let s = s.as_mut().unwrap(); - let width = LOWORD(lparam as u32) as u32; - let height = HIWORD(lparam as u32) as u32; let scale = self.scale(); let area = ScaledArea::from_px((width as f64, height as f64), scale); let size_dp = area.size_dp(); @@ -897,6 +1219,10 @@ impl WndProc for MyWndProc { None } } + DS_HANDLE_DEFERRED => { + self.handle_deferred_queue(); + Some(0) + } _ => None, } } @@ -912,8 +1238,10 @@ impl WindowBuilder { resizable: true, show_titlebar: true, present_strategy: Default::default(), - size: Size::new(500.0, 400.0), + size: Size::new(CW_USEDEFAULT as f64, CW_USEDEFAULT as f64), min_size: None, + position: Point::new(CW_USEDEFAULT as f64, CW_USEDEFAULT as f64), + state: window::WindowState::RESTORED, } } @@ -935,7 +1263,6 @@ impl WindowBuilder { } pub fn show_titlebar(&mut self, show_titlebar: bool) { - // TODO: Use this in `self.build` self.show_titlebar = show_titlebar; } @@ -947,6 +1274,14 @@ impl WindowBuilder { self.menu = Some(menu); } + pub fn set_position(&mut self, position : Point) { + self.position = position; + } + + pub fn set_window_state(&mut self, state: window::WindowState) { + self.state = state; + } + pub fn build(self) -> Result { unsafe { let class_name = super::util::CLASS_NAME.to_wide(); @@ -961,16 +1296,7 @@ impl WindowBuilder { present_strategy: self.present_strategy, }; - // Simple scaling based on System DPI - let scale_factor = if let Some(func) = OPTIONAL_FUNCTIONS.GetDpiForSystem { - // Only supported on Windows 10 - func() as f64 / SCALE_TARGET_DPI - } 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 - 1.0 - }; - let scale = Scale::new(scale_factor, scale_factor); + let scale = Scale::new(1.0, 1.0); let area = ScaledArea::from_dp(self.size, scale); let size_px = area.size_px(); @@ -991,6 +1317,10 @@ impl WindowBuilder { wndproc: Box::new(wndproc), idle_queue: Default::default(), timers: Arc::new(Mutex::new(TimerSlots::new(1))), + deferred_queue: RefCell::new(DeferredQueue::new()), + has_titlebar: Cell::new(self.show_titlebar), + is_resizable: Cell::new(self.resizable), + handle_titlebar: Cell::new(false), }; let win = Rc::new(window); let handle = WindowHandle { @@ -1015,18 +1345,21 @@ impl WindowBuilder { if !self.resizable { dwStyle &= !(WS_THICKFRAME | WS_MAXIMIZEBOX); } - + if !self.show_titlebar { + dwStyle &= !(WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPED); + } let mut dwExStyle = 0; if self.present_strategy == PresentStrategy::Flip { dwExStyle |= WS_EX_NOREDIRECTIONBITMAP; } + let hwnd = create_window( dwExStyle, class_name.as_ptr(), self.title.to_wide().as_ptr(), dwStyle, - CW_USEDEFAULT, - CW_USEDEFAULT, + self.position.x as i32, + self.position.y as i32, size_px.width as i32, size_px.height as i32, 0 as HWND, @@ -1042,6 +1375,10 @@ impl WindowBuilder { if let Some(accels) = accels { register_accel(hwnd, &accels); } + + handle.set_size(handle.get_size()); + handle.set_window_state(self.state); + Ok(handle) } } @@ -1312,35 +1649,105 @@ impl WindowHandle { } } - // TODO: Implement this - pub fn show_titlebar(&self, _show_titlebar: bool) {} + pub fn show_titlebar(&self, show_titlebar: bool) { + if let Some(w) = self.state.upgrade() { + w.has_titlebar.set(show_titlebar); + DeferredQueue::add(self.state.clone(), DeferredOp::DecorationChanged()); + } + } - pub fn resizable(&self, resizable: bool) { + // Sets the position of the window in virtual screen coordinates + pub fn set_position(&self, position: Point) { + DeferredQueue::add(self.state.clone(), DeferredOp::SetWindowSizeState(window::WindowState::RESTORED)); + DeferredQueue::add(self.state.clone(), DeferredOp::SetPosition(position)); + } + + // Gets the position of the window in virtual screen coordinates + pub fn get_position(&self) -> Point { if let Some(w) = self.state.upgrade() { let hwnd = w.hwnd.get(); unsafe { - let mut style = GetWindowLongPtrW(hwnd, GWL_STYLE); - if style == 0 { + let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0}; + if GetWindowRect(hwnd, &mut rect) == 0 { warn!( - "failed to get window style: {}", + "failed to get window rect: {}", Error::Hr(HRESULT_FROM_WIN32(GetLastError())) ); - return; - } + }; + return Point::new(rect.left as f64, rect.top as f64) + } + } + Point::new(0.0, 0.0) + } - if resizable { - style |= (WS_THICKFRAME | WS_MAXIMIZEBOX) as WindowLongPtr; - } else { - style &= !(WS_THICKFRAME | WS_MAXIMIZEBOX) as WindowLongPtr; - } + // Sets the size of the window in DP + pub fn set_size(&self, size: Size) { + DeferredQueue::add(self.state.clone(), DeferredOp::SetSize(size)); + } + + // Gets the size of the window in pixels + pub fn get_size(&self) -> Size { + if let Some(w) = self.state.upgrade() { + let hwnd = w.hwnd.get(); + unsafe { + let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0}; + if GetWindowRect(hwnd, &mut rect) == 0 { + warn!( + "failed to get window rect: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + return Size::new(width as f64, height as f64); + } + } + Size::new(0.0, 0.0) + } + + pub fn resizable(&self, resizable: bool) { + if let Some(w) = self.state.upgrade() { + w.is_resizable.set(resizable); + DeferredQueue::add(self.state.clone(), DeferredOp::DecorationChanged()); + } + } - if SetWindowLongPtrW(hwnd, GWL_STYLE, style) == 0 { + // Sets the window state. + pub fn set_window_state(&self, state : window::WindowState) { + DeferredQueue::add(self.state.clone(), DeferredOp::SetWindowSizeState(state)); + } + + // Gets the window state. + pub fn get_window_state(&self) -> window::WindowState { + // We can not store state internally because it could be modified externally. + if let Some(w) = self.state.upgrade() { + let hwnd = w.hwnd.get(); + unsafe { + let style = GetWindowLongPtrW(hwnd, GWL_STYLE) as u32; + if style == 0 { warn!( - "failed to set the window style: {}", + "failed to get window style: {}", Error::Hr(HRESULT_FROM_WIN32(GetLastError())) ); } + if (style & WS_MAXIMIZE) != 0 { + window::WindowState::MAXIMIZED + } else if (style & WS_MINIMIZE) != 0 { + window::WindowState::MINIMIZED + } else { + window::WindowState::RESTORED + } } + } else { + window::WindowState::RESTORED + } + } + + + // Allows windows to handle a custom titlebar like it was the default one. + pub fn handle_titlebar(&self, val: bool) { + if let Some(w) = self.state.upgrade() { + w.handle_titlebar.set(val); } } diff --git a/druid-shell/src/platform/x11/mod.rs b/druid-shell/src/platform/x11/mod.rs index d80a4cde82..b6d47debbd 100644 --- a/druid-shell/src/platform/x11/mod.rs +++ b/druid-shell/src/platform/x11/mod.rs @@ -39,3 +39,4 @@ pub mod error; pub mod keycodes; pub mod menu; pub mod window; +pub mod screen; diff --git a/druid-shell/src/platform/x11/screen.rs b/druid-shell/src/platform/x11/screen.rs new file mode 100644 index 0000000000..843b267714 --- /dev/null +++ b/druid-shell/src/platform/x11/screen.rs @@ -0,0 +1,22 @@ +// Copyright 2020 The Druid 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. + +//! X11 Monitors and Screen information. + +use crate::screen::Monitor; + +pub(crate) fn get_monitors() -> Vec { + log::warn!("Screen::get_monitors() is currently unimplemented for X11 platforms."); + Vec::new() +} diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 96eaa33917..c8186bd9c0 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -46,12 +46,14 @@ use crate::piet::{Piet, PietText, RenderContext}; use crate::region::Region; use crate::scale::Scale; use crate::window::{IdleToken, TimerToken, WinHandler}; +use crate::window; use super::application::Application; use super::keycodes; use super::menu::Menu; use super::util::{self, Timer}; + /// A version of XCB's `xcb_visualtype_t` struct. This was copied from the [example] in x11rb; it /// is used to interoperate with cairo. /// @@ -127,6 +129,14 @@ impl WindowBuilder { log::warn!("WindowBuilder::show_titlebar is currently unimplemented for X11 platforms."); } + pub fn set_position(&mut self, _position: Point) { + log::warn!("WindowBuilder::set_position is currently unimplemented for X11 platforms."); + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowBuilder::set_window_state is currently unimplemented for X11 platforms."); + } + pub fn set_title>(&mut self, title: S) { self.title = title.into(); } @@ -1350,6 +1360,37 @@ impl WindowHandle { } } + pub fn set_position(&self, _position: Point) { + log::warn!("WindowHandle::set_position is currently unimplemented for X11 platforms."); + } + + pub fn get_position(&self) -> Point { + log::warn!("WindowHandle::get_position is currently unimplemented for X11 platforms."); + Point::new(0.0, 0.0) + } + + pub fn set_size(&self, _size: Size) { + log::warn!("WindowHandle::set_size is currently unimplemented for X11 platforms."); + } + + pub fn get_size(&self) -> Size { + log::warn!("WindowHandle::get_size is currently unimplemented for X11 platforms."); + Size::new(0.0, 0.0) + } + + pub fn set_window_state(&self, _state: window::WindowState) { + log::warn!("WindowHandle::set_window_state is currently unimplemented for X11 platforms."); + } + + pub fn get_window_state(&self) -> window::WindowState { + log::warn!("WindowHandle::get_window_state is currently unimplemented for X11 platforms."); + window::WindowState::RESTORED + } + + pub fn handle_titlebar(&self, _val: bool) { + log::warn!("WindowHandle::handle_titlebar is currently unimplemented for X11 platforms."); + } + pub fn bring_to_front_and_focus(&self) { if let Some(w) = self.window.upgrade() { w.bring_to_front_and_focus(); diff --git a/druid-shell/src/screen.rs b/druid-shell/src/screen.rs new file mode 100644 index 0000000000..a39b412e90 --- /dev/null +++ b/druid-shell/src/screen.rs @@ -0,0 +1,97 @@ +// Copyright 2020 The Druid 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. + +//! Module to get information about monitors + +use crate::kurbo::Rect; +use crate::platform; +use std::fmt; +use std::fmt::Display; + +/// Monitor struct containing data about a monitor on the system +/// +/// Use Screen::get_monitors() to return a Vec of all the monitors on the system +#[derive(Clone)] +pub struct Monitor { + primary: bool, + rect: Rect, + // TODO: Work area, cross_platform + // https://developer.apple.com/documentation/appkit/nsscreen/1388369-visibleframe + // https://developer.gnome.org/gdk3/stable/GdkMonitor.html#gdk-monitor-get-workarea + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo + // Unsure about x11 + work_rect: Rect, +} + +impl Monitor { + #[allow(dead_code)] + pub(crate) fn new(primary: bool, rect: Rect, work_rect: Rect) -> Self { + Monitor { + primary, + rect, + work_rect, + } + } + /// Returns true if the monitor is the primary monitor. + /// The primary monitor has its origin at (0, 0) in virtual screen coordinates. + pub fn is_primary(&self) -> bool { + self.primary + } + /// Returns the monitor rectangle in virtual screen coordinates. + pub fn virtual_rect(&self) -> Rect { + self.rect + } + + /// Returns the monitor working rectangle in virtual screen coordinates. + /// The working rectangle excludes certain things like the dock and menubar on mac, + /// and the taskbar on windows. + pub fn virtual_work_rect(&self) -> Rect { + self.work_rect + } +} + +impl Display for Monitor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.primary { + write!(f, "Primary ")?; + } else { + write!(f, "Secondary ")?; + } + write!( + f, + "({}, {})({}, {})", + self.rect.x0, self.rect.x1, self.rect.y0, self.rect.y1 + )?; + Ok(()) + } +} + +/// Information about the screen and monitors +pub struct Screen {} +impl Screen { + /// Returns a vector of all the [`monitors`] on the system. + /// + /// [`monitors`]: struct.Monitor.html + pub fn get_monitors() -> Vec { + platform::screen::get_monitors() + } + + /// Returns the bounding rectangle of the total virtual screen space in pixels. + pub fn get_display_rect() -> Rect { + Self::get_monitors() + .iter() + .map(|x| x.virtual_rect()) + .fold(Rect::ZERO, |a, b| a.union(b)) + } +} diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index b961d6bd79..b68e7f5007 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -92,6 +92,14 @@ impl IdleToken { } } +/// Contains the different states a Window can be in. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum WindowState { + MAXIMIZED, + MINIMIZED, + RESTORED, +} + /// A handle to a platform window object. #[derive(Clone, Default)] pub struct WindowHandle(platform::WindowHandle); @@ -115,11 +123,80 @@ impl WindowHandle { self.0.resizable(resizable) } - /// Set whether the window should show titlebar + /// Sets the state of the window. + /// + /// [`state`]: enum.WindowState.html + pub fn set_window_state(&self, state: WindowState) { + self.0.set_window_state(state); + } + + /// Gets the state of the window. + /// + /// [`state`]: enum.WindowState.html + pub fn get_window_state(&self) -> WindowState { + self.0.get_window_state() + } + + /// Allows the operating system to handle a custom titlebar + /// like the default one. + /// + /// It should be used on Event::MouseMove in a widget: + /// `Event::MouseMove(_) => { + /// if ctx.is_hot() { + /// ctx.window().handle_titlebar(true); + /// } else { + /// ctx.window().handle_titlebar(false); + /// } + ///}` + /// + /// This might not work or behave the same across all platforms. + pub fn handle_titlebar(&self, val: bool) { + self.0.handle_titlebar(val); + } + + /// Set whether the window should show titlebar. pub fn show_titlebar(&self, show_titlebar: bool) { self.0.show_titlebar(show_titlebar) } + /// Sets the position of the window in virtual screen coordinates. + /// [`position`] The position in pixels. + /// + /// [`position`]: struct.Point.html + pub fn set_position(&self, position: Point) { + self.0.set_position(position) + } + + /// Returns the position in virtual screen coordinates. + /// [`Point`] The position in pixels. + /// + /// [`Point`]: struct.Point.html + pub fn get_position(&self) -> Point { + self.0.get_position() + } + + /// Set the window's 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(&self, size: Size) { + self.0.set_size(size) + } + + /// Gets the window size. + /// [`Size`] Window size in pixels. + /// + /// [`Size`]: struct.Size.html + pub fn get_size(&self) -> Size { + self.0.get_size() + } + /// Bring this window to the front of the window stack and give it focus. pub fn bring_to_front_and_focus(&self) { self.0.bring_to_front_and_focus() @@ -268,16 +345,24 @@ impl WindowBuilder { self.0.set_min_size(size) } - /// Set whether the window should be resizable + /// Set whether the window should be resizable. pub fn resizable(&mut self, resizable: bool) { self.0.resizable(resizable) } - /// Set whether the window should have a titlebar and decorations + /// Set whether the window should have a titlebar and decorations. pub fn show_titlebar(&mut self, show_titlebar: bool) { self.0.show_titlebar(show_titlebar) } + /// Sets the initial window position in virtual screen coordinates. + /// [`position`] Position in pixels. + /// + /// [`position`]: struct.Point.html + pub fn set_position(&mut self, position: Point) { + self.0.set_position(position); + } + /// Set the window's initial title. pub fn set_title(&mut self, title: impl Into) { self.0.set_title(title) @@ -288,6 +373,13 @@ impl WindowBuilder { self.0.set_menu(menu.into_inner()) } + /// Sets the initial state of the window. + /// + /// [`state`]: enum.WindowState.html + pub fn set_window_state(&mut self, state: WindowState) { + self.0.set_window_state(state); + } + /// Attempt to construct the platform window. /// /// If this fails, your application should exit. diff --git a/druid/src/app.rs b/druid/src/app.rs index 75887ad48f..172d4b20e9 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -15,7 +15,7 @@ //! Window building and app lifecycle. use crate::ext_event::{ExtEventHost, ExtEventSink}; -use crate::kurbo::Size; +use crate::kurbo::{Point, Size}; use crate::shell::{Application, Error as PlatformError, WindowBuilder, WindowHandle}; use crate::widget::LabelText; use crate::win_handler::{AppHandler, AppState}; @@ -24,6 +24,8 @@ use crate::{ theme, AppDelegate, Data, DruidHandler, Env, LocalizedString, MenuDesc, Widget, WidgetExt, }; +use druid_shell::WindowState; + /// A function that modifies the initial environment. type EnvSetupFn = dyn FnOnce(&mut Env, &T); @@ -44,9 +46,11 @@ pub struct WindowDesc { pub(crate) title: LabelText, pub(crate) size: Option, pub(crate) min_size: Option, + pub(crate) position: Option, pub(crate) menu: Option>, pub(crate) resizable: bool, pub(crate) show_titlebar: bool, + pub(crate) state: WindowState, /// The `WindowId` that will be assigned to this window. /// /// This can be used to track a window from when it is launched and when @@ -163,9 +167,11 @@ impl WindowDesc { title: LocalizedString::new("app-name").into(), size: None, min_size: None, + position: None, menu: MenuDesc::platform_default(), resizable: true, show_titlebar: true, + state: WindowState::RESTORED, id: WindowId::next(), } } @@ -236,6 +242,21 @@ impl WindowDesc { self } + /// Sets the initial window position in virtual screen coordinates. + /// [`position`] Position in pixels. + /// + /// [`position`]: struct.Point.html + pub fn set_position(mut self, position: Point) -> Self { + self.position = Some(position); + self + } + + /// Set initial state for the window. + pub fn set_window_state(mut self, state: WindowState) -> Self { + self.state = state; + self + } + /// Attempt to create a platform window from this `WindowDesc`. pub(crate) fn build_native( mut self, @@ -262,6 +283,12 @@ impl WindowDesc { builder.set_min_size(min_size); } + if let Some(position) = self.position { + builder.set_position(position); + } + + builder.set_window_state(self.state); + builder.set_title(self.title.display_text()); if let Some(menu) = platform_menu { builder.set_menu(menu); diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 7014145214..c9d4bb2006 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -179,7 +179,8 @@ pub use shell::keyboard_types; pub use shell::{ Application, Clipboard, ClipboardFormat, Code, Cursor, Error as PlatformError, FileDialogOptions, FileInfo, FileSpec, FormatId, HotKey, KbKey, KeyEvent, Location, Modifiers, - MouseButton, MouseButtons, RawMods, Region, Scalable, Scale, SysMods, TimerToken, WindowHandle, + Monitor, MouseButton, MouseButtons, RawMods, Region, Scalable, Scale, Screen, SysMods, + TimerToken, WindowHandle, WindowState, }; pub use crate::core::WidgetPod;