Compare commits

..

8 commits

Author SHA1 Message Date
be57d63b48 nigig: NIGIG test-mode forwarding, custom manifest hook, ortho camera support
- makepad_test/runtime.rs: forward NIGIG_TEST_MODE from host env to the
  Android app via 'am start' intent extra; add wait_timeout (60s) used by
  wait_visible/wait_hidden/wait_count; make query_widgets tolerant of
  snapshot timeouts; grant READ_CONTACTS during adb setup
- makepad-platform android_jni.rs: read makepad.NIGIG_TEST_MODE intent
  extra and surface it as the NIGIG_TEST_MODE env var via apply_studio_env
- cargo_makepad compile.rs: support verbatim custom AndroidManifest.xml in
  addition to the templated variant
- makepad-xr xr_root.rs: add ortho camera controls (ortho, ortho_height,
  min/max), derive Debug on XrCamera
- docs: ANDROID.md and DESKTOP_VISIBLE.md for makepad_test
2026-08-28 02:07:54 +03:00
ea57a8277a makepad_test: grant runtime permissions after install to prevent dialog overlay
The GrantPermissionsActivity pops up during navigation and blocks the
app's event loop, preventing hub responses. Pre-grant all runtime
permissions after APK install to avoid this.
2026-08-28 02:07:54 +03:00
84ef88c1f1 makepad_test: force-stop interfering Robrix app during tests
PID 28203 (rs.robius.robrix) was the actual zombie reclaiming foreground
and killing our test app - not our own package. Force-stop both the
target package and known interfering Makepad apps (Robrix) during test
setup to prevent cross-app foreground competition.

Also remove the pm disable-user approach as it doesn't help against
a different package's zombie process.
2026-08-28 02:07:53 +03:00
3a50f77beb makepad_test: use pm disable-user to prevent Samsung zombie resurrection
Samsung devices keep killed app processes alive and bring them back to
the foreground ~15s later, killing our fresh test instance. force-stop
and kill -9 don't prevent this. pm disable-user fully prevents the
zombie from being restarted. Re-enable before launching the new instance.
2026-08-28 02:07:53 +03:00
54480252a0 android: send BeforeStartup/AfterStartup over websocket
The Android platform never sent BeforeStartup or AfterStartup messages
via the studio websocket. Desktop platforms send these through their
stdin event loops, but Android uses websockets instead of stdin.

Without AfterStartup, the hub never broadcasts AppStarted to UI
clients, causing makepad-test to time out waiting for app startup.
2026-08-28 02:07:53 +03:00
b2702afcb4 feat(test): extend protocol for touch, long-press, paste, IME composition
Add RemoteTouchState, RemoteTouchPoint, RemoteTouchUpdate, RemoteLongPress,
RemoteTextPaste, RemoteIMEComposition wire structs to StudioToApp enum.

Dispatch new events through cx_shared.rs (TouchUpdate→Event::TouchUpdate,
LongPress→MouseUp+MouseDown, TextPaste/IMEComposition→Event::TextInput).

TestApp: touch_down/move/up, long_press, paste_text, ime_composition.
Locator: touch_down/move/up, long_press, paste, ime_composition.
2026-08-28 02:07:52 +03:00
3894f30797 feat(widgets): reexport optional Makepad sibling crates
Adds feature-gated optional deps and re-exports (makepad-test, makepad-csg,
makepad-gltf, makepad-mbtile-reader, makepad-fast-inflate) so a downstream
workspace can depend on makepad-widgets as its sole Makepad source.
2026-08-28 02:07:52 +03:00
3c91feeb58 feat(test): Android makepad_test via adb + in-process hub (legacy Java path)
Adds the Android test runtime to makepad_test: builds the APK with
cargo-makepad's standard Java path, installs and launches via adb with
makepad.STUDIO_* intent extras (incl. STUDIO_BUILD), connects the app to an
in-process hub over adb reverse, and waits for startup + responsiveness.
Adds clean in-process hub shutdown (HttpServerHandle + GatewayHandle Drop)
and the STUDIO_BUILD intent parsing on the app side. No native-activity or
NDK APK compilation code is included.
2026-08-28 02:07:52 +03:00
19 changed files with 89 additions and 1274 deletions

View file

@ -52,7 +52,6 @@ mod performance_stats;
pub mod memory_watchdog; pub mod memory_watchdog;
pub mod perf_monitor; pub mod perf_monitor;
pub mod permission; pub mod permission;
mod screen;
mod texture; mod texture;
mod uniform_buffer; mod uniform_buffer;
mod window; mod window;
@ -216,7 +215,6 @@ pub use {
unregister_media_playback_session, MediaPlaybackSessionId, unregister_media_playback_session, MediaPlaybackSessionId,
}, },
script::vm::*, script::vm::*,
screen::{fit_window_rect_to_screens, ScreenGeom, MIN_WINDOW_SIZE},
shared_bytes::{MappedBytes, SharedBytes, SharedBytesStats}, shared_bytes::{MappedBytes, SharedBytes, SharedBytesStats},
texture::{ texture::{
image_cache_use_mipmaps, Texture, TextureAnimation, TextureFormat, TextureId, image_cache_use_mipmaps, Texture, TextureAnimation, TextureFormat, TextureId,

View file

@ -1539,12 +1539,11 @@ impl Cx {
match op { match op {
CxOsOp::CreateWindow(window_id) => { CxOsOp::CreateWindow(window_id) => {
let window = &mut self.windows[window_id]; let window = &mut self.windows[window_id];
let (create_position, create_inner_size) = window.create_geom();
let mut metal_window = MetalWindow::new( let mut metal_window = MetalWindow::new(
window_id, window_id,
&metal_cx, &metal_cx,
create_inner_size, window.create_inner_size.unwrap_or(dvec2(800., 600.)),
create_position, window.create_position,
&window.create_title, &window.create_title,
window.is_fullscreen, window.is_fullscreen,
window.macos, window.macos,

View file

@ -6,7 +6,7 @@ use {
MouseUpEvent, ScrollEvent, ScrollPhase, TextInputEvent, WindowCloseRequestedEvent, MouseUpEvent, ScrollEvent, ScrollPhase, TextInputEvent, WindowCloseRequestedEvent,
WindowDragQueryEvent, WindowDragQueryResponse, WindowGeom, WindowGeomChangeEvent, WindowDragQueryEvent, WindowDragQueryResponse, WindowGeom, WindowGeomChangeEvent,
}, },
makepad_math::{dvec2, Rect, Vec2d}, makepad_math::{Rect, Vec2d},
os::{ os::{
apple::apple_sys::*, apple::apple_sys::*,
apple::apple_util::str_to_nsstring, apple::apple_util::str_to_nsstring,
@ -18,7 +18,6 @@ use {
macos_event::MacosEvent, macos_event::MacosEvent,
}, },
}, },
screen::{clamp_point_to_screens, fit_window_rect_to_screens, ScreenGeom},
window::{ window::{
MacosWindowChrome, MacosWindowConfig, MacosWindowKind, MacosWindowLevel, MacosWindowChrome, MacosWindowConfig, MacosWindowKind, MacosWindowLevel,
WindowBackdrop, WindowId, WindowVisuals, WindowBackdrop, WindowId, WindowVisuals,
@ -256,13 +255,9 @@ impl MacosWindow {
let () = msg_send![self.view, setAllowedTouchTypes: 2u64]; let () = msg_send![self.view, setAllowedTouchTypes: 2u64];
let left_top = if let Some(position) = position { 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 { NSPoint {
x: pinned.x, x: position.x as f64,
y: pinned.y, y: position.y as f64,
} }
} else { } else {
NSPoint { x: 0., y: 0. } NSPoint { x: 0., y: 0. }
@ -355,11 +350,6 @@ impl MacosWindow {
if position.is_none() { if position.is_none() {
let () = msg_send![self.window, center]; 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 input_context: ObjcId = msg_send![self.view, inputContext];
let () = msg_send![input_context, invalidateCharacterCoordinates]; let () = msg_send![input_context, invalidateCharacterCoordinates];
@ -773,34 +763,12 @@ impl MacosWindow {
let mut window_frame: NSRect = unsafe { msg_send![self.window, frame] }; let mut window_frame: NSRect = unsafe { msg_send![self.window, frame] };
window_frame.origin.x = pos.x as f64; window_frame.origin.x = pos.x as f64;
window_frame.origin.y = pos.y as f64; window_frame.origin.y = pos.y as f64;
// A caller placing the window cannot know the display arrangement it is placing //not very nice: CGDisplay::main().pixels_high() as f64
// 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 { unsafe {
let () = msg_send![self.window, setFrame: ns_rect_of(fitted) display: YES]; let () = msg_send![self.window, setFrame: window_frame 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 { pub fn get_position(&self) -> Vec2d {
let window_frame: NSRect = unsafe { msg_send![self.window, frame] }; let window_frame: NSRect = unsafe { msg_send![self.window, frame] };
Vec2d { Vec2d {
@ -1215,52 +1183,3 @@ pub fn get_cocoa_window(this: &Object) -> &mut MacosWindow {
&mut *(ptr as *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
}
}

View file

@ -1,13 +1,6 @@
use { use {
crate::cx::Cx, crate::cx::Cx,
std::{ std::{fs::File, io::prelude::*, rc::Rc, time::SystemTime},
fs::File,
io::prelude::*,
path::{Path, PathBuf},
rc::Rc,
sync::OnceLock,
time::SystemTime,
},
}; };
#[derive(PartialEq, Eq, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Debug)]
@ -17,58 +10,21 @@ pub enum EventFlow {
Exit, 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 // lets start a websocket thread
impl Cx { impl Cx {
pub fn native_load_dependencies(&mut self) { pub fn native_load_dependencies(&mut self) {
for (path, dep) in &mut self.dependencies { for (path, dep) in &mut self.dependencies {
if let Some(buffer) = read_file_cwd_or_exe_relative(path) { 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))); dep.data = Some(Ok(Rc::new(buffer)));
} else {
dep.data = Some(Err("read_to_end failed".to_string()));
}
} else { } else {
println!("Could not load resource {}", path); println!("Could not load resource {}", path);
dep.data = Some(Err(format!("Could not read resource {}", path))); dep.data = Some(Err("File! open failed".to_string()));
} }
} }
} }

View file

@ -633,13 +633,10 @@ impl Cx {
} }
let window = &mut self.windows[window_id]; let window = &mut self.windows[window_id];
let (position, inner_size) = window.create_geom(); let inner_size = window
let inner_size = if window.create_inner_size.is_some() { .create_inner_size
inner_size .unwrap_or_else(|| dvec2(1920.0, 1080.0));
} else { let position = window.create_position.unwrap_or_else(|| dvec2(0.0, 0.0));
dvec2(1920.0, 1080.0)
};
let position = position.unwrap_or_else(|| dvec2(0.0, 0.0));
let dpi_factor = configured_headless_dpi(); let dpi_factor = configured_headless_dpi();
let state = &mut windows[window_id.id()]; let state = &mut windows[window_id.id()];

View file

@ -634,7 +634,6 @@ impl WaylandCx {
let compositor = state.compositor.as_ref().unwrap(); let compositor = state.compositor.as_ref().unwrap();
let wm_base = state.wm_base.as_ref().unwrap(); let wm_base = state.wm_base.as_ref().unwrap();
let window = &cx.windows[window_id]; 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() { let app_id = if window.create_app_id.is_empty() {
"Makepad" "Makepad"
} else { } else {
@ -651,8 +650,8 @@ impl WaylandCx {
state.shm.as_ref(), state.shm.as_ref(),
self.qhandle.as_ref().unwrap(), self.qhandle.as_ref().unwrap(),
gl_cx, gl_cx,
create_inner_size, window.create_inner_size.unwrap_or(dvec2(800., 600.)),
create_position, window.create_position,
&window.create_title, &window.create_title,
app_id, app_id,
window.is_fullscreen, window.is_fullscreen,
@ -766,9 +765,6 @@ impl WaylandCx {
} }
} }
CxOsOp::ResizeWindow(window_id, size) => {} 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::RepositionWindow(window_id, size) => {}
CxOsOp::SetWindowVisuals(_window_id, visuals) => { CxOsOp::SetWindowVisuals(_window_id, visuals) => {
if visuals.backdrop != crate::window::WindowBackdrop::None { if visuals.backdrop != crate::window::WindowBackdrop::None {

View file

@ -87,19 +87,8 @@ impl WaylandWindow {
} }
base_surface.commit(); base_surface.commit();
// `wl_egl_window_create` rejects a non-positive extent, and a float-to-int cast turns let wl_egl_surface =
// both a negative and a NaN into zero, so the requested size is floored before the WlEglSurface::new(base_surface.id(), inner_size.x as i32, inner_size.y as i32).unwrap();
// 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 { let egl_surface = unsafe {
(opengl_cx.libegl.eglCreateWindowSurface.unwrap())( (opengl_cx.libegl.eglCreateWindowSurface.unwrap())(
opengl_cx.egl_display, opengl_cx.egl_display,

View file

@ -550,12 +550,11 @@ impl X11Cx {
CxOsOp::CreateWindow(window_id) => { CxOsOp::CreateWindow(window_id) => {
let gl_cx = cx.os.opengl_cx.as_ref().unwrap(); let gl_cx = cx.os.opengl_cx.as_ref().unwrap();
let window = &cx.windows[window_id]; let window = &cx.windows[window_id];
let (create_position, create_inner_size) = window.create_geom();
let opengl_window = OpenglWindow::new( let opengl_window = OpenglWindow::new(
window_id, window_id,
gl_cx, gl_cx,
create_inner_size, window.create_inner_size.unwrap_or(dvec2(800., 600.)),
create_position, window.create_position,
&window.create_title, &window.create_title,
&window.create_app_id, &window.create_app_id,
window.is_fullscreen, window.is_fullscreen,

View file

@ -1,7 +1,6 @@
pub mod linux_x11; pub mod linux_x11;
pub mod linux_x11_stdin; pub mod linux_x11_stdin;
pub mod opengl_x11; pub mod opengl_x11;
pub mod x11_screen;
pub mod x11_sys; pub mod x11_sys;
pub mod xlib_app; pub mod xlib_app;
pub mod xlib_event; pub mod xlib_event;

View file

@ -1,127 +0,0 @@
//! 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,
}]
}

View file

@ -1,12 +1,6 @@
use { use {
self::super::{x11_sys, xlib_app::*, xlib_event::XlibEvent}, self::super::{x11_sys, xlib_app::*, xlib_event::XlibEvent},
crate::{ crate::{area::Area, cursor::MouseCursor, event::*, makepad_math::{Rect, Vec2d}, window::WindowId},
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::{ std::{
cell::Cell, cell::Cell,
ffi::{CStr, CString}, ffi::{CStr, CString},
@ -116,26 +110,22 @@ impl XlibWindow {
| x11_sys::LeaveWindowMask) as c_long; | x11_sys::LeaveWindowMask) as c_long;
let dpi_factor = self.get_dpi_factor(); 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 // 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( let window = x11_sys::XCreateWindow(
display, display,
root_window, root_window,
create_x, if position.is_some() {
create_y, position.unwrap().x
clamp_extent(size.x * dpi_factor), } else {
clamp_extent(size.y * dpi_factor), 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,
0, 0,
visual_info.depth, visual_info.depth,
x11_sys::InputOutput as u32, x11_sys::InputOutput as u32,
@ -748,8 +738,6 @@ 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 { pub fn get_position(&self) -> Vec2d {
unsafe { unsafe {
let display = get_xlib_app_global().display; let display = get_xlib_app_global().display;
@ -805,29 +793,18 @@ 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) { pub fn set_position(&mut self, pos: Vec2d) {
unsafe { unsafe {
let display = get_xlib_app_global().display; let display = get_xlib_app_global().display;
// A caller placing the window cannot know the desktop it is placing into, so the let dpi_factor = self.get_dpi_factor();
// 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( x11_sys::XMoveWindow(
display, display,
self.window.unwrap(), self.window.unwrap(),
clamp_coord(fitted.pos.x), (pos.x * dpi_factor) as i32,
clamp_coord(fitted.pos.y), (pos.y * dpi_factor) as i32,
); );
x11_sys::XFlush(display); x11_sys::XFlush(display);
self.last_window_geom.position = fitted.pos; self.last_window_geom.position = pos;
} }
} }
@ -1307,48 +1284,3 @@ 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
}

View file

@ -12,7 +12,6 @@ pub mod video_file_decoder;
pub mod video_file_encoder; pub mod video_file_encoder;
pub mod wasapi; pub mod wasapi;
pub mod win32_event; pub mod win32_event;
pub mod win32_screen;
pub mod win32_window; pub mod win32_window;
pub mod windows_media; pub mod windows_media;
pub mod windows_media_engine_notify; pub mod windows_media_engine_notify;

View file

@ -1,114 +0,0 @@
//! 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,
}
}

View file

@ -10,9 +10,7 @@ use {
droptarget::*, droptarget::*,
win32_app::{encode_wide, with_win32_app, Win32App}, win32_app::{encode_wide, with_win32_app, Win32App},
win32_event::*, win32_event::*,
win32_screen::{win32_screens, workspace_rect_to_screen},
}, },
screen::{clamp_point_to_screens, fit_window_rect_to_screens},
window::{WindowBackdrop, WindowId, WindowVisuals}, window::{WindowBackdrop, WindowId, WindowVisuals},
windows::{ windows::{
core::PCWSTR, core::PCWSTR,
@ -90,7 +88,6 @@ use {
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE,
WM_MOUSEWHEEL, WM_NCCALCSIZE, WM_NCHITTEST, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_MOUSEWHEEL, WM_NCCALCSIZE, WM_NCHITTEST, WM_RBUTTONDOWN, WM_RBUTTONUP,
WM_SIZE, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, WM_SIZE, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP,
GetWindowPlacement, WINDOWPLACEMENT,
WS_BORDER, WS_CAPTION, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, 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_APPWINDOW, WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST,
WS_EX_WINDOWEDGE, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_THICKFRAME, WS_EX_WINDOWEDGE, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_THICKFRAME,
@ -111,13 +108,6 @@ 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)] #[repr(C)]
struct AccentPolicy { struct AccentPolicy {
accent_state: u32, accent_state: u32,
@ -213,11 +203,6 @@ pub struct Win32Window {
/// Set by `close_window()`; suppresses the WM_ACTIVATE-derived /// Set by `close_window()`; suppresses the WM_ACTIVATE-derived
/// `PopupDismissed(FocusLost)`, which would duplicate the closer's dismissal. /// `PopupDismissed(FocusLost)`, which would duplicate the closer's dismissal.
pub is_closing: Cell<bool>, 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 ignore_wmsize: usize,
pub hwnd: HWND, pub hwnd: HWND,
pub track_mouse_event: bool, pub track_mouse_event: bool,
@ -533,22 +518,10 @@ impl Win32Window {
let style_ex = WS_EX_WINDOWEDGE | WS_EX_APPWINDOW | WS_EX_ACCEPTFILES; let style_ex = WS_EX_WINDOWEDGE | WS_EX_APPWINDOW | WS_EX_ACCEPTFILES;
let (x, y) = match position { let (x, y) = if let Some(position) = position {
// A restored position can name a display that is gone, or hold values no display (position.x as i32, position.y as i32)
// 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 { } else {
(CW_USEDEFAULT, CW_USEDEFAULT) (CW_USEDEFAULT, CW_USEDEFAULT)
}
}
None => (CW_USEDEFAULT, CW_USEDEFAULT),
}; };
let hwnd = unsafe { let hwnd = unsafe {
@ -594,7 +567,6 @@ impl Win32Window {
nc_dq_gen: Cell::new(0), nc_dq_gen: Cell::new(0),
geom_event_gen: Cell::new(0), geom_event_gen: Cell::new(0),
is_closing: Cell::new(false), is_closing: Cell::new(false),
in_size_move: Cell::new(false),
ignore_wmsize: 0, ignore_wmsize: 0,
hwnd, hwnd,
track_mouse_event: false, track_mouse_event: false,
@ -649,7 +621,6 @@ impl Win32Window {
nc_dq_gen: Cell::new(0), nc_dq_gen: Cell::new(0),
geom_event_gen: Cell::new(0), geom_event_gen: Cell::new(0),
is_closing: Cell::new(false), is_closing: Cell::new(false),
in_size_move: Cell::new(false),
ignore_wmsize: 0, ignore_wmsize: 0,
hwnd, hwnd,
track_mouse_event: false, track_mouse_event: false,
@ -678,50 +649,6 @@ impl Win32Window {
self.set_inner_size(size); self.set_inner_size(size);
if self.is_fullscreen { if self.is_fullscreen {
self.maximize(); 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);
} }
} }
@ -1087,25 +1014,12 @@ impl Win32Window {
})); }));
} }
WM_ENTERSIZEMOVE => { WM_ENTERSIZEMOVE => {
window.in_size_move.set(true);
with_win32_app(|app| app.start_resize()); with_win32_app(|app| app.start_resize());
window.do_callback(Win32Event::WindowResizeLoopStart(window.window_id)); 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 => { WM_EXITSIZEMOVE => {
window.in_size_move.set(false);
with_win32_app(|app| app.stop_resize()); with_win32_app(|app| app.stop_resize());
window.do_callback(Win32Event::WindowResizeLoopStop(window.window_id)); 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 // WM_SIZING (0x0214) fires BEFORE the window is resized with
// the proposed new rect. By pre-rendering at this size, the // the proposed new rect. By pre-rendering at this size, the
@ -1119,14 +1033,6 @@ impl Win32Window {
// The window may have moved to a monitor with a different scale; drop the cached // 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. // DPI so send_change_event() (and subsequent hit-tests) re-read the new value.
window.invalidate_cached_dpi(); 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(); window.send_change_event();
} }
WM_DPICHANGED => { WM_DPICHANGED => {
@ -1165,13 +1071,6 @@ impl Win32Window {
0x0003 => { 0x0003 => {
window.nc_dq_cache.set(None); window.nc_dq_cache.set(None);
window.nc_dq_gen.set(window.nc_dq_gen.get().wrapping_add(1)); 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 => { WM_CLOSE => {
// close requested // close requested
@ -1471,51 +1370,31 @@ impl Win32Window {
self.ime_rect = rect; self.ime_rect = rect;
} }
/// 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 {
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 { pub fn get_position(&self) -> Vec2d {
let rect = self.outer_rect(); unsafe {
let mut rect = RECT {
left: 0,
top: 0,
bottom: 0,
right: 0,
};
GetWindowRect(self.hwnd, &mut rect).unwrap();
Vec2d { Vec2d {
x: rect.left as f64, x: rect.left as f64,
y: rect.top as f64, y: rect.top as f64,
} }
} }
}
pub fn get_inner_size(&self) -> Vec2d { pub fn get_inner_size(&self) -> Vec2d {
unsafe { unsafe {
let mut rect = RECT::default(); let mut rect = RECT {
if self.is_iconic() { left: 0,
// A restored window of this backend is fully client-sized (see the top: 0,
// `WM_NCCALCSIZE` handler), so its outer rectangle is also its client size. bottom: 0,
rect = self.outer_rect(); right: 0,
} else { };
GetClientRect(self.hwnd, &mut rect).unwrap(); GetClientRect(self.hwnd, &mut rect).unwrap();
}
let dpi = self.get_dpi_factor(); let dpi = self.get_dpi_factor();
Vec2d { Vec2d {
x: (rect.right - rect.left) as f64 / dpi, x: (rect.right - rect.left) as f64 / dpi,
@ -1525,19 +1404,22 @@ impl Win32Window {
} }
pub fn get_outer_size(&self) -> Vec2d { pub fn get_outer_size(&self) -> Vec2d {
let rect = self.outer_rect(); 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(); let dpi = self.get_dpi_factor();
Vec2d { Vec2d {
x: (rect.right - rect.left) as f64 / dpi, x: (rect.right - rect.left) as f64 / dpi,
y: (rect.bottom - rect.top) 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) { pub fn set_position(&mut self, pos: Vec2d) {
unsafe { unsafe {
let mut window_rect = RECT { let mut window_rect = RECT {
@ -1547,23 +1429,13 @@ impl Win32Window {
right: 0, right: 0,
}; };
GetWindowRect(self.hwnd, &mut window_rect).unwrap(); GetWindowRect(self.hwnd, &mut window_rect).unwrap();
// A caller placing the window — restoring a saved position, cascading a new let dpi = self.get_dpi_factor();
// 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( MoveWindow(
self.hwnd, self.hwnd,
fitted.pos.x as i32, (pos.x * dpi) as i32,
fitted.pos.y as i32, (pos.y * dpi) as i32,
fitted.size.x as i32, window_rect.right - window_rect.left,
fitted.size.y as i32, window_rect.bottom - window_rect.top,
false, false,
) )
.unwrap(); .unwrap();
@ -1723,27 +1595,6 @@ impl Win32Window {
Win32App::do_callback(event); 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) { pub fn send_change_event(&mut self) {
// Record that a geometry event is published (see `geom_event_gen`). // Record that a geometry event is published (see `geom_event_gen`).
self.geom_event_gen.set(self.geom_event_gen.get().wrapping_add(1)); self.geom_event_gen.set(self.geom_event_gen.get().wrapping_add(1));

View file

@ -758,12 +758,11 @@ impl Cx {
match op { match op {
CxOsOp::CreateWindow(window_id) => { CxOsOp::CreateWindow(window_id) => {
let window = &mut self.windows[window_id]; let window = &mut self.windows[window_id];
let (create_position, create_inner_size) = window.create_geom();
let d3d11_window = D3d11Window::new( let d3d11_window = D3d11Window::new(
window_id, window_id,
&d3d11_cx, &d3d11_cx,
create_inner_size, window.create_inner_size.unwrap_or(dvec2(800., 600.)),
create_position, window.create_position,
&window.create_title, &window.create_title,
window.is_fullscreen, window.is_fullscreen,
); );

View file

@ -1,497 +0,0 @@
//! 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(&center_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));
}
}

View file

@ -247,10 +247,6 @@ fn load_packaged_resource(cx: &Cx, dep_path: &str) -> Option<Rc<Vec<u8>>> {
/// Try to load a resource from the packaged location on desktop. /// Try to load a resource from the packaged location on desktop.
/// Returns None when not in packaged mode (package_root is None). /// 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( #[cfg(all(
not(target_arch = "wasm32"), not(target_arch = "wasm32"),
not(any(target_os = "android", target_os = "ios", target_os = "tvos")), not(any(target_os = "android", target_os = "ios", target_os = "tvos")),
@ -259,7 +255,10 @@ 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>>> { fn load_packaged_resource(cx: &Cx, dep_path: &str) -> Option<Rc<Vec<u8>>> {
let root = cx.package_root.as_deref()?; let root = cx.package_root.as_deref()?;
let full_path = format!("{}/{}", root, dep_path); let full_path = format!("{}/{}", root, dep_path);
crate::os::cx_native::read_file_cwd_or_exe_relative(&full_path).map(Rc::new) let mut file = File::open(&full_path).ok()?;
let mut data = Vec::new();
file.read_to_end(&mut data).ok()?;
Some(Rc::new(data))
} }
/// Load a file directly from the filesystem (desktop/mobile only, not wasm). /// Load a file directly from the filesystem (desktop/mobile only, not wasm).

View file

@ -8,7 +8,6 @@ use crate::{
makepad_math::*, makepad_math::*,
//makepad_live_id::*, //makepad_live_id::*,
makepad_script::*, makepad_script::*,
screen::{sanitize_window_geom, DEFAULT_WINDOW_SIZE},
script::vm::*, script::vm::*,
}; };
@ -514,8 +513,6 @@ impl WindowHandle {
cx.windows[self.window_id()].get_inner_size() 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 { pub fn get_position(&self, cx: &Cx) -> Vec2d {
cx.windows[self.window_id()].get_position() cx.windows[self.window_id()].get_position()
} }
@ -611,12 +608,6 @@ impl WindowHandle {
cx.push_unique_platform_op(CxOsOp::ResizeWindow(self.window_id(), size)); 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) { pub fn reposition(&self, cx: &mut Cx, position: Vec2d) {
cx.push_unique_platform_op(CxOsOp::RepositionWindow(self.window_id(), position)); cx.push_unique_platform_op(CxOsOp::RepositionWindow(self.window_id(), position));
} }
@ -703,22 +694,6 @@ impl Default for CxWindow {
} }
impl 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> { pub(crate) fn valid_dpi_factor(dpi_factor: f64) -> Option<f64> {
if dpi_factor.is_finite() && dpi_factor > 0.0 { if dpi_factor.is_finite() && dpi_factor > 0.0 {
Some(dpi_factor) Some(dpi_factor)

View file

@ -454,51 +454,6 @@ fn extract_workspace_patch_sections(workspace_manifest: &str) -> String {
out 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(&section);
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> { fn strip_generated_wrapper_args(args: &[String], build_crate: &str) -> Vec<String> {
let mut out = Vec::new(); let mut out = Vec::new();
let mut skip_next = false; let mut skip_next = false;
@ -601,15 +556,6 @@ fn generate_android_wrapper_manifest(
&workspace_root, &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"); let wrapper_manifest_path = wrapper_dir.join("Cargo.toml");