Merge pull request #5162 from dotCooCoo/fix/issue-4995

Fix #4995: Above Text moves a Tabs panel that already floats
This commit is contained in:
BFlat
2026-11-02 14:48:03 +07:00
committed by GitHub
3 changed files with 125 additions and 9 deletions
+10 -4
View File
@@ -787,8 +787,9 @@ fn float_space(app: &VectorcraftApp) -> FloatSpace<String> {
}
/// 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<egui::Pos2>) -> 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
+20 -4
View File
@@ -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(())
}
+95 -1
View File
@@ -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}");
}
}