diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index cdec00eaa..a35166d97 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -787,8 +787,9 @@ fn float_space(app: &VectorcraftApp) -> FloatSpace { } /// Float `panel` (open or not) as a window of its own with its top-left corner at `at`, `width` -/// wide: the Tabs panel over the text it sets. -pub(crate) fn place_floating(app: &mut VectorcraftApp, panel: &str, at: egui::Pos2, width: f32) -> Result<(), String> { +/// wide: the Tabs panel over the text it sets. `origin`: the main window's content top-left on the +/// desktop. A panel already floating in an OS window of its own moves to `at` from there (#4995). +pub(crate) fn place_floating(app: &mut VectorcraftApp, panel: &str, at: egui::Pos2, width: f32, origin: Option) -> Result<(), String> { let panel = normalize(panel).ok_or("unknown panel")?.to_string(); if !(at.x.is_finite() && at.y.is_finite() && width.is_finite()) { return Err("panel position must be finite".into()); @@ -800,8 +801,13 @@ pub(crate) fn place_floating(app: &mut VectorcraftApp, panel: &str, at: egui::Po let height = floating_window(&txn.workspace.layout, &panel).map_or(floating_size(&panel).y, |w| w.rect[3]); let rect = [at.x, at.y, width.max(1.0), height]; let whole = floating_window(&txn.workspace.layout, &panel).is_some_and(|w| w.panels().count() == 1); - let action = if whole { Action::MoveFloating { panel, rect } } else { Action::Float { panel, rect } }; + let native = whole && floating_window(&txn.workspace.layout, &panel).is_some_and(|w| w.native.is_some()); + let action = if whole { Action::MoveFloating { panel: panel.clone(), rect } } else { Action::Float { panel: panel.clone(), rect } }; txn.apply(action)?; + // The OS window follows its desktop rect, not the in-window one. + if let Some(origin) = origin.filter(|o| native && o.is_finite()) { + txn.apply(Action::SetNative { panel, rect: Some([origin.x + at.x, origin.y + at.y, rect[2], rect[3]]) })?; + } txn.commit(app, Vec::new(), None); Ok(()) } @@ -1231,7 +1237,7 @@ const DOCK_LABELS: &[(&str, &str)] = &[ ]; /// The size a panel floats at (tab strip included), whatever the size of the dock it left. -fn floating_size(panel: &str) -> egui::Vec2 { +pub(crate) fn floating_size(panel: &str) -> egui::Vec2 { match panel { "properties" | "layers" | "libraries" => egui::vec2(300.0, 420.0), // Tall enough to show its Corners and Shared Edges sections, last in it, which the Corner diff --git a/crates/ui-egui/src/panels/tabs.rs b/crates/ui-egui/src/panels/tabs.rs index 1e0818454..c8ef4331c 100644 --- a/crates/ui-egui/src/panels/tabs.rs +++ b/crates/ui-egui/src/panels/tabs.rs @@ -264,10 +264,26 @@ fn above_text(app: &mut VectorcraftApp, ctx: &egui::Context, n: &Node, span: f64 return None; } let width = span as f32 * per + RULER_LEFT * 2.0; - let height = crate::panel_docking::layout_of(&app.ui).floating.iter().find(|w| w.contains(&"tabs".to_string())).map_or(240.0, |w| w.rect[3]); - let pos = egui::pos2(on_screen.left() - RULER_LEFT, on_screen.top() - height - 6.0); - let _ = ctx; - crate::panel_docking::place_floating(app, "tabs", pos, width).ok()?; + let height = crate::panel_docking::layout_of(&app.ui) + .floating + .iter() + .find(|w| w.contains(&"tabs".to_string())) + .map_or(crate::panel_docking::floating_size("tabs").y, |w| w.rect[3]); + // Text near the top of the window: the panel goes just below the text when that place is in + // the window and the panel fits there in the canvas, else at the window's top, so its bar + // stays where it can be grabbed rather than above the screen. + let above = on_screen.top() - height - 6.0; + let below = on_screen.bottom() + 6.0; + let top = if above >= 0.0 { + above + } else if below >= 0.0 && below + height <= app.canvas_rect.map_or(f32::NEG_INFINITY, |c| c.bottom()) { + below + } else { + 0.0 + }; + let pos = egui::pos2(on_screen.left() - RULER_LEFT, top); + let origin = ctx.input(|i| i.raw.viewports.get(&egui::ViewportId::ROOT).and_then(|v| v.inner_rect)).map(|r| r.min); + crate::panel_docking::place_floating(app, "tabs", pos, width, origin).ok()?; Some(()) } diff --git a/crates/ui-egui/src/panels/tabs/tests.rs b/crates/ui-egui/src/panels/tabs/tests.rs index b598d0c2a..7b02b72ff 100644 --- a/crates/ui-egui/src/panels/tabs/tests.rs +++ b/crates/ui-egui/src/panels/tabs/tests.rs @@ -991,7 +991,8 @@ fn above_text_puts_the_ruler_over_the_text_at_the_zoom() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); app.session.execute("text.create", &json!({"x": 40, "y": 200, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap(); - app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), vec2(1000.0, 700.0))); + // The canvas starts low enough in the window for the panel to fit above the text at 200%. + app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 440.0), vec2(1000.0, 700.0))); for zoom in [1.0, 2.0] { if let Some(v) = app.view_mut() { v.zoom = zoom; @@ -1012,3 +1013,96 @@ fn above_text_puts_the_ruler_over_the_text_at_the_zoom() { } assert_eq!(app.ui.docking.unwrap().floating.len(), 1, "placed again, not floated twice"); } + +/// Position Panel Above Text moves the panel when it already floats in an OS window of its own: +/// the window's desktop rect goes above the text too, from the main window's top-left (#4995). +#[test] +fn above_text_moves_a_panel_floating_in_its_own_window() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); + app.session.execute("text.create", &json!({"x": 40, "y": 200, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), vec2(1000.0, 700.0))); + if let Some(v) = app.view_mut() { + v.zoom = 1.0; + } + let (node, _, span) = current(&app).unwrap(); + let ctx = egui::Context::default(); + above_text(&mut app, &ctx, &node, span).unwrap(); + // The panel's OS window sits elsewhere on the desktop, as after the user dragged it there. + let mut layout = app.ui.docking.clone().unwrap(); + let window = layout.floating.iter_mut().find(|w| w.contains(&"tabs".to_string())).unwrap(); + window.native = Some([1500.0, 900.0, window.rect[2], window.rect[3]]); + app.ui.docking = Some(layout); + // The main window's content starts at (200, 100) on the desktop. + let mut raw = egui::RawInput::default(); + raw.viewports.entry(egui::ViewportId::ROOT).or_default().inner_rect = + Some(egui::Rect::from_min_size(egui::pos2(200.0, 100.0), vec2(1200.0, 800.0))); + ctx.run_ui(raw, |_| {}).textures_delta.clear(); + above_text(&mut app, &ctx, &node, span).unwrap(); + let layout = app.ui.docking.clone().unwrap(); + let g = layout.floating.iter().find(|w| w.contains(&"tabs".to_string())).unwrap(); + let native = g.native.unwrap(); + assert_eq!([native[0], native[1]], [200.0 + g.rect[0], 100.0 + g.rect[1]], "the OS window goes above the text"); + assert_eq!([native[2], native[3]], [g.rect[2], g.rect[3]]); +} + +/// Text near the top of the window: Position Panel Above Text puts the panel just below the text, +/// so neither the panel nor its OS window goes above the screen and the text stays in view. Text +/// with no room below it either gets the panel at the window's top. +#[test] +fn above_text_keeps_the_panel_bar_in_the_window() { + for (text_height, below) in [(100.0, true), (760.0, false)] { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); + app.session.execute("text.create", &json!({"x": 40, "y": 10, "text": "a b", "area": {"width": 300, "height": text_height}})).unwrap(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), vec2(1000.0, 700.0))); + if let Some(v) = app.view_mut() { + v.zoom = 1.0; + } + let (node, _, span) = current(&app).unwrap(); + let xf = crate::canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let frame = xf.rect_to_screen(node.geometric_bounds().unwrap()); + assert!(frame.top() < 240.0, "no room above the text"); + let ctx = egui::Context::default(); + above_text(&mut app, &ctx, &node, span).unwrap(); + let mut layout = app.ui.docking.clone().unwrap(); + let window = layout.floating.iter_mut().find(|w| w.contains(&"tabs".to_string())).unwrap(); + let top = if below { frame.bottom() + 6.0 } else { 0.0 }; + assert_eq!(window.rect[1], top, "text {text_height} high"); + window.native = Some([1500.0, 900.0, window.rect[2], window.rect[3]]); + app.ui.docking = Some(layout); + let mut raw = egui::RawInput::default(); + raw.viewports.entry(egui::ViewportId::ROOT).or_default().inner_rect = + Some(egui::Rect::from_min_size(egui::pos2(200.0, 100.0), vec2(1200.0, 800.0))); + ctx.run_ui(raw, |_| {}).textures_delta.clear(); + above_text(&mut app, &ctx, &node, span).unwrap(); + let layout = app.ui.docking.clone().unwrap(); + let g = layout.floating.iter().find(|w| w.contains(&"tabs".to_string())).unwrap(); + assert_eq!(g.native.unwrap()[1], 100.0 + top, "text {text_height} high: the OS window"); + } +} + +/// Position Panel Above Text puts the panel at the window's top when the place below the text is +/// above the window (text panned above the canvas), or when the panel at the height it floats at +/// does not fit below the text in the canvas. +#[test] +fn above_text_puts_the_panel_at_the_top_when_below_the_text_does_not_fit() { + // The view's centre and the text's height: panned so the text is above the canvas, and a text + // whose bottom leaves room for 240 points of panel but not for the 360 it floats at. + for (center_y, text_height) in [(600.0, 100.0), (350.0, 340.0)] { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 600, "height": 800})).unwrap(); + app.session.execute("text.create", &json!({"x": 40, "y": 20, "text": "a b", "area": {"width": 300, "height": text_height}})).unwrap(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), vec2(1000.0, 700.0))); + if let Some(v) = app.view_mut() { + v.zoom = 1.0; + v.center = vectorcraft_geom::Point::new(300.0, center_y); + } + let (node, _, span) = current(&app).unwrap(); + let ctx = egui::Context::default(); + above_text(&mut app, &ctx, &node, span).unwrap(); + let layout = app.ui.docking.clone().unwrap(); + let g = layout.floating.iter().find(|w| w.contains(&"tabs".to_string())).unwrap(); + assert_eq!(g.rect[1], 0.0, "view centered at y {center_y}"); + } +}