Compare commits
11 commits
be57d63b48
...
4a166606c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a166606c0 | |||
| aad7b2a2d3 | |||
| b9629224d9 | |||
| 54860b193a | |||
| ca9b399733 | |||
| 860642b059 | |||
| 045e352e2e | |||
| a95d66c874 | |||
| db2f5e18e9 | |||
|
|
e40a5318f7 |
||
|
|
dd4c8309d9 |
19 changed files with 1274 additions and 89 deletions
|
|
@ -52,6 +52,7 @@ mod performance_stats;
|
|||
pub mod memory_watchdog;
|
||||
pub mod perf_monitor;
|
||||
pub mod permission;
|
||||
mod screen;
|
||||
mod texture;
|
||||
mod uniform_buffer;
|
||||
mod window;
|
||||
|
|
@ -215,6 +216,7 @@ pub use {
|
|||
unregister_media_playback_session, MediaPlaybackSessionId,
|
||||
},
|
||||
script::vm::*,
|
||||
screen::{fit_window_rect_to_screens, ScreenGeom, MIN_WINDOW_SIZE},
|
||||
shared_bytes::{MappedBytes, SharedBytes, SharedBytesStats},
|
||||
texture::{
|
||||
image_cache_use_mipmaps, Texture, TextureAnimation, TextureFormat, TextureId,
|
||||
|
|
|
|||
|
|
@ -1539,11 +1539,12 @@ impl Cx {
|
|||
match op {
|
||||
CxOsOp::CreateWindow(window_id) => {
|
||||
let window = &mut self.windows[window_id];
|
||||
let (create_position, create_inner_size) = window.create_geom();
|
||||
let mut metal_window = MetalWindow::new(
|
||||
window_id,
|
||||
&metal_cx,
|
||||
window.create_inner_size.unwrap_or(dvec2(800., 600.)),
|
||||
window.create_position,
|
||||
create_inner_size,
|
||||
create_position,
|
||||
&window.create_title,
|
||||
window.is_fullscreen,
|
||||
window.macos,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use {
|
|||
MouseUpEvent, ScrollEvent, ScrollPhase, TextInputEvent, WindowCloseRequestedEvent,
|
||||
WindowDragQueryEvent, WindowDragQueryResponse, WindowGeom, WindowGeomChangeEvent,
|
||||
},
|
||||
makepad_math::{Rect, Vec2d},
|
||||
makepad_math::{dvec2, Rect, Vec2d},
|
||||
os::{
|
||||
apple::apple_sys::*,
|
||||
apple::apple_util::str_to_nsstring,
|
||||
|
|
@ -18,6 +18,7 @@ use {
|
|||
macos_event::MacosEvent,
|
||||
},
|
||||
},
|
||||
screen::{clamp_point_to_screens, fit_window_rect_to_screens, ScreenGeom},
|
||||
window::{
|
||||
MacosWindowChrome, MacosWindowConfig, MacosWindowKind, MacosWindowLevel,
|
||||
WindowBackdrop, WindowId, WindowVisuals,
|
||||
|
|
@ -255,9 +256,13 @@ impl MacosWindow {
|
|||
let () = msg_send![self.view, setAllowedTouchTypes: 2u64];
|
||||
|
||||
let left_top = if let Some(position) = position {
|
||||
// A restored position can name a display that is gone. Pinning it before the
|
||||
// window is built keeps it from being ordered on screen somewhere unreachable;
|
||||
// `fit_to_screens` below corrects the finished frame.
|
||||
let pinned = clamp_point_to_screens(&macos_screens(), position);
|
||||
NSPoint {
|
||||
x: position.x as f64,
|
||||
y: position.y as f64,
|
||||
x: pinned.x,
|
||||
y: pinned.y,
|
||||
}
|
||||
} else {
|
||||
NSPoint { x: 0., y: 0. }
|
||||
|
|
@ -350,6 +355,11 @@ impl MacosWindow {
|
|||
if position.is_none() {
|
||||
let () = msg_send![self.window, center];
|
||||
}
|
||||
if !is_fullscreen {
|
||||
// A restored size and position are only as good as the display arrangement
|
||||
// they were saved on; a fullscreen window is AppKit's to place.
|
||||
self.fit_to_screens();
|
||||
}
|
||||
|
||||
let input_context: ObjcId = msg_send![self.view, inputContext];
|
||||
let () = msg_send![input_context, invalidateCharacterCoordinates];
|
||||
|
|
@ -763,12 +773,34 @@ impl MacosWindow {
|
|||
let mut window_frame: NSRect = unsafe { msg_send![self.window, frame] };
|
||||
window_frame.origin.x = pos.x as f64;
|
||||
window_frame.origin.y = pos.y as f64;
|
||||
//not very nice: CGDisplay::main().pixels_high() as f64
|
||||
// A caller placing the window cannot know the display arrangement it is placing
|
||||
// into, so the request is fitted to the displays that are actually attached.
|
||||
let fitted = fit_window_rect_to_screens(&macos_screens(), rect_of(window_frame));
|
||||
unsafe {
|
||||
let () = msg_send![self.window, setFrame: window_frame display: YES];
|
||||
let () = msg_send![self.window, setFrame: ns_rect_of(fitted) display: YES];
|
||||
};
|
||||
}
|
||||
|
||||
/// Moves and resizes the window so it sits entirely within one display's visible frame.
|
||||
///
|
||||
/// See `crate::screen::fit_window_rect_to_screens` for what counts as a fit and why it
|
||||
/// is unconditional. A window that already fits is left untouched.
|
||||
pub fn fit_to_screens(&mut self) {
|
||||
let screens = macos_screens();
|
||||
if screens.is_empty() {
|
||||
return;
|
||||
}
|
||||
let frame: NSRect = unsafe { msg_send![self.window, frame] };
|
||||
let current = rect_of(frame);
|
||||
let fitted = fit_window_rect_to_screens(&screens, current);
|
||||
if fitted == current {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let () = msg_send![self.window, setFrame: ns_rect_of(fitted) display: YES];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> Vec2d {
|
||||
let window_frame: NSRect = unsafe { msg_send![self.window, frame] };
|
||||
Vec2d {
|
||||
|
|
@ -1183,3 +1215,52 @@ pub fn get_cocoa_window(this: &Object) -> &mut MacosWindow {
|
|||
&mut *(ptr as *mut MacosWindow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an `NSRect` to makepad's rectangle, leaving Cocoa's bottom-left origin as it is.
|
||||
fn rect_of(r: NSRect) -> Rect {
|
||||
Rect {
|
||||
pos: dvec2(r.origin.x, r.origin.y),
|
||||
size: dvec2(r.size.width, r.size.height),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts makepad's rectangle back to an `NSRect`.
|
||||
fn ns_rect_of(r: Rect) -> NSRect {
|
||||
NSRect {
|
||||
origin: NSPoint {
|
||||
x: r.pos.x,
|
||||
y: r.pos.y,
|
||||
},
|
||||
size: NSSize {
|
||||
width: r.size.x,
|
||||
height: r.size.y,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The displays currently attached, in Cocoa's global point space (bottom-left origin) —
|
||||
/// the space an `NSWindow` frame is expressed in.
|
||||
pub fn macos_screens() -> Vec<ScreenGeom> {
|
||||
unsafe {
|
||||
let screens: ObjcId = msg_send![class!(NSScreen), screens];
|
||||
let count: usize = msg_send![screens, count];
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for index in 0..count {
|
||||
let screen: ObjcId = msg_send![screens, objectAtIndex: index];
|
||||
if screen == nil {
|
||||
continue;
|
||||
}
|
||||
let frame: NSRect = msg_send![screen, frame];
|
||||
let visible: NSRect = msg_send![screen, visibleFrame];
|
||||
out.push(ScreenGeom {
|
||||
bounds: rect_of(frame),
|
||||
work_area: rect_of(visible),
|
||||
// Element zero of `NSScreen.screens` is the display holding the menu bar,
|
||||
// which is the one Cocoa places windows against; `mainScreen` follows the
|
||||
// key window instead and would move under the app.
|
||||
is_primary: index == 0,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
use {
|
||||
crate::cx::Cx,
|
||||
std::{fs::File, io::prelude::*, rc::Rc, time::SystemTime},
|
||||
std::{
|
||||
fs::File,
|
||||
io::prelude::*,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::OnceLock,
|
||||
time::SystemTime,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
|
|
@ -10,21 +17,58 @@ pub enum EventFlow {
|
|||
Exit,
|
||||
}
|
||||
|
||||
/// The directory holding the running executable, queried once.
|
||||
fn exe_dir() -> Option<&'static Path> {
|
||||
static EXE_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
EXE_DIR
|
||||
.get_or_init(|| {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(Path::to_path_buf))
|
||||
})
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
/// Resolves a relative resource path against the directory holding the executable.
|
||||
///
|
||||
/// Packaged desktop layouts ship resources beside the executable and address them through a
|
||||
/// relative package root, which a plain relative open resolves against the process working
|
||||
/// directory instead. Any launcher that does not set a working directory — a URL-protocol
|
||||
/// handler, a file association, a service, a shortcut without one — then starts the app in an
|
||||
/// unrelated directory and every resource open fails, leaving a window that draws its shapes
|
||||
/// but has no fonts, icons or images. Callers retry through here so the executable's own
|
||||
/// directory is searched as well. Returns `None` for an absolute path (already anchored) and
|
||||
/// when the executable path is unavailable.
|
||||
pub fn exe_relative_path(rel: impl AsRef<Path>) -> Option<PathBuf> {
|
||||
let rel = rel.as_ref();
|
||||
if rel.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
Some(exe_dir()?.join(rel))
|
||||
}
|
||||
|
||||
/// Reads a file at `path`, falling back to the same path resolved against the executable's
|
||||
/// directory. Returns `None` when neither location holds a readable file.
|
||||
pub fn read_file_cwd_or_exe_relative(path: impl AsRef<Path>) -> Option<Vec<u8>> {
|
||||
fn read(path: &Path) -> Option<Vec<u8>> {
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
File::open(path).ok()?.read_to_end(&mut buffer).ok()?;
|
||||
Some(buffer)
|
||||
}
|
||||
let path = path.as_ref();
|
||||
read(path).or_else(|| read(&exe_relative_path(path)?))
|
||||
}
|
||||
|
||||
// lets start a websocket thread
|
||||
|
||||
impl Cx {
|
||||
pub fn native_load_dependencies(&mut self) {
|
||||
for (path, dep) in &mut self.dependencies {
|
||||
if let Ok(mut file_handle) = File::open(path) {
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
if file_handle.read_to_end(&mut buffer).is_ok() {
|
||||
dep.data = Some(Ok(Rc::new(buffer)));
|
||||
} else {
|
||||
dep.data = Some(Err("read_to_end failed".to_string()));
|
||||
}
|
||||
if let Some(buffer) = read_file_cwd_or_exe_relative(path) {
|
||||
dep.data = Some(Ok(Rc::new(buffer)));
|
||||
} else {
|
||||
println!("Could not load resource {}", path);
|
||||
dep.data = Some(Err("File! open failed".to_string()));
|
||||
dep.data = Some(Err(format!("Could not read resource {}", path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -633,10 +633,13 @@ impl Cx {
|
|||
}
|
||||
|
||||
let window = &mut self.windows[window_id];
|
||||
let inner_size = window
|
||||
.create_inner_size
|
||||
.unwrap_or_else(|| dvec2(1920.0, 1080.0));
|
||||
let position = window.create_position.unwrap_or_else(|| dvec2(0.0, 0.0));
|
||||
let (position, inner_size) = window.create_geom();
|
||||
let inner_size = if window.create_inner_size.is_some() {
|
||||
inner_size
|
||||
} else {
|
||||
dvec2(1920.0, 1080.0)
|
||||
};
|
||||
let position = position.unwrap_or_else(|| dvec2(0.0, 0.0));
|
||||
let dpi_factor = configured_headless_dpi();
|
||||
|
||||
let state = &mut windows[window_id.id()];
|
||||
|
|
|
|||
|
|
@ -634,6 +634,7 @@ impl WaylandCx {
|
|||
let compositor = state.compositor.as_ref().unwrap();
|
||||
let wm_base = state.wm_base.as_ref().unwrap();
|
||||
let window = &cx.windows[window_id];
|
||||
let (create_position, create_inner_size) = window.create_geom();
|
||||
let app_id = if window.create_app_id.is_empty() {
|
||||
"Makepad"
|
||||
} else {
|
||||
|
|
@ -650,8 +651,8 @@ impl WaylandCx {
|
|||
state.shm.as_ref(),
|
||||
self.qhandle.as_ref().unwrap(),
|
||||
gl_cx,
|
||||
window.create_inner_size.unwrap_or(dvec2(800., 600.)),
|
||||
window.create_position,
|
||||
create_inner_size,
|
||||
create_position,
|
||||
&window.create_title,
|
||||
app_id,
|
||||
window.is_fullscreen,
|
||||
|
|
@ -765,6 +766,9 @@ impl WaylandCx {
|
|||
}
|
||||
}
|
||||
CxOsOp::ResizeWindow(window_id, size) => {}
|
||||
// A Wayland client is not told where its windows are and cannot move them;
|
||||
// the compositor owns placement, so a window here is never left off-screen
|
||||
// by a restored position the way it can be on Windows, macOS and X11.
|
||||
CxOsOp::RepositionWindow(window_id, size) => {}
|
||||
CxOsOp::SetWindowVisuals(_window_id, visuals) => {
|
||||
if visuals.backdrop != crate::window::WindowBackdrop::None {
|
||||
|
|
|
|||
|
|
@ -87,8 +87,19 @@ impl WaylandWindow {
|
|||
}
|
||||
base_surface.commit();
|
||||
|
||||
let wl_egl_surface =
|
||||
WlEglSurface::new(base_surface.id(), inner_size.x as i32, inner_size.y as i32).unwrap();
|
||||
// `wl_egl_window_create` rejects a non-positive extent, and a float-to-int cast turns
|
||||
// both a negative and a NaN into zero, so the requested size is floored before the
|
||||
// call rather than allowed to panic an app at startup over a bad saved size.
|
||||
let egl_w = (inner_size.x as i32).max(1);
|
||||
let egl_h = (inner_size.y as i32).max(1);
|
||||
let wl_egl_surface = match WlEglSurface::new(base_surface.id(), egl_w, egl_h) {
|
||||
Ok(surface) => surface,
|
||||
Err(e) => {
|
||||
crate::error!("wl_egl_window_create failed at {egl_w}x{egl_h}: {e:?}");
|
||||
WlEglSurface::new(base_surface.id(), 800, 600)
|
||||
.expect("wl_egl_window_create failed at the fallback size too")
|
||||
}
|
||||
};
|
||||
let egl_surface = unsafe {
|
||||
(opengl_cx.libegl.eglCreateWindowSurface.unwrap())(
|
||||
opengl_cx.egl_display,
|
||||
|
|
|
|||
|
|
@ -550,11 +550,12 @@ impl X11Cx {
|
|||
CxOsOp::CreateWindow(window_id) => {
|
||||
let gl_cx = cx.os.opengl_cx.as_ref().unwrap();
|
||||
let window = &cx.windows[window_id];
|
||||
let (create_position, create_inner_size) = window.create_geom();
|
||||
let opengl_window = OpenglWindow::new(
|
||||
window_id,
|
||||
gl_cx,
|
||||
window.create_inner_size.unwrap_or(dvec2(800., 600.)),
|
||||
window.create_position,
|
||||
create_inner_size,
|
||||
create_position,
|
||||
&window.create_title,
|
||||
&window.create_app_id,
|
||||
window.is_fullscreen,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pub mod linux_x11;
|
||||
pub mod linux_x11_stdin;
|
||||
pub mod opengl_x11;
|
||||
pub mod x11_screen;
|
||||
pub mod x11_sys;
|
||||
pub mod xlib_app;
|
||||
pub mod xlib_event;
|
||||
|
|
|
|||
127
platform/src/os/linux/x11/x11_screen.rs
Normal file
127
platform/src/os/linux/x11/x11_screen.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
//! Display geometry for the X11 backend.
|
||||
|
||||
use {
|
||||
self::super::{x11_sys, xlib_app::get_xlib_app_global},
|
||||
crate::{makepad_math::*, screen::ScreenGeom},
|
||||
std::{
|
||||
ffi::CString,
|
||||
mem,
|
||||
os::raw::{c_int, c_long, c_uchar, c_ulong},
|
||||
ptr,
|
||||
},
|
||||
};
|
||||
|
||||
/// Reads a `CARDINAL` array property from the root window.
|
||||
///
|
||||
/// Returns an empty vector when the property is absent, which is the normal answer from a
|
||||
/// window manager that does not implement the hint.
|
||||
unsafe fn root_cardinals(name: &str) -> Vec<c_long> {
|
||||
let display = get_xlib_app_global().display;
|
||||
let Ok(name) = CString::new(name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
// `only_if_exists` = true: never define the atom, only look one up.
|
||||
let atom = unsafe { x11_sys::XInternAtom(display, name.as_ptr(), 1) };
|
||||
if atom == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let root = unsafe {
|
||||
let screen = x11_sys::XDefaultScreen(display);
|
||||
x11_sys::XRootWindow(display, screen)
|
||||
};
|
||||
|
||||
let mut actual_type: x11_sys::Atom = 0;
|
||||
let mut actual_format: c_int = 0;
|
||||
let mut n_items: c_ulong = 0;
|
||||
let mut bytes_after: c_ulong = 0;
|
||||
let mut data: *mut c_uchar = ptr::null_mut();
|
||||
// A long_length of 64 covers 16 desktops' worth of four-value work areas; anything past
|
||||
// that is left unread rather than paged in.
|
||||
let status = unsafe {
|
||||
x11_sys::XGetWindowProperty(
|
||||
display,
|
||||
root,
|
||||
atom,
|
||||
0,
|
||||
64,
|
||||
0,
|
||||
x11_sys::AnyPropertyType as c_ulong,
|
||||
&mut actual_type,
|
||||
&mut actual_format,
|
||||
&mut n_items,
|
||||
&mut bytes_after,
|
||||
&mut data,
|
||||
)
|
||||
};
|
||||
// Xlib's `Success` is zero; the constant itself is not in the bindings.
|
||||
if status != 0 || data.is_null() {
|
||||
return Vec::new();
|
||||
}
|
||||
// Xlib hands back 32-bit properties widened to `long`, whatever the wire format says.
|
||||
let out = if actual_format == 32 {
|
||||
unsafe { std::slice::from_raw_parts(data as *const c_long, n_items as usize).to_vec() }
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
unsafe { x11_sys::XFree(data as *mut _) };
|
||||
out
|
||||
}
|
||||
|
||||
/// The X screen's full extent, from the root window's geometry.
|
||||
unsafe fn root_bounds() -> Option<Rect> {
|
||||
let display = get_xlib_app_global().display;
|
||||
let root = unsafe {
|
||||
let screen = x11_sys::XDefaultScreen(display);
|
||||
x11_sys::XRootWindow(display, screen)
|
||||
};
|
||||
let mut xwa = mem::MaybeUninit::<x11_sys::XWindowAttributes>::uninit();
|
||||
if unsafe { x11_sys::XGetWindowAttributes(display, root, xwa.as_mut_ptr()) } == 0 {
|
||||
return None;
|
||||
}
|
||||
let xwa = unsafe { xwa.assume_init() };
|
||||
if xwa.width <= 0 || xwa.height <= 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Rect {
|
||||
pos: dvec2(0.0, 0.0),
|
||||
size: dvec2(xwa.width as f64, xwa.height as f64),
|
||||
})
|
||||
}
|
||||
|
||||
/// The desktop area a window may occupy, in physical pixels — the coordinate space
|
||||
/// `XMoveWindow` and `XCreateWindow` take positions in.
|
||||
///
|
||||
/// This is one entry covering the whole X screen, not one per physical monitor: splitting a
|
||||
/// Xinerama screen into its heads needs libXinerama or libXrandr, and makepad links neither.
|
||||
/// It still keeps a window on the desktop and clear of the panels, which is what a restored
|
||||
/// position can get wrong. The extent comes from the root window, and the reserved edges
|
||||
/// from the EWMH `_NET_WORKAREA` hint of the current desktop, falling back to the full extent
|
||||
/// under a window manager that publishes neither.
|
||||
pub fn x11_screens() -> Vec<ScreenGeom> {
|
||||
let Some(bounds) = (unsafe { root_bounds() }) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let desktop = unsafe { root_cardinals("_NET_CURRENT_DESKTOP") }
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.max(0) as usize;
|
||||
let areas = unsafe { root_cardinals("_NET_WORKAREA") };
|
||||
let work_area = areas
|
||||
.chunks_exact(4)
|
||||
.nth(desktop)
|
||||
.or_else(|| areas.chunks_exact(4).next())
|
||||
.map(|a| Rect {
|
||||
pos: dvec2(a[0] as f64, a[1] as f64),
|
||||
size: dvec2(a[2] as f64, a[3] as f64),
|
||||
})
|
||||
.filter(|r| r.size.x > 0.0 && r.size.y > 0.0)
|
||||
.unwrap_or(bounds);
|
||||
|
||||
vec![ScreenGeom {
|
||||
bounds,
|
||||
work_area,
|
||||
is_primary: true,
|
||||
}]
|
||||
}
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
use {
|
||||
self::super::{x11_sys, xlib_app::*, xlib_event::XlibEvent},
|
||||
crate::{area::Area, cursor::MouseCursor, event::*, makepad_math::{Rect, Vec2d}, window::WindowId},
|
||||
crate::{
|
||||
area::Area, cursor::MouseCursor, event::*,
|
||||
makepad_math::{dvec2, Rect, Vec2d},
|
||||
os::linux::x11::x11_screen::x11_screens,
|
||||
screen::fit_window_rect_to_screens,
|
||||
window::WindowId,
|
||||
},
|
||||
std::{
|
||||
cell::Cell,
|
||||
ffi::{CStr, CString},
|
||||
|
|
@ -110,22 +116,26 @@ impl XlibWindow {
|
|||
| x11_sys::LeaveWindowMask) as c_long;
|
||||
|
||||
let dpi_factor = self.get_dpi_factor();
|
||||
// A restored size and position are only as good as the desktop layout they were
|
||||
// saved on, so the request is fitted before it reaches the server. Doing it here
|
||||
// covers the geometry, the size hints and the pre-map move alike.
|
||||
let (position, size) = fit_create_geom(position, size, dpi_factor);
|
||||
// Create a window
|
||||
// X11 encodes a window position as INT16 and an extent as CARD16, and a request
|
||||
// outside those ranges is a BadValue protocol error — which, with no error handler
|
||||
// installed, terminates the process. The fit above already keeps a placement on the
|
||||
// desktop; these clamps are what guarantee the request is expressible at all.
|
||||
let (create_x, create_y) = match position {
|
||||
Some(position) => (clamp_coord(position.x), clamp_coord(position.y)),
|
||||
None => (150, 60),
|
||||
};
|
||||
let window = x11_sys::XCreateWindow(
|
||||
display,
|
||||
root_window,
|
||||
if position.is_some() {
|
||||
position.unwrap().x
|
||||
} else {
|
||||
150.0
|
||||
} as i32,
|
||||
if position.is_some() {
|
||||
position.unwrap().y
|
||||
} else {
|
||||
60.0
|
||||
} as i32,
|
||||
(size.x * dpi_factor) as u32,
|
||||
(size.y * dpi_factor) as u32,
|
||||
create_x,
|
||||
create_y,
|
||||
clamp_extent(size.x * dpi_factor),
|
||||
clamp_extent(size.y * dpi_factor),
|
||||
0,
|
||||
visual_info.depth,
|
||||
x11_sys::InputOutput as u32,
|
||||
|
|
@ -738,6 +748,8 @@ impl XlibWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/// The window's top-left corner in physical screen pixels; see [`Self::set_position`]
|
||||
/// for why positions are not scaled the way sizes are.
|
||||
pub fn get_position(&self) -> Vec2d {
|
||||
unsafe {
|
||||
let display = get_xlib_app_global().display;
|
||||
|
|
@ -793,18 +805,29 @@ impl XlibWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/// Moves the window's top-left corner to `pos`, in physical screen pixels — the same
|
||||
/// space [`Self::get_position`] reports and `XCreateWindow` takes, so
|
||||
/// `set_position(get_position())` leaves the window where it is. Sizes are logical and
|
||||
/// scale with the DPI; positions are not, because a screen coordinate on a multi-monitor
|
||||
/// desktop has no single scale factor to be logical in.
|
||||
pub fn set_position(&mut self, pos: Vec2d) {
|
||||
unsafe {
|
||||
let display = get_xlib_app_global().display;
|
||||
let dpi_factor = self.get_dpi_factor();
|
||||
// A caller placing the window cannot know the desktop it is placing into, so the
|
||||
// request is fitted to the desktop that is actually there.
|
||||
let want = Rect {
|
||||
pos,
|
||||
size: self.get_outer_size(),
|
||||
};
|
||||
let fitted = fit_window_rect_to_screens(&x11_screens(), want);
|
||||
x11_sys::XMoveWindow(
|
||||
display,
|
||||
self.window.unwrap(),
|
||||
(pos.x * dpi_factor) as i32,
|
||||
(pos.y * dpi_factor) as i32,
|
||||
clamp_coord(fitted.pos.x),
|
||||
clamp_coord(fitted.pos.y),
|
||||
);
|
||||
x11_sys::XFlush(display);
|
||||
self.last_window_geom.position = pos;
|
||||
self.last_window_geom.position = fitted.pos;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1284,3 +1307,48 @@ impl DndAtoms {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fits a requested window placement onto the desktop.
|
||||
///
|
||||
/// Takes and returns the pair `XCreateWindow` is called with: a position in physical pixels
|
||||
/// and an inner size in logical pixels. `None` leaves placement to the window manager, which
|
||||
/// already puts the window somewhere visible, so it passes straight through.
|
||||
fn fit_create_geom(
|
||||
position: Option<Vec2d>,
|
||||
size: Vec2d,
|
||||
dpi_factor: f64,
|
||||
) -> (Option<Vec2d>, Vec2d) {
|
||||
let Some(pos) = position else {
|
||||
return (None, size);
|
||||
};
|
||||
let screens = x11_screens();
|
||||
if screens.is_empty() {
|
||||
return (position, size);
|
||||
}
|
||||
let want = Rect {
|
||||
pos,
|
||||
size: dvec2(size.x * dpi_factor, size.y * dpi_factor),
|
||||
};
|
||||
let fitted = fit_window_rect_to_screens(&screens, want);
|
||||
(
|
||||
Some(fitted.pos),
|
||||
dvec2(fitted.size.x / dpi_factor, fitted.size.y / dpi_factor),
|
||||
)
|
||||
}
|
||||
|
||||
/// Clamps a window coordinate into the INT16 range the X11 protocol encodes it in.
|
||||
fn clamp_coord(v: f64) -> c_int {
|
||||
if !v.is_finite() {
|
||||
return 0;
|
||||
}
|
||||
(v as i64).clamp(-32768, 32767) as c_int
|
||||
}
|
||||
|
||||
/// Clamps a window extent into the CARD16 range the X11 protocol encodes it in. Zero is not
|
||||
/// a legal extent, so the floor is one pixel.
|
||||
fn clamp_extent(v: f64) -> u32 {
|
||||
if !v.is_finite() {
|
||||
return 1;
|
||||
}
|
||||
(v as i64).clamp(1, 65535) as u32
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub mod video_file_decoder;
|
|||
pub mod video_file_encoder;
|
||||
pub mod wasapi;
|
||||
pub mod win32_event;
|
||||
pub mod win32_screen;
|
||||
pub mod win32_window;
|
||||
pub mod windows_media;
|
||||
pub mod windows_media_engine_notify;
|
||||
|
|
|
|||
114
platform/src/os/windows/win32_screen.rs
Normal file
114
platform/src/os/windows/win32_screen.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
//! Display enumeration for the Win32 backend.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use {
|
||||
crate::{
|
||||
makepad_math::*,
|
||||
screen::ScreenGeom,
|
||||
windows::Win32::{
|
||||
Foundation::{LPARAM, RECT},
|
||||
Graphics::Gdi::{HDC, HMONITOR},
|
||||
},
|
||||
},
|
||||
std::{mem::size_of, ptr},
|
||||
};
|
||||
|
||||
/// `MONITORINFO`, absent from the vendored `windows` bindings. `cb_size` tells
|
||||
/// `GetMonitorInfoW` which layout it was handed, so it must be filled in before the call.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct MonitorInfo {
|
||||
cb_size: u32,
|
||||
rc_monitor: RECT,
|
||||
rc_work: RECT,
|
||||
dw_flags: u32,
|
||||
}
|
||||
|
||||
/// `MONITORINFOF_PRIMARY`: the display holding the origin of the virtual screen.
|
||||
const MONITORINFOF_PRIMARY: u32 = 1;
|
||||
|
||||
type MonitorEnumProc =
|
||||
unsafe extern "system" fn(HMONITOR, HDC, *mut RECT, LPARAM) -> windows_core::BOOL;
|
||||
|
||||
#[inline]
|
||||
unsafe fn EnumDisplayMonitors(
|
||||
hdc: HDC,
|
||||
clip: *const RECT,
|
||||
callback: MonitorEnumProc,
|
||||
data: LPARAM,
|
||||
) -> windows_core::BOOL {
|
||||
windows_core::link!("user32.dll" "system" fn EnumDisplayMonitors(hdc : HDC, clip : *const RECT, callback : MonitorEnumProc, data : LPARAM) -> windows_core::BOOL);
|
||||
unsafe { EnumDisplayMonitors(hdc, clip, callback, data) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
unsafe fn GetMonitorInfoW(monitor: HMONITOR, info: *mut MonitorInfo) -> windows_core::BOOL {
|
||||
windows_core::link!("user32.dll" "system" fn GetMonitorInfoW(monitor : HMONITOR, info : *mut MonitorInfo) -> windows_core::BOOL);
|
||||
unsafe { GetMonitorInfoW(monitor, info) }
|
||||
}
|
||||
|
||||
/// Converts a Win32 edge-addressed rectangle to the origin-plus-size form makepad uses.
|
||||
fn rect_of(r: RECT) -> Rect {
|
||||
Rect {
|
||||
pos: dvec2(r.left as f64, r.top as f64),
|
||||
size: dvec2((r.right - r.left) as f64, (r.bottom - r.top) as f64),
|
||||
}
|
||||
}
|
||||
|
||||
/// The displays currently attached, in physical screen pixels — the coordinate space
|
||||
/// `CreateWindowExW` and `MoveWindow` take window positions in.
|
||||
pub fn win32_screens() -> Vec<ScreenGeom> {
|
||||
unsafe extern "system" fn collect(
|
||||
monitor: HMONITOR,
|
||||
_hdc: HDC,
|
||||
_clip: *mut RECT,
|
||||
data: LPARAM,
|
||||
) -> windows_core::BOOL {
|
||||
let screens = unsafe { &mut *(data.0 as *mut Vec<ScreenGeom>) };
|
||||
let mut info = MonitorInfo {
|
||||
cb_size: size_of::<MonitorInfo>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
if unsafe { GetMonitorInfoW(monitor, &mut info) }.as_bool() {
|
||||
screens.push(ScreenGeom {
|
||||
bounds: rect_of(info.rc_monitor),
|
||||
work_area: rect_of(info.rc_work),
|
||||
is_primary: info.dw_flags & MONITORINFOF_PRIMARY != 0,
|
||||
});
|
||||
}
|
||||
// Keep enumerating; a display whose info could not be read is simply skipped.
|
||||
windows_core::BOOL(1)
|
||||
}
|
||||
|
||||
let mut screens = Vec::new();
|
||||
unsafe {
|
||||
let _ = EnumDisplayMonitors(
|
||||
HDC::default(),
|
||||
ptr::null(),
|
||||
collect,
|
||||
LPARAM(&mut screens as *mut Vec<ScreenGeom> as isize),
|
||||
);
|
||||
}
|
||||
screens
|
||||
}
|
||||
|
||||
/// Converts a rectangle in Win32 "workspace" coordinates to screen coordinates.
|
||||
///
|
||||
/// `WINDOWPLACEMENT` reports a normal top-level window in workspace coordinates: screen
|
||||
/// coordinates shifted by the primary display's reserved edges. The two spaces coincide for
|
||||
/// the usual bottom-docked taskbar and differ by its thickness when it sits at the top or on
|
||||
/// the left, so the shift is read from the primary display rather than assumed to be zero.
|
||||
pub fn workspace_rect_to_screen(r: RECT) -> RECT {
|
||||
let Some(primary) = win32_screens().into_iter().find(|s| s.is_primary) else {
|
||||
return r;
|
||||
};
|
||||
let dx = (primary.work_area.pos.x - primary.bounds.pos.x) as i32;
|
||||
let dy = (primary.work_area.pos.y - primary.bounds.pos.y) as i32;
|
||||
RECT {
|
||||
left: r.left + dx,
|
||||
top: r.top + dy,
|
||||
right: r.right + dx,
|
||||
bottom: r.bottom + dy,
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,9 @@ use {
|
|||
droptarget::*,
|
||||
win32_app::{encode_wide, with_win32_app, Win32App},
|
||||
win32_event::*,
|
||||
win32_screen::{win32_screens, workspace_rect_to_screen},
|
||||
},
|
||||
screen::{clamp_point_to_screens, fit_window_rect_to_screens},
|
||||
window::{WindowBackdrop, WindowId, WindowVisuals},
|
||||
windows::{
|
||||
core::PCWSTR,
|
||||
|
|
@ -88,6 +90,7 @@ use {
|
|||
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE,
|
||||
WM_MOUSEWHEEL, WM_NCCALCSIZE, WM_NCHITTEST, WM_RBUTTONDOWN, WM_RBUTTONUP,
|
||||
WM_SIZE, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP,
|
||||
GetWindowPlacement, WINDOWPLACEMENT,
|
||||
WS_BORDER, WS_CAPTION, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES,
|
||||
WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST,
|
||||
WS_EX_WINDOWEDGE, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_THICKFRAME,
|
||||
|
|
@ -108,6 +111,13 @@ use {
|
|||
},
|
||||
};
|
||||
|
||||
/// Whether a screen coordinate survives the conversion `CreateWindowExW` and `MoveWindow`
|
||||
/// take: a real number inside `i32`, and not the `CW_USEDEFAULT` sentinel that `i32::MIN`
|
||||
/// would be read as.
|
||||
fn is_placeable(v: f64) -> bool {
|
||||
v.is_finite() && v > i32::MIN as f64 && v < i32::MAX as f64
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct AccentPolicy {
|
||||
accent_state: u32,
|
||||
|
|
@ -203,6 +213,11 @@ pub struct Win32Window {
|
|||
/// Set by `close_window()`; suppresses the WM_ACTIVATE-derived
|
||||
/// `PopupDismissed(FocusLost)`, which would duplicate the closer's dismissal.
|
||||
pub is_closing: Cell<bool>,
|
||||
/// Whether the window is inside the system's modal move/size loop, i.e. the user is
|
||||
/// dragging it. `WM_MOVE` arrives per mouse step there, so the position is published once
|
||||
/// on the way out rather than on every step; a programmatic move, which sets no such
|
||||
/// state, publishes immediately.
|
||||
pub in_size_move: Cell<bool>,
|
||||
pub ignore_wmsize: usize,
|
||||
pub hwnd: HWND,
|
||||
pub track_mouse_event: bool,
|
||||
|
|
@ -518,10 +533,22 @@ impl Win32Window {
|
|||
|
||||
let style_ex = WS_EX_WINDOWEDGE | WS_EX_APPWINDOW | WS_EX_ACCEPTFILES;
|
||||
|
||||
let (x, y) = if let Some(position) = position {
|
||||
(position.x as i32, position.y as i32)
|
||||
} else {
|
||||
(CW_USEDEFAULT, CW_USEDEFAULT)
|
||||
let (x, y) = match position {
|
||||
// A restored position can name a display that is gone, or hold values no display
|
||||
// ever had. Pinning it now keeps `CreateWindowExW` and the sizing that follows
|
||||
// working on real coordinates; `init` fits the finished rectangle once the size is
|
||||
// known. A coordinate still out of range after pinning means no display could be
|
||||
// enumerated, so the system's own placement is used instead of a value that would
|
||||
// saturate on the way to the API.
|
||||
Some(position) => {
|
||||
let pinned = clamp_point_to_screens(&win32_screens(), position);
|
||||
if is_placeable(pinned.x) && is_placeable(pinned.y) {
|
||||
(pinned.x as i32, pinned.y as i32)
|
||||
} else {
|
||||
(CW_USEDEFAULT, CW_USEDEFAULT)
|
||||
}
|
||||
}
|
||||
None => (CW_USEDEFAULT, CW_USEDEFAULT),
|
||||
};
|
||||
|
||||
let hwnd = unsafe {
|
||||
|
|
@ -567,6 +594,7 @@ impl Win32Window {
|
|||
nc_dq_gen: Cell::new(0),
|
||||
geom_event_gen: Cell::new(0),
|
||||
is_closing: Cell::new(false),
|
||||
in_size_move: Cell::new(false),
|
||||
ignore_wmsize: 0,
|
||||
hwnd,
|
||||
track_mouse_event: false,
|
||||
|
|
@ -621,6 +649,7 @@ impl Win32Window {
|
|||
nc_dq_gen: Cell::new(0),
|
||||
geom_event_gen: Cell::new(0),
|
||||
is_closing: Cell::new(false),
|
||||
in_size_move: Cell::new(false),
|
||||
ignore_wmsize: 0,
|
||||
hwnd,
|
||||
track_mouse_event: false,
|
||||
|
|
@ -649,6 +678,50 @@ impl Win32Window {
|
|||
self.set_inner_size(size);
|
||||
if self.is_fullscreen {
|
||||
self.maximize();
|
||||
} else if !self.is_popup {
|
||||
// A restored size and position are only as good as the display layout they were
|
||||
// saved on. Popups are placed against their parent and left alone; a maximized
|
||||
// window is the system's to place.
|
||||
self.fit_to_screens();
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves and resizes the window so it sits entirely within one display's work area.
|
||||
///
|
||||
/// See `crate::screen::fit_window_rect_to_screens` for what counts as a fit and why it
|
||||
/// is unconditional. A window rectangle that already fits is left untouched, so this
|
||||
/// costs one `GetWindowRect` and a display enumeration in the common case.
|
||||
pub fn fit_to_screens(&mut self) {
|
||||
let screens = win32_screens();
|
||||
if screens.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
if unsafe { GetWindowRect(self.hwnd, &mut rect) }.is_err() {
|
||||
return;
|
||||
}
|
||||
let current = Rect {
|
||||
pos: dvec2(rect.left as f64, rect.top as f64),
|
||||
size: dvec2(
|
||||
(rect.right - rect.left) as f64,
|
||||
(rect.bottom - rect.top) as f64,
|
||||
),
|
||||
};
|
||||
let fitted = fit_window_rect_to_screens(&screens, current);
|
||||
if fitted == current {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = unsafe {
|
||||
MoveWindow(
|
||||
self.hwnd,
|
||||
fitted.pos.x as i32,
|
||||
fitted.pos.y as i32,
|
||||
fitted.size.x as i32,
|
||||
fitted.size.y as i32,
|
||||
true,
|
||||
)
|
||||
} {
|
||||
crate::error!("Fitting the window into the visible screen area failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1014,12 +1087,25 @@ impl Win32Window {
|
|||
}));
|
||||
}
|
||||
WM_ENTERSIZEMOVE => {
|
||||
window.in_size_move.set(true);
|
||||
with_win32_app(|app| app.start_resize());
|
||||
window.do_callback(Win32Event::WindowResizeLoopStart(window.window_id));
|
||||
}
|
||||
// WM_CANCELMODE (0x001F): the system is telling the window to abandon any internal
|
||||
// mode it is in. DefWindowProc normally still leaves the move/size loop through
|
||||
// WM_EXITSIZEMOVE, so this is a failsafe: `in_size_move` is the only thing gating
|
||||
// position publication, and a stuck `true` would silently stop it for the window's
|
||||
// lifetime.
|
||||
0x001F => {
|
||||
window.in_size_move.set(false);
|
||||
}
|
||||
WM_EXITSIZEMOVE => {
|
||||
window.in_size_move.set(false);
|
||||
with_win32_app(|app| app.stop_resize());
|
||||
window.do_callback(Win32Event::WindowResizeLoopStop(window.window_id));
|
||||
// A drag that only moved the window produced no WM_SIZE, so this is the one
|
||||
// chance to publish where it ended up.
|
||||
window.send_move_event();
|
||||
}
|
||||
// WM_SIZING (0x0214) fires BEFORE the window is resized with
|
||||
// the proposed new rect. By pre-rendering at this size, the
|
||||
|
|
@ -1033,6 +1119,14 @@ impl Win32Window {
|
|||
// The window may have moved to a monitor with a different scale; drop the cached
|
||||
// DPI so send_change_event() (and subsequent hit-tests) re-read the new value.
|
||||
window.invalidate_cached_dpi();
|
||||
// Minimizing does not change the window's geometry, it parks it. Publishing the
|
||||
// iconic rect would relayout the whole UI at zero size and poison whatever the
|
||||
// app persists; `outer_rect` already answers from the restored placement, so
|
||||
// there is nothing here worth reporting either.
|
||||
const SIZE_MINIMIZED: usize = 1;
|
||||
if wparam.0 == SIZE_MINIMIZED {
|
||||
return LRESULT(0);
|
||||
}
|
||||
window.send_change_event();
|
||||
}
|
||||
WM_DPICHANGED => {
|
||||
|
|
@ -1071,6 +1165,13 @@ impl Win32Window {
|
|||
0x0003 => {
|
||||
window.nc_dq_cache.set(None);
|
||||
window.nc_dq_gen.set(window.nc_dq_gen.get().wrapping_add(1));
|
||||
// Publish the new position, or the window keeps reporting — and the app keeps
|
||||
// persisting — where it used to be. A user drag is left to WM_EXITSIZEMOVE:
|
||||
// this message arrives per mouse step, and each published geometry costs a
|
||||
// full redraw on the Cx side.
|
||||
if !window.in_size_move.get() {
|
||||
window.send_move_event();
|
||||
}
|
||||
}
|
||||
WM_CLOSE => {
|
||||
// close requested
|
||||
|
|
@ -1370,31 +1471,51 @@ impl Win32Window {
|
|||
self.ime_rect = rect;
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> Vec2d {
|
||||
/// The window's outer rectangle in screen pixels, answered from the restored placement
|
||||
/// while the window is minimized.
|
||||
///
|
||||
/// A minimized window has no on-screen rectangle: `GetWindowRect` reports the off-screen
|
||||
/// parking position `(-32000, -32000)` and `GetClientRect` a zero size. An app that
|
||||
/// persists its geometry on shutdown would save those and restore, next launch, a window
|
||||
/// it can neither see nor grab — so the restored placement the system keeps for exactly
|
||||
/// this purpose is reported instead.
|
||||
fn outer_rect(&self) -> RECT {
|
||||
unsafe {
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
GetWindowRect(self.hwnd, &mut rect).unwrap();
|
||||
Vec2d {
|
||||
x: rect.left as f64,
|
||||
y: rect.top as f64,
|
||||
if self.is_iconic() {
|
||||
let mut placement = WINDOWPLACEMENT {
|
||||
length: mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
if GetWindowPlacement(self.hwnd, &mut placement).is_ok() {
|
||||
return workspace_rect_to_screen(placement.rcNormalPosition);
|
||||
}
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
GetWindowRect(self.hwnd, &mut rect).unwrap();
|
||||
rect
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's top-left corner in physical screen pixels; see [`Self::set_position`]
|
||||
/// for why positions are not scaled the way sizes are.
|
||||
pub fn get_position(&self) -> Vec2d {
|
||||
let rect = self.outer_rect();
|
||||
Vec2d {
|
||||
x: rect.left as f64,
|
||||
y: rect.top as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_inner_size(&self) -> Vec2d {
|
||||
unsafe {
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
GetClientRect(self.hwnd, &mut rect).unwrap();
|
||||
let mut rect = RECT::default();
|
||||
if self.is_iconic() {
|
||||
// A restored window of this backend is fully client-sized (see the
|
||||
// `WM_NCCALCSIZE` handler), so its outer rectangle is also its client size.
|
||||
rect = self.outer_rect();
|
||||
} else {
|
||||
GetClientRect(self.hwnd, &mut rect).unwrap();
|
||||
}
|
||||
let dpi = self.get_dpi_factor();
|
||||
Vec2d {
|
||||
x: (rect.right - rect.left) as f64 / dpi,
|
||||
|
|
@ -1404,22 +1525,19 @@ impl Win32Window {
|
|||
}
|
||||
|
||||
pub fn get_outer_size(&self) -> Vec2d {
|
||||
unsafe {
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
GetWindowRect(self.hwnd, &mut rect).unwrap();
|
||||
let dpi = self.get_dpi_factor();
|
||||
Vec2d {
|
||||
x: (rect.right - rect.left) as f64 / dpi,
|
||||
y: (rect.bottom - rect.top) as f64 / dpi,
|
||||
}
|
||||
let rect = self.outer_rect();
|
||||
let dpi = self.get_dpi_factor();
|
||||
Vec2d {
|
||||
x: (rect.right - rect.left) as f64 / dpi,
|
||||
y: (rect.bottom - rect.top) as f64 / dpi,
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the window's top-left corner to `pos`, in physical screen pixels — the same
|
||||
/// space [`Self::get_position`] reports and `CreateWindowExW` takes, so
|
||||
/// `set_position(get_position())` leaves the window where it is. Sizes are logical and
|
||||
/// scale with the DPI; positions are not, because a screen coordinate on a multi-monitor
|
||||
/// desktop has no single scale factor to be logical in.
|
||||
pub fn set_position(&mut self, pos: Vec2d) {
|
||||
unsafe {
|
||||
let mut window_rect = RECT {
|
||||
|
|
@ -1429,13 +1547,23 @@ impl Win32Window {
|
|||
right: 0,
|
||||
};
|
||||
GetWindowRect(self.hwnd, &mut window_rect).unwrap();
|
||||
let dpi = self.get_dpi_factor();
|
||||
// A caller placing the window — restoring a saved position, cascading a new
|
||||
// window — cannot know the display layout it is placing into, so the request is
|
||||
// fitted to the displays that are actually attached.
|
||||
let want = Rect {
|
||||
pos,
|
||||
size: dvec2(
|
||||
(window_rect.right - window_rect.left) as f64,
|
||||
(window_rect.bottom - window_rect.top) as f64,
|
||||
),
|
||||
};
|
||||
let fitted = fit_window_rect_to_screens(&win32_screens(), want);
|
||||
MoveWindow(
|
||||
self.hwnd,
|
||||
(pos.x * dpi) as i32,
|
||||
(pos.y * dpi) as i32,
|
||||
window_rect.right - window_rect.left,
|
||||
window_rect.bottom - window_rect.top,
|
||||
fitted.pos.x as i32,
|
||||
fitted.pos.y as i32,
|
||||
fitted.size.x as i32,
|
||||
fitted.size.y as i32,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -1595,6 +1723,27 @@ impl Win32Window {
|
|||
Win32App::do_callback(event);
|
||||
}
|
||||
|
||||
/// Publishes a position-only geometry change.
|
||||
///
|
||||
/// Moving a window does not change what it draws, so unlike [`Self::send_change_event`]
|
||||
/// this asks for no repaint; it only keeps the published geometry — which is what an app
|
||||
/// persists — in step with where the window actually is. Nothing is dispatched when the
|
||||
/// geometry is unchanged, which is also what makes this safe to call for a minimize,
|
||||
/// where `outer_rect` keeps answering from the restored placement.
|
||||
pub fn send_move_event(&mut self) {
|
||||
let new_geom = self.get_window_geom();
|
||||
if new_geom == self.last_window_geom {
|
||||
return;
|
||||
}
|
||||
let old_geom = std::mem::replace(&mut self.last_window_geom, new_geom.clone());
|
||||
self.geom_event_gen.set(self.geom_event_gen.get().wrapping_add(1));
|
||||
self.do_callback(Win32Event::WindowGeomChange(WindowGeomChangeEvent {
|
||||
window_id: self.window_id,
|
||||
old_geom,
|
||||
new_geom,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn send_change_event(&mut self) {
|
||||
// Record that a geometry event is published (see `geom_event_gen`).
|
||||
self.geom_event_gen.set(self.geom_event_gen.get().wrapping_add(1));
|
||||
|
|
|
|||
|
|
@ -758,11 +758,12 @@ impl Cx {
|
|||
match op {
|
||||
CxOsOp::CreateWindow(window_id) => {
|
||||
let window = &mut self.windows[window_id];
|
||||
let (create_position, create_inner_size) = window.create_geom();
|
||||
let d3d11_window = D3d11Window::new(
|
||||
window_id,
|
||||
&d3d11_cx,
|
||||
window.create_inner_size.unwrap_or(dvec2(800., 600.)),
|
||||
window.create_position,
|
||||
create_inner_size,
|
||||
create_position,
|
||||
&window.create_title,
|
||||
window.is_fullscreen,
|
||||
);
|
||||
|
|
|
|||
497
platform/src/screen.rs
Normal file
497
platform/src/screen.rs
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
//! Display geometry, and the policy that keeps a window inside it.
|
||||
|
||||
use crate::makepad_math::*;
|
||||
|
||||
/// The smallest window extent a fit ever produces. Small enough to leave a deliberately
|
||||
/// compact tool window alone, large enough that the window still has a title bar to grab.
|
||||
pub const MIN_WINDOW_SIZE: Vec2d = Vec2d { x: 200.0, y: 120.0 };
|
||||
|
||||
/// One display attached to the system.
|
||||
///
|
||||
/// The rectangles are in the same coordinate space as the platform's window-position API,
|
||||
/// so a backend must build them from the same system calls it positions windows with:
|
||||
/// physical pixels with a top-left origin on Windows and X11, points with Cocoa's
|
||||
/// bottom-left origin on macOS.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ScreenGeom {
|
||||
/// The display's full extent.
|
||||
pub bounds: Rect,
|
||||
/// The extent left over once the system reserves its own space — the Windows taskbar,
|
||||
/// the macOS menu bar and Dock, X11 struts. Windows are placed inside this.
|
||||
pub work_area: Rect,
|
||||
/// Whether this is the system's primary display.
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
/// Area shared by two rectangles; zero when they do not overlap.
|
||||
fn overlap_area(a: Rect, b: Rect) -> f64 {
|
||||
let w = (a.pos.x + a.size.x).min(b.pos.x + b.size.x) - a.pos.x.max(b.pos.x);
|
||||
let h = (a.pos.y + a.size.y).min(b.pos.y + b.size.y) - a.pos.y.max(b.pos.y);
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
w * h
|
||||
}
|
||||
}
|
||||
|
||||
/// Squared distance between two rectangles' centres.
|
||||
fn center_distance_sq(a: Rect, b: Rect) -> f64 {
|
||||
let d = a.center() - b.center();
|
||||
d.x * d.x + d.y * d.y
|
||||
}
|
||||
|
||||
/// The parts of `a` that `b` does not cover, as up to four rectangles.
|
||||
fn subtract(a: Rect, b: Rect) -> Vec<Rect> {
|
||||
if overlap_area(a, b) <= 0.0 {
|
||||
return vec![a];
|
||||
}
|
||||
let (ax0, ay0) = (a.pos.x, a.pos.y);
|
||||
let (ax1, ay1) = (a.pos.x + a.size.x, a.pos.y + a.size.y);
|
||||
let bx0 = b.pos.x.max(ax0);
|
||||
let by0 = b.pos.y.max(ay0);
|
||||
let bx1 = (b.pos.x + b.size.x).min(ax1);
|
||||
let by1 = (b.pos.y + b.size.y).min(ay1);
|
||||
let mut out = Vec::new();
|
||||
let mut push = |x0: f64, y0: f64, x1: f64, y1: f64| {
|
||||
if x1 > x0 && y1 > y0 {
|
||||
out.push(Rect {
|
||||
pos: dvec2(x0, y0),
|
||||
size: dvec2(x1 - x0, y1 - y0),
|
||||
});
|
||||
}
|
||||
};
|
||||
push(ax0, ay0, ax1, by0);
|
||||
push(ax0, by1, ax1, ay1);
|
||||
push(ax0, by0, bx0, by1);
|
||||
push(bx1, by0, ax1, by1);
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether `r` lies entirely within the union of `areas`, which may be several displays
|
||||
/// covering it between them.
|
||||
fn is_covered_by(areas: &[Rect], r: Rect) -> bool {
|
||||
if !is_usable(r) {
|
||||
return false;
|
||||
}
|
||||
let mut remaining = vec![r];
|
||||
for area in areas {
|
||||
let mut next = Vec::new();
|
||||
for piece in remaining.drain(..) {
|
||||
next.extend(subtract(piece, *area));
|
||||
}
|
||||
if next.is_empty() {
|
||||
return true;
|
||||
}
|
||||
remaining = next;
|
||||
}
|
||||
remaining.is_empty()
|
||||
}
|
||||
|
||||
/// Whether a rectangle is usable as a destination: real numbers, and some area to put a
|
||||
/// window in.
|
||||
fn is_usable(r: Rect) -> bool {
|
||||
r.pos.x.is_finite()
|
||||
&& r.pos.y.is_finite()
|
||||
&& r.size.x.is_finite()
|
||||
&& r.size.y.is_finite()
|
||||
&& r.size.x > 0.0
|
||||
&& r.size.y > 0.0
|
||||
}
|
||||
|
||||
/// Fits a window's outer rectangle inside the work area of the display it belongs to.
|
||||
///
|
||||
/// Every window position an app restores has to survive a display layout that may have
|
||||
/// changed completely since it was written: the display the window sat on can be gone, a
|
||||
/// docked laptop can be back on a smaller built-in panel, and a state file saved while the
|
||||
/// window was minimized holds coordinates no display ever had — Win32 reports position
|
||||
/// `(-32000, -32000)` and a zero-sized client area for a minimized window, and an app that
|
||||
/// persists that on shutdown restores a window it cannot see or grab on the next launch,
|
||||
/// with no way back short of deleting the file. Fitting therefore applies to every
|
||||
/// placement rather than only to values that look wrong.
|
||||
///
|
||||
/// A window that is already wholly on the desktop is returned untouched, including one
|
||||
/// deliberately spanning two adjacent displays — the point is to rescue placements that
|
||||
/// cannot be reached, not to enforce one window per display. Anything else moves onto the
|
||||
/// display it overlaps most, or, when it overlaps none, the display nearest its centre; its
|
||||
/// size is capped to that work area and floored at [`MIN_WINDOW_SIZE`], and its position is
|
||||
/// pulled in until the whole window is visible.
|
||||
///
|
||||
/// An empty `screens` means the backend cannot enumerate displays — Wayland, where a client
|
||||
/// is not allowed to know or choose where its windows go — and `window` is returned as-is.
|
||||
pub fn fit_window_rect_to_screens(screens: &[ScreenGeom], window: Rect) -> Rect {
|
||||
let usable: Vec<Rect> = screens
|
||||
.iter()
|
||||
.map(|s| s.work_area)
|
||||
.filter(|r| is_usable(*r))
|
||||
.collect();
|
||||
let Some(&first) = usable.first() else {
|
||||
return window;
|
||||
};
|
||||
let primary = screens
|
||||
.iter()
|
||||
.find(|s| s.is_primary && is_usable(s.work_area))
|
||||
.map_or(first, |s| s.work_area);
|
||||
|
||||
// Coordinates that are not real numbers cannot be compared or clamped, so they name no
|
||||
// display and get the primary's geometry to start from.
|
||||
let mut want = window;
|
||||
if !want.size.x.is_finite() || !want.size.y.is_finite() {
|
||||
want.size = primary.size * 0.5;
|
||||
}
|
||||
if !want.pos.x.is_finite() || !want.pos.y.is_finite() {
|
||||
want.pos = primary.pos;
|
||||
}
|
||||
|
||||
// A window already wholly on the desktop is left exactly where it is, including one
|
||||
// deliberately spanning two adjacent displays. Fitting exists to rescue a placement that
|
||||
// cannot be reached, not to enforce one window per display.
|
||||
if is_covered_by(&usable, want) {
|
||||
return want;
|
||||
}
|
||||
|
||||
let area = usable
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(|a, b| {
|
||||
let (oa, ob) = (overlap_area(*a, want), overlap_area(*b, want));
|
||||
oa.total_cmp(&ob).then_with(|| {
|
||||
// No overlap anywhere leaves every candidate tied at zero; nearest centre
|
||||
// breaks the tie, so a window off the right edge lands on the right display.
|
||||
center_distance_sq(*b, want).total_cmp(¢er_distance_sq(*a, want))
|
||||
})
|
||||
})
|
||||
.unwrap_or(primary);
|
||||
|
||||
let size = dvec2(
|
||||
want.size.x.clamp(MIN_WINDOW_SIZE.x.min(area.size.x), area.size.x),
|
||||
want.size.y.clamp(MIN_WINDOW_SIZE.y.min(area.size.y), area.size.y),
|
||||
);
|
||||
let pos = dvec2(
|
||||
want.pos.x.clamp(area.pos.x, area.pos.x + area.size.x - size.x),
|
||||
want.pos.y.clamp(area.pos.y, area.pos.y + area.size.y - size.y),
|
||||
);
|
||||
Rect { pos, size }
|
||||
}
|
||||
|
||||
/// Clamps a point into the work area of the display nearest to it, leaving room for a
|
||||
/// window of at least [`MIN_WINDOW_SIZE`] to be visible from there.
|
||||
///
|
||||
/// A window origin can break creation on its own, before there is a finished rectangle to
|
||||
/// fit: a coordinate out of the platform's integer range saturates when it reaches the
|
||||
/// system call, and the sizing that follows is done relative to wherever the window landed.
|
||||
/// Backends pin the origin through here first and fit the finished rectangle afterwards.
|
||||
///
|
||||
/// An empty `screens` returns the point unchanged, for the same reason
|
||||
/// [`fit_window_rect_to_screens`] does.
|
||||
pub fn clamp_point_to_screens(screens: &[ScreenGeom], point: Vec2d) -> Vec2d {
|
||||
fit_window_rect_to_screens(
|
||||
screens,
|
||||
Rect {
|
||||
pos: point,
|
||||
size: dvec2(0.0, 0.0),
|
||||
},
|
||||
)
|
||||
.pos
|
||||
}
|
||||
|
||||
/// The size a window falls back to when the requested one carries no usable information.
|
||||
pub const DEFAULT_WINDOW_SIZE: Vec2d = Vec2d { x: 800.0, y: 600.0 };
|
||||
|
||||
/// Reduces a requested window size and position to values a windowing system can act on,
|
||||
/// without needing to know anything about the attached displays.
|
||||
///
|
||||
/// This is the guard that has to hold everywhere, including the backends
|
||||
/// [`fit_window_rect_to_screens`] cannot help: Wayland enumerates no displays for a client
|
||||
/// and passes the size straight to `wl_egl_window_create`, which rejects a non-positive one;
|
||||
/// X11 encodes width and height as unsigned 16-bit and answers a zero with a protocol error
|
||||
/// that terminates the process by default. A saved `0`, a negative, or a `NaN` — all of which
|
||||
/// a JSON state file can hold, and which `as i32` quietly turns into `0` — must therefore
|
||||
/// never leave this function. Position is dropped rather than corrected when it is not a real
|
||||
/// number: `None` means "the system places this window", which is always a safe answer.
|
||||
pub fn sanitize_window_geom(position: Option<Vec2d>, size: Vec2d) -> (Option<Vec2d>, Vec2d) {
|
||||
// A non-positive extent carries no information about how big the window should be — it is
|
||||
// what a zeroed, truncated or minimized-window state file holds — so it gets the default
|
||||
// rather than the floor, which would restore a technically-visible 200x120 sliver. A small
|
||||
// positive size is a real request and is only raised to something grabbable.
|
||||
let size = if size.x.is_finite() && size.y.is_finite() && size.x > 0.0 && size.y > 0.0 {
|
||||
dvec2(
|
||||
size.x.max(MIN_WINDOW_SIZE.x),
|
||||
size.y.max(MIN_WINDOW_SIZE.y),
|
||||
)
|
||||
} else {
|
||||
DEFAULT_WINDOW_SIZE
|
||||
};
|
||||
let position = position.filter(|p| p.x.is_finite() && p.y.is_finite());
|
||||
(position, size)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn screen(x: f64, y: f64, w: f64, h: f64, is_primary: bool) -> ScreenGeom {
|
||||
let bounds = rect(x, y, w, h);
|
||||
ScreenGeom {
|
||||
bounds,
|
||||
work_area: bounds,
|
||||
is_primary,
|
||||
}
|
||||
}
|
||||
|
||||
fn rect(x: f64, y: f64, w: f64, h: f64) -> Rect {
|
||||
Rect {
|
||||
pos: dvec2(x, y),
|
||||
size: dvec2(w, h),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_already_inside_a_display_is_left_alone() {
|
||||
let screens = [screen(0.0, 0.0, 1920.0, 1080.0, true)];
|
||||
let want = rect(100.0, 100.0, 800.0, 600.0);
|
||||
assert_eq!(fit_window_rect_to_screens(&screens, want), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_screens_leaves_the_request_untouched() {
|
||||
let want = rect(-32000.0, -32000.0, 0.0, 0.0);
|
||||
assert_eq!(fit_window_rect_to_screens(&[], want), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_win32_minimized_sentinel_comes_back_onto_the_primary_display() {
|
||||
let screens = [screen(0.0, 0.0, 1920.0, 1080.0, true)];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(-32000.0, -32000.0, 0.0, 0.0));
|
||||
assert_eq!(fitted.pos, dvec2(0.0, 0.0));
|
||||
assert_eq!(fitted.size, MIN_WINDOW_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_past_the_right_edge_is_pulled_back_in() {
|
||||
let screens = [screen(0.0, 0.0, 1920.0, 1080.0, true)];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(1900.0, 50.0, 800.0, 600.0));
|
||||
assert_eq!(fitted, rect(1120.0, 50.0, 800.0, 600.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_larger_than_the_work_area_is_capped_to_it() {
|
||||
let screens = [ScreenGeom {
|
||||
bounds: rect(0.0, 0.0, 1920.0, 1080.0),
|
||||
work_area: rect(0.0, 0.0, 1920.0, 1040.0),
|
||||
is_primary: true,
|
||||
}];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(-500.0, -500.0, 4000.0, 4000.0));
|
||||
assert_eq!(fitted, rect(0.0, 0.0, 1920.0, 1040.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_keeps_the_secondary_display_it_sits_on() {
|
||||
let screens = [
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
screen(1920.0, 0.0, 2560.0, 1440.0, false),
|
||||
];
|
||||
let want = rect(2000.0, 200.0, 800.0, 600.0);
|
||||
assert_eq!(fit_window_rect_to_screens(&screens, want), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_on_a_display_that_is_gone_moves_to_the_nearest_one() {
|
||||
// The secondary display it was saved on is no longer attached.
|
||||
let screens = [screen(0.0, 0.0, 1920.0, 1080.0, true)];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(3000.0, 200.0, 800.0, 600.0));
|
||||
assert_eq!(fitted, rect(1120.0, 200.0, 800.0, 600.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_spanning_two_adjacent_displays_is_left_alone() {
|
||||
let screens = [
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
screen(1920.0, 0.0, 1920.0, 1080.0, false),
|
||||
];
|
||||
// The window straddles the seam but every pixel of it is on a display.
|
||||
let want = rect(1720.0, 100.0, 800.0, 600.0);
|
||||
assert_eq!(fit_window_rect_to_screens(&screens, want), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_over_a_gap_between_displays_moves_to_the_one_holding_most_of_it() {
|
||||
// Displays side by side with a gap between them, as a mismatched pair produces.
|
||||
let screens = [
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
screen(2400.0, 0.0, 1920.0, 1080.0, false),
|
||||
];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(1800.0, 100.0, 800.0, 600.0));
|
||||
assert_eq!(fitted, rect(2400.0, 100.0, 800.0, 600.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_hanging_off_the_end_of_the_arrangement_is_pulled_in() {
|
||||
let screens = [
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
screen(1920.0, 0.0, 1920.0, 1080.0, false),
|
||||
];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(3600.0, 100.0, 800.0, 600.0));
|
||||
assert_eq!(fitted, rect(3040.0, 100.0, 800.0, 600.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_spanning_displays_of_different_heights_is_not_left_hanging() {
|
||||
// The taller display sits lower, so the strip below the shorter one is off-desktop.
|
||||
let screens = [
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
screen(1920.0, 0.0, 1920.0, 1440.0, false),
|
||||
];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(1600.0, 900.0, 800.0, 400.0));
|
||||
assert!(fitted != rect(1600.0, 900.0, 800.0, 400.0));
|
||||
assert!(screens.iter().any(|s| fitted.is_inside_of(s.work_area)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_geometry_falls_back_to_the_primary_display() {
|
||||
let screens = [
|
||||
screen(-1920.0, 0.0, 1920.0, 1080.0, false),
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
];
|
||||
let fitted =
|
||||
fit_window_rect_to_screens(&screens, rect(f64::NAN, f64::INFINITY, f64::NAN, 600.0));
|
||||
assert_eq!(fitted, rect(0.0, 0.0, 960.0, 540.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cocoa_bottom_left_coordinates_fit_the_same_way() {
|
||||
// macOS reports the primary display at the origin with y growing upwards; a window
|
||||
// saved below the display comes back inside it.
|
||||
let screens = [ScreenGeom {
|
||||
bounds: rect(0.0, 0.0, 1728.0, 1117.0),
|
||||
work_area: rect(0.0, 76.0, 1728.0, 1004.0),
|
||||
is_primary: true,
|
||||
}];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(20.0, -400.0, 900.0, 700.0));
|
||||
assert_eq!(fitted, rect(20.0, 76.0, 900.0, 700.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_display_smaller_than_the_minimum_size_still_fits_a_window() {
|
||||
let screens = [screen(0.0, 0.0, 100.0, 60.0, true)];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(500.0, 500.0, 800.0, 600.0));
|
||||
assert_eq!(fitted, rect(0.0, 0.0, 100.0, 60.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_size_is_raised_to_the_minimum() {
|
||||
let screens = [screen(0.0, 0.0, 1920.0, 1080.0, true)];
|
||||
let fitted = fit_window_rect_to_screens(&screens, rect(10.0, 10.0, -800.0, -600.0));
|
||||
assert_eq!(fitted, rect(10.0, 10.0, MIN_WINDOW_SIZE.x, MIN_WINDOW_SIZE.y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coordinates_far_outside_the_integer_range_land_on_a_display() {
|
||||
let screens = [screen(0.0, 0.0, 1920.0, 1080.0, true)];
|
||||
for want in [
|
||||
rect(1e300, 1e300, 800.0, 600.0),
|
||||
rect(-1e300, -1e300, 800.0, 600.0),
|
||||
rect(f64::MAX, f64::MIN, f64::MAX, f64::MAX),
|
||||
] {
|
||||
let fitted = fit_window_rect_to_screens(&screens, want);
|
||||
assert!(fitted.is_inside_of(screens[0].work_area), "{fitted:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_fitted_rectangle_lies_within_some_work_area() {
|
||||
let screens = [
|
||||
screen(0.0, 0.0, 1920.0, 1080.0, true),
|
||||
screen(1920.0, -200.0, 2560.0, 1440.0, false),
|
||||
];
|
||||
for want in [
|
||||
rect(-32000.0, -32000.0, 0.0, 0.0),
|
||||
rect(f64::NAN, f64::NAN, f64::NAN, f64::NAN),
|
||||
rect(f64::INFINITY, f64::NEG_INFINITY, 1e12, -1e12),
|
||||
rect(1e9, 1e9, 1e9, 1e9),
|
||||
rect(4400.0, 1100.0, 300.0, 200.0),
|
||||
rect(0.0, 0.0, 0.0, 0.0),
|
||||
] {
|
||||
let fitted = fit_window_rect_to_screens(&screens, want);
|
||||
let areas: Vec<Rect> = screens.iter().map(|s| s.work_area).collect();
|
||||
assert!(is_covered_by(&areas, fitted), "{want:?} fitted to {fitted:?}");
|
||||
assert!(fitted.pos.x.is_finite() && fitted.pos.y.is_finite());
|
||||
assert!(fitted.size.x > 0.0 && fitted.size.y > 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitizing_rejects_every_size_a_windowing_system_cannot_use() {
|
||||
for bad in [
|
||||
dvec2(0.0, 0.0),
|
||||
dvec2(-800.0, -600.0),
|
||||
dvec2(f64::NAN, f64::NAN),
|
||||
dvec2(f64::INFINITY, 600.0),
|
||||
dvec2(1.0, 1.0),
|
||||
] {
|
||||
let (_, size) = sanitize_window_geom(None, bad);
|
||||
assert!(size.x >= MIN_WINDOW_SIZE.x && size.y >= MIN_WINDOW_SIZE.y, "{bad:?}");
|
||||
assert!(size.x.is_finite() && size.y.is_finite(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_size_carrying_no_information_becomes_the_default_not_the_floor() {
|
||||
// Restoring a 200x120 sliver from a zeroed state file is visible but useless.
|
||||
for empty in [
|
||||
dvec2(0.0, 0.0),
|
||||
dvec2(-800.0, -600.0),
|
||||
dvec2(0.0, 800.0),
|
||||
dvec2(f64::NAN, f64::NAN),
|
||||
] {
|
||||
assert_eq!(sanitize_window_geom(None, empty).1, DEFAULT_WINDOW_SIZE, "{empty:?}");
|
||||
}
|
||||
// A small but real request is only raised to something grabbable.
|
||||
assert_eq!(
|
||||
sanitize_window_geom(None, dvec2(50.0, 40.0)).1,
|
||||
MIN_WINDOW_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitizing_keeps_a_usable_request_intact() {
|
||||
let (pos, size) = sanitize_window_geom(Some(dvec2(-1200.0, 40.0)), dvec2(1280.0, 800.0));
|
||||
// A position on a left-hand secondary display is legitimate and is not a size problem,
|
||||
// so it survives untouched; fitting to the displays is a separate, later step.
|
||||
assert_eq!(pos, Some(dvec2(-1200.0, 40.0)));
|
||||
assert_eq!(size, dvec2(1280.0, 800.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitizing_drops_a_position_that_is_not_a_real_number() {
|
||||
assert_eq!(
|
||||
sanitize_window_geom(Some(dvec2(f64::NAN, 0.0)), dvec2(800.0, 600.0)).0,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_window_geom(Some(dvec2(0.0, f64::INFINITY)), dvec2(800.0, 600.0)).0,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clamped_point_leaves_a_minimum_window_visible() {
|
||||
let screens = [ScreenGeom {
|
||||
bounds: rect(0.0, 0.0, 1920.0, 1080.0),
|
||||
work_area: rect(0.0, 0.0, 1920.0, 1040.0),
|
||||
is_primary: true,
|
||||
}];
|
||||
assert_eq!(
|
||||
clamp_point_to_screens(&screens, dvec2(-32000.0, -32000.0)),
|
||||
dvec2(0.0, 0.0)
|
||||
);
|
||||
assert_eq!(
|
||||
clamp_point_to_screens(&screens, dvec2(1e9, 1e9)),
|
||||
dvec2(1920.0 - MIN_WINDOW_SIZE.x, 1040.0 - MIN_WINDOW_SIZE.y)
|
||||
);
|
||||
assert_eq!(
|
||||
clamp_point_to_screens(&screens, dvec2(f64::NAN, 5.0)),
|
||||
dvec2(0.0, 0.0)
|
||||
);
|
||||
assert_eq!(clamp_point_to_screens(&screens, dvec2(40.0, 50.0)), dvec2(40.0, 50.0));
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +247,10 @@ fn load_packaged_resource(cx: &Cx, dep_path: &str) -> Option<Rc<Vec<u8>>> {
|
|||
|
||||
/// Try to load a resource from the packaged location on desktop.
|
||||
/// Returns None when not in packaged mode (package_root is None).
|
||||
///
|
||||
/// A relative `package_root` (the desktop packagers use `.` beside the executable) is searched
|
||||
/// both from the working directory and from the executable's own directory, because a launcher
|
||||
/// is free to start the process anywhere — see `crate::os::cx_native::exe_relative_path`.
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
not(any(target_os = "android", target_os = "ios", target_os = "tvos")),
|
||||
|
|
@ -255,10 +259,7 @@ fn load_packaged_resource(cx: &Cx, dep_path: &str) -> Option<Rc<Vec<u8>>> {
|
|||
fn load_packaged_resource(cx: &Cx, dep_path: &str) -> Option<Rc<Vec<u8>>> {
|
||||
let root = cx.package_root.as_deref()?;
|
||||
let full_path = format!("{}/{}", root, dep_path);
|
||||
let mut file = File::open(&full_path).ok()?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data).ok()?;
|
||||
Some(Rc::new(data))
|
||||
crate::os::cx_native::read_file_cwd_or_exe_relative(&full_path).map(Rc::new)
|
||||
}
|
||||
|
||||
/// Load a file directly from the filesystem (desktop/mobile only, not wasm).
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
makepad_math::*,
|
||||
//makepad_live_id::*,
|
||||
makepad_script::*,
|
||||
screen::{sanitize_window_geom, DEFAULT_WINDOW_SIZE},
|
||||
script::vm::*,
|
||||
};
|
||||
|
||||
|
|
@ -513,6 +514,8 @@ impl WindowHandle {
|
|||
cx.windows[self.window_id()].get_inner_size()
|
||||
}
|
||||
|
||||
/// The window's top-left corner, in the space [`Self::reposition`] accepts: physical
|
||||
/// screen pixels on Windows and X11, points on macOS. Never scaled by the DPI factor.
|
||||
pub fn get_position(&self, cx: &Cx) -> Vec2d {
|
||||
cx.windows[self.window_id()].get_position()
|
||||
}
|
||||
|
|
@ -608,6 +611,12 @@ impl WindowHandle {
|
|||
cx.push_unique_platform_op(CxOsOp::ResizeWindow(self.window_id(), size));
|
||||
}
|
||||
|
||||
/// Moves the window's top-left corner to `position`, in the same space
|
||||
/// [`Self::get_position`] reports: physical screen pixels on Windows and X11, points on
|
||||
/// macOS. Unlike [`Self::resize`], which takes a logical size that scales with the DPI, a
|
||||
/// position is never scaled — a screen coordinate spanning displays of different scales
|
||||
/// has no single factor to be logical in. Backends fit the request to the displays that
|
||||
/// are actually attached, so a window cannot be placed where it could not be reached.
|
||||
pub fn reposition(&self, cx: &mut Cx, position: Vec2d) {
|
||||
cx.push_unique_platform_op(CxOsOp::RepositionWindow(self.window_id(), position));
|
||||
}
|
||||
|
|
@ -694,6 +703,22 @@ impl Default for CxWindow {
|
|||
}
|
||||
|
||||
impl CxWindow {
|
||||
/// The geometry to create this window with, reduced to values a windowing system can act
|
||||
/// on: a size no smaller than [`crate::screen::MIN_WINDOW_SIZE`], and a position that is
|
||||
/// either real coordinates or `None` for "the system places it".
|
||||
///
|
||||
/// Every backend reads its creation geometry through here, so no request — a restored
|
||||
/// state file, a DSL literal, a computed popup rect — can reach a platform call carrying a
|
||||
/// size it will reject or a coordinate that is not a number. Placing the window on a
|
||||
/// display that exists is a separate, per-backend step; see
|
||||
/// [`crate::screen::fit_window_rect_to_screens`].
|
||||
pub fn create_geom(&self) -> (Option<Vec2d>, Vec2d) {
|
||||
sanitize_window_geom(
|
||||
self.create_position,
|
||||
self.create_inner_size.unwrap_or(DEFAULT_WINDOW_SIZE),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn valid_dpi_factor(dpi_factor: f64) -> Option<f64> {
|
||||
if dpi_factor.is_finite() && dpi_factor > 0.0 {
|
||||
Some(dpi_factor)
|
||||
|
|
|
|||
|
|
@ -454,6 +454,51 @@ fn extract_workspace_patch_sections(workspace_manifest: &str) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
fn extract_workspace_dependencies_section(workspace_manifest: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut current_section: Option<String> = None;
|
||||
let mut current_body = Vec::new();
|
||||
|
||||
let flush_section =
|
||||
|out: &mut String, current_section: &mut Option<String>, current_body: &mut Vec<String>| {
|
||||
let Some(section) = current_section.take() else {
|
||||
current_body.clear();
|
||||
return;
|
||||
};
|
||||
if section != "[workspace.dependencies]" {
|
||||
current_body.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if !out.is_empty() {
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(§ion);
|
||||
out.push('\n');
|
||||
for line in current_body.iter() {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
current_body.clear();
|
||||
};
|
||||
|
||||
for raw_line in workspace_manifest.lines() {
|
||||
let trimmed = raw_line.trim();
|
||||
if trimmed.starts_with('[') && trimmed.ends_with(']') && !raw_line.starts_with(' ') {
|
||||
flush_section(&mut out, &mut current_section, &mut current_body);
|
||||
current_section = Some(trimmed.to_string());
|
||||
continue;
|
||||
}
|
||||
|
||||
if current_section.is_some() {
|
||||
current_body.push(raw_line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
flush_section(&mut out, &mut current_section, &mut current_body);
|
||||
out
|
||||
}
|
||||
|
||||
fn strip_generated_wrapper_args(args: &[String], build_crate: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut skip_next = false;
|
||||
|
|
@ -556,6 +601,15 @@ fn generate_android_wrapper_manifest(
|
|||
&workspace_root,
|
||||
));
|
||||
}
|
||||
|
||||
let workspace_deps = extract_workspace_dependencies_section(&workspace_manifest);
|
||||
if !workspace_deps.trim().is_empty() {
|
||||
wrapper_manifest.push('\n');
|
||||
wrapper_manifest.push_str(&rewrite_wrapper_manifest_paths(
|
||||
&workspace_deps,
|
||||
&workspace_root,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let wrapper_manifest_path = wrapper_dir.join("Cargo.toml");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue