docking: macOS resizes native windows itself, bands elsewhere are 6 pt with 16 pt corners

On macOS the native window is resizable by AppKit again, with its own areas
and cursors. The bar's buttons sit 19 pt in from the trailing edge, clear of
AppKit's corner area (about 15 pt into the window, a 19 pt square measured on
macOS 26), and the window sets no cursor of its own over AppKit's areas.
Elsewhere the bands are 6 pt deep with 16 pt corner Ls, and hand the resize
to the window system; the manual resize path is gone.
This commit is contained in:
aiongg committed 2026-10-24 03:40:17 +00:00
1 parent 1c24845a17
commit 308163bcb5
4 files changed
+156 -162

No files matched your search

+57 -34
View File
@@ -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<u32, Content>) {
/// 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<egui::ResizeDirection>, DockDriver<u32, Content>) {
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<u32, Content>) {
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`.
+4 -1
View File
@@ -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")]
+82 -116
View File
@@ -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
/// (<https://noheger.at/blog/2026/01/11/the-struggle-of-resizing-windows-on-macos-tahoe/>).
/// 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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::<WindowDrag>(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::<WindowDrag>(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) {
ctx.data_mut(|d| d.remove::<WindowDrag>(key));
}
if ctx.data(|d| d.get_temp::<WindowResize>(resize_key)).is_some_and(|r| !shown(r.window)) {
ctx.data_mut(|d| d.remove::<WindowResize>(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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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::<WindowResize>(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::<WindowResize>(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<Rect> {
.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 {
+13 -11
View File
@@ -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