mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 16:31:45 +00:00
1141 lines
59 KiB
Rust
1141 lines
59 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`. Palettes stay on top of other windows and hide while none of the
|
||
//! application's windows has focus; viewers are normal windows that come forward when the
|
||
//! application is activated. Where egui embeds viewports (wasm, tests) windows stay
|
||
//! in-window at their in-window rect, and so do they on Wayland, unless the app turns native
|
||
//! windows on ([`crate::docking::DockArea::native_windows`]). Tabs, groups and whole windows
|
||
//! drag between OS windows (see `cross.rs`), and so do the app's drags in panel bodies
|
||
//! (`body_drag.rs`).
|
||
|
||
use std::{fmt::Debug, hash::Hash};
|
||
|
||
use egui::{
|
||
Color32, Context, CursorIcon, Event, Id, Key, LayerId, Modifiers, Order, Pos2, RawInput, Rect, ResizeDirection, Sense, Ui, UiBuilder, Vec2,
|
||
ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, ViewportInfo, WindowLevel, pos2, vec2,
|
||
};
|
||
|
||
use super::Renderer;
|
||
use super::body_drag::{BodyRoute, Held, Queued};
|
||
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, WindowFiles, WindowKind};
|
||
|
||
/// 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;
|
||
/// Whether normal windows are raised when the application is activated. macOS brings an
|
||
/// application's windows forward itself: all of them on activation from the Dock or the
|
||
/// application switcher, the clicked one alone on a click, as its own windows do.
|
||
const RAISE_ON_ACTIVATION: bool = !cfg!(target_os = "macos");
|
||
/// 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;
|
||
|
||
/// Where the × of a native window's bar sits and where it takes clicks, in points from the
|
||
/// window's top and trailing edges, mirrored in a right-to-left layout.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub(in crate::docking) struct CloseLayout {
|
||
/// From the window's top edge to the top of the glyph.
|
||
pub glyph_top: f32,
|
||
/// From the window's trailing edge to the glyph's trailing side.
|
||
pub glyph_trailing: f32,
|
||
/// Width and height of the glyph.
|
||
pub glyph_size: f32,
|
||
/// How far the hit area reaches past the glyph to the left (to the right when mirrored).
|
||
pub hit_left: f32,
|
||
/// How far the hit area reaches past the glyph to the right (to the left when mirrored).
|
||
pub hit_right: f32,
|
||
/// How far the hit area reaches above the glyph.
|
||
pub hit_up: f32,
|
||
/// How far the hit area reaches below the glyph. Past the bar's bottom it takes clicks
|
||
/// from the tab strip under the bar.
|
||
pub hit_down: f32,
|
||
}
|
||
|
||
/// The × on macOS, where macOS puts the close button of its own windows: centered in the
|
||
/// 18 pt bar, its trailing side 8 pt in. AppKit's corner resize area ([`APPKIT_CORNER`])
|
||
/// covers its trailing half and takes presses there, so the hit area reaches away from the
|
||
/// corner: left of the glyph, and down to the bar's bottom.
|
||
pub(in crate::docking) const MACOS_CLOSE: CloseLayout =
|
||
CloseLayout { glyph_top: 4.5, glyph_trailing: 8.0, glyph_size: 9.0, hit_left: 10.0, hit_right: 0.0, hit_up: 0.0, hit_down: 4.5 };
|
||
|
||
/// The × elsewhere: an 18 pt square the height of the bar, its trailing side 7 pt in so the
|
||
/// side's resize band ([`EDGE`]) and the corner's grab run down beside it, with the glyph at
|
||
/// its center.
|
||
pub(in crate::docking) const OTHER_CLOSE: CloseLayout =
|
||
CloseLayout { glyph_top: 4.5, glyph_trailing: 11.5, glyph_size: 9.0, hit_left: 4.5, hit_right: 4.5, hit_up: 4.5, hit_down: 4.5 };
|
||
|
||
/// Opacity of a native window while its bar or only tab drags it somewhere it can dock: the
|
||
/// window stays above the main window, so the drop preview under the pointer shows only
|
||
/// through it.
|
||
#[cfg(any(target_os = "macos", test))]
|
||
const DRAG_OPACITY: f64 = 0.75;
|
||
|
||
/// Opacity of the window `drag` moves: faded while the drag can dock (its only tab or group, or
|
||
/// a bar with [`crate::docking::DockStyle::title_bar_docks`]), opaque for a plain move and while
|
||
/// Ctrl/Cmd keeps the drop floating (`float_only`).
|
||
#[cfg(any(target_os = "macos", test))]
|
||
pub(in crate::docking) fn drag_opacity(drag: Option<&WindowDrag>, float_only: bool, title_bar_docks: bool) -> f64 {
|
||
match drag {
|
||
Some(drag) if !float_only && (drag.docks || title_bar_docks) => DRAG_OPACITY,
|
||
_ => 1.0,
|
||
}
|
||
}
|
||
|
||
/// Sets the opacity of the AppKit windows titled `title`, never one titled `main` (the main
|
||
/// window's title). egui has no viewport command for a window's opacity, and a transparent
|
||
/// surface would need the main window transparent too (eframe picks one alpha mode for every
|
||
/// viewport), so this goes to AppKit, which fades the content, corners and shadow together.
|
||
/// The window is found by title: its native handle is a raw `NSView` pointer, and turning that
|
||
/// into an `NSWindow` needs `unsafe`, which the workspace forbids. Two floats with one title
|
||
/// fade together.
|
||
#[cfg(target_os = "macos")]
|
||
fn set_window_opacity(title: &str, main: Option<&str>, opacity: f64) {
|
||
if title.is_empty() || main == Some(title) {
|
||
return;
|
||
}
|
||
let Some(mtm) = objc2::MainThreadMarker::new() else { return };
|
||
let title = objc2_foundation::NSString::from_str(title);
|
||
for window in objc2_app_kit::NSApplication::sharedApplication(mtm).windows().iter() {
|
||
if window.title().isEqualToString(&title) && window.alphaValue() != opacity {
|
||
window.setAlphaValue(opacity);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Whether native windows outline their ×'s hit area, and on macOS AppKit's resize areas:
|
||
/// `CRAFT_DOCK_DEBUG_CLOSE=1`, read once.
|
||
fn debug_close() -> bool {
|
||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||
*ON.get_or_init(|| std::env::var("CRAFT_DOCK_DEBUG_CLOSE").is_ok_and(|v| v == "1"))
|
||
}
|
||
|
||
/// 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,
|
||
/// Shown, as it opened or was last told.
|
||
shown: 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.
|
||
pub(super) 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() && self.native_windows.unwrap_or_else(|| !wayland(ctx, env_session()));
|
||
let origin = ctx.input(|i| i.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(Pos2::ZERO);
|
||
let (visible, activated) = 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 {
|
||
// The application's own policy: a locked area keeps its native windows.
|
||
let allowed = window.panels().all(|p| self.content.allow_native(p) && self.content.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, visible, activated);
|
||
self.raise_next(ctx, &natives);
|
||
// 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);
|
||
}
|
||
self.cross.viewports = natives.iter().map(|(w, _, viewport)| (w.id, *viewport)).collect();
|
||
}
|
||
self.map_body_route(ctx, native);
|
||
for (window, rect, viewport) in natives {
|
||
if let Some(key) = self.native_window(ctx, window, rect, viewport, visible) {
|
||
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: palettes
|
||
/// above the rest, each the window focused most recently first. A window whose position is
|
||
/// unknown is left out. `visible`: palettes show. `activated`: the application was just
|
||
/// activated, so the main window and its normal windows are raised (see
|
||
/// [`Self::raise_next`]).
|
||
fn map_desktop(&mut self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)], visible: bool, activated: bool) {
|
||
let key = self.id.with("native-order");
|
||
// Focus order, front first; `None` is the main window.
|
||
let mut order = ctx.data(|d| d.get_temp::<Vec<Option<u64>>>(key)).unwrap_or_default();
|
||
order.retain(|id| id.is_none_or(|id| natives.iter().any(|(w, ..)| w.id == id)));
|
||
// The main window comes to the front when it gains focus, and on a press in it: the
|
||
// window system raises it then, though it may have kept focus.
|
||
let main_key = self.id.with("native-main-focused");
|
||
let (main_focused, pressed) = ctx.input(|i| (i.viewport().focused == Some(true), i.pointer.any_pressed()));
|
||
let gained = main_focused && !ctx.data(|d| d.get_temp::<bool>(main_key)).unwrap_or(false);
|
||
ctx.data_mut(|d| d.insert_temp(main_key, main_focused));
|
||
if gained || pressed || !order.contains(&None) {
|
||
let front = order.contains(&None);
|
||
order.retain(Option::is_some);
|
||
if front { order.insert(0, None) } else { order.push(None) }
|
||
}
|
||
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(&Some(window.id)) {
|
||
order.retain(|id| *id != Some(window.id));
|
||
order.insert(0, Some(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))),
|
||
};
|
||
let normal = self.normal_window(window);
|
||
if let Some(placed) = placed.filter(|r| r.is_finite() && (visible || normal)) {
|
||
windows.push((window.id, placed, normal));
|
||
}
|
||
}
|
||
if activated && RAISE_ON_ACTIVATION {
|
||
// The back one first, so each lands in front of those raised before it, and the
|
||
// window just focused, at the front of the order, last.
|
||
// While input is blocked the main window, with the modal, ends in front.
|
||
if self.input_blocked {
|
||
order.retain(Option::is_some);
|
||
order.insert(0, None);
|
||
}
|
||
let queue: Vec<Option<u64>> = order
|
||
.iter()
|
||
.rev()
|
||
.filter(|id| id.is_none_or(|id| natives.iter().any(|(w, ..)| w.id == id && self.normal_window(w))))
|
||
.copied()
|
||
.collect();
|
||
// Blocked, the main window comes in front of a palette the app was activated through.
|
||
if self.input_blocked || queue.iter().any(Option::is_some) {
|
||
ctx.data_mut(|d| d.insert_temp(self.id.with("native-raise"), queue));
|
||
}
|
||
}
|
||
let rank = |id: Option<u64>| order.iter().position(|o| *o == id);
|
||
windows.sort_by_key(|(id, _, normal)| (*normal, rank(Some(*id))));
|
||
self.cross.front = windows.iter().filter(|(id, _, normal)| !normal || rank(Some(*id)) < rank(None)).count();
|
||
self.cross.windows = windows.into_iter().map(|(id, rect, _)| (id, rect)).collect();
|
||
ctx.data_mut(|d| d.insert_temp(key, order));
|
||
self.cross.main = ctx.input(|i| i.viewport().outer_rect.and(i.viewport().inner_rect)).filter(|r| r.is_finite());
|
||
}
|
||
|
||
/// Raise the next window [`Self::map_desktop`] queued when the application was activated,
|
||
/// the main window (`None`) or a normal window: one per pass, since egui sends the commands
|
||
/// of different viewports in no fixed order. Toggling the window level raises a window without focusing it: on Windows
|
||
/// `HWND_NOTOPMOST` puts the window in front of every window that is not topmost, and on
|
||
/// X11 the window manager raises a window as it takes `_NET_WM_STATE_ABOVE`. winit has no
|
||
/// plain raise. `ViewportCommand::Focus` would take focus from the window the user
|
||
/// activated, and egui-winit ignores it for a window that has focus, so focus could not be
|
||
/// handed back. `Visible(true)` does nothing to a shown window.
|
||
fn raise_next(&self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)]) {
|
||
let key = self.id.with("native-raise");
|
||
let Some(mut queue) = ctx.data_mut(|d| d.remove_temp::<Vec<Option<u64>>>(key)) else { return };
|
||
// A window that became a palette since activation keeps its `AlwaysOnTop` level.
|
||
let viewport = |id: Option<u64>| match id {
|
||
None => Some(ViewportId::ROOT),
|
||
Some(id) => natives.iter().find(|(w, ..)| w.id == id && self.normal_window(w)).map(|(.., v)| *v),
|
||
};
|
||
queue.retain(|id| viewport(*id).is_some());
|
||
if queue.is_empty() {
|
||
return;
|
||
}
|
||
if let Some(viewport) = viewport(queue.remove(0)) {
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::WindowLevel(WindowLevel::Normal));
|
||
}
|
||
if !queue.is_empty() {
|
||
ctx.data_mut(|d| d.insert_temp(key, queue));
|
||
ctx.request_repaint();
|
||
}
|
||
}
|
||
|
||
/// Whether palettes show this frame, and whether the application was just activated: one
|
||
/// of its windows has focus after none had it for long enough to hide the palettes.
|
||
/// Losing focus again stops the raise of normal windows that activation started.
|
||
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 = if focused { now } else { ctx.data(|d| d.get_temp::<f64>(key)).unwrap_or(now) };
|
||
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));
|
||
let away_key = self.id.with("native-away");
|
||
let away = ctx.data(|d| d.get_temp::<bool>(away_key)).unwrap_or(false);
|
||
ctx.data_mut(|d| {
|
||
d.insert_temp(away_key, !focused && (away || !visible));
|
||
if !focused {
|
||
d.remove::<Vec<Option<u64>>>(self.id.with("native-raise"));
|
||
}
|
||
});
|
||
(visible, focused && away)
|
||
}
|
||
|
||
/// A window holding a viewer is a normal window, so it neither vanishes nor sits over other
|
||
/// applications; the rest are palettes.
|
||
fn normal_window(&self, window: &Floating<P>) -> bool {
|
||
window.panels().any(|p| self.content.window_kind(p) == WindowKind::Normal)
|
||
}
|
||
|
||
/// One floating window in its own viewport. `visible`: palettes show. Returns the key of
|
||
/// its [`Place`].
|
||
fn native_window(&mut self, ctx: &Context, window: &Floating<P>, rect: [f32; 4], viewport: ViewportId, visible: bool) -> Option<Id> {
|
||
let normal = self.normal_window(window);
|
||
let visible = visible || normal;
|
||
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, shown: true }
|
||
}
|
||
};
|
||
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.clone())
|
||
.with_position([spawn[0], spawn[1]])
|
||
.with_inner_size([spawn[2], spawn[3]])
|
||
.with_min_inner_size(MIN_SIZE)
|
||
// A blocked palette could cover the modal, and its bar moves nothing: the main
|
||
// window can come in front of it at the normal level.
|
||
.with_window_level(if normal || self.input_blocked { WindowLevel::Normal } else { 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(!self.input_blocked && window.panels().all(|p| self.permissions(p).movable));
|
||
#[cfg(not(target_os = "macos"))]
|
||
let builder = builder.with_decorations(false);
|
||
// A blocked palette keeps showing while the app is away: showing it on the app's return
|
||
// would raise it over the modal and, on macOS, give it focus.
|
||
let visible = visible || (self.input_blocked && place.shown);
|
||
// Only on a change: showing a shown window raises it and, on macOS, takes focus.
|
||
if place.shown != visible {
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Visible(visible));
|
||
place.shown = visible;
|
||
}
|
||
if visible && !self.input_blocked && 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 blocked = self.input_blocked;
|
||
ctx.with_plugin(|input: &mut NativeInput| input.windows.insert(viewport, self.id));
|
||
let report = ctx.show_viewport_immediate(viewport, builder, |ui, class| {
|
||
// `cancel_drag` reaches this viewport's drag here.
|
||
let cancelled = ui.ctx().data(|d| d.get_temp::<Vec<Id>>(crate::docking::surface::native_cancel_key(self.id)));
|
||
if ui.ctx().dragged_id().is_some_and(|id| cancelled.is_some_and(|ids| ids.contains(&id))) {
|
||
ui.ctx().stop_dragging();
|
||
}
|
||
let full = ui.max_rect();
|
||
// An embedded fallback reads the main window's input: its close request, place,
|
||
// keys and resizes are not this window's.
|
||
let own = matches!(class, ViewportClass::Immediate | ViewportClass::Deferred);
|
||
let released = if blocked && own { block_input(ui) } else { Vec::new() };
|
||
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.cross.pointed = None;
|
||
self.floating_body(ui, window, body, full);
|
||
self.cross.held |= ui.ctx().dragged_id().is_some_and(|id| self.owned_interactions.contains(&id));
|
||
if debug_close() {
|
||
debug_outlines(ui, full, &buttons);
|
||
}
|
||
if !own {
|
||
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);
|
||
if blocked {
|
||
(surface.targets, surface.dock_targets, surface.windows) = Default::default();
|
||
cover(ui, self.id.with(("native-blocked", window.id)), full, self.style.blocked_dim);
|
||
if !released.is_empty() {
|
||
self.output.forwarded_modifiers = Some(ui.input(|i| i.modifiers));
|
||
self.output.forwarded_events.extend(released);
|
||
}
|
||
} else {
|
||
self.paint_cross(ui, &mut surface);
|
||
self.route_body_drag(ui.ctx(), Some(window.id));
|
||
self.forward_events(ui);
|
||
self.report_files(ui, window, viewport);
|
||
}
|
||
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);
|
||
// Set every frame from the drag state, so any way a drag ends (a drop, Escape, a focus
|
||
// loss, the window closing) brings the window back to full opacity. AppKit finds the
|
||
// window by title, so a window sharing the dragged one's title stays faded with it.
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
let main = ctx.input(|i| i.raw.viewports.get(&ViewportId::ROOT).and_then(|v| v.title.clone()));
|
||
let drag = ctx.data(|d| d.get_temp::<WindowDrag>(drag_key));
|
||
let dragged = drag.as_ref().and_then(|drag| self.layout.floating.iter().find(|w| w.id == drag.window));
|
||
let this = dragged.and_then(|w| crate::docking::dock_model::first_active(&w.root)).is_some_and(|p| self.label_of(p) == title);
|
||
let float_only = drag.is_some_and(|drag| self.cross.pointer.is_some_and(|p| p.window == Some(drag.window) && p.command));
|
||
let opacity = if this { drag_opacity(drag.as_ref(), float_only, self.style.title_bar_docks) } else { 1.0 };
|
||
set_window_opacity(&title, main.as_deref(), opacity);
|
||
}
|
||
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. A
|
||
// window the application would refuse to close stays open.
|
||
if !blocked && window.panels().all(|p| self.permissions(p).closable) {
|
||
self.output.actions.push(Action::CloseWindow { panel: first });
|
||
return Some(key);
|
||
}
|
||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::CancelClose);
|
||
}
|
||
// 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 (close_hit, close_glyph) = close_rects(rect, if OS_RESIZES { MACOS_CLOSE } else { OTHER_CLOSE }, style.right_to_left);
|
||
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).close_at(close_hit, close_glyph);
|
||
}
|
||
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));
|
||
}
|
||
|
||
/// Files dragged over or dropped on the window this frame, with the panel under the pointer.
|
||
fn report_files(&mut self, ui: &Ui, window: &Floating<P>, viewport: ViewportId) {
|
||
let (hovered, dropped, pos) = ui.input(|i| (i.raw.hovered_files.clone(), i.raw.dropped_files.clone(), i.pointer.latest_pos()));
|
||
if hovered.is_empty() && dropped.is_empty() {
|
||
return;
|
||
}
|
||
// The window system may send no pointer moves while files are dragged in.
|
||
let Some(panel) = self.cross.pointed.clone().or_else(|| crate::docking::dock_model::first_active(&window.root).cloned()) else { return };
|
||
// A window whose own pass is discarded draws twice: keep one report, with the latest
|
||
// hovered files and every dropped one.
|
||
if let Some(files) = self.output.window_files.iter_mut().find(|f| f.viewport == viewport) {
|
||
files.panel = panel;
|
||
files.pos = pos;
|
||
files.hovered = hovered;
|
||
files.dropped.extend(dropped);
|
||
} else {
|
||
self.output.window_files.push(WindowFiles { window: window.id, viewport, panel, pos, hovered, dropped });
|
||
}
|
||
}
|
||
|
||
/// 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Input hooks of dock areas for their native windows, run by egui before it reads each
|
||
/// window's input. eframe runs `App::raw_input_hook` for the main window only. App drags in
|
||
/// panel bodies cross windows here too (`body_drag.rs`).
|
||
#[derive(Default)]
|
||
pub(super) struct NativeInput {
|
||
hooks: std::collections::HashMap<Id, egui::mutex::Mutex<InputHook>>,
|
||
/// The dock area of each native window shown this frame.
|
||
windows: std::collections::HashMap<ViewportId, Id>,
|
||
/// Each dock area's app drag in a panel body.
|
||
pub routes: std::collections::HashMap<Id, BodyRoute>,
|
||
/// Pointer events routed to each window, for its next input.
|
||
pub queued: Queued,
|
||
/// Owners' held releases, for their input in a later main-window pass.
|
||
pub held: Held,
|
||
/// The main window's passes so far.
|
||
pub pass: u64,
|
||
}
|
||
|
||
pub(in crate::docking) type InputHook = Box<dyn FnMut(&Context, &mut RawInput) + Send>;
|
||
|
||
impl egui::Plugin for NativeInput {
|
||
fn debug_name(&self) -> &'static str {
|
||
"craft-ui docking native input"
|
||
}
|
||
|
||
fn input_hook(&mut self, ctx: &Context, input: &mut RawInput) {
|
||
let viewport = input.viewport_id;
|
||
if viewport == ViewportId::ROOT {
|
||
self.pass += 1;
|
||
// Every window draws in the main window's pass: events for one that has not drawn
|
||
// since the last pass are for a window that is gone.
|
||
let pass = self.pass;
|
||
self.queued.retain(|_, (at, _)| *at + 1 >= pass);
|
||
self.held.retain(|_, (at, _)| *at + 1 >= pass);
|
||
}
|
||
// Taken before routing, which may queue more for this window's next input. A held
|
||
// release waits for a later main-window pass: a discarded pass reruns within its own.
|
||
let waiting = self.queued.remove(&viewport);
|
||
let held = if self.held.get(&viewport).is_some_and(|(at, _)| *at < self.pass) { self.held.remove(&viewport) } else { None };
|
||
let mut routed = false;
|
||
for route in self.routes.values_mut() {
|
||
routed |= route.route(input, &mut self.queued, &mut self.held, self.pass);
|
||
}
|
||
// Before the window's own events: they happened earlier, and a held release must not
|
||
// end a press that follows it.
|
||
let earlier =
|
||
held.into_iter().flat_map(|(_, events)| events).chain(waiting.into_iter().flat_map(|(_, events)| events.into_iter().map(|(_, e)| e)));
|
||
input.events.splice(0..0, earlier);
|
||
if routed {
|
||
ctx.request_repaint_of(ViewportId::ROOT);
|
||
}
|
||
if let Some(hook) = self.windows.get(&input.viewport_id).and_then(|area| self.hooks.get(area)) {
|
||
(hook.lock())(ctx, input);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Set or clear the input hook of the dock area `area` for this frame, and forget its native
|
||
/// windows until they show again.
|
||
pub(in crate::docking) fn set_input_hook(ctx: &Context, area: Id, hook: Option<InputHook>) {
|
||
if ctx.plugin_opt::<NativeInput>().is_none() {
|
||
ctx.add_plugin(NativeInput::default());
|
||
}
|
||
ctx.with_plugin(|input: &mut NativeInput| {
|
||
input.windows.retain(|_, a| *a != area);
|
||
match hook {
|
||
Some(hook) => input.hooks.insert(area, egui::mutex::Mutex::new(hook)),
|
||
None => input.hooks.remove(&area),
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 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;
|
||
|
||
/// What the environment says of winit's backend.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub(in crate::docking) enum Session {
|
||
/// `WAYLAND_DISPLAY` or `WAYLAND_SOCKET` is set: winit picks Wayland unless the app forces
|
||
/// X11 in code (`EventLoopBuilderExtX11::with_x11`); winit reads no variable that does.
|
||
Wayland,
|
||
/// None of those nor `DISPLAY` is set on a Unix desktop: a Wayland app started with only
|
||
/// `WAYLAND_SOCKET`, which winit unsets as it connects, or no display at all (tests, CI).
|
||
Unknown,
|
||
/// X11 or another backend.
|
||
Other,
|
||
}
|
||
|
||
/// The [`Session`] of a Unix desktop (`unix`) whose environment variable `name` is set and
|
||
/// not empty when `set(name)`.
|
||
pub(in crate::docking) fn session(unix: bool, set: impl Fn(&str) -> bool) -> Session {
|
||
if !unix {
|
||
Session::Other
|
||
} else if set("WAYLAND_DISPLAY") || set("WAYLAND_SOCKET") {
|
||
Session::Wayland
|
||
} else if set("DISPLAY") {
|
||
Session::Other
|
||
} else {
|
||
Session::Unknown
|
||
}
|
||
}
|
||
|
||
/// This process's [`Session`], read once.
|
||
fn env_session() -> Session {
|
||
static SESSION: std::sync::OnceLock<Session> = std::sync::OnceLock::new();
|
||
*SESSION.get_or_init(|| {
|
||
session(cfg!(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd")), |name| {
|
||
std::env::var_os(name).is_some_and(|value| !value.is_empty())
|
||
})
|
||
})
|
||
}
|
||
|
||
/// Whether the main window runs on Wayland, given the environment's `session`. egui does not
|
||
/// name the backend, but on Wayland the main window never reports its outer rect, while under
|
||
/// X11 it does from the first frame on (eframe reads it as the window opens). A minimized X11
|
||
/// window reports none either, so once one has, the backend is not Wayland. Without a Wayland
|
||
/// variable, only a real OS window (one that reports its native pixels per point) counts.
|
||
pub(in crate::docking) fn wayland(ctx: &Context, session: Session) -> bool {
|
||
let root = |test: fn(&ViewportInfo) -> bool| ctx.input(|i| i.raw.viewports.get(&ViewportId::ROOT).is_some_and(test));
|
||
match session {
|
||
Session::Other => return false,
|
||
Session::Unknown if !root(|v| v.native_pixels_per_point.is_some()) => return false,
|
||
Session::Wayland | Session::Unknown => {}
|
||
}
|
||
let key = Id::new("craft-ui-dock-window-positions");
|
||
if ctx.data(|d| d.get_temp::<bool>(key).unwrap_or(false)) {
|
||
return false;
|
||
}
|
||
let reported = root(|v| v.outer_rect.is_some());
|
||
if reported {
|
||
ctx.data_mut(|d| d.insert_temp(key, true));
|
||
}
|
||
!reported
|
||
}
|
||
|
||
/// 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()
|
||
}
|
||
|
||
/// The start of a blocked native window's frame: its widgets are disabled, and the pointer,
|
||
/// keys, text and scrolling are taken out of its input before anything reads them, so a panel
|
||
/// that reads input directly sees none either. Returns the key releases it took out, for the
|
||
/// app: a key pressed before the block would otherwise stay down in the app's shortcuts.
|
||
fn block_input(ui: &mut Ui) -> Vec<Event> {
|
||
// Disabling fades what the window paints, its opaque background too; the cover dims it.
|
||
let opacity = ui.opacity();
|
||
ui.disable();
|
||
ui.set_opacity(opacity);
|
||
let kept = |e: &Event| matches!(e, Event::Screenshot { .. } | Event::WindowFocused(_));
|
||
let released = ui.input(|i| i.events.iter().filter(|e| matches!(e, Event::Key { pressed: false, .. })).cloned().collect());
|
||
ui.input_mut(|i| {
|
||
i.events.retain(kept);
|
||
i.raw.events.retain(kept);
|
||
// File drops are fields of their own, not events.
|
||
i.raw.dropped_files.clear();
|
||
i.raw.hovered_files.clear();
|
||
i.keys_down.clear();
|
||
i.smooth_scroll_delta = Vec2::ZERO;
|
||
i.pointer = Default::default();
|
||
});
|
||
ui.memory_mut(|m| m.stop_text_input());
|
||
released
|
||
}
|
||
|
||
/// The end of a blocked native window's frame: `dim` over its content `full`, in a layer above
|
||
/// its popups that takes the pointer, so a popup left open takes no click either.
|
||
fn cover(ui: &Ui, id: Id, full: Rect, dim: Color32) {
|
||
let cover = Ui::new(ui.ctx().clone(), id, UiBuilder::new().layer_id(LayerId::new(Order::Tooltip, id)).max_rect(full));
|
||
cover.painter().rect_filled(full, 0.0, dim);
|
||
cover.interact(full, id.with("sink"), Sense::CLICK | Sense::DRAG);
|
||
}
|
||
|
||
/// 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),
|
||
]
|
||
}
|
||
|
||
/// Over a native window's content `full`: its bar's `buttons` outlined in red and, on macOS,
|
||
/// AppKit's resize areas in orange.
|
||
fn debug_outlines(ui: &Ui, full: Rect, buttons: &[Rect]) {
|
||
let stroke = |color| egui::Stroke::new(1.0, color);
|
||
for button in buttons {
|
||
ui.painter().rect_stroke(*button, 0.0, stroke(egui::Color32::RED), egui::StrokeKind::Inside);
|
||
}
|
||
if OS_RESIZES {
|
||
for area in appkit_resize_areas(full) {
|
||
ui.painter().rect_stroke(area, 0.0, stroke(egui::Color32::ORANGE), egui::StrokeKind::Inside);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The hit area and glyph of the × in a native window's bar `bar`, laid out by `layout` from
|
||
/// the bar's top and trailing edges.
|
||
pub(in crate::docking) fn close_rects(bar: Rect, layout: CloseLayout, right_to_left: bool) -> (Rect, Rect) {
|
||
let l = layout;
|
||
let glyph = Rect::from_min_size(pos2(bar.right() - l.glyph_trailing - l.glyph_size, bar.top() + l.glyph_top), Vec2::splat(l.glyph_size));
|
||
let hit = Rect::from_min_max(glyph.min - vec2(l.hit_left, l.hit_up), glyph.max + vec2(l.hit_right, l.hit_down));
|
||
let mirror = |r: Rect| {
|
||
if right_to_left {
|
||
Rect::from_x_y_ranges((bar.left() + bar.right() - r.right())..=(bar.left() + bar.right() - r.left()), r.y_range())
|
||
} else {
|
||
r
|
||
}
|
||
};
|
||
(mirror(hit), mirror(glyph))
|
||
}
|
||
|
||
/// 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,
|
||
}
|
||
}
|