Detailed changes
@@ -793,7 +793,7 @@ dependencies = [
"url",
"wayland-backend",
"wayland-client",
- "wayland-protocols 0.32.9",
+ "wayland-protocols",
"zbus",
]
@@ -7370,7 +7370,7 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
- "wayland-protocols 0.31.2",
+ "wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3",
@@ -18927,18 +18927,6 @@ dependencies = [
"xcursor",
]
-[[package]]
-name = "wayland-protocols"
-version = "0.31.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
-dependencies = [
- "bitflags 2.9.4",
- "wayland-backend",
- "wayland-client",
- "wayland-scanner",
-]
-
[[package]]
name = "wayland-protocols"
version = "0.32.9"
@@ -18953,14 +18941,14 @@ dependencies = [
[[package]]
name = "wayland-protocols-plasma"
-version = "0.2.0"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
+checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
- "wayland-protocols 0.31.2",
+ "wayland-protocols",
"wayland-scanner",
]
@@ -18973,7 +18961,7 @@ dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
- "wayland-protocols 0.32.9",
+ "wayland-protocols",
"wayland-scanner",
]
@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
"client_system",
"dlopen",
], optional = true }
-wayland-client = { version = "0.31.2", optional = true }
-wayland-cursor = { version = "0.31.1", optional = true }
-wayland-protocols = { version = "0.31.2", features = [
+wayland-client = { version = "0.31.11", optional = true }
+wayland-cursor = { version = "0.31.11", optional = true }
+wayland-protocols = { version = "0.32.9", features = [
"client",
"staging",
"unstable",
], optional = true }
-wayland-protocols-plasma = { version = "0.2.0", features = [
+wayland-protocols-plasma = { version = "0.3.9", features = [
"client",
], optional = true }
wayland-protocols-wlr = { version = "0.3.9", features = [
@@ -5,6 +5,7 @@ use gpui::{
struct SubWindow {
custom_titlebar: bool,
+ is_dialog: bool,
}
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
@@ -23,7 +24,10 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
}
impl Render for SubWindow {
- fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let window_bounds =
+ WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
+
div()
.flex()
.flex_col()
@@ -52,8 +56,28 @@ impl Render for SubWindow {
.child(
div()
.p_8()
+ .flex()
+ .flex_col()
.gap_2()
.child("SubWindow")
+ .when(self.is_dialog, |div| {
+ div.child(button("Open Nested Dialog", move |_, cx| {
+ cx.open_window(
+ WindowOptions {
+ window_bounds: Some(window_bounds),
+ kind: WindowKind::Dialog,
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| SubWindow {
+ custom_titlebar: false,
+ is_dialog: true,
+ })
+ },
+ )
+ .unwrap();
+ }))
+ })
.child(button("Close", |window, _| {
window.remove_window();
})),
@@ -86,6 +110,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
+ is_dialog: false,
})
},
)
@@ -101,6 +126,39 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
+ is_dialog: false,
+ })
+ },
+ )
+ .unwrap();
+ }))
+ .child(button("Floating", move |_, cx| {
+ cx.open_window(
+ WindowOptions {
+ window_bounds: Some(window_bounds),
+ kind: WindowKind::Floating,
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| SubWindow {
+ custom_titlebar: false,
+ is_dialog: false,
+ })
+ },
+ )
+ .unwrap();
+ }))
+ .child(button("Dialog", move |_, cx| {
+ cx.open_window(
+ WindowOptions {
+ window_bounds: Some(window_bounds),
+ kind: WindowKind::Dialog,
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| SubWindow {
+ custom_titlebar: false,
+ is_dialog: true,
})
},
)
@@ -116,6 +174,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: true,
+ is_dialog: false,
})
},
)
@@ -131,6 +190,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
+ is_dialog: false,
})
},
)
@@ -147,6 +207,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
+ is_dialog: false,
})
},
)
@@ -162,6 +223,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
+ is_dialog: false,
})
},
)
@@ -177,6 +239,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
+ is_dialog: false,
})
},
)
@@ -1348,6 +1348,10 @@ pub enum WindowKind {
/// docks, notifications or wallpapers.
#[cfg(all(target_os = "linux", feature = "wayland"))]
LayerShell(layer_shell::LayerShellOptions),
+
+ /// A window that appears on top of its parent window and blocks interaction with it
+ /// until the modal window is closed
+ Dialog,
}
/// The appearance of the window, as defined by the operating system.
@@ -36,12 +36,6 @@ use wayland_client::{
wl_shm_pool, wl_surface,
},
};
-use wayland_protocols::wp::cursor_shape::v1::client::{
- wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
-};
-use wayland_protocols::wp::fractional_scale::v1::client::{
- wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
-};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
self, ZwpPrimarySelectionOfferV1,
};
@@ -61,6 +55,14 @@ use wayland_protocols::xdg::decoration::zv1::client::{
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
+use wayland_protocols::{
+ wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
+ xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
+};
+use wayland_protocols::{
+ wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
+ xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
+};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
@@ -122,6 +124,7 @@ pub struct Globals {
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
+ pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
pub executor: ForegroundExecutor,
}
@@ -132,6 +135,7 @@ impl Globals {
qh: QueueHandle<WaylandClientStatePtr>,
seat: wl_seat::WlSeat,
) -> Self {
+ let dialog_v = XdgWmDialogV1::interface().version;
Globals {
activation: globals.bind(&qh, 1..=1, ()).ok(),
compositor: globals
@@ -160,6 +164,7 @@ impl Globals {
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
+ dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
executor,
qh,
}
@@ -729,10 +734,7 @@ impl LinuxClient for WaylandClient {
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let mut state = self.0.borrow_mut();
- let parent = state
- .keyboard_focused_window
- .as_ref()
- .and_then(|w| w.toplevel());
+ let parent = state.keyboard_focused_window.clone();
let (window, surface_id) = WaylandWindow::new(
handle,
@@ -751,7 +753,12 @@ impl LinuxClient for WaylandClient {
fn set_cursor_style(&self, style: CursorStyle) {
let mut state = self.0.borrow_mut();
- let need_update = state.cursor_style != Some(style);
+ let need_update = state.cursor_style != Some(style)
+ && (state.mouse_focused_window.is_none()
+ || state
+ .mouse_focused_window
+ .as_ref()
+ .is_some_and(|w| !w.is_blocked()));
if need_update {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
@@ -1011,7 +1018,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
}
}
-fn get_window(
+pub(crate) fn get_window(
mut state: &mut RefMut<WaylandClientState>,
surface_id: &ObjectId,
) -> Option<WaylandWindowStatePtr> {
@@ -1654,6 +1661,30 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
if let Some(window) = state.mouse_focused_window.clone() {
+ if window.is_blocked() {
+ let default_style = CursorStyle::Arrow;
+ if state.cursor_style != Some(default_style) {
+ let serial = state.serial_tracker.get(SerialKind::MouseEnter);
+ state.cursor_style = Some(default_style);
+
+ if let Some(cursor_shape_device) = &state.cursor_shape_device {
+ cursor_shape_device.set_shape(serial, default_style.to_shape());
+ } else {
+ // cursor-shape-v1 isn't supported, set the cursor using a surface.
+ let wl_pointer = state
+ .wl_pointer
+ .clone()
+ .expect("window is focused by pointer");
+ let scale = window.primary_output_scale();
+ state.cursor.set_icon(
+ &wl_pointer,
+ serial,
+ default_style.to_icon_names(),
+ scale,
+ );
+ }
+ }
+ }
if state
.keyboard_focused_window
.as_ref()
@@ -2225,3 +2256,27 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
}
}
}
+
+impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
+ fn event(
+ _: &mut Self,
+ _: &XdgWmDialogV1,
+ _: <XdgWmDialogV1 as Proxy>::Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle<Self>,
+ ) {
+ }
+}
+
+impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
+ fn event(
+ _state: &mut Self,
+ _proxy: &XdgDialogV1,
+ _event: <XdgDialogV1 as Proxy>::Event,
+ _data: &(),
+ _conn: &Connection,
+ _qhandle: &QueueHandle<Self>,
+ ) {
+ }
+}
@@ -7,7 +7,7 @@ use std::{
};
use blade_graphics as gpu;
-use collections::HashMap;
+use collections::{FxHashSet, HashMap};
use futures::channel::oneshot::Receiver;
use raw_window_handle as rwh;
@@ -20,7 +20,7 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
use wayland_protocols::{
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
- xdg::shell::client::xdg_toplevel::XdgToplevel,
+ xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
};
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
@@ -29,7 +29,7 @@ use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
- WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
+ WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
layer_shell::LayerShellNotSupportedError, px, size,
};
use crate::{
@@ -87,6 +87,8 @@ struct InProgressConfigure {
pub struct WaylandWindowState {
surface_state: WaylandSurfaceState,
acknowledged_first_configure: bool,
+ parent: Option<WaylandWindowStatePtr>,
+ children: FxHashSet<ObjectId>,
pub surface: wl_surface::WlSurface,
app_id: Option<String>,
appearance: WindowAppearance,
@@ -126,7 +128,7 @@ impl WaylandSurfaceState {
surface: &wl_surface::WlSurface,
globals: &Globals,
params: &WindowParams,
- parent: Option<XdgToplevel>,
+ parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<Self> {
// For layer_shell windows, create a layer surface instead of an xdg surface
if let WindowKind::LayerShell(options) = ¶ms.kind {
@@ -178,10 +180,28 @@ impl WaylandSurfaceState {
.get_xdg_surface(&surface, &globals.qh, surface.id());
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
- if params.kind == WindowKind::Floating {
- toplevel.set_parent(parent.as_ref());
+ let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
+
+ if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
+ toplevel.set_parent(xdg_parent.as_ref());
}
+ let dialog = if params.kind == WindowKind::Dialog {
+ let dialog = globals.dialog.as_ref().map(|dialog| {
+ let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
+ xdg_dialog.set_modal();
+ xdg_dialog
+ });
+
+ if let Some(parent) = parent.as_ref() {
+ parent.add_child(surface.id());
+ }
+
+ dialog
+ } else {
+ None
+ };
+
if let Some(size) = params.window_min_size {
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
}
@@ -198,6 +218,7 @@ impl WaylandSurfaceState {
xdg_surface,
toplevel,
decoration,
+ dialog,
}))
}
}
@@ -206,6 +227,7 @@ pub struct WaylandXdgSurfaceState {
xdg_surface: xdg_surface::XdgSurface,
toplevel: xdg_toplevel::XdgToplevel,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
+ dialog: Option<XdgDialogV1>,
}
pub struct WaylandLayerSurfaceState {
@@ -258,7 +280,13 @@ impl WaylandSurfaceState {
xdg_surface,
toplevel,
decoration: _decoration,
+ dialog,
}) => {
+ // drop the dialog before toplevel so compositor can explicitly unapply it's effects
+ if let Some(dialog) = dialog {
+ dialog.destroy();
+ }
+
// The role object (toplevel) must always be destroyed before the xdg_surface.
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
toplevel.destroy();
@@ -288,6 +316,7 @@ impl WaylandWindowState {
globals: Globals,
gpu_context: &BladeContext,
options: WindowParams,
+ parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<Self> {
let renderer = {
let raw_window = RawWindow {
@@ -319,6 +348,8 @@ impl WaylandWindowState {
Ok(Self {
surface_state,
acknowledged_first_configure: false,
+ parent,
+ children: FxHashSet::default(),
surface,
app_id: None,
blur: None,
@@ -391,6 +422,10 @@ impl Drop for WaylandWindow {
fn drop(&mut self) {
let mut state = self.0.state.borrow_mut();
let surface_id = state.surface.id();
+ if let Some(parent) = state.parent.as_ref() {
+ parent.state.borrow_mut().children.remove(&surface_id);
+ }
+
let client = state.client.clone();
state.renderer.destroy();
@@ -448,10 +483,10 @@ impl WaylandWindow {
client: WaylandClientStatePtr,
params: WindowParams,
appearance: WindowAppearance,
- parent: Option<XdgToplevel>,
+ parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<(Self, ObjectId)> {
let surface = globals.compositor.create_surface(&globals.qh, ());
- let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent)?;
+ let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone())?;
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -473,6 +508,7 @@ impl WaylandWindow {
globals,
gpu_context,
params,
+ parent,
)?)),
callbacks: Rc::new(RefCell::new(Callbacks::default())),
});
@@ -501,6 +537,16 @@ impl WaylandWindowStatePtr {
Rc::ptr_eq(&self.state, &other.state)
}
+ pub fn add_child(&self, child: ObjectId) {
+ let mut state = self.state.borrow_mut();
+ state.children.insert(child);
+ }
+
+ pub fn is_blocked(&self) -> bool {
+ let state = self.state.borrow();
+ !state.children.is_empty()
+ }
+
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
@@ -818,6 +864,9 @@ impl WaylandWindowStatePtr {
}
pub fn handle_ime(&self, ime: ImeInput) {
+ if self.is_blocked() {
+ return;
+ }
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -894,6 +943,21 @@ impl WaylandWindowStatePtr {
}
pub fn close(&self) {
+ let state = self.state.borrow();
+ let client = state.client.get_client();
+ #[allow(clippy::mutable_key_type)]
+ let children = state.children.clone();
+ drop(state);
+
+ for child in children {
+ let mut client_state = client.borrow_mut();
+ let window = get_window(&mut client_state, &child);
+ drop(client_state);
+
+ if let Some(child) = window {
+ child.close();
+ }
+ }
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
fun()
@@ -901,6 +965,9 @@ impl WaylandWindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
+ if self.is_blocked() {
+ return;
+ }
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
&& !fun(input.clone()).propagate
{
@@ -222,7 +222,7 @@ pub struct X11ClientState {
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
impl X11ClientStatePtr {
- fn get_client(&self) -> Option<X11Client> {
+ pub fn get_client(&self) -> Option<X11Client> {
self.0.upgrade().map(X11Client)
}
@@ -752,7 +752,7 @@ impl X11Client {
}
}
- fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
+ pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
let state = self.0.borrow();
state
.windows
@@ -789,12 +789,12 @@ impl X11Client {
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
let mut state = self.0.borrow_mut();
- if atom == state.atoms.WM_DELETE_WINDOW {
+ if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
// window "x" button clicked by user
- if window.should_close() {
- // Rest of the close logic is handled in drop_window()
- window.close();
- }
+ // Rest of the close logic is handled in drop_window()
+ drop(state);
+ window.close();
+ state = self.0.borrow_mut();
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
window.state.borrow_mut().last_sync_counter =
Some(x11rb::protocol::sync::Int64 {
@@ -1216,6 +1216,33 @@ impl X11Client {
Event::XinputMotion(event) => {
let window = self.get_window(event.event)?;
let mut state = self.0.borrow_mut();
+ if window.is_blocked() {
+ // We want to set the cursor to the default arrow
+ // when the window is blocked
+ let style = CursorStyle::Arrow;
+
+ let current_style = state
+ .cursor_styles
+ .get(&window.x_window)
+ .unwrap_or(&CursorStyle::Arrow);
+ if *current_style != style
+ && let Some(cursor) = state.get_cursor_icon(style)
+ {
+ state.cursor_styles.insert(window.x_window, style);
+ check_reply(
+ || "Failed to set cursor style",
+ state.xcb_connection.change_window_attributes(
+ window.x_window,
+ &ChangeWindowAttributesAux {
+ cursor: Some(cursor),
+ ..Default::default()
+ },
+ ),
+ )
+ .log_err();
+ state.xcb_connection.flush().log_err();
+ };
+ }
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
@@ -1489,7 +1516,7 @@ impl LinuxClient for X11Client {
let parent_window = state
.keyboard_focused_window
.and_then(|focused_window| state.windows.get(&focused_window))
- .map(|window| window.window.x_window);
+ .map(|w| w.window.clone());
let x_window = state
.xcb_connection
.generate_id()
@@ -1544,7 +1571,15 @@ impl LinuxClient for X11Client {
.cursor_styles
.get(&focused_window)
.unwrap_or(&CursorStyle::Arrow);
- if *current_style == style {
+
+ let window = state
+ .mouse_focused_window
+ .and_then(|w| state.windows.get(&w));
+
+ let should_change = *current_style != style
+ && (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
+
+ if !should_change {
return;
}
@@ -11,6 +11,7 @@ use crate::{
};
use blade_graphics as gpu;
+use collections::FxHashSet;
use raw_window_handle as rwh;
use util::{ResultExt, maybe};
use x11rb::{
@@ -74,6 +75,7 @@ x11rb::atom_manager! {
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION,
_NET_WM_WINDOW_TYPE_DIALOG,
+ _NET_WM_STATE_MODAL,
_NET_WM_SYNC,
_NET_SUPPORTED,
_MOTIF_WM_HINTS,
@@ -249,6 +251,8 @@ pub struct Callbacks {
pub struct X11WindowState {
pub destroyed: bool,
+ parent: Option<X11WindowStatePtr>,
+ children: FxHashSet<xproto::Window>,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
@@ -394,7 +398,7 @@ impl X11WindowState {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
- parent_window: Option<xproto::Window>,
+ parent_window: Option<X11WindowStatePtr>,
) -> anyhow::Result<Self> {
let x_screen_index = params
.display_id
@@ -546,8 +550,8 @@ impl X11WindowState {
)?;
}
- if params.kind == WindowKind::Floating {
- if let Some(parent_window) = parent_window {
+ if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
+ if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
// WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
// a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
// place the floating window in relation to the main window.
@@ -563,11 +567,23 @@ impl X11WindowState {
),
)?;
}
+ }
+
+ let parent = if params.kind == WindowKind::Dialog
+ && let Some(parent) = parent_window
+ {
+ parent.add_child(x_window);
+
+ Some(parent)
+ } else {
+ None
+ };
+ if params.kind == WindowKind::Dialog {
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
check_reply(
- || "X11 ChangeProperty32 setting window type for floating window failed.",
+ || "X11 ChangeProperty32 setting window type for dialog window failed.",
xcb.change_property32(
xproto::PropMode::REPLACE,
x_window,
@@ -576,6 +592,20 @@ impl X11WindowState {
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
),
)?;
+
+ // We set the modal state for dialog windows, so that the window manager
+ // can handle it appropriately (e.g., prevent interaction with the parent window
+ // while the dialog is open).
+ check_reply(
+ || "X11 ChangeProperty32 setting modal state for dialog window failed.",
+ xcb.change_property32(
+ xproto::PropMode::REPLACE,
+ x_window,
+ atoms._NET_WM_STATE,
+ xproto::AtomEnum::ATOM,
+ &[atoms._NET_WM_STATE_MODAL],
+ ),
+ )?;
}
check_reply(
@@ -667,6 +697,8 @@ impl X11WindowState {
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
Ok(Self {
+ parent,
+ children: FxHashSet::default(),
client,
executor,
display,
@@ -720,6 +752,11 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
impl Drop for X11Window {
fn drop(&mut self) {
let mut state = self.0.state.borrow_mut();
+
+ if let Some(parent) = state.parent.as_ref() {
+ parent.state.borrow_mut().children.remove(&self.0.x_window);
+ }
+
state.renderer.destroy();
let destroy_x_window = maybe!({
@@ -734,8 +771,6 @@ impl Drop for X11Window {
.log_err();
if destroy_x_window.is_some() {
- // Mark window as destroyed so that we can filter out when X11 events
- // for it still come in.
state.destroyed = true;
let this_ptr = self.0.clone();
@@ -773,7 +808,7 @@ impl X11Window {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
- parent_window: Option<xproto::Window>,
+ parent_window: Option<X11WindowStatePtr>,
) -> anyhow::Result<Self> {
let ptr = X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
@@ -979,7 +1014,31 @@ impl X11WindowStatePtr {
Ok(())
}
+ pub fn add_child(&self, child: xproto::Window) {
+ let mut state = self.state.borrow_mut();
+ state.children.insert(child);
+ }
+
+ pub fn is_blocked(&self) -> bool {
+ let state = self.state.borrow();
+ !state.children.is_empty()
+ }
+
pub fn close(&self) {
+ let state = self.state.borrow();
+ let client = state.client.clone();
+ #[allow(clippy::mutable_key_type)]
+ let children = state.children.clone();
+ drop(state);
+
+ if let Some(client) = client.get_client() {
+ for child in children {
+ if let Some(child_window) = client.get_window(child) {
+ child_window.close();
+ }
+ }
+ }
+
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
fun()
@@ -994,6 +1053,9 @@ impl X11WindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
+ if self.is_blocked() {
+ return;
+ }
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
&& !fun(input.clone()).propagate
{
@@ -1016,6 +1078,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_commit(&self, text: String) {
+ if self.is_blocked() {
+ return;
+ }
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1026,6 +1091,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_preedit(&self, text: String) {
+ if self.is_blocked() {
+ return;
+ }
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1036,6 +1104,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_unmark(&self) {
+ if self.is_blocked() {
+ return;
+ }
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1046,6 +1117,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_delete(&self) {
+ if self.is_blocked() {
+ return;
+ }
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -62,9 +62,12 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
NSWindowStyleMask::from_bits_retain(1 << 7);
+// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
#[allow(non_upper_case_globals)]
const NSNormalWindowLevel: NSInteger = 0;
#[allow(non_upper_case_globals)]
+const NSFloatingWindowLevel: NSInteger = 3;
+#[allow(non_upper_case_globals)]
const NSPopUpWindowLevel: NSInteger = 101;
#[allow(non_upper_case_globals)]
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
@@ -423,6 +426,8 @@ struct MacWindowState {
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
activated_least_once: bool,
+ // The parent window if this window is a sheet (Dialog kind)
+ sheet_parent: Option<id>,
}
impl MacWindowState {
@@ -622,11 +627,16 @@ impl MacWindow {
}
let native_window: id = match kind {
- WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
+ WindowKind::Normal => {
+ msg_send![WINDOW_CLASS, alloc]
+ }
WindowKind::PopUp => {
style_mask |= NSWindowStyleMaskNonactivatingPanel;
msg_send![PANEL_CLASS, alloc]
}
+ WindowKind::Floating | WindowKind::Dialog => {
+ msg_send![PANEL_CLASS, alloc]
+ }
};
let display = display_id
@@ -729,6 +739,7 @@ impl MacWindow {
select_previous_tab_callback: None,
toggle_tab_bar_callback: None,
activated_least_once: false,
+ sheet_parent: None,
})));
(*native_window).set_ivar(
@@ -779,9 +790,18 @@ impl MacWindow {
content_view.addSubview_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
+ let app: id = NSApplication::sharedApplication(nil);
+ let main_window: id = msg_send![app, mainWindow];
+ let mut sheet_parent = None;
+
match kind {
WindowKind::Normal | WindowKind::Floating => {
- native_window.setLevel_(NSNormalWindowLevel);
+ if kind == WindowKind::Floating {
+ // Let the window float keep above normal windows.
+ native_window.setLevel_(NSFloatingWindowLevel);
+ } else {
+ native_window.setLevel_(NSNormalWindowLevel);
+ }
native_window.setAcceptsMouseMovedEvents_(YES);
if let Some(tabbing_identifier) = tabbing_identifier {
@@ -816,10 +836,23 @@ impl MacWindow {
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
);
}
+ WindowKind::Dialog => {
+ if !main_window.is_null() {
+ let parent = {
+ let active_sheet: id = msg_send![main_window, attachedSheet];
+ if active_sheet.is_null() {
+ main_window
+ } else {
+ active_sheet
+ }
+ };
+ let _: () =
+ msg_send![parent, beginSheet: native_window completionHandler: nil];
+ sheet_parent = Some(parent);
+ }
+ }
}
- let app = NSApplication::sharedApplication(nil);
- let main_window: id = msg_send![app, mainWindow];
if allows_automatic_window_tabbing
&& !main_window.is_null()
&& main_window != native_window
@@ -861,7 +894,11 @@ impl MacWindow {
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
// is different from the primary screen.
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
- window.0.lock().move_traffic_light();
+ {
+ let mut window_state = window.0.lock();
+ window_state.move_traffic_light();
+ window_state.sheet_parent = sheet_parent;
+ }
pool.drain();
@@ -938,6 +975,7 @@ impl Drop for MacWindow {
let mut this = self.0.lock();
this.renderer.destroy();
let window = this.native_window;
+ let sheet_parent = this.sheet_parent.take();
this.display_link.take();
unsafe {
this.native_window.setDelegate_(nil);
@@ -946,6 +984,9 @@ impl Drop for MacWindow {
this.executor
.spawn(async move {
unsafe {
+ if let Some(parent) = sheet_parent {
+ let _: () = msg_send![parent, endSheet: window];
+ }
window.close();
window.autorelease();
}
@@ -270,6 +270,14 @@ impl WindowsWindowInner {
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
let callback = { self.state.callbacks.close.take() };
+ // Re-enable parent window if this was a modal dialog
+ if let Some(parent_hwnd) = self.parent_hwnd {
+ unsafe {
+ let _ = EnableWindow(parent_hwnd, true);
+ let _ = SetForegroundWindow(parent_hwnd);
+ }
+ }
+
if let Some(callback) = callback {
callback();
}
@@ -83,6 +83,7 @@ pub(crate) struct WindowsWindowInner {
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
pub(crate) platform_window_handle: HWND,
+ pub(crate) parent_hwnd: Option<HWND>,
}
impl WindowsWindowState {
@@ -241,6 +242,7 @@ impl WindowsWindowInner {
main_receiver: context.main_receiver.clone(),
platform_window_handle: context.platform_window_handle,
system_settings: WindowsSystemSettings::new(context.display),
+ parent_hwnd: context.parent_hwnd,
}))
}
@@ -368,6 +370,7 @@ struct WindowCreateContext {
disable_direct_composition: bool,
directx_devices: DirectXDevices,
invalidate_devices: Arc<AtomicBool>,
+ parent_hwnd: Option<HWND>,
}
impl WindowsWindow {
@@ -390,6 +393,20 @@ impl WindowsWindow {
invalidate_devices,
} = creation_info;
register_window_class(icon);
+ let parent_hwnd = if params.kind == WindowKind::Dialog {
+ let parent_window = unsafe { GetActiveWindow() };
+ if parent_window.is_invalid() {
+ None
+ } else {
+ // Disable the parent window to make this dialog modal
+ unsafe {
+ EnableWindow(parent_window, false).as_bool();
+ };
+ Some(parent_window)
+ }
+ } else {
+ None
+ };
let hide_title_bar = params
.titlebar
.as_ref()
@@ -416,8 +433,14 @@ impl WindowsWindow {
if params.is_minimizable {
dwstyle |= WS_MINIMIZEBOX;
}
+ let dwexstyle = if params.kind == WindowKind::Dialog {
+ dwstyle |= WS_POPUP | WS_CAPTION;
+ WS_EX_DLGMODALFRAME
+ } else {
+ WS_EX_APPWINDOW
+ };
- (WS_EX_APPWINDOW, dwstyle)
+ (dwexstyle, dwstyle)
};
if !disable_direct_composition {
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
@@ -449,6 +472,7 @@ impl WindowsWindow {
disable_direct_composition,
directx_devices,
invalidate_devices,
+ parent_hwnd,
};
let creation_result = unsafe {
CreateWindowExW(
@@ -460,7 +484,7 @@ impl WindowsWindow {
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
- None,
+ parent_hwnd,
None,
Some(hinstance.into()),
Some(&context as *const _ as *const _),