diff --git a/Cargo.lock b/Cargo.lock index e703f7c5..729286af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -953,6 +953,9 @@ dependencies = [ "egui_kittest", "image", "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "serde", "serde_json", ] diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 6b1f8145..b35b71a3 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -15,6 +15,12 @@ egui = { workspace = true } log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +# Fading a native window while it is dragged (`NSWindow.alphaValue`); winit already uses these. +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" +objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "objc2-core-foundation", "NSApplication", "NSResponder", "NSWindow"] } +objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSArray", "NSEnumerator", "NSString"] } + [features] serde = ["dep:serde"] # Floating windows may move to their own OS windows (desktop egui backends with viewports). diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index 21595078..1d100b6c 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -80,6 +80,32 @@ pub(in crate::docking) const MACOS_CLOSE: CloseLayout = 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: the window stays above the +/// main window, so the drop preview under the pointer shows only through it. +#[cfg(target_os = "macos")] +const DRAG_OPACITY: f64 = 0.5; + +/// 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 { @@ -344,7 +370,7 @@ impl Renderer<'_, P> { 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_title(title.clone()) .with_position([spawn[0], spawn[1]]) .with_inner_size([spawn[2], spawn[3]]) .with_min_inner_size(MIN_SIZE) @@ -421,6 +447,13 @@ impl Renderer<'_, P> { (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. + #[cfg(target_os = "macos")] + { + let main = ctx.input(|i| i.raw.viewports.get(&ViewportId::ROOT).and_then(|v| v.title.clone())); + set_window_opacity(&title, main.as_deref(), if moving { DRAG_OPACITY } else { 1.0 }); + } if let Some(placed) = report.placed.filter(|_| !moving) { let waiting = place.pending.is_some_and(|until| now < until); if !differs(placed, place.known) {