mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-10 04:22:58 +00:00
egui-winit starts RawInput::focused false and only a winit Focused event sets it. A window that egui never heard was focused cancelled every tab, group and window drag and every resize on its first frame. Gestures now end on a focused-to-unfocused transition (or Escape); a window that never reported focus still drags while the pointer is pressed in it.
712 lines
37 KiB
Rust
712 lines
37 KiB
Rust
//! Floating windows in their own OS windows (feature `native-windows`). Where the backend has
|
||
//! viewports, every floating window whose panels allow it is drawn with
|
||
//! `show_viewport_immediate`, always on top of other windows and hidden while none of the
|
||
//! application's windows has focus. Where egui embeds viewports (wasm, tests) windows stay
|
||
//! in-window at their in-window rect. Tabs, groups and whole windows drag between OS windows
|
||
//! (see `cross.rs`).
|
||
|
||
use std::{fmt::Debug, hash::Hash};
|
||
|
||
use egui::{
|
||
Context, CursorIcon, Event, Id, Key, Modifiers, Pos2, Rect, ResizeDirection, Sense, Ui, Vec2, ViewportBuilder, ViewportClass, ViewportCommand,
|
||
ViewportId, ViewportInfo, WindowLevel, pos2, vec2,
|
||
};
|
||
|
||
use super::Renderer;
|
||
use super::cross::{DragPointer, Surface};
|
||
use crate::docking::chrome::{ChromeStyle, TitleBar};
|
||
pub(in crate::docking) use crate::docking::placement::BAR_HEIGHT;
|
||
use crate::docking::placement::{kept_reachable, monitor_of, restore, sanitized};
|
||
use crate::docking::{Action, Drag, Floating, GroupDrag, GroupGesture, Node};
|
||
|
||
/// Smallest native window.
|
||
const MIN_SIZE: [f32; 2] = [160.0, 100.0];
|
||
/// How long floating windows stay after the application's last window lost focus, so focus
|
||
/// moving between our own windows does not hide them.
|
||
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, 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 = 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)]
|
||
pub(in crate::docking) struct WindowDrag {
|
||
/// [`Floating::id`] of the dragged window.
|
||
pub window: u64,
|
||
/// Where the pointer pressed, in the window's local points.
|
||
pub grab: Vec2,
|
||
/// Desktop position of the window's content as last commanded.
|
||
pub to: Pos2,
|
||
/// Desktop position of the window's content when the drag began, for Escape.
|
||
pub start: Pos2,
|
||
/// The drag began on the window's only tab or group, so it docks whatever
|
||
/// [`crate::docking::DockStyle::title_bar_docks`] says.
|
||
pub docks: bool,
|
||
}
|
||
|
||
/// 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)]
|
||
struct Place {
|
||
/// Where it opened. The builder keeps it, so egui sends no move of its own.
|
||
spawn: [f32; 4],
|
||
/// As last reported or commanded. [`Floating::native`] follows it; a `native` that differs
|
||
/// was changed by the application (undo, reset, a reloaded layout), and the window moves.
|
||
known: [f32; 4],
|
||
/// Until when reports that differ from `known` are a commanded move that has not landed.
|
||
pending: Option<f64>,
|
||
/// The window system put the frame, not the content, where it opened: corrected once.
|
||
corrected: bool,
|
||
}
|
||
|
||
/// Whether two rects differ by more than rounding.
|
||
fn differs(a: [f32; 4], b: [f32; 4]) -> bool {
|
||
a.iter().zip(b).any(|(a, b)| (a - b).abs() > 0.5)
|
||
}
|
||
|
||
/// Desktop position of a viewport's content, and its offset from the window's outer
|
||
/// position, where the window system reports both.
|
||
fn content_of(info: &ViewportInfo) -> Option<(Pos2, Vec2)> {
|
||
let (outer, inner) = (info.outer_rect?, info.inner_rect?);
|
||
(outer.min.is_finite() && inner.min.is_finite()).then(|| (inner.min, inner.min - outer.min))
|
||
}
|
||
|
||
/// The viewport of the native window `window` of the dock area `area`. The `n`th window with
|
||
/// an id an earlier window of the same frame took gets its own.
|
||
pub(in crate::docking) fn viewport_id(area: Id, window: u64, n: usize) -> ViewportId {
|
||
match n {
|
||
0 => ViewportId::from_hash_of((area, "native", window)),
|
||
n => ViewportId::from_hash_of((area, "native", window, n)),
|
||
}
|
||
}
|
||
|
||
/// Whether floating windows show: while one of the application's windows has focus, and until
|
||
/// [`HIDE_DELAY`] after `last_focused`, when one last had it.
|
||
pub(in crate::docking) fn floats_shown(focused: bool, now: f64, last_focused: f64) -> bool {
|
||
focused || now - last_focused < HIDE_DELAY
|
||
}
|
||
|
||
/// The desktop rect of a native viewport's content. None when the backend reports no
|
||
/// positions.
|
||
fn placed(info: &ViewportInfo) -> Option<[f32; 4]> {
|
||
let (min, _) = content_of(info)?;
|
||
let size = info.inner_rect?.size();
|
||
size.is_finite().then(|| sanitized([min.x, min.y, size.x, size.y]))
|
||
}
|
||
|
||
/// What a native window's frame reports back to the main window.
|
||
struct Report<P> {
|
||
closed: bool,
|
||
placed: Option<[f32; 4]>,
|
||
/// From the window's outer position to its content.
|
||
frame: Vec2,
|
||
monitor_size: Option<Vec2>,
|
||
/// Its drop targets, for drags from other windows.
|
||
surface: Option<Surface<P>>,
|
||
}
|
||
|
||
impl<P> Default for Report<P> {
|
||
fn default() -> Self {
|
||
Self { closed: false, placed: None, frame: Vec2::ZERO, monitor_size: None, surface: None }
|
||
}
|
||
}
|
||
|
||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||
/// Show the native windows and return the windows to draw in-window.
|
||
pub(super) fn native_windows<'w>(&mut self, ctx: &Context, windows: Vec<&'w Floating<P>>) -> Vec<&'w Floating<P>> {
|
||
let native = !ctx.embed_viewports();
|
||
let origin = ctx.input(|i| i.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(Pos2::ZERO);
|
||
let visibility = if native { self.float_visibility(ctx) } else { (true, false) };
|
||
self.cross.on = native;
|
||
let mut shown = Vec::new();
|
||
let mut in_window = Vec::new();
|
||
let mut ids = Vec::new();
|
||
let mut natives = Vec::new();
|
||
for window in windows {
|
||
let allowed = window.panels().all(|p| self.content.allow_native(p) && self.permissions(p).floatable);
|
||
if !native || !allowed || window.panels().next().is_none() {
|
||
in_window.push(window);
|
||
continue;
|
||
}
|
||
// Every float is native: one still in-window opens over its place in the app window.
|
||
let rect = window.native.unwrap_or_else(|| {
|
||
let [x, y, w, h] = window.rect;
|
||
sanitized([origin.x + x, origin.y + y, w, h])
|
||
});
|
||
let n = ids.iter().filter(|id| **id == window.id).count();
|
||
ids.push(window.id);
|
||
natives.push((window, rect, viewport_id(self.id, window.id, n)));
|
||
}
|
||
if native {
|
||
#[cfg(debug_assertions)]
|
||
self.warn_rebuilt(ctx, &natives);
|
||
self.escape_elsewhere(ctx, None);
|
||
// 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));
|
||
}
|
||
self.map_desktop(ctx, &natives, visibility.0);
|
||
// The main window owns drags of the panels it draws.
|
||
let id = self.id;
|
||
let dragged = ctx.data(|d| {
|
||
d.get_temp::<Drag<P>>(id.with("drag")).map(|d| d.panel).or(d.get_temp::<GroupDrag<P>>(id.with("group-drag")).map(|d| d.panel))
|
||
});
|
||
if dragged.is_some_and(|panel| !natives.iter().any(|(w, ..)| w.contains(&panel))) {
|
||
let origin = self.cross.main.map(|m| m.min);
|
||
self.record_pointer(ctx, None, origin, None);
|
||
}
|
||
}
|
||
for (window, rect, viewport) in natives {
|
||
if let Some(key) = self.native_window(ctx, window, rect, viewport, visibility) {
|
||
shown.push(key);
|
||
}
|
||
}
|
||
// Forget where windows that are no longer native were told to open.
|
||
let key = self.id.with("native-shown");
|
||
let previous = ctx.data_mut(|d| d.remove_temp::<Vec<Id>>(key)).unwrap_or_default();
|
||
ctx.data_mut(|d| {
|
||
for id in previous.iter().filter(|id| !shown.contains(id)) {
|
||
d.remove::<Place>(*id);
|
||
}
|
||
d.insert_temp(key, shown);
|
||
});
|
||
in_window
|
||
}
|
||
|
||
/// Debug builds: warn once when for [`REBUILT_FRAMES`] frames running, every native window
|
||
/// is new, with the panels of the last frame's. The app builds its layout anew each frame
|
||
/// without [`Floating::with_id`], so each frame opens new OS windows. A layout loaded
|
||
/// again does it once.
|
||
#[cfg(debug_assertions)]
|
||
fn warn_rebuilt(&self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)]) {
|
||
let (key, warned) = (self.id.with("native-previous"), self.id.with("native-rebuilt"));
|
||
let current: Vec<(u64, Vec<P>)> = natives.iter().map(|(w, ..)| (w.id, w.panels().cloned().collect())).collect();
|
||
let (runs, previous) = ctx.data_mut(|d| d.remove_temp::<(usize, Vec<(u64, Vec<P>)>)>(key)).unwrap_or_default();
|
||
let runs = if rebuilt(&previous, ¤t) { runs + 1 } else { 0 };
|
||
if runs >= REBUILT_FRAMES && !ctx.data(|d| d.get_temp::<bool>(warned).unwrap_or(false)) {
|
||
log::warn!(
|
||
"craft-ui docking: the native floating windows get new ids every frame, so each frame opens new OS windows. \
|
||
Keep the layout across frames, or give its windows ids with Floating::with_id."
|
||
);
|
||
ctx.data_mut(|d| d.insert_temp(warned, true));
|
||
}
|
||
ctx.data_mut(|d| d.insert_temp(key, (runs, current)));
|
||
}
|
||
|
||
/// The main window and the shown native windows on the desktop, front first: the window
|
||
/// focused most recently first. A window whose position is unknown is left out.
|
||
fn map_desktop(&mut self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)], visible: bool) {
|
||
let key = self.id.with("native-order");
|
||
let mut order = ctx.data(|d| d.get_temp::<Vec<u64>>(key)).unwrap_or_default();
|
||
order.retain(|id| natives.iter().any(|(w, ..)| w.id == *id));
|
||
let mut windows = Vec::new();
|
||
for (window, rect, viewport) in natives {
|
||
let info = ctx.input(|i| i.raw.viewports.get(viewport).cloned());
|
||
// A new window opens in front.
|
||
if info.as_ref().is_some_and(|i| i.focused == Some(true)) || !order.contains(&window.id) {
|
||
order.retain(|id| *id != window.id);
|
||
order.insert(0, window.id);
|
||
}
|
||
let [x, y, w, h] = *rect;
|
||
let placed = match info {
|
||
None => Some(Rect::from_min_size(pos2(x, y), vec2(w, h))),
|
||
Some(info) => placed(&info).map(|[x, y, w, h]| Rect::from_min_size(pos2(x, y), vec2(w, h))),
|
||
};
|
||
if let Some(placed) = placed.filter(|r| r.is_finite() && visible) {
|
||
windows.push((window.id, placed));
|
||
}
|
||
}
|
||
windows.sort_by_key(|(id, _)| order.iter().position(|o| o == id));
|
||
ctx.data_mut(|d| d.insert_temp(key, order));
|
||
self.cross.windows = windows;
|
||
self.cross.main = ctx.input(|i| i.viewport().outer_rect.and(i.viewport().inner_rect)).filter(|r| r.is_finite());
|
||
}
|
||
|
||
/// Whether floating windows show this frame, and whether that changed since the last.
|
||
fn float_visibility(&mut self, ctx: &Context) -> (bool, bool) {
|
||
let (focused, now) = ctx.input(|i| {
|
||
let infos = || i.raw.viewports.values();
|
||
// A backend that reports no focus at all keeps the windows.
|
||
let focused = i.focused || infos().any(|v| v.focused == Some(true)) || infos().all(|v| v.focused.is_none());
|
||
(focused, i.time)
|
||
});
|
||
self.cross.focus_lost = crate::focus::turned_false(ctx, self.id.with("app-focus"), focused);
|
||
let key = self.id.with("native-visibility");
|
||
let (last, was) = ctx.data(|d| d.get_temp::<(f64, bool)>(key)).unwrap_or((now, true));
|
||
let last = if focused { now } else { last };
|
||
let visible = floats_shown(focused, now, last);
|
||
if visible && !focused {
|
||
ctx.request_repaint_after(std::time::Duration::from_secs_f64((HIDE_DELAY - (now - last)).max(0.0)));
|
||
}
|
||
ctx.data_mut(|d| d.insert_temp(key, (last, visible)));
|
||
(visible, visible != was)
|
||
}
|
||
|
||
/// One floating window in its own viewport. Returns the key of its [`Place`].
|
||
fn native_window(&mut self, ctx: &Context, window: &Floating<P>, rect: [f32; 4], viewport: ViewportId, visibility: (bool, bool)) -> Option<Id> {
|
||
let (visible, toggled) = visibility;
|
||
let first = window.panels().next()?.clone();
|
||
let key = self.id.with(("native-place", viewport));
|
||
let now = ctx.input(|i| i.time);
|
||
let drag_key = self.id.with("window-drag");
|
||
let moving = |ctx: &Context| ctx.data(|d| d.get_temp::<WindowDrag>(drag_key)).is_some_and(|d| d.window == window.id);
|
||
let info = ctx.input(|i| i.raw.viewports.get(&viewport).cloned());
|
||
let frame = info.as_ref().and_then(content_of).map_or(Vec2::ZERO, |(_, frame)| frame);
|
||
let mut place = match ctx.data(|d| d.get_temp::<Place>(key)) {
|
||
Some(mut place) => {
|
||
// The application changed the window's place (undo, reset, a reloaded layout):
|
||
// the OS window moves there, kept where its bar can be grabbed.
|
||
if !moving(ctx)
|
||
&& let Some(native) = window.native.filter(|native| differs(*native, place.known))
|
||
{
|
||
let target = kept_reachable(native, &self.monitors);
|
||
place_window(ctx, viewport, target, frame);
|
||
(place.known, place.pending) = (target, Some(now + MOVE_WAIT));
|
||
}
|
||
place
|
||
}
|
||
None => {
|
||
// A window of a loaded layout opens at its saved place, moved onto a connected
|
||
// monitor. One made this session opens where it was put.
|
||
let seen_key = self.id.with("native-seen");
|
||
let mut seen = ctx.data(|d| d.get_temp::<std::collections::HashSet<u64>>(seen_key)).unwrap_or_default();
|
||
let spawn = if window.native.is_some() && !seen.contains(&window.id) {
|
||
let (main, size) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().monitor_size));
|
||
restore(rect, window.monitor.as_ref(), &self.monitors, main, size)
|
||
} else {
|
||
sanitized(rect)
|
||
};
|
||
seen.insert(window.id);
|
||
ctx.data_mut(|d| d.insert_temp(seen_key, seen));
|
||
Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false }
|
||
}
|
||
};
|
||
let spawn = place.spawn;
|
||
let title = crate::docking::dock_model::first_active(&window.root).map(|p| self.label_of(p)).unwrap_or_default();
|
||
let builder = ViewportBuilder::default()
|
||
.with_title(title)
|
||
.with_position([spawn[0], spawn[1]])
|
||
.with_inner_size([spawn[2], spawn[3]])
|
||
.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, 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(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) {
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Visible(visible));
|
||
}
|
||
if visible && self.activation.as_ref().is_some_and(|panel| window.contains(panel)) {
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Focus);
|
||
}
|
||
let (targets, docks, windows) = (self.targets.len(), self.dock_targets.len(), self.frame.windows.len());
|
||
let report = ctx.show_viewport_immediate(viewport, builder, |ui, class| {
|
||
let full = ui.max_rect();
|
||
ui.painter().rect_filled(full, 0.0, self.style.floating_background);
|
||
let buttons = self.native_bar(ui, window);
|
||
let body = Rect::from_min_max(pos2(full.left(), (full.top() + BAR_HEIGHT).min(full.bottom())), full.max);
|
||
self.floating_body(ui, window, body, full);
|
||
// An embedded fallback reads the main window's input: its close request, place,
|
||
// keys and resizes are not this window's.
|
||
if !matches!(class, ViewportClass::Immediate | ViewportClass::Deferred) {
|
||
return Report::default();
|
||
}
|
||
if window.panels().all(|p| self.permissions(p).movable) {
|
||
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,
|
||
origin: None,
|
||
bounds: full,
|
||
targets: self.targets.split_off(targets),
|
||
dock_targets: self.dock_targets.split_off(docks),
|
||
windows: self.frame.windows.split_off(windows),
|
||
};
|
||
surface.origin = self.window_gestures(ui, window);
|
||
self.paint_cross(ui, &mut surface);
|
||
self.forward_events(ui);
|
||
ui.input(|i| Report {
|
||
closed: i.viewport().close_requested(),
|
||
placed: placed(i.viewport()),
|
||
frame: content_of(i.viewport()).map_or(Vec2::ZERO, |(_, frame)| frame),
|
||
monitor_size: i.viewport().monitor_size,
|
||
surface: Some(surface),
|
||
})
|
||
});
|
||
// Targets in another OS window are in its coordinates. An embedded fallback drew its
|
||
// targets in the main window's.
|
||
if report.surface.is_some() {
|
||
self.targets.truncate(targets);
|
||
self.dock_targets.truncate(docks);
|
||
self.frame.windows.truncate(windows);
|
||
}
|
||
self.cross.surfaces.extend(report.surface);
|
||
if let Some(to) = self.cross.moved.take() {
|
||
(place.known[0], place.known[1], place.pending) = (to.x, to.y, Some(now + MOVE_WAIT));
|
||
}
|
||
let moving = moving(ctx);
|
||
if let Some(placed) = report.placed.filter(|_| !moving) {
|
||
let waiting = place.pending.is_some_and(|until| now < until);
|
||
if !differs(placed, place.known) {
|
||
place.pending = None;
|
||
} else if waiting {
|
||
// The window system put the window's frame where its content should be.
|
||
let outer = pos2(placed[0], placed[1]) - report.frame;
|
||
if !place.corrected && report.frame.length() > 0.5 && outer.distance(pos2(place.known[0], place.known[1])) < 0.5 {
|
||
place.corrected = true;
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::OuterPosition(pos2(place.known[0], place.known[1]) - report.frame));
|
||
}
|
||
} else {
|
||
(place.known, place.pending) = (placed, None);
|
||
}
|
||
}
|
||
ctx.data_mut(|d| d.insert_temp(key, place));
|
||
if report.closed {
|
||
// Like the bar's ×: its panels close and reopen from the application's menu.
|
||
self.output.actions.push(Action::CloseWindow { panel: first });
|
||
return Some(key);
|
||
}
|
||
// The layout follows the window's place, once a bar drag is over.
|
||
let known = place.known;
|
||
if !moving && window.native.is_none_or(|native| differs(native, known)) {
|
||
self.output.actions.push(Action::SetNative { panel: first.clone(), rect: Some(known) });
|
||
// The in-window rect follows, for builds that draw the window in-window.
|
||
if window.panels().all(|p| self.permissions(p).movable) {
|
||
let origin = ctx.input(|i| i.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(Pos2::ZERO);
|
||
let [x, y, w, h] = known;
|
||
self.output.actions.push(Action::MoveFloating { panel: first.clone(), rect: [x - origin.x, y - origin.y, w, h] });
|
||
}
|
||
}
|
||
let monitor = monitor_of(&self.monitors, known).cloned();
|
||
let size = match monitor {
|
||
Some(_) => None,
|
||
None => report.monitor_size.filter(|s| s.is_finite() && s.x > 0.0 && s.y > 0.0).map(|s| [s.x, s.y]),
|
||
};
|
||
if (monitor.is_some() || size.is_some()) && (monitor != window.monitor || size != window.monitor_size) {
|
||
self.output.actions.push(Action::SetMonitor { panel: first, monitor, monitor_size: size });
|
||
}
|
||
Some(key)
|
||
}
|
||
|
||
/// The bar at the top of a native window: its title and ×. Dragging it moves the window.
|
||
/// Returns the rects of its buttons.
|
||
fn native_bar(&mut self, ui: &mut Ui, window: &Floating<P>) -> Vec<Rect> {
|
||
let Some(first) = window.panels().next().cloned() else { return Vec::new() };
|
||
let full = ui.max_rect();
|
||
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);
|
||
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();
|
||
// 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);
|
||
}
|
||
let response = bar.show(ui, rect, &style);
|
||
if response.close.as_ref().is_some_and(|close| close.clicked()) {
|
||
self.output.actions.push(Action::CloseWindow { panel: first });
|
||
}
|
||
if movable && response.bar.drag_started() {
|
||
self.start_window_drag(ui, window.id, false);
|
||
}
|
||
response.close.iter().chain(&response.menu).map(|r| r.rect).collect()
|
||
}
|
||
|
||
/// After a native window's tree drew: a drag of its only tab, or of its only group, moves
|
||
/// the window as its bar does. Then the window follows a bar drag, and writes the pointer
|
||
/// of a drag it owns. Returns the desktop position of the window's content, where known.
|
||
fn window_gestures(&mut self, ui: &Ui, window: &Floating<P>) -> Option<Pos2> {
|
||
let ctx = ui.ctx();
|
||
let (drag_key, group_key) = (self.id.with("drag"), self.id.with("group-drag"));
|
||
let tab = ctx.data(|d| d.get_temp::<Drag<P>>(drag_key)).filter(|d| window.contains(&d.panel));
|
||
let group = ctx.data(|d| d.get_temp::<GroupDrag<P>>(group_key)).filter(|d| window.contains(&d.panel));
|
||
let lone = window.panels().count() == 1 && tab.is_some();
|
||
let whole = matches!(window.root, Node::Tabs { .. }) && group.as_ref().is_some_and(|g| g.gesture == GroupGesture::Group);
|
||
if (lone || whole) && window.panels().all(|p| self.permissions(p).movable) {
|
||
ctx.data_mut(|d| {
|
||
d.remove::<Drag<P>>(drag_key);
|
||
d.remove::<GroupDrag<P>>(group_key);
|
||
});
|
||
self.start_window_drag(ui, window.id, true);
|
||
}
|
||
self.escape_elsewhere(ctx, Some(window.id));
|
||
let origin = ui.input(|i| content_of(i.viewport())).map(|(origin, _)| origin);
|
||
if self.follow_window_drag(ui, window.id) {
|
||
return origin;
|
||
}
|
||
let owns = ctx.data(|d| d.get_temp::<Drag<P>>(drag_key).map(|d| d.panel).or(d.get_temp::<GroupDrag<P>>(group_key).map(|d| d.panel)));
|
||
if owns.is_some_and(|panel| window.contains(&panel)) && self.record_pointer(ctx, Some(window.id), origin, None).escape {
|
||
// The drag's cancel, not a shortcut.
|
||
ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape));
|
||
}
|
||
origin
|
||
}
|
||
|
||
/// Start moving the window with the pointer, from the position the window system reports,
|
||
/// or, where it reports none (Wayland), by handing the move to it. `docks`: the drag began
|
||
/// on the window's only tab or group.
|
||
fn start_window_drag(&mut self, ui: &Ui, window: u64, docks: bool) {
|
||
let press = ui.input(|i| i.pointer.press_origin().or(i.pointer.latest_pos()));
|
||
match (ui.input(|i| content_of(i.viewport())), press) {
|
||
(Some((origin, _)), Some(press)) => {
|
||
let drag = WindowDrag { window, grab: press.to_vec2(), to: origin, start: origin, docks };
|
||
ui.ctx().data_mut(|d| d.insert_temp(self.id.with("window-drag"), drag));
|
||
}
|
||
_ => ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag),
|
||
}
|
||
}
|
||
|
||
/// Move the window by `OuterPosition` while the button is held, so the pressed point stays
|
||
/// under the pointer. Escape puts it back; losing focus leaves it. Returns whether the
|
||
/// window is moving.
|
||
fn follow_window_drag(&mut self, ui: &Ui, window: u64) -> bool {
|
||
let key = self.id.with("window-drag");
|
||
let Some(mut drag) = ui.ctx().data(|d| d.get_temp::<WindowDrag>(key)).filter(|d| d.window == window) else { return false };
|
||
let content = ui.input(|i| content_of(i.viewport()));
|
||
let frame = content.map_or(Vec2::ZERO, |(_, frame)| frame);
|
||
// A pointer move of this frame is relative to where the window was when the window
|
||
// system sent it, which is where it reports the window now: a move commanded earlier
|
||
// may not have landed yet. Without one the window stays, since the last position egui
|
||
// keeps (button events included) was measured against an older place.
|
||
let moved = ui.input(|i| i.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None }));
|
||
let mut step = None;
|
||
if let (Some((origin, _)), Some(local)) = (content, moved.filter(|p| p.is_finite())) {
|
||
let to = origin + (local.to_vec2() - drag.grab);
|
||
if to.distance(drag.to) > 0.0 {
|
||
(drag.to, step) = (to, Some(to));
|
||
ui.ctx().data_mut(|d| d.insert_temp(key, drag));
|
||
}
|
||
}
|
||
// The pointer on the desktop is the pressed point of the window where it was last
|
||
// put, so the origin is that place, less the local pointer.
|
||
let local = ui.input(|i| i.pointer.latest_pos()).filter(|p| p.is_finite());
|
||
let origin = local.map(|local| drag.to + drag.grab - local.to_vec2()).or(content.map(|(origin, _)| origin));
|
||
let pointer = self.record_pointer(ui.ctx(), Some(window), origin, Some(&drag));
|
||
let move_to = |r: &mut Self, to: Pos2| {
|
||
ui.ctx().send_viewport_cmd(ViewportCommand::OuterPosition(to - frame));
|
||
r.cross.moved = Some(to);
|
||
};
|
||
if pointer.escape || self.cross.focus_lost {
|
||
if pointer.escape {
|
||
move_to(self, drag.start);
|
||
ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape));
|
||
}
|
||
ui.ctx().data_mut(|d| d.remove::<WindowDrag>(key));
|
||
return true;
|
||
}
|
||
if let Some(to) = step {
|
||
move_to(self, to);
|
||
}
|
||
if !pointer.down {
|
||
ui.ctx().data_mut(|d| d.remove::<WindowDrag>(key));
|
||
// Left with too little of its bar on a monitor to grab it: back onto the nearest.
|
||
if let Some(size) = ui.input(|i| i.viewport().inner_rect.map(|r| r.size())) {
|
||
let [x, y, ..] = kept_reachable([drag.to.x, drag.to.y, size.x, size.y], &self.monitors);
|
||
if pos2(x, y).distance(drag.to) > 0.5 {
|
||
move_to(self, pos2(x, y));
|
||
}
|
||
}
|
||
self.end_window_drag(drag, pointer);
|
||
}
|
||
true
|
||
}
|
||
|
||
/// Release of a bar drag. The window already sits where it was dragged; over a target in
|
||
/// another window it docks there once every window has drawn (`cross.rs`).
|
||
fn end_window_drag(&mut self, drag: WindowDrag, pointer: DragPointer) {
|
||
self.cross.release = Some((drag, pointer));
|
||
}
|
||
|
||
/// 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) {
|
||
if ui.memory(|m| m.focused().is_some()) {
|
||
return;
|
||
}
|
||
let events = ui.input(|i| {
|
||
i.events
|
||
.iter()
|
||
.filter(|e| matches!(e, Event::Key { .. } | Event::Text(_) | Event::Copy | Event::Cut | Event::Paste(_)))
|
||
.cloned()
|
||
.collect::<Vec<_>>()
|
||
});
|
||
if !events.is_empty() {
|
||
self.output.forwarded_modifiers = Some(ui.input(|i| i.modifiers));
|
||
self.output.forwarded_events.extend(events);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Frames running whose native windows are all new, with the last frame's panels, before
|
||
/// [`Renderer::warn_rebuilt`] warns.
|
||
#[cfg(debug_assertions)]
|
||
const REBUILT_FRAMES: usize = 3;
|
||
|
||
/// Whether every window of `current` is new to `previous`, and they hold the same panels:
|
||
/// windows, with their panels, as `(id, panels)`.
|
||
#[cfg(debug_assertions)]
|
||
fn rebuilt<P: Eq + Hash>(previous: &[(u64, Vec<P>)], current: &[(u64, Vec<P>)]) -> bool {
|
||
!current.is_empty() && current.iter().all(|(id, _)| previous.iter().all(|(p, _)| p != id)) && panels(previous) == panels(current)
|
||
}
|
||
|
||
#[cfg(debug_assertions)]
|
||
fn panels<P: Eq + Hash>(windows: &[(u64, Vec<P>)]) -> std::collections::HashSet<&Vec<P>> {
|
||
windows.iter().map(|(_, panels)| panels).collect()
|
||
}
|
||
|
||
/// Move and size a native window so its content is the desktop rect `to`. The size goes
|
||
/// first, so the move puts the top-left corner in place whichever corner the window system
|
||
/// keeps on a resize (AppKit's frame origin is the bottom left).
|
||
fn place_window(ctx: &Context, viewport: ViewportId, to: [f32; 4], frame: Vec2) {
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::InnerSize(vec2(to[2], to[3])));
|
||
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.
|
||
pub(in crate::docking) fn resize_bands(rect: Rect, buttons: &[Rect]) -> Vec<(ResizeDirection, Rect)> {
|
||
use ResizeDirection as D;
|
||
let (l, r, t, b, e, c) = (rect.left(), rect.right(), rect.top(), rect.bottom(), EDGE, CORNER);
|
||
let band = |x0: f32, y0: f32, x1: f32, y1: f32| Rect::from_min_max(pos2(x0, y0), pos2(x1, y1));
|
||
let bands = [
|
||
(D::NorthWest, band(l, t, l + c, t + e)),
|
||
(D::NorthWest, band(l, t, l + e, t + c)),
|
||
(D::NorthEast, band(r - c, t, r, t + e)),
|
||
(D::NorthEast, band(r - e, t, r, t + c)),
|
||
(D::SouthWest, band(l, b - e, l + c, b)),
|
||
(D::SouthWest, band(l, b - c, l + e, b)),
|
||
(D::SouthEast, band(r - c, b - e, r, b)),
|
||
(D::SouthEast, band(r - e, b - c, r, b)),
|
||
(D::North, band(l + c, t, r - c, t + e)),
|
||
(D::South, band(l + c, b - e, r - c, b)),
|
||
(D::West, band(l, t + c, l + e, b - c)),
|
||
(D::East, band(r - e, t + c, r, b - c)),
|
||
];
|
||
let mut out = Vec::new();
|
||
for (direction, band) in bands {
|
||
let mut pieces = vec![band];
|
||
for button in buttons {
|
||
pieces = pieces.into_iter().flat_map(|piece| outside(piece, *button)).collect();
|
||
}
|
||
out.extend(pieces.into_iter().filter(|p| p.is_positive()).map(|p| (direction, p)));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The parts of `rect` outside `hole`: up to four rects.
|
||
fn outside(rect: Rect, hole: Rect) -> Vec<Rect> {
|
||
let overlap = rect.intersect(hole);
|
||
if !overlap.is_positive() {
|
||
return vec![rect];
|
||
}
|
||
[
|
||
Rect::from_min_max(rect.min, pos2(rect.right(), overlap.top())),
|
||
Rect::from_min_max(pos2(rect.left(), overlap.bottom()), rect.max),
|
||
Rect::from_min_max(pos2(rect.left(), overlap.top()), overlap.left_bottom()),
|
||
Rect::from_min_max(overlap.right_top(), pos2(rect.right(), overlap.bottom())),
|
||
]
|
||
.into_iter()
|
||
.filter(|r| r.is_positive())
|
||
.collect()
|
||
}
|
||
|
||
fn resize_cursor(direction: ResizeDirection) -> CursorIcon {
|
||
use {CursorIcon as C, ResizeDirection as D};
|
||
match direction {
|
||
D::North => C::ResizeNorth,
|
||
D::South => C::ResizeSouth,
|
||
D::East => C::ResizeEast,
|
||
D::West => C::ResizeWest,
|
||
D::NorthEast => C::ResizeNorthEast,
|
||
D::NorthWest => C::ResizeNorthWest,
|
||
D::SouthEast => C::ResizeSouthEast,
|
||
D::SouthWest => C::ResizeSouthWest,
|
||
}
|
||
}
|