Panels: floating panels reopen on the monitor where they were left

Squashed from #6038 onto main.
This commit is contained in:
dotCooCoo
2026-11-08 11:22:40 +07:00
committed by bflatastic
parent 2f09d9998e
commit b17a744a9c
8 changed files with 162 additions and 0 deletions
+7
View File
@@ -125,6 +125,13 @@ impl App {
}
open_files(&mut self.app, files);
}
let (now, zoom) = (ctx.input(|i| i.time), ctx.zoom_factor());
if let Some(w) = &self.winit_window
&& window::monitors_due(self.monitors_listed, now, zoom)
{
self.app.services.dock_monitors = window::dock_monitors(w, zoom);
self.monitors_listed = Some((now, zoom));
}
self.app.tick(ctx);
if self.pending_maximize {
if let Some(w) = &self.winit_window {
+3
View File
@@ -111,6 +111,8 @@ struct App {
pending_maximize: bool,
/// Frames left to retry [`window::apply_pending_maximize`].
maximize_tries: u8,
/// The time and UI zoom of the last monitor list ([`window::monitors_due`]).
monitors_listed: Option<(f64, f32)>,
}
/// `vectorcraft --help`. Headless work (convert, run, MCP) is `vectorcraft-cli`'s.
@@ -505,6 +507,7 @@ fn main() -> std::process::ExitCode {
winit_window,
pending_maximize,
maximize_tries: 8,
monitors_listed: None,
}))
}),
)
+85
View File
@@ -3,6 +3,7 @@
//! the size, position and maximized state saved with the UI preferences (`UiState::window`), moved
//! onto a monitor that is still connected.
use vectorcraft_ui_egui::DockMonitor;
use vectorcraft_ui_egui::state::WindowGeometry;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
@@ -14,6 +15,9 @@ pub const MIN_SIZE: [f32; 2] = [800.0, 500.0];
/// say how much of the screen they take): a monitor with less room opens it maximized.
const SCREEN_ROOM: [f32; 2] = [64.0, 128.0];
/// Seconds between readings of the monitor list, which picks up a monitor connected while the app runs.
const MONITORS_EVERY: f64 = 1.0;
/// A monitor: its top-left corner and size in physical pixels, and its scale factor.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Monitor {
@@ -67,6 +71,15 @@ impl Monitor {
i32::try_from(want.max(lo).min(hi)).unwrap_or_default()
})
}
/// This monitor for floating panels, in the points egui reports for a window on it: physical
/// pixels divided by the monitor's scale factor and the UI zoom.
fn docking(&self, name: Option<String>, zoom: f32) -> DockMonitor {
let scale = self.scale * zoom;
let [x, y] = self.pos;
let [w, h] = self.size;
DockMonitor { name, rect: [x, y, w, h].map(|v| v as f32 / scale), scale }
}
}
/// Where the window opens: `saved` (the last session's geometry) kept on a connected monitor, else
@@ -109,6 +122,33 @@ fn fit(size: [f32; 2], room: [f32; 2]) -> [f32; 2] {
std::array::from_fn(|i| size[i].min(room[i]).max(MIN_SIZE[i].min(room[i])).max(1.0))
}
/// Whether to list the monitors again: on the first frame, when the UI zoom changed, and at most
/// every [`MONITORS_EVERY`] seconds while the app draws. `listed` holds the last list's time and zoom.
pub fn monitors_due(listed: Option<(f64, f32)>, now: f64, zoom: f32) -> bool {
listed.is_none_or(|(at, listed_zoom)| listed_zoom != zoom || now - at >= MONITORS_EVERY)
}
/// The connected monitors for floating panels (`Services::dock_monitors`).
pub fn dock_monitors(window: &winit::window::Window, zoom: f32) -> Vec<DockMonitor> {
let monitors: Vec<_> = window.available_monitors().map(|m| (Monitor::new(m.position(), m.size(), m.scale_factor()), m.name())).collect();
docking_list(&monitors, zoom, cfg!(target_os = "macos"))
}
/// The list for floating panels, empty when `monitors` have different scales and `shared_points` is
/// false. macOS has one desktop of points. Elsewhere a new window multiplies its saved position by the
/// scale of the monitor it is created on, which can put a window saved on another scale off every
/// monitor. With an empty list craft-ui opens the window on the main window's monitor.
///
/// A name that two monitors share is left out, so craft-ui finds those monitors by their rects: macOS
/// can give two displays of one model the same name.
fn docking_list(monitors: &[(Monitor, Option<String>)], zoom: f32, shared_points: bool) -> Vec<DockMonitor> {
if !shared_points && monitors.windows(2).any(|pair| pair[0].0.scale != pair[1].0.scale) {
return Vec::new();
}
let unique = |name: &String| monitors.iter().filter(|(_, other)| other.as_ref() == Some(name)).count() == 1;
monitors.iter().map(|(monitor, name)| monitor.docking(name.clone().filter(unique), zoom)).collect()
}
/// Size and place the new window (eframe shows it after drawing the first frame) and return its
/// geometry. On Linux the window may not be mapped yet, so [`apply_pending_maximize`] must run
/// until the host reports maximized (fixes #3635).
@@ -204,6 +244,51 @@ mod tests {
assert_eq!(place(saved(Some([9000, 9000]), [1e9, 1e9], true), &[], None).size, DEFAULT_SIZE);
}
#[test]
fn floating_panels_get_monitors_in_the_points_a_window_on_them_reports() {
let dock = |m: Monitor, zoom| m.docking(Some("m".into()), zoom);
let plain = dock(monitor(1920, 0, 1920, 1080, 1.0), 1.0);
assert_eq!(plain, DockMonitor { name: Some("m".into()), rect: [1920.0, 0.0, 1920.0, 1080.0], scale: 1.0 });
// A window at (3000, 200) on this monitor reports (1500, 100).
let scaled = dock(monitor(1920, -200, 3840, 2160, 2.0), 1.0);
assert_eq!(scaled, DockMonitor { name: Some("m".into()), rect: [960.0, -100.0, 1920.0, 1080.0], scale: 2.0 });
// The UI zoom divides what egui reports as well.
let zoomed = dock(monitor(-2560, 0, 2560, 1440, 1.0), 1.25);
assert_eq!(zoomed, DockMonitor { name: Some("m".into()), rect: [-2048.0, 0.0, 2048.0, 1152.0], scale: 1.25 });
}
#[test]
fn monitors_of_different_scales_are_listed_only_on_a_desktop_of_shared_points() {
let named = |m: Monitor, name: &str| (m, Some(name.to_string()));
let same = [named(monitor(0, 0, 1920, 1080, 1.5), "a"), named(monitor(1920, 0, 1920, 1080, 1.5), "b")];
assert_eq!(docking_list(&same, 1.0, false).len(), 2);
let mixed = [named(monitor(0, 0, 3840, 2160, 2.0), "a"), named(monitor(3840, 0, 1920, 1080, 1.0), "b")];
assert_eq!(docking_list(&mixed, 1.0, false), Vec::new());
assert_eq!(docking_list(&mixed, 1.0, true).len(), 2);
assert_eq!(docking_list(&[], 1.0, false), Vec::new());
}
#[test]
fn a_name_that_two_monitors_share_is_left_out_of_the_list() {
let list = [
(monitor(0, 0, 1920, 1080, 1.0), Some("Monitor #1".to_string())),
(monitor(1920, 0, 1920, 1080, 1.0), Some("Monitor #1".to_string())),
(monitor(3840, 0, 1920, 1080, 1.0), Some("Monitor #2".to_string())),
(monitor(5760, 0, 1920, 1080, 1.0), None),
];
let names: Vec<_> = docking_list(&list, 1.0, false).into_iter().map(|m| m.name).collect();
assert_eq!(names, [None, None, Some("Monitor #2".to_string()), None]);
}
#[test]
fn the_monitor_list_is_read_on_the_first_frame_then_once_a_second_or_on_a_zoom_change() {
assert!(monitors_due(None, 0.0, 1.0));
assert!(!monitors_due(Some((5.0, 1.0)), 5.0, 1.0));
assert!(!monitors_due(Some((5.0, 1.0)), 5.5, 1.0));
assert!(monitors_due(Some((5.0, 1.0)), 6.0, 1.0));
assert!(monitors_due(Some((5.0, 1.0)), 5.5, 1.25));
}
#[test]
fn the_saved_window_comes_back_where_it_was() {
let two = [monitor(0, 0, 2560, 1440, 1.0), monitor(2560, 0, 1920, 1080, 1.0)];