diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 0b7542f4..7a98bc94 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -30,6 +30,7 @@ use { Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, + window::{Window, WindowType}, xwayland::XScalingMode, }, bincode::Options, @@ -342,6 +343,74 @@ impl ConfigClient { self.send(&ClientMessage::SeatMove { seat, direction }); } + pub fn window_move(&self, window: Window, direction: Direction) { + self.send(&ClientMessage::WindowMove { window, direction }); + } + + pub fn window_exists(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::WindowExists { window }); + get_response!(res, false, WindowExists { exists }); + exists + } + + pub fn window_client(&self, window: Window) -> Client { + let res = self.send_with_response(&ClientMessage::GetWindowClient { window }); + get_response!(res, Client(0), GetWindowClient { client }); + client + } + + pub fn get_workspace_window(&self, workspace: Workspace) -> Window { + let res = self.send_with_response(&ClientMessage::GetWorkspaceWindow { workspace }); + get_response!(res, Window(0), GetWorkspaceWindow { window }); + window + } + + pub fn get_seat_keyboard_window(&self, seat: Seat) -> Window { + let res = self.send_with_response(&ClientMessage::GetSeatKeyboardWindow { seat }); + get_response!(res, Window(0), GetSeatKeyboardWindow { window }); + window + } + + pub fn focus_window(&self, seat: Seat, window: Window) { + self.send(&ClientMessage::SeatFocusWindow { seat, window }); + } + + pub fn window_title(&self, window: Window) -> String { + let res = self.send_with_response(&ClientMessage::GetWindowTitle { window }); + get_response!(res, String::new(), GetWindowTitle { title }); + title + } + + pub fn window_type(&self, window: Window) -> WindowType { + let res = self.send_with_response(&ClientMessage::GetWindowType { window }); + get_response!(res, WindowType(0), GetWindowType { kind }); + kind + } + + pub fn window_id(&self, window: Window) -> String { + let res = self.send_with_response(&ClientMessage::GetWindowId { window }); + get_response!(res, String::new(), GetWindowId { id }); + id + } + + pub fn window_parent(&self, window: Window) -> Window { + let res = self.send_with_response(&ClientMessage::GetWindowParent { window }); + get_response!(res, Window(0), GetWindowParent { window }); + window + } + + pub fn window_children(&self, window: Window) -> Vec { + let res = self.send_with_response(&ClientMessage::GetWindowChildren { window }); + get_response!(res, vec![], GetWindowChildren { windows }); + windows + } + + pub fn window_is_visible(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowIsVisible { window }); + get_response!(res, false, GetWindowIsVisible { visible }); + visible + } + pub fn unbind>(&self, seat: Seat, mod_sym: T) { let mod_sym = mod_sym.into(); if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, mod_sym)) { @@ -374,6 +443,12 @@ impl ConfigClient { mono } + pub fn window_mono(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowMono { window }); + get_response!(res, false, GetWindowMono { mono }); + mono + } + pub fn get_timer(&self, name: &str) -> Timer { let res = self.send_with_response(&ClientMessage::GetTimer { name }); get_response!(res, Timer(0), GetTimer { timer }); @@ -421,6 +496,12 @@ impl ConfigClient { workspace } + pub fn get_window_workspace(&self, window: Window) -> Workspace { + let res = self.send_with_response(&ClientMessage::GetWindowWorkspace { window }); + get_response!(res, Workspace(0), GetWindowWorkspace { workspace }); + workspace + } + pub fn get_seat_keyboard_workspace(&self, seat: Seat) -> Workspace { let res = self.send_with_response(&ClientMessage::GetSeatKeyboardWorkspace { seat }); get_response!(res, Workspace(0), GetSeatKeyboardWorkspace { workspace }); @@ -455,12 +536,22 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatWorkspace { seat, workspace }); } + pub fn set_window_workspace(&self, window: Window, workspace: Workspace) { + self.send(&ClientMessage::SetWindowWorkspace { window, workspace }); + } + pub fn seat_split(&self, seat: Seat) -> Axis { let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); get_response!(res, Axis::Horizontal, GetSplit { axis }); axis } + pub fn window_split(&self, window: Window) -> Axis { + let res = self.send_with_response(&ClientMessage::GetWindowSplit { window }); + get_response!(res, Axis::Horizontal, GetWindowSplit { axis }); + axis + } + pub fn disable_pointer_constraint(&self, seat: Seat) { self.send(&ClientMessage::DisablePointerConstraint { seat }); } @@ -482,6 +573,16 @@ impl ConfigClient { fullscreen } + pub fn set_window_fullscreen(&self, window: Window, fullscreen: bool) { + self.send(&ClientMessage::SetWindowFullscreen { window, fullscreen }); + } + + pub fn get_window_fullscreen(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowFullscreen { window }); + get_response!(res, false, GetWindowFullscreen { fullscreen }); + fullscreen + } + pub fn reset_font(&self) { self.send(&ClientMessage::ResetFont); } @@ -510,6 +611,16 @@ impl ConfigClient { self.set_seat_floating(seat, !self.get_seat_floating(seat)); } + pub fn get_window_floating(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowFloating { window }); + get_response!(res, false, GetWindowFloating { floating }); + floating + } + + pub fn set_window_floating(&self, window: Window, floating: bool) { + self.send(&ClientMessage::SetWindowFloating { window, floating }); + } + pub fn reset_colors(&self) { self.send(&ClientMessage::ResetColors); } @@ -553,6 +664,10 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatMono { seat, mono }); } + pub fn set_window_mono(&self, window: Window, mono: bool) { + self.send(&ClientMessage::SetWindowMono { window, mono }); + } + pub fn set_env(&self, key: &str, val: &str) { self.send(&ClientMessage::SetEnv { key, val }); } @@ -587,14 +702,26 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatSplit { seat, axis }); } + pub fn set_window_split(&self, window: Window, axis: Axis) { + self.send(&ClientMessage::SetWindowSplit { window, axis }); + } + pub fn create_seat_split(&self, seat: Seat, axis: Axis) { self.send(&ClientMessage::CreateSeatSplit { seat, axis }); } + pub fn create_window_split(&self, window: Window, axis: Axis) { + self.send(&ClientMessage::CreateWindowSplit { window, axis }); + } + pub fn seat_close(&self, seat: Seat) { self.send(&ClientMessage::SeatClose { seat }); } + pub fn close_window(&self, window: Window) { + self.send(&ClientMessage::WindowClose { window }); + } + pub fn focus_seat_parent(&self, seat: Seat) { self.send(&ClientMessage::FocusSeatParent { seat }); } @@ -802,6 +929,16 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatFloatPinned { seat, pinned }); } + pub fn get_window_pinned(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowFloatPinned { window }); + get_response!(res, false, GetWindowFloatPinned { pinned }); + pinned + } + + pub fn set_window_pinned(&self, window: Window, pinned: bool) { + self.send(&ClientMessage::SetWindowFloatPinned { window, pinned }); + } + pub fn connector_connected(&self, connector: Connector) -> bool { let res = self.send_with_response(&ClientMessage::ConnectorConnected { connector }); get_response!(res, false, ConnectorConnected { connected }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 03eb85e3..07b66d54 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -15,6 +15,7 @@ use { ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode, connector_type::ConnectorType, }, + window::{Window, WindowType}, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -576,6 +577,93 @@ pub enum ClientMessage<'a> { ClientIsXwayland { client: Client, }, + WindowExists { + window: Window, + }, + GetWindowClient { + window: Window, + }, + GetWorkspaceWindow { + workspace: Workspace, + }, + GetSeatKeyboardWindow { + seat: Seat, + }, + SeatFocusWindow { + seat: Seat, + window: Window, + }, + GetWindowTitle { + window: Window, + }, + GetWindowType { + window: Window, + }, + GetWindowId { + window: Window, + }, + GetWindowIsVisible { + window: Window, + }, + GetWindowParent { + window: Window, + }, + GetWindowWorkspace { + window: Window, + }, + GetWindowChildren { + window: Window, + }, + GetWindowSplit { + window: Window, + }, + SetWindowSplit { + window: Window, + axis: Axis, + }, + GetWindowMono { + window: Window, + }, + SetWindowMono { + window: Window, + mono: bool, + }, + WindowMove { + window: Window, + direction: Direction, + }, + CreateWindowSplit { + window: Window, + axis: Axis, + }, + WindowClose { + window: Window, + }, + GetWindowFloating { + window: Window, + }, + SetWindowFloating { + window: Window, + floating: bool, + }, + SetWindowWorkspace { + window: Window, + workspace: Workspace, + }, + SetWindowFullscreen { + window: Window, + fullscreen: bool, + }, + GetWindowFullscreen { + window: Window, + }, + GetWindowFloatPinned { + window: Window, + }, + SetWindowFloatPinned { + window: Window, + pinned: bool, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -748,6 +836,54 @@ pub enum Response { ClientIsXwayland { is_xwayland: bool, }, + WindowExists { + exists: bool, + }, + GetWindowClient { + client: Client, + }, + GetSeatKeyboardWindow { + window: Window, + }, + GetWorkspaceWindow { + window: Window, + }, + GetWindowParent { + window: Window, + }, + GetWindowChildren { + windows: Vec, + }, + GetWindowTitle { + title: String, + }, + GetWindowType { + kind: WindowType, + }, + GetWindowId { + id: String, + }, + GetWindowWorkspace { + workspace: Workspace, + }, + GetWindowFloating { + floating: bool, + }, + GetWindowSplit { + axis: Axis, + }, + GetWindowMono { + mono: bool, + }, + GetWindowFullscreen { + fullscreen: bool, + }, + GetWindowFloatPinned { + pinned: bool, + }, + GetWindowIsVisible { + visible: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index c6851597..0a6045dc 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -10,6 +10,7 @@ use { input::{acceleration::AccelProfile, capability::Capability}, keyboard::{Keymap, mods::Modifiers}, video::Connector, + window::Window, }, serde::{Deserialize, Serialize}, std::time::Duration, @@ -478,6 +479,20 @@ impl Seat { pub fn toggle_float_pinned(self) { self.set_float_pinned(!self.float_pinned()); } + + /// Returns the focused window. + /// + /// If no window is focused, [`Window::exists`] returns false. + pub fn window(self) -> Window { + get!(Window(0)).get_seat_keyboard_window(self) + } + + /// Puts the keyboard focus on the window. + /// + /// This has no effect if the window is not visible. + pub fn focus_window(self, window: Window) { + get!().focus_window(self, window) + } } /// A focus-follows-mouse mode. diff --git a/jay-config/src/keyboard/mods.rs b/jay-config/src/keyboard/mods.rs index 5e98da84..6568ed2e 100644 --- a/jay-config/src/keyboard/mods.rs +++ b/jay-config/src/keyboard/mods.rs @@ -3,35 +3,42 @@ use { crate::{ModifiedKeySym, keyboard::syms::KeySym}, serde::{Deserialize, Serialize}, - std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}, + std::ops::BitOr, }; -/// Zero or more keyboard modifiers -#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash, Debug)] -pub struct Modifiers(pub u32); +bitflags! { + /// Zero or more keyboard modifiers + #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash)] + pub struct Modifiers(pub u32) { + /// The Shift modifier + pub const SHIFT = 1 << 0, + /// The CapsLock modifier. + pub const LOCK = 1 << 1, + /// The Ctrl modifier. + pub const CTRL = 1 << 2, + /// The Mod1 modifier, i.e., Alt. + pub const MOD1 = 1 << 3, + /// The Mod2 modifier, i.e., NumLock. + pub const MOD2 = 1 << 4, + /// The Mod3 modifier. + pub const MOD3 = 1 << 5, + /// The Mod4 modifier, i.e., Logo. + pub const MOD4 = 1 << 6, + /// The Mod5 modifier. + pub const MOD5 = 1 << 7, + + /// Synthetic modifier matching key release events. + /// + /// This can be used to execute a callback on key release. + pub const RELEASE = 1 << 31, + } +} impl Modifiers { /// No modifiers. pub const NONE: Self = Modifiers(0); } -/// The Shift modifier -pub const SHIFT: Modifiers = Modifiers(1 << 0); -/// The CapsLock modifier. -pub const LOCK: Modifiers = Modifiers(1 << 1); -/// The Ctrl modifier. -pub const CTRL: Modifiers = Modifiers(1 << 2); -/// The Mod1 modifier, i.e., Alt. -pub const MOD1: Modifiers = Modifiers(1 << 3); -/// The Mod2 modifier, i.e., NumLock. -pub const MOD2: Modifiers = Modifiers(1 << 4); -/// The Mod3 modifier. -pub const MOD3: Modifiers = Modifiers(1 << 5); -/// The Mod4 modifier, i.e., Logo. -pub const MOD4: Modifiers = Modifiers(1 << 6); -/// The Mod5 modifier. -pub const MOD5: Modifiers = Modifiers(1 << 7); - /// Alias for `LOCK`. pub const CAPS: Modifiers = LOCK; /// Alias for `MOD1`. @@ -41,19 +48,6 @@ pub const NUM: Modifiers = MOD2; /// Alias for `MOD4`. pub const LOGO: Modifiers = MOD4; -/// Synthetic modifier matching key release events. -/// -/// This can be used to execute a callback on key release. -pub const RELEASE: Modifiers = Modifiers(1 << 31); - -impl BitOr for Modifiers { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self::Output { - Self(self.0 | rhs.0) - } -} - impl BitOr for Modifiers { type Output = ModifiedKeySym; @@ -64,23 +58,3 @@ impl BitOr for Modifiers { } } } - -impl BitAnd for Modifiers { - type Output = Self; - - fn bitand(self, rhs: Self) -> Self::Output { - Self(self.0 & rhs.0) - } -} - -impl BitOrAssign for Modifiers { - fn bitor_assign(&mut self, rhs: Self) { - self.0 |= rhs.0 - } -} - -impl BitAndAssign for Modifiers { - fn bitand_assign(&mut self, rhs: Self) { - self.0 &= rhs.0 - } -} diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index b0570033..39b6f6cc 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -46,7 +46,9 @@ #[expect(unused_imports)] use crate::input::Seat; use { - crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector}, + crate::{ + _private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector, window::Window, + }, serde::{Deserialize, Serialize}, std::{ fmt::{Debug, Display, Formatter}, @@ -70,6 +72,7 @@ pub mod tasks; pub mod theme; pub mod timer; pub mod video; +pub mod window; pub mod xwayland; /// A planar direction. @@ -174,6 +177,13 @@ impl Workspace { pub fn move_to_output(self, output: Connector) { get!().move_to_output(WorkspaceSource::Explicit(self), output); } + + /// Returns the root container of this workspace. + /// + /// If no such container exists, [`Window::exists`] returns false. + pub fn window(self) -> Window { + get!(Window(0)).get_workspace_window(self) + } } /// Returns the workspace with the given name. diff --git a/jay-config/src/macros.rs b/jay-config/src/macros.rs index 012d8d79..03b87581 100644 --- a/jay-config/src/macros.rs +++ b/jay-config/src/macros.rs @@ -43,40 +43,89 @@ macro_rules! get { }}; } -// #[macro_export] -// macro_rules! log { -// ($lvl:expr, $($arg:tt)+) => ({ -// $crate::log( -// $lvl, -// &format!($($args)*), -// ); -// }) -// } -// -// #[macro_export] -// macro_rules! trace { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Trace, $($arg)+) -// } -// } -// -// #[macro_export] -// macro_rules! debug { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Debug, $($arg)+) -// } -// } -// -// #[macro_export] -// macro_rules! info { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Info, $($arg)+) -// } -// } -// -// #[macro_export] -// macro_rules! info { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Info, $($arg)+) -// } -// } +macro_rules! bitflags { + ( + $(#[$attr1:meta])* + $vis1:vis struct $name:ident($vis2:vis $rep:ty) { + $( + $(#[$attr2:meta])* + $vis3:vis const $var:ident = $val:expr, + )* + } + ) => { + $(#[$attr1])* + $vis1 struct $name($vis2 $rep); + + $( + $(#[$attr2])* + $vis3 const $var: $name = $name($val); + )* + + impl std::ops::BitOr for $name { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } + } + + impl std::ops::BitAnd for $name { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } + } + + impl std::ops::BitOrAssign for $name { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } + } + + impl std::ops::BitAndAssign for $name { + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } + } + + impl std::ops::BitXorAssign for $name { + fn bitxor_assign(&mut self, rhs: Self) { + self.0 ^= rhs.0; + } + } + + impl std::ops::Not for $name { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } + } + + impl std::fmt::Debug for $name { + #[allow(clippy::allow_attributes, clippy::bad_bit_mask, unused_mut)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut any = false; + let mut v = self.0; + $( + if $val != 0 && v & $val == $val { + if any { + write!(f, "|")?; + } + any = true; + write!(f, "{}", stringify!($var))?; + v &= !$val; + } + )* + if !any || v != 0 { + if any { + write!(f, "|")?; + } + write!(f, "0x{:x}", v)?; + } + Ok(()) + } + } + } +} diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs new file mode 100644 index 00000000..ebf35122 --- /dev/null +++ b/jay-config/src/window.rs @@ -0,0 +1,204 @@ +//! Tools for inspecting and manipulating windows. + +use { + crate::{Axis, Direction, Workspace, client::Client}, + serde::{Deserialize, Serialize}, +}; + +/// A toplevel window. +/// +/// A toplevel window is anything that can be stored within a container tile or within a +/// floating window. +/// +/// There are currently four types of windows: +/// +/// - Containers +/// - Placeholders that take the place of a window when it goes fullscreen +/// - XDG toplevels +/// - X windows +/// +/// You can find out the type of a window by using the [`Window::type_`] function. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct Window(pub u64); + +bitflags! { + /// The type of a window. + #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)] + pub struct WindowType(pub u64) { + /// A container. + pub const CONTAINER = 1 << 0, + /// A placeholder. + pub const PLACEHOLDER = 1 << 1, + /// An XDG toplevel. + pub const XDG_TOPLEVEL = 1 << 2, + /// An X window. + pub const X_WINDOW = 1 << 3, + } +} + +/// A window created by a client. +/// +/// This is the same as `XDG_TOPLEVEL | X_WINDOW`. +pub const CLIENT_WINDOW: WindowType = WindowType(XDG_TOPLEVEL.0 | X_WINDOW.0); + +impl Window { + /// Returns whether the window exists. + pub fn exists(self) -> bool { + self.0 != 0 && get!(false).window_exists(self) + } + + /// Returns whether the window does not exist. + /// + /// This is a shorthand for `!self.exists()`. + pub fn does_not_exist(self) -> bool { + !self.exists() + } + + /// Returns the client of the window. + /// + /// If the window does not have a client, [`Client::exists`] return false. + pub fn client(self) -> Client { + get!(Client(0)).window_client(self) + } + + /// Returns the title of the window. + pub fn title(self) -> String { + get!().window_title(self) + } + + /// Returns the type of the window. + pub fn type_(self) -> WindowType { + get!(WindowType(0)).window_type(self) + } + + /// Returns the identifier of the window. + /// + /// This is the identifier used in the `ext-foreign-toplevel-list-v1` protocol. + pub fn id(self) -> String { + get!().window_id(self) + } + + /// Returns whether this window is visible. + pub fn is_visible(self) -> bool { + get!().window_is_visible(self) + } + + /// Returns the parent of this window. + /// + /// If this window has no parent, [`Window::exists`] returns false. + pub fn parent(self) -> Window { + get!(Window(0)).window_parent(self) + } + + /// Returns the children of this window. + /// + /// Only containers have children. + pub fn children(self) -> Vec { + get!().window_children(self) + } + + /// Moves the window in the specified direction. + pub fn move_(self, direction: Direction) { + get!().window_move(self, direction) + } + + /// Returns whether the parent-container of the window is in mono-mode. + pub fn mono(self) -> bool { + get!(false).window_mono(self) + } + + /// Sets whether the parent-container of the window is in mono-mode. + pub fn set_mono(self, mono: bool) { + get!().set_window_mono(self, mono) + } + + /// Toggles whether the parent-container of the window is in mono-mode. + pub fn toggle_mono(self) { + self.set_mono(!self.mono()); + } + + /// Returns the split axis of the parent-container of the window. + pub fn split(self) -> Axis { + get!(Axis::Horizontal).window_split(self) + } + + /// Sets the split axis of the parent-container of the window. + pub fn set_split(self, axis: Axis) { + get!().set_window_split(self, axis) + } + + /// Toggles the split axis of the parent-container of the window. + pub fn toggle_split(self) { + self.set_split(self.split().other()); + } + + /// Creates a new container with the specified split in place of the window. + pub fn create_split(self, axis: Axis) { + get!().create_window_split(self, axis); + } + + /// Requests the window to be closed. + pub fn close(self) { + get!().close_window(self); + } + + /// Returns whether the window is floating. + pub fn floating(self) -> bool { + get!().get_window_floating(self) + } + /// Sets whether the window is floating. + pub fn set_floating(self, floating: bool) { + get!().set_window_floating(self, floating); + } + + /// Toggles whether the window is floating. + /// + /// You can do the same by double-clicking on the header. + pub fn toggle_floating(self) { + self.set_floating(!self.floating()); + } + + /// Returns the workspace that this window belongs to. + /// + /// If no such workspace exists, `exists` returns `false` for the returned workspace. + pub fn workspace(self) -> Workspace { + get!(Workspace(0)).get_window_workspace(self) + } + + /// Moves the window to the workspace. + pub fn set_workspace(self, workspace: Workspace) { + get!().set_window_workspace(self, workspace) + } + + /// Toggles whether the currently focused window is fullscreen. + pub fn toggle_fullscreen(self) { + self.set_fullscreen(!self.fullscreen()) + } + /// Returns whether the window is fullscreen. + pub fn fullscreen(self) -> bool { + get!(false).get_window_fullscreen(self) + } + + /// Sets whether the window is fullscreen. + pub fn set_fullscreen(self, fullscreen: bool) { + get!().set_window_fullscreen(self, fullscreen) + } + + /// Gets whether the window is pinned. + /// + /// If a floating window is pinned, it will stay visible even when switching to a + /// different workspace. + pub fn float_pinned(self) -> bool { + get!().get_window_pinned(self) + } + + /// Sets whether the window is pinned. + pub fn set_float_pinned(self, pinned: bool) { + get!().set_window_pinned(self, pinned); + } + + /// Toggles whether the window is pinned. + pub fn toggle_float_pinned(self) { + self.set_float_pinned(!self.float_pinned()); + } +} diff --git a/src/config.rs b/src/config.rs index 429714cf..c35b90de 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,8 +9,8 @@ use { ifs::wl_seat::SeatId, state::State, utils::{ - clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, unlink_on_drop::UnlinkOnDrop, - xrd::xrd, + clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, + toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd, }, }, bincode::Options, @@ -151,6 +151,15 @@ impl ConfigProxy { event, }); } + + pub fn toplevel_removed(&self, id: ToplevelIdentifier) { + let Some(handler) = self.handler.get() else { + return; + }; + if let Some(win) = handler.windows_from_tl_id.remove(&id) { + handler.windows_to_tl_id.remove(&win); + } + } } impl Drop for ConfigProxy { @@ -202,6 +211,9 @@ impl ConfigProxy { timers_by_id: Default::default(), pollable_id: Default::default(), pollables: Default::default(), + window_ids: NumCell::new(1), + windows_from_tl_id: Default::default(), + windows_to_tl_id: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index 212c8041..83c1abdf 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -19,7 +19,9 @@ use { theme::{Color, ThemeSized}, tree::{ ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, OutputNode, - TearingMode, VrrMode, WsMoveConfig, move_ws_to_output, + TearingMode, ToplevelNode, VrrMode, WorkspaceNode, WsMoveConfig, move_ws_to_output, + toplevel_create_split, toplevel_parent_container, toplevel_set_floating, + toplevel_set_workspace, }, utils::{ asyncevent::AsyncEvent, @@ -30,6 +32,7 @@ use { oserror::OsError, stack::Stack, timer::{TimerError, TimerFd}, + toplevel_identifier::ToplevelIdentifier, }, }, bincode::Options, @@ -57,6 +60,7 @@ use { TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, Transform, VrrMode as ConfigVrrMode, }, + window::Window, xwayland::XScalingMode, }, libloading::Library, @@ -89,6 +93,10 @@ pub(super) struct ConfigProxyHandler { pub pollable_id: NumCell, pub pollables: CopyHashMap>, + + pub window_ids: NumCell, + pub windows_from_tl_id: CopyHashMap, + pub windows_to_tl_id: CopyHashMap, } pub struct Pollable { @@ -315,6 +323,24 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_fullscreen(&self, window: Window) -> Result<(), CphError> { + let tl = self.get_window(window)?; + self.respond(Response::GetWindowFullscreen { + fullscreen: tl.tl_data().is_fullscreen.get(), + }); + Ok(()) + } + + fn handle_set_window_fullscreen( + &self, + window: Window, + fullscreen: bool, + ) -> Result<(), CphError> { + let tl = self.get_window(window)?; + tl.tl_set_fullscreen(fullscreen); + Ok(()) + } + fn handle_set_keymap(&self, seat: Seat, keymap: Keymap) -> Result<(), CphError> { let seat = self.get_seat(seat)?; let keymap = if keymap.is_invalid() { @@ -492,6 +518,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_window_close(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + window.tl_close(); + Ok(()) + } + fn handle_seat_focus(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.move_focus(direction.into()); @@ -504,6 +536,14 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> { + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.move_child(window, direction.into()); + } + Ok(()) + } + fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; let (rate, delay) = seat.get_rate(); @@ -530,6 +570,11 @@ impl ConfigProxyHandler { } } + fn get_existing_workspace(&self, ws: Workspace) -> Result>, CphError> { + self.get_workspace(ws) + .map(|ws| self.state.workspaces.get(&*ws)) + } + fn get_device_handler_data( &self, device: InputDevice, @@ -720,8 +765,8 @@ impl ConfigProxyHandler { } fn handle_get_workspace_capture(&self, workspace: Workspace) -> Result<(), CphError> { - let name = self.get_workspace(workspace)?; - let capture = match self.state.workspaces.get(name.as_str()) { + let ws = self.get_existing_workspace(workspace)?; + let capture = match ws { Some(ws) => ws.may_capture.get(), None => self.state.default_workspace_capture.get(), }; @@ -734,8 +779,7 @@ impl ConfigProxyHandler { workspace: Workspace, capture: bool, ) -> Result<(), CphError> { - let name = self.get_workspace(workspace)?; - if let Some(ws) = self.state.workspaces.get(name.as_str()) { + if let Some(ws) = self.get_existing_workspace(workspace)? { ws.may_capture.set(capture); ws.update_has_captures(); } @@ -856,6 +900,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> { + let window = self.get_window(window)?; + let name = self.get_workspace(ws)?; + let workspace = match self.state.workspaces.get(name.deref()) { + Some(ws) => ws, + _ => match window.node_output() { + Some(o) => o.create_workspace(name.deref()), + _ => return Ok(()), + }, + }; + toplevel_set_workspace(&self.state, window, &workspace); + Ok(()) + } + fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; let name = dev.device.name(); @@ -888,13 +946,10 @@ impl ConfigProxyHandler { ) -> Result<(), CphError> { let output = self.get_output_node(connector)?; let ws = match workspace { - WorkspaceSource::Explicit(ws) => { - let name = self.get_workspace(ws)?; - match self.state.workspaces.get(name.as_str()) { - Some(ws) => ws, - _ => return Ok(()), - } - } + WorkspaceSource::Explicit(ws) => match self.get_existing_workspace(ws)? { + Some(ws) => ws, + _ => return Ok(()), + }, WorkspaceSource::Seat(s) => match self.get_seat(s)?.get_output().workspace.get() { Some(ws) => ws, _ => return Ok(()), @@ -1180,6 +1235,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_float_pinned(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowFloatPinned { + pinned: window.tl_pinned(), + }); + Ok(()) + } + + fn handle_set_window_float_pinned(&self, window: Window, pinned: bool) -> Result<(), CphError> { + let window = self.get_window(window)?; + window.tl_set_pinned(true, pinned); + Ok(()) + } + fn handle_set_vrr_mode( &self, connector: Option, @@ -1360,6 +1429,24 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowMono { + mono: toplevel_parent_container(&*window) + .map(|c| c.mono_child.is_some()) + .unwrap_or(false), + }); + Ok(()) + } + + fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> { + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.set_mono(mono.then_some(window.as_ref())); + } + Ok(()) + } + fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetSplit { @@ -1377,6 +1464,25 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowSplit { + axis: toplevel_parent_container(&*window) + .map(|c| c.split.get()) + .unwrap_or(ContainerSplit::Horizontal) + .into(), + }); + Ok(()) + } + + fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.set_split(axis.into()); + } + Ok(()) + } + fn handle_add_shortcut( &self, seat: Seat, @@ -1480,6 +1586,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_create_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { + let window = self.get_window(window)?; + toplevel_create_split(&self.state, window, axis.into()); + Ok(()) + } + fn handle_focus_seat_parent(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.focus_parent(); @@ -1509,6 +1621,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowFloating { + floating: window.tl_data().is_floating.get(), + }); + Ok(()) + } + + fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> { + let window = self.get_window(window)?; + toplevel_set_floating(&self.state, window, floating); + Ok(()) + } + fn handle_add_pollable(self: &Rc, fd: i32) -> Result<(), CphError> { let fd = match fcntl_dupfd_cloexec(fd, 0) { Ok(fd) => Rc::new(fd), @@ -1577,6 +1703,28 @@ impl ConfigProxyHandler { Ok(()) } + fn tl_to_window(&self, tl: &dyn ToplevelNode) -> Window { + self.tl_id_to_window(tl.tl_data().identifier.get()) + } + + fn tl_id_to_window(&self, tl: ToplevelIdentifier) -> Window { + if let Some(win) = self.windows_from_tl_id.get(&tl) { + return win; + } + let id = Window(self.window_ids.fetch_add(1)); + self.windows_from_tl_id.set(tl, id); + self.windows_to_tl_id.set(id, tl); + id + } + + fn get_window(&self, window: Window) -> Result, CphError> { + self.windows_to_tl_id + .get(&window) + .and_then(|id| self.state.toplevels.get(&id)) + .and_then(|tl| tl.upgrade()) + .ok_or(CphError::WindowDoesNotExist(window)) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -1752,6 +1900,123 @@ impl ConfigProxyHandler { self.state.clients.kill(ClientId::from_raw(client.0)); } + fn handle_get_workspace_window(&self, ws: Workspace) -> Result<(), CphError> { + let window = self + .get_existing_workspace(ws)? + .and_then(|ws| ws.container.get()) + .map(|c| self.tl_to_window(&*c)) + .unwrap_or(Window(0)); + self.respond(Response::GetWorkspaceWindow { window }); + Ok(()) + } + + fn handle_get_seat_keyboard_window(&self, seat: Seat) -> Result<(), CphError> { + let window = self + .get_seat(seat)? + .get_keyboard_node() + .node_toplevel() + .map(|tl| self.tl_to_window(&*tl)) + .unwrap_or(Window(0)); + self.respond(Response::GetSeatKeyboardWindow { window }); + Ok(()) + } + + fn handle_seat_focus_window(&self, seat: Seat, window_id: Window) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + let window = self.get_window(window_id)?; + if !window.node_visible() { + return Err(CphError::WindowNotVisible(window_id)); + } + seat.focus_toplevel(window); + Ok(()) + } + + fn handle_get_window_title(&self, window: Window) -> Result<(), CphError> { + let title = self.get_window(window)?.tl_data().title.borrow().clone(); + self.respond(Response::GetWindowTitle { title }); + Ok(()) + } + + fn handle_get_window_type(&self, window: Window) -> Result<(), CphError> { + let kind = self.get_window(window)?.tl_data().kind.to_window_type(); + self.respond(Response::GetWindowType { kind }); + Ok(()) + } + + fn handle_window_exists(&self, window: Window) { + self.respond(Response::WindowExists { + exists: self.get_window(window).is_ok(), + }); + } + + fn handle_get_window_id(&self, window: Window) -> Result<(), CphError> { + let id = self + .get_window(window)? + .tl_data() + .identifier + .get() + .to_string(); + self.respond(Response::GetWindowId { id: id.to_string() }); + Ok(()) + } + + fn handle_get_window_is_visible(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowIsVisible { + visible: window.node_visible(), + }); + Ok(()) + } + + fn handle_get_window_client(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowClient { + client: window + .tl_data() + .client + .as_ref() + .map(|c| ConfigClient(c.id.raw())) + .unwrap_or(ConfigClient(0)), + }); + Ok(()) + } + + fn handle_get_window_parent(&self, window: Window) -> Result<(), CphError> { + let window = self + .get_window(window)? + .tl_data() + .parent + .get() + .and_then(|tl| tl.node_into_toplevel()) + .map(|tl| self.tl_to_window(&*tl)) + .unwrap_or(Window(0)); + self.respond(Response::GetWindowParent { window }); + Ok(()) + } + + fn handle_get_window_workspace(&self, window: Window) -> Result<(), CphError> { + let workspace = self + .get_window(window)? + .tl_data() + .workspace + .get() + .map(|ws| self.get_workspace_by_name(&ws.name)) + .unwrap_or(Workspace(0)); + self.respond(Response::GetWindowWorkspace { workspace }); + Ok(()) + } + + fn handle_get_window_children(&self, window: Window) -> Result<(), CphError> { + let mut windows = vec![]; + if let Some(c) = self.get_window(window)?.node_into_container() { + for c in c.children.iter() { + windows.push(self.tl_to_window(&*c.node)); + } + } + self.respond(Response::GetWindowChildren { windows }); + Ok(()) + } + pub fn handle_request(self: &Rc, msg: &[u8]) { if let Err(e) = self.handle_request_(msg) { log::error!("Could not handle client request: {}", ErrorFmt(e)); @@ -2171,6 +2436,82 @@ impl ConfigProxyHandler { .handle_client_is_xwayland(client) .wrn("client_is_xwayland")?, ClientMessage::ClientKill { client } => self.handle_client_kill(client), + ClientMessage::WindowExists { window } => self.handle_window_exists(window), + ClientMessage::GetWorkspaceWindow { workspace } => self + .handle_get_workspace_window(workspace) + .wrn("get_workspace_window")?, + ClientMessage::GetSeatKeyboardWindow { seat } => self + .handle_get_seat_keyboard_window(seat) + .wrn("get_seat_keyboard_window")?, + ClientMessage::SeatFocusWindow { seat, window } => self + .handle_seat_focus_window(seat, window) + .wrn("seat_focus_window")?, + ClientMessage::GetWindowTitle { window } => self + .handle_get_window_title(window) + .wrn("get_window_title")?, + ClientMessage::GetWindowType { window } => { + self.handle_get_window_type(window).wrn("get_window_type")? + } + ClientMessage::GetWindowId { window } => { + self.handle_get_window_id(window).wrn("get_window_id")? + } + ClientMessage::GetWindowParent { window } => self + .handle_get_window_parent(window) + .wrn("get_window_parent")?, + ClientMessage::GetWindowWorkspace { window } => self + .handle_get_window_workspace(window) + .wrn("get_window_workspace")?, + ClientMessage::GetWindowChildren { window } => self + .handle_get_window_children(window) + .wrn("get_window_children")?, + ClientMessage::GetWindowSplit { window } => self + .handle_get_window_split(window) + .wrn("get_window_split")?, + ClientMessage::SetWindowSplit { window, axis } => self + .handle_set_window_split(window, axis) + .wrn("set_window_split")?, + ClientMessage::GetWindowMono { window } => { + self.handle_get_window_mono(window).wrn("get_window_mono")? + } + ClientMessage::SetWindowMono { window, mono } => self + .handle_set_window_mono(window, mono) + .wrn("set_window_mono")?, + ClientMessage::WindowMove { window, direction } => self + .handle_window_move(window, direction) + .wrn("window_move")?, + ClientMessage::CreateWindowSplit { window, axis } => self + .handle_create_window_split(window, axis) + .wrn("create_window_split")?, + ClientMessage::WindowClose { window } => { + self.handle_window_close(window).wrn("close_window")? + } + ClientMessage::GetWindowFloating { window } => self + .handle_get_window_floating(window) + .wrn("get_window_floating")?, + ClientMessage::SetWindowFloating { window, floating } => self + .handle_set_window_floating(window, floating) + .wrn("set_window_floating")?, + ClientMessage::SetWindowWorkspace { window, workspace } => self + .handle_set_window_workspace(window, workspace) + .wrn("set_window_workspace")?, + ClientMessage::SetWindowFullscreen { window, fullscreen } => self + .handle_set_window_fullscreen(window, fullscreen) + .wrn("set_window_fullscreen")?, + ClientMessage::GetWindowFullscreen { window } => self + .handle_get_window_fullscreen(window) + .wrn("get_window_fullscreen")?, + ClientMessage::GetWindowFloatPinned { window } => self + .handle_get_window_float_pinned(window) + .wrn("get_window_float_pinned")?, + ClientMessage::SetWindowFloatPinned { window, pinned } => self + .handle_set_window_float_pinned(window, pinned) + .wrn("set_window_float_pinned")?, + ClientMessage::GetWindowIsVisible { window } => self + .handle_get_window_is_visible(window) + .wrn("get_window_is_visible")?, + ClientMessage::GetWindowClient { window } => self + .handle_get_window_client(window) + .wrn("get_window_client")?, } Ok(()) } @@ -2248,6 +2589,10 @@ enum CphError { UnknownTransferFunction(ConfigTransferFunction), #[error("Client {0:?} does not exist")] ClientDoesNotExist(ConfigClient), + #[error("Window {0:?} does not exist")] + WindowDoesNotExist(Window), + #[error("Window {0:?} is not visible")] + WindowNotVisible(Window), } trait WithRequestName { diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index b37978ba..f9fffa7a 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -79,7 +79,8 @@ use { state::{DeviceHandlerData, State}, tree::{ ContainerNode, ContainerSplit, Direction, FoundNode, Node, OutputNode, ToplevelNode, - WorkspaceNode, generic_node_visitor, + WorkspaceNode, generic_node_visitor, toplevel_create_split, toplevel_parent_container, + toplevel_set_floating, toplevel_set_workspace, }, utils::{ asyncevent::AsyncEvent, bindings::PerClientBindings, clonecell::CloneCell, @@ -401,6 +402,10 @@ impl WlSeatGlobal { self.cursor_user_group.latest_output() } + pub fn get_keyboard_node(&self) -> Rc { + self.keyboard_node.get() + } + pub fn get_keyboard_output(&self) -> Option> { self.keyboard_node.get().node_output() } @@ -410,38 +415,7 @@ impl WlSeatGlobal { Some(tl) => tl, _ => return, }; - if tl.tl_data().is_fullscreen.get() { - return; - } - let old_ws = match tl.tl_data().workspace.get() { - Some(ws) => ws, - _ => return, - }; - if old_ws.id == ws.id { - return; - } - let cn = match tl.tl_data().parent.get() { - Some(cn) => cn, - _ => return, - }; - let kb_foci = collect_kb_foci(tl.clone()); - cn.cnode_remove_child2(&*tl, true); - if !ws.visible.get() { - for focus in kb_foci { - old_ws.clone().node_do_focus(&focus, Direction::Unspecified); - } - } - if tl.tl_data().is_floating.get() { - self.state.map_floating( - tl.clone(), - tl.tl_data().float_width.get(), - tl.tl_data().float_height.get(), - ws, - None, - ); - } else { - self.state.map_tiled_on(tl, ws); - } + toplevel_set_workspace(&self.state, tl, ws); } pub fn mark_last_active(self: &Rc) { @@ -556,11 +530,7 @@ impl WlSeatGlobal { pub fn kb_parent_container(&self) -> Option> { if let Some(tl) = self.keyboard_node.get().node_toplevel() { - if let Some(parent) = tl.tl_data().parent.get() { - if let Some(container) = parent.node_into_container() { - return Some(container); - } - } + return toplevel_parent_container(&*tl); } None } @@ -595,21 +565,7 @@ impl WlSeatGlobal { Some(tl) => tl, _ => return, }; - if tl.tl_data().is_fullscreen.get() { - return; - } - let ws = match tl.tl_data().workspace.get() { - Some(ws) => ws, - _ => return, - }; - let pn = match tl.tl_data().parent.get() { - Some(pn) => pn, - _ => return, - }; - if let Some(pn) = pn.node_into_containing_node() { - let cn = ContainerNode::new(&self.state, &ws, tl.clone(), axis); - pn.cnode_replace_child(&*tl, cn); - } + toplevel_create_split(&self.state, tl, axis); } pub fn focus_parent(self: &Rc) { @@ -634,29 +590,7 @@ impl WlSeatGlobal { Some(tl) => tl, _ => return, }; - self.set_tl_floating(tl, floating); - } - - pub fn set_tl_floating(self: &Rc, tl: Rc, floating: bool) { - let data = tl.tl_data(); - if data.is_fullscreen.get() { - return; - } - if data.is_floating.get() == floating { - return; - } - let parent = match data.parent.get() { - Some(p) => p, - _ => return, - }; - if !floating { - parent.cnode_remove_child2(&*tl, true); - self.state.map_tiled(tl); - } else if let Some(ws) = data.workspace.get() { - parent.cnode_remove_child2(&*tl, true); - let (width, height) = data.float_size(&ws); - self.state.map_floating(tl, width, height, &ws, None); - } + toplevel_set_floating(&self.state, tl, floating); } pub fn get_rate(&self) -> (i32, i32) { diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 9c8178ad..1c6664c2 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -13,7 +13,7 @@ use { tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, StackedNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, WorkspaceNode, default_tile_drag_destination, + ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_destination, }, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::LinkedNode}, wire::WlSurfaceId, @@ -205,16 +205,19 @@ impl Xwindow { if xsurface.xwindow.is_some() { return Err(XWindowError::AlreadyAttached); } + let id = data.state.node_ids.next(); let slf = Rc::new_cyclic(|weak| { let tld = ToplevelData::new( &data.state, data.info.title.borrow_mut().clone().unwrap_or_default(), Some(surface.client.clone()), + ToplevelType::XWindow, + id, weak, ); tld.pos.set(surface.extents.get()); Self { - id: data.state.node_ids.next(), + id, data: data.clone(), display_link: Default::default(), toplevel_data: tld, diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index cca80ab2..0931ff8d 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -27,7 +27,8 @@ use { tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelNodeId, WorkspaceNode, default_tile_drag_destination, + ToplevelNodeBase, ToplevelNodeId, ToplevelType, WorkspaceNode, + default_tile_drag_destination, }, utils::{clonecell::CloneCell, hash_map_ext::HashMapExt}, wire::{XdgToplevelId, xdg_toplevel::*}, @@ -133,11 +134,12 @@ impl XdgToplevel { states.insert(STATE_CONSTRAINED_BOTTOM); } let state = &surface.surface.client.state; + let node_id = state.node_ids.next(); Self { id, state: state.clone(), xdg: surface.clone(), - node_id: state.node_ids.next(), + node_id, parent: Default::default(), children: RefCell::new(Default::default()), states: RefCell::new(states), @@ -152,6 +154,8 @@ impl XdgToplevel { state, String::new(), Some(surface.surface.client.clone()), + ToplevelType::XdgToplevel, + node_id, slf, ), drag: Default::default(), diff --git a/src/state.rs b/src/state.rs index ff0a278d..3c96c63b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -951,6 +951,13 @@ impl State { self.workspace_managers.clear(); } + pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { + self.toplevels.remove(&id); + if let Some(config) = self.config.get() { + config.toplevel_removed(id); + } + } + pub fn damage_hardware_cursors(&self, render: bool) { for output in self.root.outputs.lock().values() { if let Some(hc) = output.hardware_cursor.get() { diff --git a/src/tree/container.rs b/src/tree/container.rs index 7ede6614..fd56767f 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -19,7 +19,8 @@ use { tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, OutputNode, TddType, TileDragDestination, ToplevelData, ToplevelNode, ToplevelNodeBase, - WorkspaceNode, default_tile_drag_bounds, walker::NodeVisitor, + ToplevelType, WorkspaceNode, default_tile_drag_bounds, toplevel_set_floating, + walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, @@ -212,8 +213,9 @@ impl ContainerNode { let child_node_ref = child_node.clone(); let mut child_nodes = AHashMap::new(); child_nodes.insert(child.node_id(), child_node); + let id = state.node_ids.next(); let slf = Rc::new_cyclic(|weak| Self { - id: state.node_ids.next(), + id, split: Cell::new(split), mono_child: CloneCell::new(None), mono_body: Cell::new(Default::default()), @@ -237,7 +239,14 @@ impl ContainerNode { state: state.clone(), render_data: Default::default(), scroller: Default::default(), - toplevel_data: ToplevelData::new(state, Default::default(), None, weak), + toplevel_data: ToplevelData::new( + state, + Default::default(), + None, + ToplevelType::Container, + id, + weak, + ), attention_requests: Default::default(), }); child.tl_set_parent(slf.clone()); @@ -1239,7 +1248,7 @@ impl ContainerNode { && kind == SeatOpKind::Move { drop(seat_datas); - seat.set_tl_floating(child.node.clone(), true); + toplevel_set_floating(&self.state, child.node.clone(), true); return; } seat_data.op = Some(SeatOp { diff --git a/src/tree/float.rs b/src/tree/float.rs index e75335ec..f030a13e 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -16,7 +16,7 @@ use { tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, - walker::NodeVisitor, + toplevel_set_floating, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, @@ -603,7 +603,7 @@ impl FloatNode { { if let Some(tl) = self.child.get() { drop(cursors); - seat.set_tl_floating(tl, false); + toplevel_set_floating(&self.state, tl, false); return; } } diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index f28eee0b..1dd3ee3b 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -12,7 +12,7 @@ use { tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, default_tile_drag_destination, + ToplevelNodeBase, ToplevelType, default_tile_drag_destination, }, utils::{ asyncevent::AsyncEvent, errorfmt::ErrorFmt, on_drop_event::OnDropEvent, @@ -49,12 +49,15 @@ pub async fn placeholder_render_textures(state: Rc) { impl PlaceholderNode { pub fn new_for(state: &Rc, node: Rc, slf: &Weak) -> Self { + let id = state.node_ids.next(); Self { - id: state.node_ids.next(), + id, toplevel: ToplevelData::new( state, node.tl_data().title.borrow().clone(), node.node_client(), + ToplevelType::Placeholder, + id, slf, ), destroyed: Default::default(), @@ -65,9 +68,17 @@ impl PlaceholderNode { } pub fn new_empty(state: &Rc, slf: &Weak) -> Self { + let id = state.node_ids.next(); Self { - id: state.node_ids.next(), - toplevel: ToplevelData::new(state, String::new(), None, slf), + id, + toplevel: ToplevelData::new( + state, + String::new(), + None, + ToplevelType::Placeholder, + id, + slf, + ), destroyed: Default::default(), update_textures_scheduled: Default::default(), state: state.clone(), diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 45aed49e..dc8c1857 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -30,6 +30,7 @@ use { JayToplevelId, }, }, + jay_config::{window, window::WindowType}, std::{ cell::{Cell, RefCell}, ops::Deref, @@ -254,7 +255,29 @@ impl ToplevelOpt { } } +#[derive(Debug)] +pub enum ToplevelType { + Container, + Placeholder, + XdgToplevel, + XWindow, +} + +impl ToplevelType { + pub fn to_window_type(&self) -> WindowType { + match self { + ToplevelType::Container => window::CONTAINER, + ToplevelType::Placeholder => window::PLACEHOLDER, + ToplevelType::XdgToplevel => window::XDG_TOPLEVEL, + ToplevelType::XWindow => window::X_WINDOW, + } + } +} + pub struct ToplevelData { + #[expect(dead_code)] + pub node_id: NodeId, + pub kind: ToplevelType, pub self_active: Cell, pub client: Option>, pub state: Rc, @@ -291,11 +314,16 @@ impl ToplevelData { state: &Rc, title: String, client: Option>, + kind: ToplevelType, + node_id: impl Into, slf: &Weak, ) -> Self { + let node_id = node_id.into(); let id = toplevel_identifier(); state.toplevels.set(id, slf.clone()); Self { + node_id, + kind, self_active: Cell::new(false), client, state: state.clone(), @@ -372,7 +400,7 @@ impl ToplevelData { { let id = toplevel_identifier(); let prev = self.identifier.replace(id); - self.state.toplevels.remove(&prev); + self.state.remove_toplevel_id(prev); self.state.toplevels.set(id, self.slf.clone()); } { @@ -620,7 +648,7 @@ impl ToplevelData { impl Drop for ToplevelData { fn drop(&mut self) { - self.state.toplevels.remove(&self.identifier.get()); + self.state.remove_toplevel_id(self.identifier.get()); } } @@ -662,3 +690,82 @@ pub fn default_tile_drag_bounds(t: &T, split: Cont ContainerSplit::Vertical => t.node_absolute_position().height() / FACTOR, } } + +pub fn toplevel_parent_container(tl: &dyn ToplevelNode) -> Option> { + if let Some(parent) = tl.tl_data().parent.get() { + if let Some(container) = parent.node_into_container() { + return Some(container); + } + } + None +} + +pub fn toplevel_create_split(state: &Rc, tl: Rc, axis: ContainerSplit) { + if tl.tl_data().is_fullscreen.get() { + return; + } + let ws = match tl.tl_data().workspace.get() { + Some(ws) => ws, + _ => return, + }; + let pn = match tl.tl_data().parent.get() { + Some(pn) => pn, + _ => return, + }; + if let Some(pn) = pn.node_into_containing_node() { + let cn = ContainerNode::new(state, &ws, tl.clone(), axis); + pn.cnode_replace_child(&*tl, cn); + } +} + +pub fn toplevel_set_floating(state: &Rc, tl: Rc, floating: bool) { + let data = tl.tl_data(); + if data.is_fullscreen.get() { + return; + } + if data.is_floating.get() == floating { + return; + } + let parent = match data.parent.get() { + Some(p) => p, + _ => return, + }; + if !floating { + parent.cnode_remove_child2(&*tl, true); + state.map_tiled(tl); + } else if let Some(ws) = data.workspace.get() { + parent.cnode_remove_child2(&*tl, true); + let (width, height) = data.float_size(&ws); + state.map_floating(tl, width, height, &ws, None); + } +} + +pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: &Rc) { + if tl.tl_data().is_fullscreen.get() { + return; + } + let old_ws = match tl.tl_data().workspace.get() { + Some(ws) => ws, + _ => return, + }; + if old_ws.id == ws.id { + return; + } + let cn = match tl.tl_data().parent.get() { + Some(cn) => cn, + _ => return, + }; + let kb_foci = collect_kb_foci(tl.clone()); + cn.cnode_remove_child2(&*tl, true); + if !ws.visible.get() { + for focus in kb_foci { + old_ws.clone().node_do_focus(&focus, Direction::Unspecified); + } + } + if tl.tl_data().is_floating.get() { + let (width, height) = tl.tl_data().float_size(ws); + state.map_floating(tl.clone(), width, height, ws, None); + } else { + state.map_tiled_on(tl, ws); + } +} diff --git a/src/utils/clonecell.rs b/src/utils/clonecell.rs index 1364aa81..aebb5c72 100644 --- a/src/utils/clonecell.rs +++ b/src/utils/clonecell.rs @@ -1,9 +1,12 @@ use { - crate::utils::{ - linkedlist::NodeRef, - ptr_ext::{MutPtrExt, PtrExt}, + crate::{ + tree::NodeId, + utils::{ + linkedlist::NodeRef, + ptr_ext::{MutPtrExt, PtrExt}, + }, }, - jay_config::keyboard::mods::Modifiers, + jay_config::{keyboard::mods::Modifiers, window::Window}, std::{ cell::UnsafeCell, fmt::{Debug, Formatter}, @@ -97,3 +100,7 @@ unsafe impl UnsafeCellCloneSafe for usize {} unsafe impl UnsafeCellCloneSafe for (A, B) {} unsafe impl UnsafeCellCloneSafe for Modifiers {} + +unsafe impl UnsafeCellCloneSafe for NodeId {} + +unsafe impl UnsafeCellCloneSafe for Window {} diff --git a/src/utils/toplevel_identifier.rs b/src/utils/toplevel_identifier.rs index 0825e299..09d894ee 100644 --- a/src/utils/toplevel_identifier.rs +++ b/src/utils/toplevel_identifier.rs @@ -1,5 +1,8 @@ use { - crate::utils::opaque::{OPAQUE_LEN, Opaque, OpaqueError, opaque}, + crate::utils::{ + clonecell::UnsafeCellCloneSafe, + opaque::{OPAQUE_LEN, Opaque, OpaqueError, opaque}, + }, arrayvec::ArrayString, std::{ fmt::{Display, Formatter}, @@ -10,6 +13,8 @@ use { #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub struct ToplevelIdentifier(Opaque); +unsafe impl UnsafeCellCloneSafe for ToplevelIdentifier {} + pub fn toplevel_identifier() -> ToplevelIdentifier { ToplevelIdentifier(opaque()) }