aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-01-29 19:34:12 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-01-30 12:30:57 +0400
commit59ff331597633dc66113e5070d672ba70035421b (patch)
tree68d8c275a7661337ea64a6f8254a51a41224ab72
parentb813f99abd2d6e09eb72e8c0083e92b486b4b210 (diff)
downloadniri-59ff331597633dc66113e5070d672ba70035421b.tar.gz
niri-59ff331597633dc66113e5070d672ba70035421b.tar.bz2
niri-59ff331597633dc66113e5070d672ba70035421b.zip
Implement wlr-foreign-toplevel-management
The parent event isn't sent but whatever.
-rw-r--r--src/handlers/mod.rs57
-rw-r--r--src/layout/mod.rs21
-rw-r--r--src/main.rs1
-rw-r--r--src/niri.rs9
-rw-r--r--src/protocols/foreign_toplevel.rs417
-rw-r--r--src/protocols/mod.rs1
6 files changed, 506 insertions, 0 deletions
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index d44d9e8c..f0620a5b 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -14,6 +14,7 @@ use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::input;
+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -46,7 +47,9 @@ use smithay::{
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
};
+use crate::delegate_foreign_toplevel;
use crate::niri::{ClientState, State};
+use crate::protocols::foreign_toplevel::{ForeignToplevelHandler, ForeignToplevelManagerState};
use crate::utils::output_size;
impl SeatHandler for State {
@@ -291,3 +294,57 @@ impl SecurityContextHandler for State {
}
}
delegate_security_context!(State);
+
+impl ForeignToplevelHandler for State {
+ fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
+ &mut self.niri.foreign_toplevel_state
+ }
+
+ fn activate(&mut self, wl_surface: WlSurface) {
+ if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
+ let window = window.clone();
+ self.niri.layout.activate_window(&window);
+ self.niri.queue_redraw_all();
+ }
+ }
+
+ fn close(&mut self, wl_surface: WlSurface) {
+ if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
+ window.toplevel().send_close();
+ }
+ }
+
+ fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
+ if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
+ {
+ if !window
+ .toplevel()
+ .current_state()
+ .capabilities
+ .contains(xdg_toplevel::WmCapabilities::Fullscreen)
+ {
+ return;
+ }
+
+ let window = window.clone();
+
+ if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
+ if &requested_output != current_output {
+ self.niri
+ .layout
+ .move_window_to_output(window.clone(), &requested_output);
+ }
+ }
+
+ self.niri.layout.set_fullscreen(&window, true);
+ }
+ }
+
+ fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
+ if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
+ let window = window.clone();
+ self.niri.layout.set_fullscreen(&window, false);
+ }
+ }
+}
+delegate_foreign_toplevel!(State);
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 67b1b139..bedc0979 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -776,6 +776,27 @@ impl<W: LayoutElement> Layout<W> {
mon.workspaces.iter().flat_map(|ws| ws.windows())
}
+ pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>)) {
+ match &self.monitor_set {
+ MonitorSet::Normal { monitors, .. } => {
+ for mon in monitors {
+ for ws in &mon.workspaces {
+ for win in ws.windows() {
+ f(win, Some(&mon.output));
+ }
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces } => {
+ for ws in workspaces {
+ for win in ws.windows() {
+ f(win, None);
+ }
+ }
+ }
+ }
+ }
+
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
diff --git a/src/main.rs b/src/main.rs
index cb8ebea6..ec32546d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,6 +15,7 @@ mod input;
mod ipc;
mod layout;
mod niri;
+mod protocols;
mod render_helpers;
mod screenshot_ui;
mod utils;
diff --git a/src/niri.rs b/src/niri.rs
index e0f5a498..268dd3f4 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -103,6 +103,7 @@ use crate::hotkey_overlay::HotkeyOverlay;
use crate::input::{apply_libinput_settings, TabletData};
use crate::ipc::server::IpcServer;
use crate::layout::{Layout, MonitorRenderElement};
+use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
use crate::pw_utils::{Cast, PipeWire};
use crate::render_helpers::NiriRenderer;
use crate::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
@@ -151,6 +152,7 @@ pub struct Niri {
pub kde_decoration_state: KdeDecorationState,
pub layer_shell_state: WlrLayerShellState,
pub session_lock_state: SessionLockManagerState,
+ pub foreign_toplevel_state: ForeignToplevelManagerState,
pub shm_state: ShmState,
pub output_manager_state: OutputManagerState,
pub dmabuf_state: DmabufState,
@@ -327,6 +329,7 @@ impl State {
self.refresh_popup_grab();
self.update_keyboard_focus();
self.refresh_pointer_focus();
+ foreign_toplevel::refresh(self);
{
let _span = tracy_client::span!("flush_clients");
@@ -855,6 +858,11 @@ impl Niri {
!client.get_data::<ClientState>().unwrap().restricted
});
+ let foreign_toplevel_state =
+ ForeignToplevelManagerState::new::<State, _>(&display_handle, |client| {
+ !client.get_data::<ClientState>().unwrap().restricted
+ });
+
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name());
seat.add_keyboard(
config_.input.keyboard.xkb.to_xkb_config(),
@@ -972,6 +980,7 @@ impl Niri {
kde_decoration_state,
layer_shell_state,
session_lock_state,
+ foreign_toplevel_state,
text_input_state,
input_method_state,
virtual_keyboard_state,
diff --git a/src/protocols/foreign_toplevel.rs b/src/protocols/foreign_toplevel.rs
new file mode 100644
index 00000000..88e2f13f
--- /dev/null
+++ b/src/protocols/foreign_toplevel.rs
@@ -0,0 +1,417 @@
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+
+use arrayvec::ArrayVec;
+use smithay::output::Output;
+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
+use smithay::reexports::wayland_protocols_wlr;
+use smithay::reexports::wayland_server::backend::ClientId;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::reexports::wayland_server::{
+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
+};
+use smithay::wayland::compositor::with_states;
+use smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceData};
+use wayland_protocols_wlr::foreign_toplevel::v1::server::{
+ zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
+};
+use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
+use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
+
+use crate::niri::State;
+
+const VERSION: u32 = 3;
+
+pub struct ForeignToplevelManagerState {
+ display: DisplayHandle,
+ instances: Vec<ZwlrForeignToplevelManagerV1>,
+ toplevels: HashMap<WlSurface, ToplevelData>,
+}
+
+pub trait ForeignToplevelHandler {
+ fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
+ fn activate(&mut self, wl_surface: WlSurface);
+ fn close(&mut self, wl_surface: WlSurface);
+ fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
+ fn unset_fullscreen(&mut self, wl_surface: WlSurface);
+}
+
+struct ToplevelData {
+ title: Option<String>,
+ app_id: Option<String>,
+ states: ArrayVec<u32, 3>,
+ output: Option<Output>,
+ instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
+ // FIXME: parent.
+}
+
+pub struct ForeignToplevelGlobalData {
+ filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
+}
+
+impl ForeignToplevelManagerState {
+ pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
+ where
+ D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
+ D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
+ D: 'static,
+ F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
+ {
+ let global_data = ForeignToplevelGlobalData {
+ filter: Box::new(filter),
+ };
+ display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
+ Self {
+ display: display.clone(),
+ instances: Vec::new(),
+ toplevels: HashMap::new(),
+ }
+ }
+}
+
+pub fn refresh(state: &mut State) {
+ let _span = tracy_client::span!("foreign_toplevel::refresh");
+
+ let protocol_state = &mut state.niri.foreign_toplevel_state;
+
+ // Handle closed windows.
+ protocol_state.toplevels.retain(|surface, data| {
+ if state.niri.layout.find_window_and_output(surface).is_some() {
+ return true;
+ }
+
+ for instance in data.instances.keys() {
+ instance.closed();
+ }
+
+ false
+ });
+
+ // Handle new and existing windows.
+ state.niri.layout.with_windows(|window, output| {
+ let wl_surface = window.toplevel().wl_surface();
+
+ with_states(wl_surface, |states| {
+ let role = states
+ .data_map
+ .get::<XdgToplevelSurfaceData>()
+ .unwrap()
+ .lock()
+ .unwrap();
+
+ let states = foreign_toplevel_state(&role.current.states);
+
+ match protocol_state.toplevels.entry(wl_surface.clone()) {
+ Entry::Occupied(entry) => {
+ // Existing window, check if anything changed.
+ let data = entry.into_mut();
+
+ let mut new_title = None;
+ if data.title != role.title {
+ data.title = role.title.clone();
+ new_title = role.title.as_deref();
+
+ if new_title.is_none() {
+ error!("toplevel title changed to None");
+ }
+ }
+
+ let mut new_app_id = None;
+ if data.app_id != role.app_id {
+ data.app_id = role.app_id.clone();
+ new_app_id = role.app_id.as_deref();
+
+ if new_app_id.is_none() {
+ error!("toplevel app_id changed to None");
+ }
+ }
+
+ let mut states_changed = false;
+ if data.states != states {
+ data.states = states;
+ states_changed = true;
+ }
+
+ let mut output_changed = false;
+ if data.output.as_ref() != output {
+ data.output = output.cloned();
+ output_changed = true;
+ }
+
+ let something_changed = new_title.is_some()
+ || new_app_id.is_some()
+ || states_changed
+ || output_changed;
+
+ if something_changed {
+ for (instance, outputs) in &mut data.instances {
+ if let Some(new_title) = new_title {
+ instance.title(new_title.to_owned());
+ }
+ if let Some(new_app_id) = new_app_id {
+ instance.app_id(new_app_id.to_owned());
+ }
+ if states_changed {
+ instance.state(
+ data.states.iter().flat_map(|x| x.to_ne_bytes()).collect(),
+ );
+ }
+ if output_changed {
+ for wl_output in outputs.drain(..) {
+ instance.output_leave(&wl_output);
+ }
+ if let Some(output) = &data.output {
+ if let Some(client) = instance.client() {
+ for wl_output in output.client_outputs(&client) {
+ instance.output_enter(&wl_output);
+ outputs.push(wl_output);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (instance, outputs) in &mut data.instances {
+ // Clean up dead wl_outputs.
+ outputs.retain(|x| x.is_alive());
+
+ let mut send_done = something_changed;
+
+ // If the client bound any more wl_outputs, send enter for them.
+ if let Some(output) = &data.output {
+ if let Some(client) = instance.client() {
+ for wl_output in output.client_outputs(&client) {
+ if !outputs.iter().any(|x| x == &wl_output) {
+ instance.output_enter(&wl_output);
+ outputs.push(wl_output);
+ send_done = true;
+ }
+ }
+ }
+ }
+
+ if send_done {
+ instance.done();
+ }
+ }
+ }
+ Entry::Vacant(entry) => {
+ // New window, start tracking it.
+ let mut data = ToplevelData {
+ title: role.title.clone(),
+ app_id: role.app_id.clone(),
+ states,
+ output: output.cloned(),
+ instances: HashMap::new(),
+ };
+
+ for manager in &protocol_state.instances {
+ if let Some(client) = manager.client() {
+ data.add_instance::<State>(&protocol_state.display, &client, manager);
+ }
+ }
+
+ entry.insert(data);
+ }
+ }
+ });
+ });
+}
+
+impl ToplevelData {
+ fn add_instance<D>(
+ &mut self,
+ handle: &DisplayHandle,
+ client: &Client,
+ manager: &ZwlrForeignToplevelManagerV1,
+ ) where
+ D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
+ D: 'static,
+ {
+ let toplevel = client
+ .create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
+ .unwrap();
+ manager.toplevel(&toplevel);
+
+ if let Some(title) = &self.title {
+ toplevel.title(title.clone());
+ }
+ if let Some(app_id) = &self.app_id {
+ toplevel.app_id(app_id.clone());
+ }
+
+ toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
+
+ let mut outputs = Vec::new();
+ if let Some(output) = &self.output {
+ for wl_output in output.client_outputs(client) {
+ toplevel.output_enter(&wl_output);
+ outputs.push(wl_output);
+ }
+ }
+
+ toplevel.done();
+
+ self.instances.insert(toplevel, outputs);
+ }
+}
+
+impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
+ for ForeignToplevelManagerState
+where
+ D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
+ D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
+ D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
+ D: ForeignToplevelHandler,
+{
+ fn bind(
+ state: &mut D,
+ handle: &DisplayHandle,
+ client: &Client,
+ resource: New<ZwlrForeignToplevelManagerV1>,
+ _global_data: &ForeignToplevelGlobalData,
+ data_init: &mut DataInit<'_, D>,
+ ) {
+ let manager = data_init.init(resource, ());
+
+ let state = state.foreign_toplevel_manager_state();
+
+ for data in state.toplevels.values_mut() {
+ data.add_instance::<D>(handle, client, &manager);
+ }
+
+ state.instances.push(manager);
+ }
+
+ fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
+ (global_data.filter)(&client)
+ }
+}
+
+impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
+where
+ D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
+ D: ForeignToplevelHandler,
+{
+ fn request(
+ state: &mut D,
+ _client: &Client,
+ resource: &ZwlrForeignToplevelManagerV1,
+ request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
+ _data: &(),
+ _dhandle: &DisplayHandle,
+ _data_init: &mut DataInit<'_, D>,
+ ) {
+ match request {
+ zwlr_foreign_toplevel_manager_v1::Request::Stop => {
+ resource.finished();
+
+ let state = state.foreign_toplevel_manager_state();
+ state.instances.retain(|x| x != resource);
+ }
+ _ => unreachable!(),
+ }
+ }
+
+ fn destroyed(
+ state: &mut D,
+ _client: ClientId,
+ resource: &ZwlrForeignToplevelManagerV1,
+ _data: &(),
+ ) {
+ let state = state.foreign_toplevel_manager_state();
+ state.instances.retain(|x| x != resource);
+ }
+}
+
+impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
+where
+ D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
+ D: ForeignToplevelHandler,
+{
+ fn request(
+ state: &mut D,
+ _client: &Client,
+ resource: &ZwlrForeignToplevelHandleV1,
+ request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
+ _data: &(),
+ _dhandle: &DisplayHandle,
+ _data_init: &mut DataInit<'_, D>,
+ ) {
+ let protocol_state = state.foreign_toplevel_manager_state();
+
+ let Some((surface, _)) = protocol_state
+ .toplevels
+ .iter()
+ .find(|(_, data)| data.instances.contains_key(resource))
+ else {
+ return;
+ };
+ let surface = surface.clone();
+
+ match request {
+ zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
+ zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
+ zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
+ zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
+ zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
+ state.activate(surface);
+ }
+ zwlr_foreign_toplevel_handle_v1::Request::Close => {
+ state.close(surface);
+ }
+ zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
+ zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
+ zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
+ state.set_fullscreen(surface, output);
+ }
+ zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
+ state.unset_fullscreen(surface);
+ }
+ _ => unreachable!(),
+ }
+ }
+
+ fn destroyed(
+ state: &mut D,
+ _client: ClientId,
+ resource: &ZwlrForeignToplevelHandleV1,
+ _data: &(),
+ ) {
+ let state = state.foreign_toplevel_manager_state();
+ for data in state.toplevels.values_mut() {
+ data.instances.retain(|instance, _| instance != resource);
+ }
+ }
+}
+
+fn foreign_toplevel_state(states: &ToplevelStateSet) -> ArrayVec<u32, 3> {
+ let mut rv = ArrayVec::new();
+ if states.contains(xdg_toplevel::State::Maximized) {
+ rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
+ }
+ if states.contains(xdg_toplevel::State::Activated) {
+ rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
+ }
+ if states.contains(xdg_toplevel::State::Fullscreen) {
+ rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
+ }
+ rv
+}
+
+#[macro_export]
+macro_rules! delegate_foreign_toplevel {
+ ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
+ smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
+ smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
+ ] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
+ smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
+ smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
+ ] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
+ smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
+ smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
+ ] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
+ };
+}
diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs
new file mode 100644
index 00000000..06d80157
--- /dev/null
+++ b/src/protocols/mod.rs
@@ -0,0 +1 @@
+pub mod foreign_toplevel;