diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 24975f0d..89269c66 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -3,7 +3,7 @@ use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, Vec2, ViewportCommand, epaint::ClippedShape, pos2, vec2}; -use super::render::{DragPointer, WindowDrag, floats_shown, resize_bands, resized}; +use super::render::{APPKIT_CORNER, DragPointer, WindowDrag, appkit_resize_areas, buttons_rect, floats_shown, resize_bands}; use super::testing::DockDriver; use super::{Action, DockContent, Floating, Hit, Layout, Monitor, Node, Permissions, Placement, Target}; use crate::layout::{SplitAxis, SplitSize}; @@ -1000,10 +1000,12 @@ fn a_drag_ends_when_its_native_window_or_panel_goes() { } /// Press at `from` in the native window of panel 5, move the pointer by `by` on the desktop -/// and release. Returns the window's OS rect and the driver. -fn resize_by(from: Pos2, by: Vec2) -> (Rect, DockDriver) { +/// and release. Returns the window's OS rect, the resize the window handed to the window +/// system, and the driver. +fn press_and_drag(from: Pos2, by: Vec2) -> (Rect, Option, DockDriver) { let mut d = driver(layout(true)); let id = window(&d, 5).id; + let viewport = d.viewport(id); let press = os_window(&d, 5).map_or(Pos2::ZERO, |(rect, _)| rect.min) + from.to_vec2(); d.window_step(id, vec![Event::PointerMoved(from), button(from, true)]); // The pointer is where it was moved on the desktop, wherever the window is now. @@ -1015,53 +1017,57 @@ fn resize_by(from: Pos2, by: Vec2) -> (Rect, DockDriver) { let pos = local(&d, by); d.window_step(id, vec![Event::PointerMoved(pos), button(pos, false)]); d.step(vec![]); - (os_window(&d, 5).map_or(Rect::NOTHING, |(rect, _)| rect), d) + let resize = d.host().commands.iter().find_map(|(v, c)| match c { + ViewportCommand::BeginResize(direction) if *v == viewport => Some(*direction), + _ => None, + }); + (os_window(&d, 5).map_or(Rect::NOTHING, |(rect, _)| rect), resize, d) } #[test] fn the_bars_close_button_wins_over_the_resize_bands() { + use egui::ResizeDirection as D; let mut d = driver(layout(true)); let id = window(&d, 5).id; let viewport = d.viewport(id); - // The × of the 240 pt wide window spans 217..235 across and the whole bar down. Its top + // The × of the 240 pt wide window spans 215..233 across and the whole bar down. Its top // right is next to the window's top-right corner, but it closes the window. - let out = d.window_click(id, pos2(234.0, 1.0)); + let out = d.window_click(id, pos2(232.0, 1.0)); assert!(out.actions.contains(&Action::CloseWindow { panel: 5 }), "{:?}", out.actions); - assert!(!d.host().commands.iter().any(|(v, c)| *v == viewport && matches!(c, ViewportCommand::InnerSize(_))), "not resized"); + assert!(!d.host().commands.iter().any(|(v, c)| *v == viewport && matches!(c, ViewportCommand::BeginResize(_))), "not resized"); - // Beside it, at the window's edges, the bands resize the window from (400, 130), 240 by - // 300, keeping the opposite edges. - let rect = |x0: f32, y0: f32, x1: f32, y1: f32| Rect::from_min_max(pos2(x0, y0), pos2(x1, y1)); - let (resized, d) = resize_by(pos2(238.0, 2.0), vec2(20.0, 20.0)); - assert_eq!(resized, rect(400.0, 150.0, 660.0, 430.0), "the top-right corner"); - assert_eq!(window(&d, 5).native, Some([400.0, 150.0, 260.0, 280.0]), "the layout follows"); - assert_eq!(resize_by(pos2(238.0, 15.0), vec2(20.0, 20.0)).0, rect(400.0, 130.0, 660.0, 430.0), "the right edge beside the ×"); - assert_eq!(resize_by(pos2(120.0, 1.0), vec2(20.0, -20.0)).0, rect(400.0, 110.0, 640.0, 430.0), "the top edge above the bar"); - assert_eq!(resize_by(pos2(2.0, 298.0), vec2(-20.0, 20.0)).0, rect(380.0, 130.0, 640.0, 450.0), "the bottom-left corner"); - assert_eq!(resize_by(pos2(2.0, 150.0), vec2(200.0, 0.0)).0, rect(480.0, 130.0, 640.0, 430.0), "no narrower than 160"); + // Beside it, at the window's edges, the bands hand the resize to the window system. + let resize = |from: Pos2| press_and_drag(from, vec2(20.0, 20.0)).1; + assert_eq!(resize(pos2(238.0, 2.0)), Some(D::NorthEast), "the top-right corner"); + assert_eq!(resize(pos2(237.0, 15.0)), Some(D::NorthEast), "the corner beside the ×"); + assert_eq!(resize(pos2(237.0, 100.0)), Some(D::East), "the right edge"); + assert_eq!(resize(pos2(120.0, 1.0)), Some(D::North), "the top edge above the bar"); + assert_eq!(resize(pos2(2.0, 298.0)), Some(D::SouthWest), "the bottom-left corner"); // Inside a corner's L, the bar moves the window. - let (moved, _) = resize_by(pos2(10.0, 10.0), vec2(20.0, 20.0)); - assert_eq!(moved, rect(420.0, 150.0, 660.0, 450.0)); + let rect = |x0: f32, y0: f32, x1: f32, y1: f32| Rect::from_min_max(pos2(x0, y0), pos2(x1, y1)); + let (moved, resize, _) = press_and_drag(pos2(10.0, 10.0), vec2(20.0, 20.0)); + assert_eq!((moved, resize), (rect(420.0, 150.0, 660.0, 450.0), None)); } #[test] fn resize_bands_are_a_few_points_deep_and_leave_out_the_bar_buttons() { let rect = Rect::from_min_size(Pos2::ZERO, vec2(240.0, 300.0)); - let close = Rect::from_center_size(pos2(226.0, 9.0), vec2(18.0, 18.0)); + let close = Rect::from_center_size(pos2(224.0, 9.0), vec2(18.0, 18.0)); let bands = resize_bands(rect, &[close]); let at = |p: Pos2| bands.iter().find(|(_, band)| band.contains(p)).map(|(direction, _)| *direction); for (_, band) in &bands { assert!(!band.intersect(close).is_positive(), "{band:?} covers the ×"); - assert!(band.width().min(band.height()) <= 4.0, "{band:?} is deeper than an edge band"); + assert!(band.width().min(band.height()) <= 6.0, "{band:?} is deeper than an edge band"); } - // Each edge resizes along its whole length, outside the ×, from 12 pt corners. + // Each edge resizes along its whole length, outside the ×, from 16 pt corners. use egui::ResizeDirection as D; // Which of an edge's three parts `v` is in: the first corner, the edge, the last corner. - let part = |v: f32, length: f32| usize::from(v >= 12.0) + usize::from(v > length - 12.0); + let part = |v: f32, length: f32| usize::from(v >= 16.0) + usize::from(v > length - 16.0); for i in 0..240 { let x = i as f32 + 0.5; let top = [D::NorthWest, D::North, D::NorthEast][part(x, 240.0)]; assert_eq!(at(pos2(x, 0.5)), (!close.contains(pos2(x, 0.5))).then_some(top), "top at {x}"); + assert_eq!(at(pos2(x, 5.5)), (!close.contains(pos2(x, 5.5))).then_some(top), "6 pt down at {x}"); assert_eq!(at(pos2(x, 299.5)), Some([D::SouthWest, D::South, D::SouthEast][part(x, 240.0)]), "bottom at {x}"); } for i in 0..300 { @@ -1069,20 +1075,37 @@ fn resize_bands_are_a_few_points_deep_and_leave_out_the_bar_buttons() { assert_eq!(at(pos2(0.5, y)), Some([D::NorthWest, D::West, D::SouthWest][part(y, 300.0)]), "left at {y}"); assert_eq!(at(pos2(239.5, y)), Some([D::NorthEast, D::East, D::SouthEast][part(y, 300.0)]), "right at {y}"); } - assert_eq!(at(pos2(6.0, 6.0)), None, "a corner is an L along its edges"); - assert_eq!(at(pos2(120.0, 4.5)), None, "the bar below the top band"); + assert_eq!(at(pos2(10.0, 10.0)), None, "a corner is an L along its edges"); + assert_eq!(at(pos2(120.0, 6.5)), None, "the bar below the top band"); } #[test] -fn a_manual_resize_keeps_the_opposite_edges_and_the_minimum_size() { - use egui::ResizeDirection as D; - let (start, min) = ([100.0, 50.0, 300.0, 200.0], [160.0, 100.0]); - // Right edge 400, bottom 250. - assert_eq!(resized(start, D::NorthWest, vec2(-20.0, -10.0), min), [80.0, 40.0, 320.0, 210.0]); - assert_eq!(resized(start, D::East, vec2(30.0, 99.0), min), [100.0, 50.0, 330.0, 200.0]); - assert_eq!(resized(start, D::South, vec2(30.0, -20.0), min), [100.0, 50.0, 300.0, 180.0]); - assert_eq!(resized(start, D::SouthWest, vec2(500.0, -500.0), min), [240.0, 50.0, 160.0, 100.0]); - assert_eq!(resized(start, D::NorthEast, vec2(-500.0, 500.0), min), [100.0, 150.0, 160.0, 100.0]); +fn on_macos_the_bars_buttons_keep_clear_of_appkits_corner_resize_areas() { + use crate::docking::chrome::{ChromeStyle, TitleBar}; + let ctx = egui::Context::default(); + for right_to_left in [false, true] { + let mut buttons = Vec::new(); + let full = Rect::from_min_size(Pos2::ZERO, vec2(240.0, 300.0)); + let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(full), ..Default::default() }, |ui| { + let style = ChromeStyle { right_to_left, ..ChromeStyle::from_ui(ui) }; + let bar = Rect::from_min_size(full.min, vec2(full.width(), crate::docking::placement::BAR_HEIGHT)); + let shown = TitleBar::new(egui::Id::new("bar"), "Move").title("Panel").menu("Menu").close("Close").show( + ui, + buttons_rect(bar, right_to_left, true), + &style, + ); + buttons.extend(shown.close.iter().chain(&shown.menu).map(|r| r.rect)); + }); + output.textures_delta.clear(); + assert_eq!(buttons.len(), 2); + let [top_left, top_right, ..] = appkit_resize_areas(full); + let corner = if right_to_left { top_left } else { top_right }; + assert_eq!(corner.width(), APPKIT_CORNER); + for button in buttons { + // The × keeps 4 pt clear across; the bar, 18 pt high, cannot keep it below. + assert!(corner.expand2(vec2(4.0, 0.0)).intersect(button).width() <= 0.0, "{button:?} reaches into {corner:?}"); + } + } } /// [`layout`] built anew, its window with id 7 when `with_id`. diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index 862117f1..36ce1f11 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -15,7 +15,10 @@ mod iconic; #[cfg(feature = "native-windows")] mod native; #[cfg(all(test, feature = "native-windows"))] -pub(in crate::docking) use {cross::DragPointer, native::WindowDrag, native::floats_shown, native::resize_bands, native::resized}; +pub(in crate::docking) use { + cross::DragPointer, + native::{APPKIT_CORNER, WindowDrag, appkit_resize_areas, buttons_rect, floats_shown, resize_bands}, +}; /// The viewport of the native window `window` of the dock area `area`. #[cfg(feature = "native-windows")] diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index 529d1c7e..0279e28c 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -26,14 +26,27 @@ const MIN_SIZE: [f32; 2] = [160.0, 100.0]; const HIDE_DELAY: f64 = 0.25; /// How long reports of a native window's place may lag a move the dock area commanded. const MOVE_WAIT: f64 = 0.5; -/// How far the resize bands reach into a native window from its edges. -const EDGE: f32 = 4.0; +/// How far the resize bands reach into a native window from its edges, where the window has +/// no frame of its own to resize from. +const EDGE: f32 = 6.0; /// How far a corner's grab runs along each of its two edges. -const CORNER: f32 = 12.0; -/// The window system runs a resize from our bands (`BeginResize`). winit cannot on macOS, so -/// there the window follows the pointer by `InnerSize` and `OuterPosition`. Unit tests take -/// that path on every platform, since the test host carries it out. -const SYSTEM_RESIZE: bool = !cfg!(any(target_os = "macos", test)); +const CORNER: f32 = 16.0; +/// macOS keeps the window's frame, and AppKit resizes the window from its edges and corners. +/// Elsewhere the window is undecorated and resizes from our bands, which hand the resize to +/// the window system (`BeginResize`). +const OS_RESIZES: bool = cfg!(target_os = "macos"); +/// How far AppKit's corner resize area reaches into a titled, resizable window along each +/// edge. AppKit takes a press there before the content sees it, and shows its resize cursor. +/// Measured on macOS 26 as a 19 pt square at each corner, 62 % of which would lie inside a +/// square-cornered window: about 15 pt inside and 4 pt outside +/// (). +/// There is no published measurement for earlier releases. +pub(in crate::docking) const APPKIT_CORNER: f32 = 15.0; +/// How far AppKit's edge resize area reaches into the content between the corners: a few +/// points; most of it lies outside the window. +const APPKIT_EDGE: f32 = 4.0; +/// Room between AppKit's corner area and the bar's buttons. +const CORNER_MARGIN: f32 = 4.0; /// A drag of a native window's bar, in the context's data under the area's `"window-drag"`. #[derive(Clone, Copy, Debug, PartialEq)] @@ -51,21 +64,6 @@ pub(in crate::docking) struct WindowDrag { pub docks: bool, } -/// A resize of a native window from one of its bands where the window system does not run it -/// ([`SYSTEM_RESIZE`]), in the context's data under the area's `"window-resize"`. -#[derive(Clone, Copy, Debug)] -struct WindowResize { - /// [`Floating::id`] of the resized window. - window: u64, - direction: ResizeDirection, - /// Desktop rect of the window's content when the drag began. - start: [f32; 4], - /// Desktop point where the pointer pressed. - press: Pos2, - /// Desktop rect of the window's content as last commanded. - to: [f32; 4], -} - /// What the dock area knows of a shown native window's place, in the context's data under /// the area's `("native-place", viewport)`. Rects are desktop rects of the window's content. #[derive(Clone, Copy, Debug)] @@ -163,15 +161,11 @@ impl Renderer<'_, P> { #[cfg(debug_assertions)] self.warn_rebuilt(ctx, &natives); self.escape_elsewhere(ctx, None); - // A window closed or docked during its own bar drag or resize ends it. - let shown = |window: u64| natives.iter().any(|(w, ..)| w.id == window); - let (key, resize_key) = (self.id.with("window-drag"), self.id.with("window-resize")); - if ctx.data(|d| d.get_temp::(key)).is_some_and(|d| !shown(d.window)) { + // A window closed or docked during its own bar drag ends the drag. + let key = self.id.with("window-drag"); + if ctx.data(|d| d.get_temp::(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) { ctx.data_mut(|d| d.remove::(key)); } - if ctx.data(|d| d.get_temp::(resize_key)).is_some_and(|r| !shown(r.window)) { - ctx.data_mut(|d| d.remove::(resize_key)); - } self.map_desktop(ctx, &natives, visibility.0); // The main window owns drags of the panels it draws. let id = self.id; @@ -317,16 +311,16 @@ impl Renderer<'_, P> { .with_min_inner_size(MIN_SIZE) .with_window_level(WindowLevel::AlwaysOnTop) .with_taskbar(false); - // The bar replaces the OS title bar. macOS keeps the frame for its shadow and rounded - // corners, but not its resizing: AppKit's resize areas reach into the content over the - // bar's × and take the pointer from it. The bands resize the window instead. + // The bar replaces the OS title bar. macOS keeps the frame, with its title bar and + // buttons hidden: AppKit resizes a movable window from its edges and corners with its + // own cursors, and draws its rounded corners and standard shadow. #[cfg(target_os = "macos")] let builder = builder .with_titlebar_shown(false) .with_title_shown(false) .with_fullsize_content_view(true) .with_titlebar_buttons_shown(false) - .with_resizable(false); + .with_resizable(window.panels().all(|p| self.permissions(p).movable)); #[cfg(not(target_os = "macos"))] let builder = builder.with_decorations(false); if toggled || (info.is_none() && !visible) { @@ -348,8 +342,11 @@ impl Renderer<'_, P> { return Report::default(); } if window.panels().all(|p| self.permissions(p).movable) { - self.resize_edges(ui, window.id, full, &buttons); - self.follow_window_resize(ui, window.id); + if OS_RESIZES { + leave_cursor_to_os(ui, full); + } else { + resize_edges(ui, self.id.with(("native-edges", window.id)), full, &buttons); + } } let mut surface = Surface { window: window.id, @@ -433,14 +430,14 @@ impl Renderer<'_, P> { let style = ChromeStyle::from_dock(self.style); let rect = Rect::from_min_size(full.min, vec2(full.width(), BAR_HEIGHT.min(full.height()))); ui.painter().rect_filled(rect, 0.0, style.title_background); - // The buttons keep clear of the side's resize band, so the corner's grab runs down - // beside them. - let rect = if style.right_to_left { rect.with_min_x(rect.left() + EDGE) } else { rect.with_max_x(rect.right() - EDGE) }; + let rect = buttons_rect(rect, style.right_to_left, OS_RESIZES); let movable = window.panels().all(|p| self.permissions(p).movable); let closable = self.style.close_button && window.panels().all(|p| self.permissions(p).closable); let (move_label, close_label) = (self.style.move_window_label.clone(), self.style.close_window_label.clone()); let title = crate::docking::dock_model::first_active(&window.root).map(|p| self.label_of(p)).unwrap_or_default(); - let mut bar = TitleBar::new(self.id.with(("native-bar", window.id)), &move_label).title(title).movable(movable); + // On macOS the bar keeps the arrow, as native title bars do: AppKit's resize areas + // cover its ends and top edge, and a cursor set over them flickers with AppKit's. + let mut bar = TitleBar::new(self.id.with(("native-bar", window.id)), &move_label).title(title).movable(movable && !OS_RESIZES); if closable { bar = bar.close(&close_label); } @@ -559,62 +556,6 @@ impl Renderer<'_, P> { self.cross.release = Some((drag, pointer)); } - /// The resize bands of a movable native window. Drawn after its content, so they take the - /// pointer at its edges; the bar's `buttons` are left out. - fn resize_edges(&mut self, ui: &Ui, window: u64, full: Rect, buttons: &[Rect]) { - let id = self.id.with(("native-edges", window)); - for (i, (direction, band)) in resize_bands(full, buttons).into_iter().enumerate() { - let response = ui.interact(band, id.with(i), Sense::drag()).on_hover_cursor(resize_cursor(direction)); - if response.drag_started() { - self.start_window_resize(ui, window, direction); - } - } - } - - /// Start a resize: by the window system where it runs one, else by following the pointer - /// from the place the window system reports. - fn start_window_resize(&mut self, ui: &Ui, window: u64, direction: ResizeDirection) { - let press = ui.input(|i| i.pointer.press_origin().or(i.pointer.latest_pos())); - let content = ui.input(|i| placed(i.viewport())); - match (SYSTEM_RESIZE, content, press) { - (false, Some(start), Some(press)) => { - let resize = WindowResize { window, direction, start, press: pos2(start[0], start[1]) + press.to_vec2(), to: start }; - ui.ctx().data_mut(|d| d.insert_temp(self.id.with("window-resize"), resize)); - } - _ => ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction)), - } - } - - /// Size and move the window while the button is held, the edges opposite the grabbed band - /// in place, no smaller than [`MIN_SIZE`]. Released, a window left with too little of its - /// bar on a monitor goes back onto the nearest, as after a bar drag. - fn follow_window_resize(&mut self, ui: &Ui, window: u64) { - let key = self.id.with("window-resize"); - let Some(mut resize) = ui.ctx().data(|d| d.get_temp::(key)).filter(|r| r.window == window) else { return }; - ui.ctx().set_cursor_icon(resize_cursor(resize.direction)); - let content = ui.input(|i| content_of(i.viewport())); - let frame = content.map_or(Vec2::ZERO, |(_, frame)| frame); - // As in a bar drag: a pointer move is relative to where the window system reports the - // window now. - let moved = ui.input(|i| i.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None })); - let mut to = resize.to; - if let (Some((origin, _)), Some(local)) = (content, moved.filter(|p| p.is_finite())) { - to = resized(resize.start, resize.direction, origin + local.to_vec2() - resize.press, MIN_SIZE); - } - let over = !ui.input(|i| i.pointer.primary_down()) || !self.cross.focused; - if over { - to = kept_reachable(to, &self.monitors); - ui.ctx().data_mut(|d| d.remove::(key)); - } - if differs(to, resize.to) { - place_window(ui.ctx(), ui.ctx().viewport_id(), to, frame); - resize.to = to; - if !over { - ui.ctx().data_mut(|d| d.insert_temp(key, resize)); - } - } - } - /// Keys, text and clipboard events the window's widgets left over, for the application's /// shortcuts. Nothing while a widget of the window has keyboard focus: its keys are its own. fn forward_events(&mut self, ui: &Ui) { @@ -660,6 +601,52 @@ fn place_window(ctx: &Context, viewport: ViewportId, to: [f32; 4], frame: Vec2) ctx.send_viewport_cmd_to(viewport, ViewportCommand::OuterPosition(pos2(to[0], to[1]) - frame)); } +/// The resize bands of a movable native window without a frame of its own. Drawn after its +/// content, so they take the pointer at its edges; the bar's `buttons` are left out. +fn resize_edges(ui: &Ui, id: Id, full: Rect, buttons: &[Rect]) { + for (i, (direction, band)) in resize_bands(full, buttons).into_iter().enumerate() { + let response = ui.interact(band, id.with(i), Sense::drag()).on_hover_cursor(resize_cursor(direction)); + if response.drag_started() { + ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction)); + } + } +} + +/// Over AppKit's resize areas of the window's content `full`, the default cursor whatever the +/// content asked for: AppKit shows its resize cursor there, and any other cursor we set fights +/// it. +fn leave_cursor_to_os(ui: &Ui, full: Rect) { + if ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| appkit_resize_areas(full).iter().any(|area| area.contains(p))) { + ui.ctx().set_cursor_icon(CursorIcon::Default); + } +} + +/// AppKit's resize areas inside a window's content `rect`: a square at each corner and a strip +/// along each edge. +pub(in crate::docking) fn appkit_resize_areas(rect: Rect) -> [Rect; 8] { + let (c, e) = (Vec2::splat(APPKIT_CORNER), APPKIT_EDGE); + [ + Rect::from_min_size(rect.left_top(), c), + Rect::from_min_size(rect.right_top() - vec2(c.x, 0.0), c), + Rect::from_min_size(rect.left_bottom() - vec2(0.0, c.y), c), + Rect::from_min_size(rect.right_bottom() - c, c), + rect.with_max_y(rect.top() + e), + rect.with_min_y(rect.bottom() - e), + rect.with_max_x(rect.left() + e), + rect.with_min_x(rect.right() - e), + ] +} + +/// The part of a native window's bar `rect` its buttons are laid out in, its trailing end +/// pulled in. Where AppKit resizes the window (`os_resizes`) the buttons keep clear of its +/// corner area, which takes presses from the content; the bar is too short to keep them below +/// it. Elsewhere they keep clear of the side's resize band, so the corner's grab runs down +/// beside them. +pub(in crate::docking) fn buttons_rect(rect: Rect, right_to_left: bool, os_resizes: bool) -> Rect { + let inset = if os_resizes { APPKIT_CORNER + CORNER_MARGIN } else { EDGE }; + if right_to_left { rect.with_min_x(rect.left() + inset) } else { rect.with_max_x(rect.right() - inset) } +} + /// The bands along the edges of a native window's content `rect` that resize it: an L at each /// corner, [`EDGE`] deep and [`CORNER`] along both edges, and the edges between them. They /// leave out the bar's `buttons`, which take their own clicks. @@ -709,27 +696,6 @@ fn outside(rect: Rect, hole: Rect) -> Vec { .collect() } -/// The desktop rect `start` with the edges `direction` names moved by `delta`, the opposite -/// edges kept, and no smaller than `min`. -pub(in crate::docking) fn resized(start: [f32; 4], direction: ResizeDirection, delta: Vec2, min: [f32; 2]) -> [f32; 4] { - use ResizeDirection as D; - let [x, y, w, h] = start; - let (mut left, mut top, mut right, mut bottom) = (x, y, x + w, y + h); - if matches!(direction, D::West | D::NorthWest | D::SouthWest) { - left = (left + delta.x).min(right - min[0]); - } - if matches!(direction, D::East | D::NorthEast | D::SouthEast) { - right = (right + delta.x).max(left + min[0]); - } - if matches!(direction, D::North | D::NorthWest | D::NorthEast) { - top = (top + delta.y).min(bottom - min[1]); - } - if matches!(direction, D::South | D::SouthWest | D::SouthEast) { - bottom = (bottom + delta.y).max(top + min[1]); - } - [left, top, right - left, bottom - top] -} - fn resize_cursor(direction: ResizeDirection) -> CursorIcon { use {CursorIcon as C, ResizeDirection as D}; match direction { diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 853f77cf..d014c0f9 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -347,17 +347,19 @@ that is not floatable, stays in-window. Floating windows get no taskbar or Alt-T - **Bar.** Each window has a bar at the top, 18 points high, in the floating title bar's style: the title (the shown panel of its first group) and ×. Dragging the bar moves the window. On Wayland, where a window cannot be placed, the bar hands the move to the compositor. On - Windows and Linux the OS frame is off. On macOS the frame stays for its shadow and rounded - corners, with its title bar and buttons hidden, and is not resizable: AppKit's resize areas - reach well into the content and would take clicks from the ×. -- **Resize.** A movable window resizes from bands 4 pt deep along its edges. Each corner is an - L along both edges, 12 pt long and as deep as the edges. The bands leave out the bar's - buttons, which sit 4 pt in from the window's edge so the corner's grab runs down beside - them; elsewhere the top band lies over the bar's drag area. The OS runs the resize - (`ViewportCommand::BeginResize`), except on macOS, where winit cannot: there the window - follows the pointer by `InnerSize` and `OuterPosition`, keeping the opposite edges and the - 160 by 100 pt minimum, and a window released with too little of its bar on a monitor moves - back onto the nearest one, as after a bar drag. + Windows and Linux the OS frame is off. On macOS the frame stays, with its title bar and + buttons hidden, for its resizing, rounded corners and standard window shadow. The dock area + paints no shadow of its own around a native window. +- **Resize.** A movable window resizes from its edges and corners. On macOS AppKit does it, + with its own areas and cursors. Its corner area reaches about 15 pt into the window along + each edge (a 19 pt square, measured on macOS 26) and takes presses before the content sees + them, so the bar's buttons sit 19 pt in from the window's trailing edge; the 18 pt bar is too + short to keep them below it. Over AppKit's areas the window sets no cursor of its own, which + would flicker with AppKit's. Elsewhere the window resizes from bands 6 pt deep along its + edges, and each corner is an L along both edges, 16 pt long and as deep as the edges. The + bands leave out the bar's buttons, which sit 6 pt in from the window's edge so the corner's + grab runs down beside them; elsewhere the top band lies over the bar's drag area. The OS + runs the resize from a band (`ViewportCommand::BeginResize`). - **Close.** The bar's × and the OS close request close the window's panels, as the floating window's × does. They reopen from the app's Window menu. - **Place.** `Floating::native` is the desktop rect of the window's content (egui's