diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5d823..efd4866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,10 @@ #### Clients & Protocols +- `wlr-layer-shell-unstable-v1` support - Added public api constants for the roles of `wl_shell_surface`, `zxdg_toplevel` and `xdg_toplevel`. See the `shell::legacy` and `shell::xdg` modules for these constants. -- Whether a surface is toplevel equivalent can be determined with the new function `shell::is_toplevel_equivalent`. +- Whether a surface is toplevel equivalent can be determined with the new function `shell::is_toplevel_equivalent`. - Setting the parent of a toplevel surface is now possible with the `xdg::ToplevelSurface::set_parent` function. - Add support for the zxdg-foreign-v2 protocol. diff --git a/anvil/src/drawing.rs b/anvil/src/drawing.rs index 4d386c7..53e9807 100644 --- a/anvil/src/drawing.rs +++ b/anvil/src/drawing.rs @@ -20,6 +20,7 @@ use smithay::{ SurfaceAttributes, TraversalAction, }, seat::CursorImageAttributes, + shell::wlr_layer::Layer, }, }; @@ -241,6 +242,59 @@ where result } +pub fn draw_layers( + renderer: &mut R, + frame: &mut F, + window_map: &WindowMap, + layer: Layer, + output_rect: Rectangle, + output_scale: f32, + log: &::slog::Logger, +) -> Result<(), SwapBuffersError> +where + R: Renderer + ImportAll, + F: Frame, + E: std::error::Error + Into, + T: Texture + 'static, +{ + let mut result = Ok(()); + + window_map + .layers + .with_layers_from_bottom_to_top(&layer, |layer_surface| { + // skip layers that do not overlap with a given output + if !output_rect.overlaps(layer_surface.bbox) { + return; + } + + let mut initial_place: Point = layer_surface.location; + initial_place.x -= output_rect.loc.x; + + if let Some(wl_surface) = layer_surface.surface.get_surface() { + // this surface is a root of a subsurface tree that needs to be drawn + if let Err(err) = + draw_surface_tree(renderer, frame, wl_surface, initial_place, output_scale, log) + { + result = Err(err); + } + + window_map.with_child_popups(wl_surface, |popup| { + let location = popup.location(); + let draw_location = initial_place + location; + if let Some(wl_surface) = popup.get_surface() { + if let Err(err) = + draw_surface_tree(renderer, frame, wl_surface, draw_location, output_scale, log) + { + result = Err(err); + } + } + }); + } + }); + + result +} + pub fn draw_dnd_icon( renderer: &mut R, frame: &mut F, diff --git a/anvil/src/input_handler.rs b/anvil/src/input_handler.rs index 2985793..e2f06d2 100644 --- a/anvil/src/input_handler.rs +++ b/anvil/src/input_handler.rs @@ -211,6 +211,11 @@ impl AnvilState { }, crate::winit::OUTPUT_NAME, ); + + let output_mut = self.output_map.borrow(); + let output = output_mut.find_by_name(crate::winit::OUTPUT_NAME).unwrap(); + + self.window_map.borrow_mut().layers.arange_layers(output); } _ => { // other events are not handled in anvil (yet) diff --git a/anvil/src/output_map.rs b/anvil/src/output_map.rs index 98583b8..df367bf 100644 --- a/anvil/src/output_map.rs +++ b/anvil/src/output_map.rs @@ -4,7 +4,10 @@ use smithay::{ reexports::{ wayland_protocols::xdg_shell::server::xdg_toplevel, wayland_server::{ - protocol::{wl_output, wl_surface::WlSurface}, + protocol::{ + wl_output, + wl_surface::{self, WlSurface}, + }, Display, Global, UserDataMap, }, }, @@ -22,6 +25,7 @@ pub struct Output { output: output::Output, global: Option>, surfaces: Vec, + layer_surfaces: RefCell>, current_mode: Mode, scale: f32, output_scale: i32, @@ -60,6 +64,7 @@ impl Output { output, location, surfaces: Vec::new(), + layer_surfaces: Default::default(), current_mode: mode, scale, output_scale, @@ -101,6 +106,16 @@ impl Output { pub fn current_mode(&self) -> Mode { self.current_mode } + + /// Add a layer surface to this output + pub fn add_layer_surface(&self, layer: wl_surface::WlSurface) { + self.layer_surfaces.borrow_mut().push(layer); + } + + /// Get all layer surfaces assigned to this output + pub fn layer_surfaces(&self) -> Vec { + self.layer_surfaces.borrow().iter().cloned().collect() + } } impl Drop for Output { @@ -171,6 +186,7 @@ impl OutputMap { // and move them to the primary output let primary_output_location = self.with_primary().map(|o| o.location()).unwrap_or_default(); let mut window_map = self.window_map.borrow_mut(); + // TODO: This is a bit unfortunate, we save the windows in a temp vector // cause we can not call window_map.set_location within the closure. let mut windows_to_move = Vec::new(); @@ -218,6 +234,10 @@ impl OutputMap { for (window, location) in windows_to_move.drain(..) { window_map.set_location(&window, location); } + + for output in self.outputs.iter() { + window_map.layers.arange_layers(output); + } } pub fn add(&mut self, name: N, physical: PhysicalProperties, mode: Mode) -> &Output @@ -293,6 +313,15 @@ impl OutputMap { self.find(|o| o.output.owns(output)) } + pub fn find_by_layer_surface(&self, surface: &wl_surface::WlSurface) -> Option<&Output> { + self.find(|o| { + o.layer_surfaces + .borrow() + .iter() + .any(|s| s.as_ref().equals(surface.as_ref())) + }) + } + pub fn find_by_name(&self, name: N) -> Option<&Output> where N: AsRef, @@ -388,9 +417,10 @@ impl OutputMap { pub fn refresh(&mut self) { // Clean-up dead surfaces - self.outputs - .iter_mut() - .for_each(|o| o.surfaces.retain(|s| s.as_ref().is_alive())); + self.outputs.iter_mut().for_each(|o| { + o.surfaces.retain(|s| s.as_ref().is_alive()); + o.layer_surfaces.borrow_mut().retain(|s| s.as_ref().is_alive()); + }); let window_map = self.window_map.clone(); diff --git a/anvil/src/shell.rs b/anvil/src/shell.rs index 7509ccb..73320bd 100644 --- a/anvil/src/shell.rs +++ b/anvil/src/shell.rs @@ -22,6 +22,7 @@ use smithay::{ seat::{AxisFrame, GrabStartData, PointerGrab, PointerInnerHandle, Seat}, shell::{ legacy::{wl_shell_init, ShellRequest, ShellState as WlShellState, ShellSurfaceKind}, + wlr_layer::{LayerShellRequest, LayerSurfaceAttributes}, xdg::{ xdg_shell_init, Configure, ShellState as XdgShellState, SurfaceCachedState, XdgPopupSurfaceRoleAttributes, XdgRequest, XdgToplevelSurfaceRoleAttributes, @@ -329,7 +330,8 @@ pub fn init_shell(display: Rc>, log: ::sl move |surface, mut ddata| { let anvil_state = ddata.get::>().unwrap(); let window_map = anvil_state.window_map.as_ref(); - surface_commit(&surface, &*window_map) + let output_map = anvil_state.output_map.as_ref(); + surface_commit(&surface, &*window_map, &*output_map) }, log.clone(), ); @@ -822,6 +824,38 @@ pub fn init_shell(display: Rc>, log: ::sl log.clone(), ); + let layer_window_map = window_map.clone(); + let layer_output_map = output_map.clone(); + smithay::wayland::shell::wlr_layer::wlr_layer_shell_init( + &mut *display.borrow_mut(), + move |event, mut ddata| match event { + LayerShellRequest::NewLayerSurface { + surface, + output, + layer, + .. + } => { + let output_map = layer_output_map.borrow(); + let anvil_state = ddata.get::>().unwrap(); + + let output = output.and_then(|output| output_map.find_by_output(&output)); + let output = output.unwrap_or_else(|| { + output_map + .find_by_position(anvil_state.pointer_location.to_i32_round()) + .unwrap_or_else(|| output_map.with_primary().unwrap()) + }); + + if let Some(wl_surface) = surface.get_surface() { + output.add_layer_surface(wl_surface.clone()); + + layer_window_map.borrow_mut().layers.insert(surface, layer); + } + } + LayerShellRequest::AckConfigure { .. } => {} + }, + log.clone(), + ); + ShellHandles { xdg_state: xdg_shell_state, wl_state: wl_shell_state, @@ -937,7 +971,11 @@ impl SurfaceData { } } -fn surface_commit(surface: &wl_surface::WlSurface, window_map: &RefCell) { +fn surface_commit( + surface: &wl_surface::WlSurface, + window_map: &RefCell, + output_map: &RefCell, +) { #[cfg(feature = "xwayland")] super::xwayland::commit_hook(surface); @@ -1055,4 +1093,25 @@ fn surface_commit(surface: &wl_surface::WlSurface, window_map: &RefCell>() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }) + .unwrap(); + if !initial_configure_sent { + layer.surface.send_configure(); + } + + if let Some(output) = output_map.borrow().find_by_layer_surface(surface) { + window_map.layers.arange_layers(output); + } + } } diff --git a/anvil/src/udev.rs b/anvil/src/udev.rs index 30d2533..8811df2 100644 --- a/anvil/src/udev.rs +++ b/anvil/src/udev.rs @@ -55,6 +55,7 @@ use smithay::{ wayland::{ output::{Mode, PhysicalProperties}, seat::CursorImageStatus, + shell::wlr_layer::Layer, }, }; #[cfg(feature = "egl")] @@ -744,9 +745,34 @@ fn render_surface( Transform::Flipped180, // Scanout is rotated |renderer, frame| { frame.clear([0.8, 0.8, 0.9, 1.0])?; + + for layer in [Layer::Background, Layer::Bottom] { + draw_layers( + renderer, + frame, + window_map, + layer, + output_geometry, + output_scale, + logger, + )?; + } + // draw the surfaces draw_windows(renderer, frame, window_map, output_geometry, output_scale, logger)?; + for layer in [Layer::Top, Layer::Overlay] { + draw_layers( + renderer, + frame, + window_map, + layer, + output_geometry, + output_scale, + logger, + )?; + } + // set cursor if output_geometry.to_f64().contains(pointer_location) { let (ptr_x, ptr_y) = pointer_location.into(); diff --git a/anvil/src/window_map.rs b/anvil/src/window_map.rs index 0ca4b4c..b96e334 100644 --- a/anvil/src/window_map.rs +++ b/anvil/src/window_map.rs @@ -8,6 +8,7 @@ use smithay::{ compositor::{with_states, with_surface_tree_downward, SubsurfaceCachedState, TraversalAction}, shell::{ legacy::ShellSurface, + wlr_layer::Layer, xdg::{PopupSurface, SurfaceCachedState, ToplevelSurface, XdgPopupSurfaceRoleAttributes}, }, }, @@ -17,6 +18,9 @@ use crate::shell::SurfaceData; #[cfg(feature = "xwayland")] use crate::xwayland::X11Surface; +mod layer_map; +pub use layer_map::{LayerMap, LayerSurface}; + #[derive(Clone, PartialEq)] pub enum Kind { Xdg(ToplevelSurface), @@ -242,6 +246,8 @@ pub struct Popup { pub struct WindowMap { windows: Vec, popups: Vec, + + pub layers: LayerMap, } impl WindowMap { @@ -268,11 +274,26 @@ impl WindowMap { &self, point: Point, ) -> Option<(wl_surface::WlSurface, Point)> { + if let Some(res) = self.layers.get_surface_under(&Layer::Overlay, point) { + return Some(res); + } + if let Some(res) = self.layers.get_surface_under(&Layer::Top, point) { + return Some(res); + } + for w in &self.windows { if let Some(surface) = w.matching(point) { return Some(surface); } } + + if let Some(res) = self.layers.get_surface_under(&Layer::Bottom, point) { + return Some(res); + } + if let Some(res) = self.layers.get_surface_under(&Layer::Background, point) { + return Some(res); + } + None } @@ -330,6 +351,7 @@ impl WindowMap { pub fn refresh(&mut self) { self.windows.retain(|w| w.toplevel.alive()); self.popups.retain(|p| p.popup.alive()); + self.layers.refresh(); for w in &mut self.windows { w.self_update(); } @@ -404,5 +426,6 @@ impl WindowMap { for window in &self.windows { window.send_frame(time); } + self.layers.send_frames(time); } } diff --git a/anvil/src/window_map/layer_map.rs b/anvil/src/window_map/layer_map.rs new file mode 100644 index 0000000..52a28a3 --- /dev/null +++ b/anvil/src/window_map/layer_map.rs @@ -0,0 +1,253 @@ +use std::cell::RefCell; + +use smithay::{ + reexports::wayland_server::protocol::wl_surface, + utils::{Logical, Point, Rectangle}, + wayland::{ + compositor::{with_states, with_surface_tree_downward, SubsurfaceCachedState, TraversalAction}, + shell::wlr_layer::{self, Anchor, LayerSurfaceCachedState}, + }, +}; + +use crate::{output_map::Output, shell::SurfaceData}; + +pub struct LayerSurface { + pub surface: wlr_layer::LayerSurface, + pub location: Point, + pub bbox: Rectangle, + pub layer: wlr_layer::Layer, +} + +impl LayerSurface { + /// Finds the topmost surface under this point if any and returns it together with the location of this + /// surface. + fn matching(&self, point: Point) -> Option<(wl_surface::WlSurface, Point)> { + if !self.bbox.to_f64().contains(point) { + return None; + } + // need to check more carefully + let found = RefCell::new(None); + if let Some(wl_surface) = self.surface.get_surface() { + with_surface_tree_downward( + wl_surface, + self.location, + |wl_surface, states, location| { + let mut location = *location; + let data = states.data_map.get::>(); + + if states.role == Some("subsurface") { + let current = states.cached_state.current::(); + location += current.location; + } + + let contains_the_point = data + .map(|data| { + data.borrow() + .contains_point(&*states.cached_state.current(), point - location.to_f64()) + }) + .unwrap_or(false); + if contains_the_point { + *found.borrow_mut() = Some((wl_surface.clone(), location)); + } + + TraversalAction::DoChildren(location) + }, + |_, _, _| {}, + |_, _, _| { + // only continue if the point is not found + found.borrow().is_none() + }, + ); + } + found.into_inner() + } + + fn self_update(&mut self) { + let mut bounding_box = Rectangle::from_loc_and_size(self.location, (0, 0)); + if let Some(wl_surface) = self.surface.get_surface() { + with_surface_tree_downward( + wl_surface, + self.location, + |_, states, &loc| { + let mut loc = loc; + let data = states.data_map.get::>(); + + if let Some(size) = data.and_then(|d| d.borrow().size()) { + if states.role == Some("subsurface") { + let current = states.cached_state.current::(); + loc += current.location; + } + + // Update the bounding box. + bounding_box = bounding_box.merge(Rectangle::from_loc_and_size(loc, size)); + + TraversalAction::DoChildren(loc) + } else { + // If the parent surface is unmapped, then the child surfaces are hidden as + // well, no need to consider them here. + TraversalAction::SkipChildren + } + }, + |_, _, _| {}, + |_, _, _| true, + ); + } + self.bbox = bounding_box; + + if let Some(surface) = self.surface.get_surface() { + self.layer = with_states(surface, |states| { + let current = states.cached_state.current::(); + current.layer + }) + .unwrap(); + } + } + + /// Sends the frame callback to all the subsurfaces in this + /// window that requested it + fn send_frame(&self, time: u32) { + if let Some(wl_surface) = self.surface.get_surface() { + with_surface_tree_downward( + wl_surface, + (), + |_, _, &()| TraversalAction::DoChildren(()), + |_, states, &()| { + // the surface may not have any user_data if it is a subsurface and has not + // yet been commited + SurfaceData::send_frame(&mut *states.cached_state.current(), time) + }, + |_, _, &()| true, + ); + } + } +} + +#[derive(Default)] +pub struct LayerMap { + surfaces: Vec, +} + +impl LayerMap { + pub fn insert(&mut self, surface: wlr_layer::LayerSurface, layer: wlr_layer::Layer) { + let mut layer = LayerSurface { + location: Default::default(), + bbox: Rectangle::default(), + surface, + layer, + }; + layer.self_update(); + self.surfaces.insert(0, layer); + } + + pub fn get_surface_under( + &self, + layer: &wlr_layer::Layer, + point: Point, + ) -> Option<(wl_surface::WlSurface, Point)> { + for l in self.surfaces.iter().filter(|s| &s.layer == layer) { + if let Some(surface) = l.matching(point) { + return Some(surface); + } + } + None + } + + pub fn with_layers_from_bottom_to_top(&self, layer: &wlr_layer::Layer, mut f: Func) + where + Func: FnMut(&LayerSurface), + { + for l in self.surfaces.iter().filter(|s| &s.layer == layer).rev() { + f(l) + } + } + + pub fn refresh(&mut self) { + self.surfaces.retain(|l| l.surface.alive()); + + for l in self.surfaces.iter_mut() { + l.self_update(); + } + } + + /// Finds the layer corresponding to the given `WlSurface`. + pub fn find(&self, surface: &wl_surface::WlSurface) -> Option<&LayerSurface> { + self.surfaces.iter().find_map(|l| { + if l.surface + .get_surface() + .map(|s| s.as_ref().equals(surface.as_ref())) + .unwrap_or(false) + { + Some(l) + } else { + None + } + }) + } + + pub fn arange_layers(&mut self, output: &Output) { + let output_rect = output.geometry(); + + // Get all layer surfaces assigned to this output + let surfaces: Vec<_> = output + .layer_surfaces() + .into_iter() + .map(|s| s.as_ref().clone()) + .collect(); + + // Find layers for this output + let filtered_layers = self.surfaces.iter_mut().filter(|l| { + l.surface + .get_surface() + .map(|s| surfaces.contains(s.as_ref())) + .unwrap_or(false) + }); + + for layer in filtered_layers { + let surface = if let Some(surface) = layer.surface.get_surface() { + surface + } else { + continue; + }; + + let data = with_states(surface, |states| { + *states.cached_state.current::() + }) + .unwrap(); + + let x = if data.size.w == 0 || data.anchor.contains(Anchor::LEFT) { + output_rect.loc.x + } else if data.anchor.contains(Anchor::RIGHT) { + output_rect.loc.x + (output_rect.size.w - data.size.w) + } else { + output_rect.loc.x + ((output_rect.size.w / 2) - (data.size.w / 2)) + }; + + let y = if data.size.h == 0 || data.anchor.contains(Anchor::TOP) { + output_rect.loc.y + } else if data.anchor.contains(Anchor::BOTTOM) { + output_rect.loc.y + (output_rect.size.h - data.size.h) + } else { + output_rect.loc.y + ((output_rect.size.h / 2) - (data.size.h / 2)) + }; + + let location: Point = (x, y).into(); + + layer + .surface + .with_pending_state(|state| { + state.size = Some(output_rect.size); + }) + .unwrap(); + + layer.surface.send_configure(); + + layer.location = location; + } + } + + pub fn send_frames(&self, time: u32) { + for layer in &self.surfaces { + layer.send_frame(time); + } + } +} diff --git a/anvil/src/winit.rs b/anvil/src/winit.rs index e1b9f09..8e3e87c 100644 --- a/anvil/src/winit.rs +++ b/anvil/src/winit.rs @@ -16,6 +16,7 @@ use smithay::{ wayland::{ output::{Mode, PhysicalProperties}, seat::CursorImageStatus, + shell::wlr_layer::Layer, }, }; @@ -144,15 +145,34 @@ pub fn run_winit(log: Logger) { .render(|renderer, frame| { frame.clear([0.8, 0.8, 0.9, 1.0])?; + let window_map = &*state.window_map.borrow(); + + for layer in [Layer::Background, Layer::Bottom] { + draw_layers( + renderer, + frame, + window_map, + layer, + output_geometry, + output_scale, + &log, + )?; + } + // draw the windows - draw_windows( - renderer, - frame, - &*state.window_map.borrow(), - output_geometry, - output_scale, - &log, - )?; + draw_windows(renderer, frame, window_map, output_geometry, output_scale, &log)?; + + for layer in [Layer::Top, Layer::Overlay] { + draw_layers( + renderer, + frame, + window_map, + layer, + output_geometry, + output_scale, + &log, + )?; + } let (x, y) = state.pointer_location.into(); // draw the dnd icon if any diff --git a/src/wayland/shell/mod.rs b/src/wayland/shell/mod.rs index 470bf67..03a9dd5 100644 --- a/src/wayland/shell/mod.rs +++ b/src/wayland/shell/mod.rs @@ -22,6 +22,8 @@ use wayland_server::protocol::wl_surface::WlSurface; pub mod legacy; pub mod xdg; +pub mod wlr_layer; + /// Represents the possible errors returned from /// a surface ping #[derive(Debug, Error)] diff --git a/src/wayland/shell/wlr_layer/handlers.rs b/src/wayland/shell/wlr_layer/handlers.rs new file mode 100644 index 0000000..3e97e78 --- /dev/null +++ b/src/wayland/shell/wlr_layer/handlers.rs @@ -0,0 +1,298 @@ +use std::{convert::TryFrom, ops::Deref, sync::Mutex}; + +use wayland_protocols::wlr::unstable::layer_shell::v1::server::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; +use wayland_server::{protocol::wl_surface, DispatchData, Filter, Main}; + +use crate::wayland::{compositor, shell::wlr_layer::Layer, Serial}; + +use super::{ + Anchor, KeyboardInteractivity, LayerShellRequest, LayerSurfaceAttributes, LayerSurfaceCachedState, + Margins, ShellUserData, +}; + +use super::LAYER_SURFACE_ROLE; + +pub(super) fn layer_shell_implementation( + shell: Main, + request: zwlr_layer_shell_v1::Request, + dispatch_data: DispatchData<'_>, +) { + let data = shell.as_ref().user_data().get::().unwrap(); + match request { + zwlr_layer_shell_v1::Request::GetLayerSurface { + id, + surface, + output, + layer, + namespace, + } => { + let layer = match Layer::try_from(layer) { + Ok(layer) => layer, + Err((err, msg)) => { + id.as_ref().post_error(err as u32, msg); + return; + } + }; + + if compositor::give_role(&surface, LAYER_SURFACE_ROLE).is_err() { + shell.as_ref().post_error( + zwlr_layer_shell_v1::Error::Role as u32, + "Surface already has a role.".into(), + ); + return; + } + + compositor::with_states(&surface, |states| { + states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(LayerSurfaceAttributes::new(id.deref().clone())) + }); + + states.cached_state.pending::().layer = layer; + }) + .unwrap(); + + compositor::add_commit_hook(&surface, |surface| { + compositor::with_states(surface, |states| { + let mut guard = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + + let pending = states.cached_state.pending::(); + + if pending.size.w == 0 && !pending.anchor.anchored_horizontally() { + guard.surface.as_ref().post_error( + zwlr_layer_surface_v1::Error::InvalidSize as u32, + "width 0 requested without setting left and right anchors".into(), + ); + return; + } + + if pending.size.h == 0 && !pending.anchor.anchored_vertically() { + guard.surface.as_ref().post_error( + zwlr_layer_surface_v1::Error::InvalidSize as u32, + "height 0 requested without setting top and bottom anchors".into(), + ); + return; + } + + if let Some(state) = guard.last_acked.clone() { + guard.current = state; + } + }) + .unwrap(); + }); + + id.quick_assign(|surface, req, dispatch_data| { + layer_surface_implementation(surface.deref().clone(), req, dispatch_data) + }); + + id.assign_destructor(Filter::new( + |layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, _, _| { + let data = layer_surface + .as_ref() + .user_data() + .get::() + .unwrap(); + + // remove this surface from the known ones (as well as any leftover dead surface) + data.shell_data + .shell_state + .lock() + .unwrap() + .known_layers + .retain(|other| other.alive()); + }, + )); + + id.as_ref().user_data().set(|| LayerSurfaceUserData { + shell_data: data.clone(), + wl_surface: surface.clone(), + }); + + let handle = super::LayerSurface { + wl_surface: surface, + shell_surface: id.deref().clone(), + }; + + data.shell_state.lock().unwrap().known_layers.push(handle.clone()); + + let mut user_impl = data.user_impl.borrow_mut(); + (&mut *user_impl)( + LayerShellRequest::NewLayerSurface { + surface: handle, + output, + layer, + namespace, + }, + dispatch_data, + ); + } + zwlr_layer_shell_v1::Request::Destroy => { + // Handled by destructor + } + _ => {} + } +} + +fn layer_surface_implementation( + layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + request: zwlr_layer_surface_v1::Request, + dispatch_data: DispatchData<'_>, +) { + match request { + zwlr_layer_surface_v1::Request::SetSize { width, height } => { + with_surface_pending_state(&layer_surface, |data| { + data.size = (width as i32, height as i32).into(); + }); + } + zwlr_layer_surface_v1::Request::SetAnchor { anchor } => { + match Anchor::try_from(anchor) { + Ok(anchor) => { + with_surface_pending_state(&layer_surface, |data| { + data.anchor = Anchor::from_bits(anchor.bits()).unwrap_or_default(); + }); + } + Err((err, msg)) => { + layer_surface.as_ref().post_error(err as u32, msg); + } + }; + } + zwlr_layer_surface_v1::Request::SetExclusiveZone { zone } => { + with_surface_pending_state(&layer_surface, |data| { + data.exclusive_zone = zone.into(); + }); + } + zwlr_layer_surface_v1::Request::SetMargin { + top, + right, + bottom, + left, + } => { + with_surface_pending_state(&layer_surface, |data| { + data.margin = Margins { + top, + right, + bottom, + left, + }; + }); + } + zwlr_layer_surface_v1::Request::SetKeyboardInteractivity { + keyboard_interactivity, + } => { + match KeyboardInteractivity::try_from(keyboard_interactivity) { + Ok(keyboard_interactivity) => { + with_surface_pending_state(&layer_surface, |data| { + data.keyboard_interactivity = keyboard_interactivity; + }); + } + Err((err, msg)) => { + layer_surface.as_ref().post_error(err as u32, msg); + } + }; + } + zwlr_layer_surface_v1::Request::SetLayer { layer } => { + match Layer::try_from(layer) { + Ok(layer) => { + with_surface_pending_state(&layer_surface, |data| { + data.layer = layer; + }); + } + Err((err, msg)) => { + layer_surface.as_ref().post_error(err as u32, msg); + } + }; + } + zwlr_layer_surface_v1::Request::GetPopup { popup } => { + let data = layer_surface + .as_ref() + .user_data() + .get::() + .unwrap(); + + let parent_surface = data.wl_surface.clone(); + + let data = popup + .as_ref() + .user_data() + .get::() + .unwrap(); + + compositor::with_states(&data.wl_surface, move |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .parent = Some(parent_surface); + }) + .unwrap(); + } + zwlr_layer_surface_v1::Request::AckConfigure { serial } => { + let data = layer_surface + .as_ref() + .user_data() + .get::() + .unwrap(); + + let serial = Serial::from(serial); + let surface = &data.wl_surface; + + let found_configure = compositor::with_states(surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .ack_configure(serial) + }) + .unwrap(); + + let configure = match found_configure { + Some(configure) => configure, + None => { + layer_surface.as_ref().post_error( + zwlr_layer_surface_v1::Error::InvalidSurfaceState as u32, + format!("wrong configure serial: {}", ::from(serial)), + ); + return; + } + }; + + let mut user_impl = data.shell_data.user_impl.borrow_mut(); + (&mut *user_impl)( + LayerShellRequest::AckConfigure { + surface: data.wl_surface.clone(), + configure, + }, + dispatch_data, + ); + } + _ => {} + } +} + +struct LayerSurfaceUserData { + shell_data: ShellUserData, + wl_surface: wl_surface::WlSurface, +} + +fn with_surface_pending_state(layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, f: F) -> T +where + F: FnOnce(&mut LayerSurfaceCachedState) -> T, +{ + let data = layer_surface + .as_ref() + .user_data() + .get::() + .unwrap(); + compositor::with_states(&data.wl_surface, |states| { + f(&mut *states.cached_state.pending::()) + }) + .unwrap() +} diff --git a/src/wayland/shell/wlr_layer/mod.rs b/src/wayland/shell/wlr_layer/mod.rs new file mode 100644 index 0000000..6ac7a30 --- /dev/null +++ b/src/wayland/shell/wlr_layer/mod.rs @@ -0,0 +1,454 @@ +//! Utilities for handling shell surfaces with the `wlr_layer_shell` protocol +//! +//! This interface should be suitable for the implementation of many desktop shell components, +//! and a broad number of other applications that interact with the desktop. +//! +//! ### Initialization +//! +//! To initialize this handler, simple use the [`wlr_layer_shell_init`] function provided in this module. +//! You need to provide a closure that will be invoked whenever some action is required from you, +//! are represented by the [`LayerShellRequest`] enum. +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use smithay::wayland::shell::wlr_layer::{wlr_layer_shell_init, LayerShellRequest}; +//! +//! # let mut display = wayland_server::Display::new(); +//! let (shell_state, _) = wlr_layer_shell_init( +//! &mut display, +//! // your implementation +//! |event: LayerShellRequest, dispatch_data| { /* handle the shell requests here */ }, +//! None // put a logger if you want +//! ); +//! +//! // You're now ready to go! +//! ``` + +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, Mutex}, +}; + +use wayland_protocols::wlr::unstable::layer_shell::v1::server::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; +use wayland_server::{ + protocol::{wl_output::WlOutput, wl_surface}, + DispatchData, Display, Filter, Global, Main, +}; + +use crate::{ + utils::{DeadResource, Logical, Size}, + wayland::{ + compositor::{self, Cacheable}, + Serial, SERIAL_COUNTER, + }, +}; + +mod handlers; +mod types; + +pub use types::{Anchor, ExclusiveZone, KeyboardInteractivity, Layer, Margins}; + +/// The role of a wlr_layer_shell_surface +pub const LAYER_SURFACE_ROLE: &str = "zwlr_layer_surface_v1"; + +/// Attributes for layer surface +#[derive(Debug)] +pub struct LayerSurfaceAttributes { + surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + /// Defines if the surface has received at least one + /// layer_surface.ack_configure from the client + pub configured: bool, + /// The serial of the last acked configure + pub configure_serial: Option, + /// Holds the state if the surface has sent the initial + /// configure event to the client. It is expected that + /// during the first commit a initial + /// configure event is sent to the client + pub initial_configure_sent: bool, + /// Holds the configures the server has sent out + /// to the client waiting to be acknowledged by + /// the client. All pending configures that are older + /// than the acknowledged one will be discarded during + /// processing layer_surface.ack_configure. + pending_configures: Vec, + /// Holds the pending state as set by the server. + pub server_pending: Option, + /// Holds the last server_pending state that has been acknowledged + /// by the client. This state should be cloned to the current + /// during a commit. + pub last_acked: Option, + /// Holds the current state of the layer after a successful + /// commit. + pub current: LayerSurfaceState, +} + +impl LayerSurfaceAttributes { + fn new(surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1) -> Self { + Self { + surface, + configured: false, + configure_serial: None, + initial_configure_sent: false, + pending_configures: Vec::new(), + server_pending: None, + last_acked: None, + current: Default::default(), + } + } + + fn ack_configure(&mut self, serial: Serial) -> Option { + let configure = self + .pending_configures + .iter() + .find(|configure| configure.serial == serial) + .cloned()?; + + self.last_acked = Some(configure.state.clone()); + + self.configured = true; + self.configure_serial = Some(serial); + self.pending_configures.retain(|c| c.serial > serial); + Some(configure) + } +} + +/// State of a layer surface +#[derive(Debug, Default, Clone, PartialEq)] +pub struct LayerSurfaceState { + /// The suggested size of the surface + pub size: Option>, +} + +/// Represents the client pending state +#[derive(Debug, Default, Clone, Copy)] +pub struct LayerSurfaceCachedState { + /// The size requested by the client + pub size: Size, + /// Anchor bitflags, describing how the layers surface should be positioned and sized + pub anchor: Anchor, + /// Descripton of exclusive zone + pub exclusive_zone: ExclusiveZone, + /// Describes distance from the anchor point of the output + pub margin: Margins, + /// Describes how keyboard events are delivered to this surface + pub keyboard_interactivity: KeyboardInteractivity, + /// The layer that the surface is rendered on + pub layer: Layer, +} + +impl Cacheable for LayerSurfaceCachedState { + fn commit(&mut self) -> Self { + *self + } + fn merge_into(self, into: &mut Self) { + *into = self; + } +} + +/// Shell global state +/// +/// This state allows you to retrieve a list of surfaces +/// currently known to the shell global. +#[derive(Debug)] +pub struct LayerShellState { + known_layers: Vec, +} + +impl LayerShellState { + /// Access all the shell surfaces known by this handler + pub fn layer_surfaces(&self) -> &[LayerSurface] { + &self.known_layers[..] + } +} + +#[derive(Clone)] +struct ShellUserData { + log: ::slog::Logger, + user_impl: Rc)>>, + shell_state: Arc>, +} + +/// Create a new `wlr_layer_shell` globals +pub fn wlr_layer_shell_init( + display: &mut Display, + implementation: Impl, + logger: L, +) -> ( + Arc>, + Global, +) +where + L: Into>, + Impl: FnMut(LayerShellRequest, DispatchData<'_>) + 'static, +{ + let log = crate::slog_or_fallback(logger); + let shell_state = Arc::new(Mutex::new(LayerShellState { + known_layers: Vec::new(), + })); + + let shell_data = ShellUserData { + log: log.new(slog::o!("smithay_module" => "layer_shell_handler")), + user_impl: Rc::new(RefCell::new(implementation)), + shell_state: shell_state.clone(), + }; + + let layer_shell_global = display.create_global( + 4, + Filter::new( + move |(shell, _version): (Main, _), _, _ddata| { + shell.quick_assign(self::handlers::layer_shell_implementation); + shell.as_ref().user_data().set({ + let shell_data = shell_data.clone(); + move || shell_data + }); + }, + ), + ); + + (shell_state, layer_shell_global) +} + +/// A handle to a layer surface +#[derive(Debug, Clone)] +pub struct LayerSurface { + wl_surface: wl_surface::WlSurface, + shell_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, +} + +impl std::cmp::PartialEq for LayerSurface { + fn eq(&self, other: &Self) -> bool { + self.alive() && other.alive() && self.wl_surface == other.wl_surface + } +} + +impl LayerSurface { + /// Is the layer surface referred by this handle still alive? + pub fn alive(&self) -> bool { + self.shell_surface.as_ref().is_alive() && self.wl_surface.as_ref().is_alive() + } + + /// Gets the current pending state for a configure + /// + /// Returns `Some` if either no initial configure has been sent or + /// the `server_pending` is `Some` and different from the last pending + /// configure or `last_acked` if there is no pending + /// + /// Returns `None` if either no `server_pending` or the pending + /// has already been sent to the client or the pending is equal + /// to the `last_acked` + fn get_pending_state(&self, attributes: &mut LayerSurfaceAttributes) -> Option { + if !attributes.initial_configure_sent { + return Some(attributes.server_pending.take().unwrap_or_default()); + } + + let server_pending = match attributes.server_pending.take() { + Some(state) => state, + None => { + return None; + } + }; + + let last_state = attributes + .pending_configures + .last() + .map(|c| &c.state) + .or_else(|| attributes.last_acked.as_ref()); + + if let Some(state) = last_state { + if state == &server_pending { + return None; + } + } + + Some(server_pending) + } + + /// Send a configure event to this layer surface to suggest it a new configuration + /// + /// The serial of this configure will be tracked waiting for the client to ACK it. + /// + /// You can manipulate the state that will be sent to the client with the [`with_pending_state`](#method.with_pending_state) + /// method. + pub fn send_configure(&self) { + if let Some(surface) = self.get_surface() { + let configure = compositor::with_states(surface, |states| { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + if let Some(pending) = self.get_pending_state(&mut *attributes) { + let configure = LayerSurfaceConfigure { + serial: SERIAL_COUNTER.next_serial(), + state: pending, + }; + + attributes.pending_configures.push(configure.clone()); + attributes.initial_configure_sent = true; + + Some(configure) + } else { + None + } + }) + .unwrap_or(None); + + // send surface configure + if let Some(configure) = configure { + let (width, height) = configure.state.size.unwrap_or_default().into(); + let serial = configure.serial; + self.shell_surface + .configure(serial.into(), width as u32, height as u32); + } + } + } + + /// Make sure this surface was configured + /// + /// Returns `true` if it was, if not, returns `false` and raise + /// a protocol error to the associated layer surface. Also returns `false` + /// if the surface is already destroyed. + pub fn ensure_configured(&self) -> bool { + if !self.alive() { + return false; + } + let configured = compositor::with_states(&self.wl_surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .configured + }) + .unwrap(); + if !configured { + self.shell_surface.as_ref().post_error( + zwlr_layer_shell_v1::Error::AlreadyConstructed as u32, + "layer_surface has never been configured".into(), + ); + } + configured + } + + /// Send a "close" event to the client + pub fn send_close(&self) { + self.shell_surface.closed() + } + + /// Access the underlying `wl_surface` of this layer surface + /// + /// Returns `None` if the layer surface actually no longer exists. + pub fn get_surface(&self) -> Option<&wl_surface::WlSurface> { + if self.alive() { + Some(&self.wl_surface) + } else { + None + } + } + + /// Allows the pending state of this layer to + /// be manipulated. + /// + /// This should be used to inform the client about size and state changes, + /// for example after a resize request from the client. + /// + /// The state will be sent to the client when calling [`send_configure`](#method.send_configure). + pub fn with_pending_state(&self, f: F) -> Result + where + F: FnOnce(&mut LayerSurfaceState) -> T, + { + if !self.alive() { + return Err(DeadResource); + } + + Ok(compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + if attributes.server_pending.is_none() { + attributes.server_pending = Some(attributes.current.clone()); + } + + let server_pending = attributes.server_pending.as_mut().unwrap(); + f(server_pending) + }) + .unwrap()) + } + + /// Gets a copy of the current state of this layer + /// + /// Returns `None` if the underlying surface has been + /// destroyed + pub fn current_state(&self) -> Option { + if !self.alive() { + return None; + } + + Some( + compositor::with_states(&self.wl_surface, |states| { + let attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + + attributes.current.clone() + }) + .unwrap(), + ) + } +} + +/// A configure message for layer surfaces +#[derive(Debug, Clone)] +pub struct LayerSurfaceConfigure { + /// The state associated with this configure + pub state: LayerSurfaceState, + + /// A serial number to track ACK from the client + /// + /// This should be an ever increasing number, as the ACK-ing + /// from a client for a serial will validate all pending lower + /// serials. + pub serial: Serial, +} + +/// Events generated by layer shell surfaces +/// +/// Depending on what you want to do, you might ignore some of them +#[derive(Debug)] +pub enum LayerShellRequest { + /// A new layer surface was created + /// + /// You likely need to send a [`LayerSurfaceConfigure`] to the surface, to hint the + /// client as to how its layer surface should be sized. + NewLayerSurface { + /// the surface + surface: LayerSurface, + /// The output that the layer will be displayed on + /// + /// None means that the compositor should decide which output to use, + /// Generally this will be the one that the user most recently interacted with + output: Option, + /// This values indicate on which layer a surface should be rendered on + layer: Layer, + /// namespace that defines the purpose of the layer surface + namespace: String, + }, + + /// A surface has acknowledged a configure serial. + AckConfigure { + /// The surface. + surface: wl_surface::WlSurface, + /// The configure serial. + configure: LayerSurfaceConfigure, + }, +} diff --git a/src/wayland/shell/wlr_layer/types.rs b/src/wayland/shell/wlr_layer/types.rs new file mode 100644 index 0000000..2fe383c --- /dev/null +++ b/src/wayland/shell/wlr_layer/types.rs @@ -0,0 +1,231 @@ +use std::{cmp::Ordering, convert::TryFrom}; + +use wayland_protocols::wlr::unstable::layer_shell::v1::server::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; + +/// Available layers for surfaces +/// +/// These values indicate which layers a surface can be rendered in. +/// They are ordered by z depth, bottom-most first. +/// Traditional shell surfaces will typically be rendered between the bottom and top layers. +/// Fullscreen shell surfaces are typically rendered at the top layer. +/// Multiple surfaces can share a single layer, and ordering within a single layer is undefined. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layer { + /// The lowest layer, used usualy for wallpapers + Background, + /// The layer bellow the windows and above the wallpaper + Bottom, + /// The layer above the windows and bellow overlay + Top, + /// The top layer above all other layers + Overlay, +} + +impl TryFrom for Layer { + type Error = (zwlr_layer_shell_v1::Error, String); + + fn try_from(layer: zwlr_layer_shell_v1::Layer) -> Result { + match layer { + zwlr_layer_shell_v1::Layer::Background => Ok(Self::Background), + zwlr_layer_shell_v1::Layer::Bottom => Ok(Self::Bottom), + zwlr_layer_shell_v1::Layer::Top => Ok(Self::Top), + zwlr_layer_shell_v1::Layer::Overlay => Ok(Self::Overlay), + _ => Err(( + zwlr_layer_shell_v1::Error::InvalidLayer, + format!("invalid layer: {:?}", layer), + )), + } + } +} + +impl Default for Layer { + fn default() -> Self { + Self::Background + } +} + +/// Types of keyboard interaction possible for a layer shell surface +/// +/// The rationale for this is twofold: +/// - some applications are not interested in keyboard events +/// and not allowing them to be focused can improve the desktop experience +/// - some applications will want to take exclusive keyboard focus. +#[derive(Debug, Clone, Copy)] +pub enum KeyboardInteractivity { + /// This value indicates that this surface is not interested in keyboard events + /// and the compositor should never assign it the keyboard focus. + /// + /// This is the default value, set for newly created layer shell surfaces. + /// + /// This is useful for e.g. desktop widgets that display information + /// or only have interaction with non-keyboard input devices. + None, + /// Request exclusive keyboard focus if this surface is above the shell surface layer. + /// + /// For the top and overlay layers, the seat will always give exclusive keyboard focus + /// to the top-most layer which has keyboard interactivity set to exclusive. + /// If this layer contains multiple surfaces with keyboard interactivity set to exclusive, + /// the compositor determines the one receiving keyboard events in an implementation- defined manner. + /// In this case, no guarantee is made when this surface will receive keyboard focus (if ever). + /// + /// For the bottom and background layers, the compositor is allowed to use normal focus semantics. + /// + /// This setting is mainly intended for applications that need to + /// ensure they receive all keyboard events, such as a lock screen or a password prompt. + Exclusive, + /// This requests the compositor to allow this surface + /// to be focused and unfocused by the user in an implementation-defined manner. + /// The user should be able to unfocus this surface even regardless of the layer it is on. + /// + /// Typically, the compositor will want to use its normal mechanism + /// to manage keyboard focus between layer shell surfaces + /// with this setting and regular toplevels on the desktop layer (e.g. click to focus). + /// Nevertheless, it is possible for a compositor to require a special interaction + /// to focus or unfocus layer shell surfaces + /// + /// This setting is mainly intended for desktop shell components that allow keyboard interaction. + /// Using this option can allow implementing a desktop shell that can be fully usable without the mouse. + OnDemand, +} + +impl Default for KeyboardInteractivity { + fn default() -> Self { + Self::None + } +} + +impl TryFrom for KeyboardInteractivity { + type Error = (zwlr_layer_surface_v1::Error, String); + + fn try_from(ki: zwlr_layer_surface_v1::KeyboardInteractivity) -> Result { + match ki { + zwlr_layer_surface_v1::KeyboardInteractivity::None => Ok(Self::None), + zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive => Ok(Self::Exclusive), + zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand => Ok(Self::OnDemand), + _ => Err(( + zwlr_layer_surface_v1::Error::InvalidKeyboardInteractivity, + format!("wrong keyboard interactivity value: {:?}", ki), + )), + } + } +} + +bitflags::bitflags! { + /// Anchor bitflags, describing how the layers surface should be positioned and sized + pub struct Anchor: u32 { + /// The top edge of the anchor rectangle + const TOP = 1; + /// The bottom edge of the anchor rectangle + const BOTTOM = 2; + /// The left edge of the anchor rectangle + const LEFT = 4; + /// The right edge of the anchor rectangle + const RIGHT = 8; + } +} + +impl Anchor { + /// Check if anchored horizontally + /// + /// If it is anchored to `left` and `right` anchor at the same time + /// it returns `true` + pub fn anchored_horizontally(&self) -> bool { + self.contains(Self::LEFT) && self.contains(Self::RIGHT) + } + /// Check if anchored vertically + /// + /// If it is anchored to `top` and `bottom` anchor at the same time + /// it returns `true` + pub fn anchored_vertically(&self) -> bool { + self.contains(Self::TOP) && self.contains(Self::BOTTOM) + } +} + +impl Default for Anchor { + fn default() -> Self { + Self::empty() + } +} + +impl TryFrom for Anchor { + type Error = (zwlr_layer_surface_v1::Error, String); + + fn try_from(anchor: zwlr_layer_surface_v1::Anchor) -> Result { + match Anchor::from_bits(anchor.bits()) { + Some(a) => Ok(a), + _ => Err(( + zwlr_layer_surface_v1::Error::InvalidAnchor, + format!("invalid anchor {:?}", anchor), + )), + } + } +} + +/// Exclusive zone descriptor +#[derive(Debug, Clone, Copy)] +pub enum ExclusiveZone { + /// Requests that the compositor avoids occluding an area with other surfaces. + /// + /// A exclusive zone value is the distance from the edge in surface-local coordinates to consider exclusive. + /// + /// A exclusive value is only meaningful if the surface is + /// anchored to one edge or an edge and both perpendicular edges. + /// + /// If the surface is: + /// - not anchored + /// - anchored to only two perpendicular edges (a corner), + /// - anchored to only two parallel edges or anchored to all edges, + /// + /// The exclusive value should be treated the same as [`ExclusiveZone::Neutral`]. + Exclusive(u32), + /// If set to Neutral, + /// the surface indicates that it would like to be moved to avoid occluding surfaces with a exclusive zone. + Neutral, + /// If set to DontCare, + /// the surface indicates that it would not like to be moved to accommodate for other surfaces, + /// and the compositor should extend it all the way to the edges it is anchored to. + DontCare, +} + +impl Default for ExclusiveZone { + fn default() -> Self { + Self::Neutral + } +} + +impl From for ExclusiveZone { + fn from(v: i32) -> Self { + match v.cmp(&0) { + Ordering::Greater => Self::Exclusive(v as u32), + Ordering::Equal => Self::Neutral, + Ordering::Less => Self::DontCare, + } + } +} + +impl From for i32 { + fn from(z: ExclusiveZone) -> i32 { + match z { + ExclusiveZone::Exclusive(v) => v as i32, + ExclusiveZone::Neutral => 0, + ExclusiveZone::DontCare => -1, + } + } +} + +/// Describes distance from the anchor point of the output, in surface-local coordinates. +/// +/// If surface did not anchor curtain edge, margin for that edge should be ignored. +/// +/// The exclusive zone should includes the margins. +#[derive(Debug, Default, Clone, Copy)] +pub struct Margins { + /// Distance from [`Anchor::TOP`] + pub top: i32, + /// Distance from [`Anchor::TOP`] + pub right: i32, + /// Distance from [`Anchor::BOTTOM`] + pub bottom: i32, + /// Distance from [`Anchor::LEFT`] + pub left: i32, +} diff --git a/src/wayland/shell/xdg/mod.rs b/src/wayland/shell/xdg/mod.rs index b5e2bd0..6ddc174 100644 --- a/src/wayland/shell/xdg/mod.rs +++ b/src/wayland/shell/xdg/mod.rs @@ -89,7 +89,7 @@ use wayland_server::{ use super::PingError; // handlers for the xdg_shell protocol -mod xdg_handlers; +pub(super) mod xdg_handlers; // compatibility handlers for the zxdg_shell_v6 protocol, its earlier version mod zxdgv6_handlers;