mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 02:37:00 -06:00
Merge mac-titlebar update: macOS: dragging a title-bar control no longer moves the window
Conflict: apps/designcraft/Cargo.toml: one objc2/objc2-app-kit entry with next's NSTextInputContext and the branch's NSApplication/NSResponder/NSWindow features; next's fmv-macos-events kept Conflict: apps/designcraft/src/main.rs: next's App fields, GPU startup, IME discard and Apple events kept; the branch's `movable` field and `mac_window::sync_movable` call added before `startup.leave()`
This commit is contained in:
@@ -39,9 +39,10 @@ winsafe = { version = "0.0.29", default-features = false, features = ["user"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21", default-features = false }
|
||||
# The input context, to drop the IME's marked text when the Type tool ends a composition.
|
||||
# The input context, to drop the IME's marked text when the Type tool ends a composition; the
|
||||
# window's `movable` flag (see `mac_window`). The versions muda uses, through safe methods.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSTextInputContext"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSTextInputContext", "NSWindow"] }
|
||||
# Finder double-clicks, Open With, Dock drops and `open -a DesignCraft brochure.idml` arrive as a
|
||||
# kAEOpenDocuments Apple event, which winit 0.30 doesn't handle. This crate (also used by PhotoCraft,
|
||||
# PdfCraft and WordCraft) registers the handler behind a safe API (Objective-C interop needs unsafe,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//! macOS: AppKit moves the window when a drag starts anywhere in the title-bar band, on the
|
||||
//! application bar's controls too. The window is made unmovable while the pointer is on one of
|
||||
//! them (`designcraft_ui_egui::title_bar::window_movable`), so only the bar's empty space moves it.
|
||||
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2_app_kit::{NSApplication, NSWindowStyleMask};
|
||||
|
||||
/// Sets the window's `movable` flag from this frame's title bar. `applied` holds the value last
|
||||
/// set, so AppKit is only called when it changes.
|
||||
pub fn sync_movable(ctx: &egui::Context, applied: &mut Option<bool>) {
|
||||
let movable = designcraft_ui_egui::title_bar::window_movable(ctx);
|
||||
if *applied == Some(movable) {
|
||||
return;
|
||||
}
|
||||
// eframe runs the app on the main thread; anywhere else AppKit is out of reach.
|
||||
let Some(mtm) = MainThreadMarker::new() else {
|
||||
return;
|
||||
};
|
||||
let mut found = false;
|
||||
for window in NSApplication::sharedApplication(mtm).windows().to_vec() {
|
||||
// The main window is the only one with a full-size content view.
|
||||
if window.styleMask().contains(NSWindowStyleMask::FullSizeContentView) {
|
||||
window.setMovable(movable);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// Before the window is on screen there is nothing to set yet; try again next frame.
|
||||
if found {
|
||||
*applied = Some(movable);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ mod control_server;
|
||||
mod gpu;
|
||||
mod logging;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_window;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
mod profile;
|
||||
|
||||
@@ -34,6 +36,9 @@ struct App {
|
||||
/// Documents and quit requests from Finder, the Dock and Open With.
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events: fmv_macos_events::Inbox,
|
||||
/// The window's `movable` flag as last set; see `mac_window`.
|
||||
#[cfg(target_os = "macos")]
|
||||
movable: Option<bool>,
|
||||
/// Frames begun so far.
|
||||
frames: u32,
|
||||
}
|
||||
@@ -74,6 +79,8 @@ impl eframe::App for App {
|
||||
if self.app.take_ime_discard() {
|
||||
discard_marked_text();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
mac_window::sync_movable(ui.ctx(), &mut self.movable);
|
||||
self.startup.leave();
|
||||
}
|
||||
fn on_exit(&mut self) {
|
||||
@@ -461,6 +468,8 @@ fn main() -> std::process::ExitCode {
|
||||
startup: created,
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events: events.connect(&cc.egui_ctx),
|
||||
#[cfg(target_os = "macos")]
|
||||
movable: None,
|
||||
frames: 0,
|
||||
}))
|
||||
}),
|
||||
|
||||
@@ -983,6 +983,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mac_window_is_not_movable_on_the_title_bar_controls() {
|
||||
use super::{MAC_APP_BAR, MAC_CONTENT_LEFT};
|
||||
let mut app = app();
|
||||
app.integrated_titlebar = true;
|
||||
let mut h = test_window::open(app, vec2(1400.0, 900.0));
|
||||
let bar = test_window::panel_rect(&h, "app_bar");
|
||||
let mid = bar.min.y + MAC_APP_BAR / 2.0;
|
||||
// The search field is the bar's last control: 125 wide, 4 from the 10-wide right margin.
|
||||
let search = pos2(bar.max.x - 10.0 - 4.0 - 60.0, mid);
|
||||
let controls = [rect(&h, "Home").center(), rect(&h, "Share").center(), rect(&h, "File").center(), search];
|
||||
let mut movable_at = |at: egui::Pos2| {
|
||||
h.hover_at(at);
|
||||
h.run_steps(2);
|
||||
crate::title_bar::window_movable(&h.ctx)
|
||||
};
|
||||
for at in [pos2(bar.center().x, mid), pos2(bar.min.x + MAC_CONTENT_LEFT - 4.0, bar.min.y + 4.0), pos2(bar.center().x, 500.0)] {
|
||||
assert!(movable_at(at), "empty title bar or the page at {at:?}");
|
||||
}
|
||||
for at in controls {
|
||||
assert!(!movable_at(at), "control at {at:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_narrow_window_scrolls_the_app_bar() {
|
||||
let w = 640.0;
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
//! when dragged and follows the system's title-bar double-click setting.
|
||||
//!
|
||||
//! The window has a full-size content view, so AppKit hands every click in the title-bar band to
|
||||
//! the app (that is how the bar's own controls work) and neither drags nor zooms the window
|
||||
//! itself.
|
||||
//! the app (that is how the bar's own controls work) and does not zoom the window. It does move
|
||||
//! the window when a drag starts anywhere in the band, on the bar's controls too, unless the
|
||||
//! window is not movable. [`window_movable`] tells the macOS shell when the pointer is on a
|
||||
//! control, so it can switch that off before the press.
|
||||
|
||||
use std::sync::{Once, OnceLock};
|
||||
|
||||
@@ -70,6 +72,12 @@ pub fn double_click_command(action: DoubleClick, maximized: bool) -> Option<egui
|
||||
pub fn empty_area(ui: &mut egui::Ui, rect: egui::Rect) {
|
||||
prefetch();
|
||||
let resp = ui.interact(rect, ui.id().with("title_bar_empty"), egui::Sense::click());
|
||||
// A control in this layer covers the empty area under the pointer. Another layer on top (a
|
||||
// modal's backdrop, a tooltip) leaves the window movable, as AppKit's own title bar would be.
|
||||
let on_control =
|
||||
ui.ctx().pointer_hover_pos().is_some_and(|p| rect.contains(p) && ui.ctx().layer_id_at(p) == Some(ui.layer_id())) && !resp.hovered();
|
||||
let pass = ui.ctx().cumulative_pass_nr();
|
||||
ui.data_mut(|d| d.insert_temp(movable_id(), (pass, !on_control)));
|
||||
if resp.double_clicked() {
|
||||
let maximized = ui.input(|i| i.viewport().maximized).unwrap_or(false);
|
||||
if let Some(cmd) = double_click_command(action(), maximized) {
|
||||
@@ -91,6 +99,20 @@ pub fn empty_area(ui: &mut egui::Ui, rect: egui::Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
fn movable_id() -> egui::Id {
|
||||
egui::Id::new("title_bar_window_movable")
|
||||
}
|
||||
|
||||
/// Whether AppKit may move the window when a drag starts in the title-bar band: false while the
|
||||
/// pointer is on one of the bar's controls, so pressing and dragging it does not move the window.
|
||||
/// True when neither this pass nor the one before drew a title bar.
|
||||
pub fn window_movable(ctx: &egui::Context) -> bool {
|
||||
match ctx.data(|d| d.get_temp::<(u64, bool)>(movable_id())) {
|
||||
Some((pass, movable)) if ctx.cumulative_pass_nr().saturating_sub(pass) <= 1 => movable,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user