diff --git a/Cargo.lock b/Cargo.lock index 9893e62..62f22d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,28 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -23,12 +45,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "bindgen" version = "0.72.1" @@ -61,6 +101,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + [[package]] name = "cc" version = "1.2.24" @@ -96,6 +142,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -218,12 +273,47 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown 0.15.5", + "ttf-parser 0.21.1", +] + [[package]] name = "glob" version = "0.3.2" @@ -244,6 +334,11 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hecs" @@ -379,6 +474,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "nom" version = "7.1.3" @@ -432,12 +537,34 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser 0.25.1", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -598,6 +725,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "smallvec" version = "1.15.0" @@ -635,6 +768,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.11.1" @@ -725,6 +864,32 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -742,6 +907,18 @@ dependencies = [ "winnow", ] +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1135,8 +1312,10 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" name = "xwayland-satellite" version = "0.7.0" dependencies = [ + "ab_glyph", "anyhow", "bitflags 2.9.1", + "fontdue", "hecs", "log", "macros", @@ -1146,6 +1325,7 @@ dependencies = [ "sd-notify", "smithay-client-toolkit", "testwl", + "tiny-skia", "vergen-gitcl", "wayland-client", "wayland-protocols", diff --git a/Cargo.toml b/Cargo.toml index f81a41a..eead711 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,9 @@ sd-notify = { version = "0.4.2", optional = true } macros = { version = "0.1.0", path = "macros" } hecs = { version = "0.10.5", features = ["macros"] } num_enum = "0.7.4" +tiny-skia = "0.11.4" +ab_glyph = "0.2.32" +fontdue = "0.9.3" [features] default = [] diff --git a/OpenSans-Regular.ttf b/OpenSans-Regular.ttf new file mode 100644 index 0000000..134d225 Binary files /dev/null and b/OpenSans-Regular.ttf differ diff --git a/src/server/clientside.rs b/src/server/clientside.rs index f6aecc6..a3d7eb3 100644 --- a/src/server/clientside.rs +++ b/src/server/clientside.rs @@ -1,3 +1,5 @@ +use super::decoration::DecorationMarker; + use super::ObjectEvent; use hecs::{Entity, World}; use smithay_client_toolkit::{ @@ -19,7 +21,8 @@ use wayland_client::protocol::{ wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_region::WlRegion, wl_registry::WlRegistry, wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, - wl_surface::WlSurface, wl_touch::WlTouch, + wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface, + wl_touch::WlTouch, }; use wayland_client::{ delegate_noop, event_created_child, @@ -164,6 +167,7 @@ impl MyWorld { pub type Event = ::Event; delegate_noop!(MyWorld: WlCompositor); +delegate_noop!(MyWorld: WlSubcompositor); delegate_noop!(MyWorld: WlRegion); delegate_noop!(MyWorld: ignore WlShm); delegate_noop!(MyWorld: ignore ZwpLinuxDmabufV1); @@ -179,8 +183,8 @@ delegate_noop!(MyWorld: ZwpTabletManagerV2); delegate_noop!(MyWorld: XdgActivationV1); delegate_noop!(MyWorld: ZxdgDecorationManagerV1); delegate_noop!(MyWorld: WpFractionalScaleManagerV1); -delegate_noop!(MyWorld: ignore ZxdgToplevelDecorationV1); delegate_noop!(MyWorld: ZwpPrimarySelectionDeviceManagerV1); +delegate_noop!(MyWorld: WlSubsurface); impl Dispatch for MyWorld { fn event( @@ -236,6 +240,18 @@ impl Dispatch for MyWorld { } } +impl Dispatch for MyWorld { + fn event( + _: &mut Self, + _: &WlSurface, + _: ::Event, + _: &DecorationMarker, + _: &Connection, + _: &QueueHandle, + ) { + } +} + macro_rules! push_events { ($type:ident) => { impl Dispatch<$type, Entity> for MyWorld { @@ -270,6 +286,7 @@ push_events!(WlTouch); push_events!(ZwpConfinedPointerV1); push_events!(ZwpLockedPointerV1); push_events!(WpFractionalScaleV1); +push_events!(ZxdgToplevelDecorationV1); pub(crate) struct LateInitObjectKey { key: OnceLock, diff --git a/src/server/decoration.rs b/src/server/decoration.rs new file mode 100644 index 0000000..393e75b --- /dev/null +++ b/src/server/decoration.rs @@ -0,0 +1,480 @@ +use crate::server::{InnerServerState, ServerState, SurfaceRole}; +use crate::{X11Selection, XConnection}; + +use ab_glyph::{Font, FontRef, Glyph, PxScaleFont, ScaleFont}; +use hecs::{CommandBuffer, Entity, World}; +use log::{error, warn}; +use smithay_client_toolkit::registry::SimpleGlobal; +use smithay_client_toolkit::shm::slot::SlotPool; +use std::sync::LazyLock; +use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Stroke, Transform}; +use tiny_skia::{ColorU8, Rect}; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::protocol::wl_shm; +use wayland_client::protocol::wl_subsurface::WlSubsurface; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::Proxy; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1; +use wayland_protocols::xdg::shell::client::xdg_toplevel::XdgToplevel; +use xcb::x; + +#[derive(Debug)] +pub struct DecorationsData { + pub wl: Option, + // Boxed to avoid making ToplevelData so much bigger than PopupData + pub satellite: Option>, +} + +pub struct DecorationMarker { + pub parent: Entity, +} + +#[derive(Debug)] +pub struct DecorationsDataSatellite { + surface: WlSurface, + subsurface: WlSubsurface, + pool: Entity, + viewport: WpViewport, + scale: f32, + pixmap: Pixmap, + x_data: DecorationsBox, + title: Option, + title_rect: Rect, +} + +impl Drop for DecorationsDataSatellite { + fn drop(&mut self) { + self.subsurface.destroy(); + self.surface.destroy(); + self.viewport.destroy(); + } +} + +impl DecorationsDataSatellite { + pub const TITLEBAR_HEIGHT: i32 = 25; + + pub fn try_new( + state: &InnerServerState, + parent: &WlSurface, + title: Option<&str>, + ) -> Option<(Box, Option)> { + let mut new_pool = None; + let mut query = state.world.query::<&SlotPool>(); + let pool_entity = if let Some((pool_entity, _)) = query.into_iter().next() { + pool_entity + } else { + new_pool = Some( + SlotPool::new(1, &SimpleGlobal::from_bound(state.shm.clone())) + .inspect_err(|e| { + warn!("Couldn't create slot pool for decorations: {e:?}"); + }) + .ok()?, + ); + state.world.reserve_entity() + }; + + let surface = state.compositor.create_surface( + &state.qh, + DecorationMarker { + parent: parent.data().copied().unwrap(), + }, + ); + let subsurface = { + state + .subcompositor + .get_subsurface(&surface, parent, &state.qh, ()) + }; + subsurface.set_desync(); + subsurface.set_position(0, -Self::TITLEBAR_HEIGHT); + let viewport = state.viewporter.get_viewport(&surface, &state.qh, ()); + + Some(( + Self { + surface, + subsurface, + pool: pool_entity, + viewport, + x_data: DecorationsBox::default(), + pixmap: Pixmap::new(1, 1).unwrap(), + scale: 1.0, + title: title.map(str::to_string), + title_rect: Rect::from_ltrb(0.0, 0.0, 0.0, 0.0).unwrap(), + } + .into(), + new_pool.map(|p| { + let mut buf = CommandBuffer::new(); + buf.insert_one(pool_entity, p); + buf + }), + )) + } + + fn pool<'a>(&self, world: &'a World) -> hecs::RefMut<'a, SlotPool> { + world.get::<&mut SlotPool>(self.pool).unwrap() + } + + fn update_buffer(&mut self, world: &World) { + let mut pool = self.pool(world); + let (buffer, data) = match pool.create_buffer( + self.pixmap.width() as i32, + self.pixmap.height() as i32, + self.pixmap.width() as i32 * 4, + wl_shm::Format::Xrgb8888, + ) { + Ok(b) => b, + Err(err) => { + error!("Failed to create buffer for decorations: {err:?}"); + return; + } + }; + + draw_pixmap_to_buffer(&self.pixmap, data); + buffer.attach_to(&self.surface).unwrap(); + self.surface.commit(); + } + + pub fn draw_decorations(&mut self, world: &World, width: i32, parent_scale_factor: f32) { + if width <= 0 { + return; + } + + self.scale = parent_scale_factor; + let mut drawn_width = (width as f32 * self.scale).ceil() as i32; + let drawn_height = (Self::TITLEBAR_HEIGHT as f32 * self.scale).ceil() as i32; + + let x = x_pixmap(drawn_height as u32, self.scale, self.x_data.hovered); + if x.width() > drawn_width as u32 { + drawn_width = x.width() as i32; + } + + let title = self.title.as_ref().and_then(|t| { + let width = (drawn_width as u32).saturating_sub(x.width()); + if width > 0 { + title_pixmap(t, width, drawn_height as u32, self.scale) + } else { + None + } + }); + + // Draw the bar and its components + let mut bar = Pixmap::new(drawn_width as u32, drawn_height as u32).unwrap(); + bar.fill(Color::WHITE); + + if let Some(title) = title { + bar.draw_pixmap( + 0, + 0, + title.as_ref(), + &Default::default(), + Transform::identity(), + None, + ); + self.title_rect = + Rect::from_xywh(0.0, 0.0, title.width() as f32, title.height() as f32).unwrap(); + } + + bar.draw_pixmap( + (bar.width() - x.width()) as i32, + 0, + x.as_ref(), + &Default::default(), + Transform::identity(), + None, + ); + self.x_data = DecorationsBox { + rect: Rect::from_ltrb( + width as f32 - Self::TITLEBAR_HEIGHT as f32, + 0.0, + width as f32, + Self::TITLEBAR_HEIGHT as f32, + ) + .unwrap(), + hovered: false, + }; + + self.pixmap = bar; + self.viewport.set_destination(width, Self::TITLEBAR_HEIGHT); + self.update_buffer(world); + } + + fn redraw_x_pixmap(&mut self, world: &World) { + let x = x_pixmap(self.pixmap.height(), self.scale, self.x_data.hovered); + + self.pixmap.draw_pixmap( + (self.pixmap.width() - x.width()) as i32, + 0, + x.as_ref(), + &Default::default(), + Transform::identity(), + None, + ); + + self.surface.damage_buffer( + (self.pixmap.width() - x.width()) as i32, + 0, + x.width() as i32, + x.height() as i32, + ); + self.update_buffer(world); + } + + pub fn set_title(&mut self, world: &World, title: &str) { + self.title = Some(title.to_string()); + + // Don't draw title if there's not enough space + let title_pixmap = title_pixmap( + title, + self.pixmap.width() - self.x_data.rect.width() as u32, + self.pixmap.height(), + self.scale, + ); + + let new_title_rect = title_pixmap + .as_ref() + .map(|p| Rect::from_xywh(0.0, 0.0, p.width() as f32, p.height() as f32).unwrap()) + .unwrap_or_else(|| Rect::from_ltrb(0.0, 0.0, 0.0, 0.0).unwrap()); + + let last_title_rect = std::mem::replace(&mut self.title_rect, new_title_rect); + + // Clear last title with white + let mut paint = Paint::default(); + paint.set_color(Color::WHITE); + self.pixmap + .fill_rect(last_title_rect, &paint, Transform::identity(), None); + + if let Some(p) = title_pixmap.as_ref() { + self.pixmap.draw_pixmap( + 0, + 0, + p.as_ref(), + &Default::default(), + Transform::identity(), + None, + ); + } + + let damaged_width = last_title_rect + .width() + .max(title_pixmap.map(|p| p.width() as f32).unwrap_or(0.0)); + + self.surface + .damage_buffer(0, 0, damaged_width as i32, last_title_rect.height() as i32); + self.update_buffer(world); + } + + fn handle_motion(&mut self, world: &World, x: f64, y: f64) { + if self.x_data.check_hovered(x as f32, y as f32) { + self.redraw_x_pixmap(world); + } + } + + fn handle_leave(&mut self, world: &World) { + if self.x_data.hovered { + self.x_data.hovered = false; + self.redraw_x_pixmap(world); + } + } + + /// Returns true if the toplevel should be closed + fn handle_click(&self, toplevel: &XdgToplevel, seat: &WlSeat, serial: u32) -> bool { + if self.x_data.hovered { + true + } else { + toplevel._move(seat, serial); + false + } + } +} + +#[derive(Debug)] +struct DecorationsBox { + rect: Rect, + hovered: bool, +} + +impl Default for DecorationsBox { + fn default() -> Self { + Self { + rect: Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(), + hovered: false, + } + } +} + +impl DecorationsBox { + /// Returns true if hover state changed. + fn check_hovered(&mut self, x: f32, y: f32) -> bool { + let old_hovered = self.hovered; + self.hovered = (self.rect.left()..=self.rect.right()).contains(&x) + && (self.rect.top()..=self.rect.bottom()).contains(&y); + + old_hovered != self.hovered + } +} + +fn draw_pixmap_to_buffer(pixmap: &Pixmap, buffer: &mut [u8]) { + // TODO: support big endian? + for (data, pixel) in buffer.chunks_exact_mut(4).zip(pixmap.pixels()) { + data[0] = pixel.blue(); + data[1] = pixel.green(); + data[2] = pixel.red(); + data[3] = pixel.alpha(); + } +} + +fn x_pixmap(bar_height: u32, scale: f32, hovered: bool) -> Pixmap { + let mut x = Pixmap::new(bar_height, bar_height).unwrap(); + if hovered { + x.fill(Color::from_rgba(1.0, 0.0, 0.0, 0.8).unwrap()); + } else { + x.fill(Color::WHITE); + } + let size = x.width() as f32; + let margin = 8.4 * scale; + + let mut line = PathBuilder::new(); + line.move_to(margin, margin); + line.line_to(size - margin, size - margin); + line.move_to(size - margin, margin); + line.line_to(margin, size - margin); + let line = line.finish().unwrap(); + x.stroke_path( + &line, + &Default::default(), + &Stroke { + width: scale + 0.5, + ..Default::default() + }, + Default::default(), + None, + ); + + x +} + +fn title_pixmap(title: &str, max_width: u32, height: u32, scale: f32) -> Option { + if title.is_empty() { + return None; + } + + let (glyphs, font) = layout_title_glyphs(title, max_width, height, scale); + + let width = glyphs + .last() + .map(|g| g.position.x + font.h_advance(g.id)) + .unwrap() + .ceil() as u32; + + let mut pixmap = Pixmap::new(width, height).unwrap(); + let data = pixmap.pixels_mut(); + + for glyph in glyphs { + if let Some(og) = font.outline_glyph(glyph) { + let bounds = og.px_bounds(); + og.draw(|x, y, coverage| { + let pixel_idx = + ((bounds.min.x as u32 + x) + (bounds.min.y as u32 + y) * width) as usize; + + data[pixel_idx] = + ColorU8::from_rgba(0, 0, 0, (coverage * 255.0) as u8).premultiply(); + }); + } + } + + Some(pixmap) +} + +fn layout_title_glyphs( + text: &str, + max_width: u32, + height: u32, + scale: f32, +) -> (Vec, PxScaleFont<&FontRef<'_>>) { + const TEXT_SIZE: f32 = 10.0; + const TEXT_MARGIN: f32 = 11.0; + static FONT: LazyLock> = LazyLock::new(|| { + FontRef::try_from_slice(include_bytes!("../../OpenSans-Regular.ttf")).unwrap() + }); + + let mut ret = Vec::::new(); + + let px_scale = FONT.pt_to_px_scale(TEXT_SIZE * scale).unwrap(); + let font = FONT.as_scaled(px_scale); + for c in text.chars() { + let mut glyph = font.scaled_glyph(c); + // This centers the glyphs vertically + glyph.position.y = (height as f32 / 2.0) - font.descent(); + if let Some(previous) = ret.last() { + glyph.position.x = previous.position.x + + font.h_advance(previous.id) + + font.kern(glyph.id, previous.id); + } else { + glyph.position.x = TEXT_MARGIN * scale; + } + if (glyph.position.x + font.h_advance(glyph.id)).ceil() as u32 > max_width { + break; + } + + ret.push(glyph); + } + + (ret, font) +} + +fn get_decoration( + world: &World, + parent: Entity, +) -> Option>> { + let role = world.get::<&mut SurfaceRole>(parent).ok()?; + Some(hecs::RefMut::map(role, |role| { + let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role else { + unreachable!() + }; + + toplevel.decoration.satellite.as_mut().unwrap() + })) +} + +pub fn handle_pointer_leave(state: &InnerServerState, parent: Entity) { + if let Some(mut decoration) = get_decoration(&state.world, parent) { + decoration.handle_leave(&state.world); + } +} + +pub fn handle_pointer_motion( + state: &InnerServerState, + parent: Entity, + surface_x: f64, + surface_y: f64, +) { + if let Some(mut decoration) = get_decoration(&state.world, parent) { + decoration.handle_motion(&state.world, surface_x, surface_y); + } +} + +pub fn handle_pointer_click( + state: &mut ServerState, + parent: Entity, + seat: &WlSeat, + serial: u32, +) { + let Ok(mut role) = state.world.get::<&mut SurfaceRole>(parent) else { + return; + }; + let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role else { + unreachable!() + }; + + if toplevel + .decoration + .satellite + .as_mut() + .unwrap() + .handle_click(&toplevel.toplevel, seat, serial) + { + let window = *state.world.get::<&x::Window>(parent).unwrap(); + drop(role); + state.close_x_window(window); + } +} diff --git a/src/server/event.rs b/src/server/event.rs index 215a4f3..ad85bde 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -1,7 +1,8 @@ use super::clientside::LateInitObjectKey; +use super::decoration::DecorationMarker; use super::*; -use hecs::CommandBuffer; -use log::{debug, trace, warn}; +use hecs::{CommandBuffer, World}; +use log::{debug, error, trace, warn}; use macros::simple_event_shunt; use std::os::fd::AsFd; use wayland_client::{protocol as client, Proxy}; @@ -35,6 +36,7 @@ use wayland_protocols::{ viewporter::client::wp_viewport::WpViewport, }, xdg::{ + decoration::zv1::client::zxdg_toplevel_decoration_v1, shell::client::{xdg_popup, xdg_surface, xdg_toplevel}, xdg_output::zv1::{ client::zxdg_output_v1, server::zxdg_output_v1::ZxdgOutputV1 as XdgOutputServer, @@ -64,6 +66,7 @@ pub(crate) enum SurfaceEvents { Toplevel(xdg_toplevel::Event), Popup(xdg_popup::Event), FractionalScale(wp_fractional_scale_v1::Event), + DecorationEvent(zxdg_toplevel_decoration_v1::Event), } macro_rules! impl_from { ($type:ty, $variant:ident) => { @@ -79,6 +82,7 @@ impl_from!(xdg_surface::Event, XdgSurface); impl_from!(xdg_toplevel::Event, Toplevel); impl_from!(xdg_popup::Event, Popup); impl_from!(wp_fractional_scale_v1::Event, FractionalScale); +impl_from!(zxdg_toplevel_decoration_v1::Event, DecorationEvent); impl Event for SurfaceEvents { fn handle(self, target: Entity, state: &mut ServerState) { @@ -114,6 +118,63 @@ impl Event for SurfaceEvents { } _ => unreachable!(), }, + SurfaceEvents::DecorationEvent(event) => { + use zxdg_toplevel_decoration_v1::{Event, Mode}; + let Event::Configure { mode } = event else { + error!("unhandled toplevel decoration event: {event:?}"); + return; + }; + + let entity = state.world.entity(target).unwrap(); + let Some(window_data) = entity.get::<&WindowData>() else { + return; + }; + let Ok(mode) = mode.into_result() else { + warn!("unknown decoration mode: {mode:?}"); + return; + }; + + let needs_server_side_decorations = window_data + .attrs + .decorations + .is_none_or(|d| d == Decorations::Server); + + if mode == Mode::ServerSide || !needs_server_side_decorations { + let mut role = entity.get::<&mut SurfaceRole>().unwrap(); + if let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role { + toplevel.decoration.satellite.take(); + } + return; + } + + let Some((sat_decoration, buf)) = entity + .get::<&client::wl_surface::WlSurface>() + .and_then(|surface| { + DecorationsDataSatellite::try_new( + state, + &surface, + window_data.attrs.title.as_ref().map(WmName::name), + ) + }) + else { + warn!("Needed to create decorations for window, but couldn't create them!"); + return; + }; + + let mut role = entity.get::<&mut SurfaceRole>().unwrap(); + // This should always be the case, but, you never know. + if let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role { + toplevel.decoration.satellite = Some(sat_decoration); + } else { + warn!("Created a decoration for a surface that isn't a toplevel?"); + } + + drop(window_data); + drop(role); + if let Some(mut buf) = buf { + buf.run_on(&mut state.world); + } + } } } } @@ -223,8 +284,13 @@ impl SurfaceEvents { drop(xdg); if let Some(pending) = pending { - let mut query = data.query::<(&SurfaceScaleFactor, &x::Window, &mut WindowData)>(); - let (scale_factor, window, window_data) = query.get().unwrap(); + let mut query = data.query::<( + &SurfaceScaleFactor, + &x::Window, + &mut WindowData, + &mut SurfaceRole, + )>(); + let (scale_factor, window, window_data, role) = query.get().unwrap(); let window = *window; let x = (pending.x.max(0) as f64 * scale_factor.0) as i32 + window_data.output_offset.x; @@ -234,7 +300,7 @@ impl SurfaceEvents { } else { window_data.attrs.dims.width }; - let height = if pending.height > 0 { + let mut height = if pending.height > 0 { (pending.height as f64 * scale_factor.0) as u16 } else { window_data.attrs.dims.height @@ -243,6 +309,20 @@ impl SurfaceEvents { "configuring {} ({window:?}): {x}x{y}, {width}x{height}", data.get::<&WlSurface>().unwrap().id(), ); + + if let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role { + if let Some(d) = &mut toplevel.decoration.satellite { + let surface_width = (width as f64 / scale_factor.0) as i32; + d.draw_decorations(&state.world, surface_width, scale_factor.0 as f32); + height = height + .saturating_sub( + (DecorationsDataSatellite::TITLEBAR_HEIGHT as f64 * scale_factor.0) + as u16, + ) + .max(DecorationsDataSatellite::TITLEBAR_HEIGHT as u16); + } + } + connection.set_window_dims( window, PendingSurfaceState { @@ -446,7 +526,16 @@ impl Event for client::wl_seat::Event { } struct PendingEnter(client::wl_pointer::Event); -struct CurrentSurface(Entity); +enum CurrentSurface { + Xwayland(Entity), + Decoration(Entity), +} + +impl CurrentSurface { + fn is_decoration(&self) -> bool { + matches!(self, Self::Decoration(..)) + } +} pub struct LastClickSerial(pub client::wl_seat::WlSeat, pub u32); impl Event for client::wl_pointer::Event { @@ -511,7 +600,6 @@ impl Event for client::wl_pointer::Event { let state = &mut state.inner; let mut cmd = CommandBuffer::new(); let pending_enter = state.world.remove_one::(target).ok(); - let server = state.world.get::<&WlPointer>(target).unwrap(); let surface_entity = surface.data().copied(); let mut query = surface_entity.and_then(|e| { state @@ -521,10 +609,20 @@ impl Event for client::wl_pointer::Event { }); let Some((surface, role, scale, window)) = query.as_mut().and_then(|q| q.get()) else { - warn!("could not enter surface: stale surface"); + if let Some(&DecorationMarker { parent }) = surface.data() { + drop(query); + state + .world + .insert_one(target, CurrentSurface::Decoration(parent)) + .unwrap(); + } else { + warn!("could not enter surface: stale surface"); + } + return; }; + let server = state.world.get::<&WlPointer>(target).unwrap(); cmd.insert(target, (*scale,)); let surface_is_popup = matches!(role, SurfaceRole::Popup(_)); @@ -535,7 +633,7 @@ impl Event for client::wl_pointer::Event { if !surface_is_popup { state.last_hovered = Some(*window); } - cmd.insert(target, (CurrentSurface(surface_entity.unwrap()),)); + cmd.insert_one(target, CurrentSurface::Xwayland(surface_entity.unwrap())); }; if !surface_is_popup { @@ -565,13 +663,18 @@ impl Event for client::wl_pointer::Event { drop(server); cmd.run_on(&mut state.world); } - client::wl_pointer::Event::Leave { serial, surface } => { + Self::Leave { serial, surface } => { let _ = state.world.remove_one::(target); - let _ = state.world.remove_one::(target); if !surface.is_alive() { return; } debug!("leaving surface ({serial})"); + if let Ok(CurrentSurface::Decoration(parent)) = + state.world.remove_one::(target) + { + decoration::handle_pointer_leave(state, parent); + return; + } if let Some(surface) = surface .data() .copied() @@ -586,7 +689,7 @@ impl Event for client::wl_pointer::Event { warn!("could not leave surface: stale surface"); } } - client::wl_pointer::Event::Motion { + Self::Motion { time, surface_x, surface_y, @@ -594,6 +697,13 @@ impl Event for client::wl_pointer::Event { if !handle_pending_enter(target, state, "motion") { return; } + { + let surface = state.world.get::<&CurrentSurface>(target).unwrap(); + if let CurrentSurface::Decoration(parent) = &*surface { + decoration::handle_pointer_motion(state, *parent, surface_x, surface_y); + return; + } + } let (server, scale) = state .world .query_one_mut::<(&WlPointer, &SurfaceScaleFactor)>(target) @@ -606,7 +716,7 @@ impl Event for client::wl_pointer::Event { ); server.motion(time, surface_x * scale.0, surface_y * scale.0); } - client::wl_pointer::Event::Button { + Self::Button { serial, time, button, @@ -616,13 +726,14 @@ impl Event for client::wl_pointer::Event { return; } let mut cmd = CommandBuffer::new(); - let (server, seat, CurrentSurface(surface)) = state + + let mut query = state .world - .query_one_mut::<(&WlPointer, &client::wl_seat::WlSeat, &CurrentSurface)>( - target, - ) + .query_one::<(&WlPointer, &client::wl_seat::WlSeat, &CurrentSurface)>(target) .unwrap(); + let (server, seat, current_surface) = query.get().unwrap(); + // from linux/input-event-codes.h mod button_codes { pub const LEFT: u32 = 0x110; @@ -631,14 +742,33 @@ impl Event for client::wl_pointer::Event { if button_state == WEnum::Value(client::wl_pointer::ButtonState::Pressed) && button == button_codes::LEFT { - cmd.insert(*surface, (LastClickSerial(seat.clone(), serial),)); + match current_surface { + CurrentSurface::Xwayland(entity) => { + cmd.insert(*entity, (LastClickSerial(seat.clone(), serial),)); + } + CurrentSurface::Decoration(parent) => { + let seat = seat.clone(); + let parent = *parent; + drop(query); + decoration::handle_pointer_click(state, parent, &seat, serial); + return; + } + } } server.button(serial, time, button, convert_wenum(button_state)); + drop(query); cmd.run_on(&mut state.world); } _ => { - let server = state.world.get::<&WlPointer>(target).unwrap(); + let (server, current_surface) = state + .world + .query_one_mut::<(&WlPointer, Option<&CurrentSurface>)>(target) + .unwrap(); + + if current_surface.is_some_and(CurrentSurface::is_decoration) { + return; + } simple_event_shunt! { server, self => [ Frame, diff --git a/src/server/mod.rs b/src/server/mod.rs index ce6af7b..73e8ddd 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,4 +1,5 @@ mod clientside; +mod decoration; mod dispatch; mod event; pub(crate) mod selection; @@ -9,7 +10,8 @@ use self::event::*; use crate::xstate::{Decorations, MoveResizeDirection, WindowDims, WmHints, WmName, WmNormalHints}; use crate::{timespec_from_millis, X11Selection, XConnection}; use clientside::MyWorld; -use hecs::{Entity, World}; +use decoration::{DecorationsData, DecorationsDataSatellite}; +use hecs::Entity; use log::{debug, warn}; use rustix::event::{poll, PollFd, PollFlags}; use smithay_client_toolkit::activation::ActivationState; @@ -17,14 +19,13 @@ use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; +use wayland_client::protocol::wl_subcompositor::WlSubcompositor; use wayland_client::{ globals::{registry_queue_init, Global}, protocol as client, Connection, EventQueue, Proxy, QueueHandle, }; use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; -use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::{ - self, ZxdgToplevelDecorationV1, -}; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::{self}; use wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment; use wayland_protocols::{ wp::{ @@ -194,7 +195,7 @@ impl SurfaceRole { fn destroy(&mut self) { match self { SurfaceRole::Toplevel(Some(ref mut t)) => { - if let Some(decoration) = t.decoration.take() { + if let Some(decoration) = t.decoration.wl.take() { decoration.destroy(); } t.toplevel.destroy(); @@ -222,7 +223,7 @@ struct ToplevelData { toplevel: XdgToplevel, xdg: XdgSurfaceData, fullscreen: bool, - decoration: Option, + decoration: decoration::DecorationsData, } #[derive(Debug)] @@ -447,6 +448,9 @@ pub struct InnerServerState { last_hovered: Option, xdg_wm_base: XdgWmBase, + compositor: client::wl_compositor::WlCompositor, + subcompositor: WlSubcompositor, + shm: client::wl_shm::WlShm, viewporter: WpViewporter, fractional_scale: Option, decoration_manager: Option, @@ -482,6 +486,18 @@ impl ServerState> { warn!("xdg_wm_base version 2 detected. Popup repositioning will not work, and some popups may not work correctly."); } + let compositor = global_list + .bind::(&qh, 4..=6, ()) + .expect("Could not bind wl_compositor"); + + let subcompositor = global_list + .bind::(&qh, 1..=1, ()) + .expect("Could not bind wl_subcompositor"); + + let shm = global_list + .bind::(&qh, 1..=1, ()) + .expect("Could not bind wl_shm"); + let viewporter = global_list .bind::(&qh, 1..=1, ()) .expect("Could not bind wp_viewporter"); @@ -511,6 +527,7 @@ impl ServerState> { .contents() .with_list(|globals| handle_globals::(&dh, globals)); + let world = MyWorld::new(global_list); let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap(); let inner = InnerServerState { @@ -525,6 +542,9 @@ impl ServerState> { last_focused_toplevel: None, last_hovered: None, xdg_wm_base, + compositor, + subcompositor, + shm, viewporter, fractional_scale, selection_states, @@ -544,7 +564,7 @@ impl ServerState> { updated_outputs: Vec::new(), new_scale: None, decoration_manager, - world: MyWorld::new(global_list), + world, }; Self { inner, @@ -778,9 +798,12 @@ impl InnerServerState { return; }; - if let Some(role) = data.get::<&SurfaceRole>() { - if let SurfaceRole::Toplevel(Some(data)) = &*role { + if let Some(mut role) = data.get::<&mut SurfaceRole>() { + if let SurfaceRole::Toplevel(Some(data)) = &mut *role { data.toplevel.set_title(title.name().to_string()); + if let Some(d) = &mut data.decoration.satellite { + d.set_title(&self.world, title.name()); + } } } } @@ -850,10 +873,6 @@ impl InnerServerState { } pub fn set_win_decorations(&mut self, window: x::Window, decorations: Decorations) { - if self.decoration_manager.is_none() { - return; - }; - let Some(data) = self .windows .get(&window) @@ -870,10 +889,9 @@ impl InnerServerState { debug!("setting {window:?} decorations {decorations:?}"); if let Some(role) = data.get::<&SurfaceRole>() { if let SurfaceRole::Toplevel(Some(data)) = &*role { - data.decoration - .as_ref() - .unwrap() - .set_mode(decorations.into()); + if let Some(decoration) = &data.decoration.wl { + decoration.set_mode(decorations.into()); + } } } win.attrs.decorations = Some(decorations); @@ -1312,8 +1330,9 @@ impl InnerServerState { toplevel.set_fullscreen(None); } - let decoration = self.decoration_manager.as_ref().map(|decoration_manager| { - let decoration = decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, ()); + let wl_decoration = self.decoration_manager.as_ref().map(|decoration_manager| { + let decoration = + decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, entity); decoration.set_mode( window .attrs @@ -1323,10 +1342,29 @@ impl InnerServerState { decoration }); + // X11 side wants server side decorations, but compositor won't provide them + // so we provide our own + let surface = self .world .get::<&client::wl_surface::WlSurface>(entity) .unwrap(); + let needs_satellite_decorations = wl_decoration.is_none() + && window + .attrs + .decorations + .is_none_or(|d| d == Decorations::Server); + let (sat_decoration, buf) = needs_satellite_decorations + .then(|| { + DecorationsDataSatellite::try_new( + self, + &surface, + window.attrs.title.as_ref().map(WmName::name), + ) + }) + .flatten() + .unzip(); + if let (Some(activation_state), Some(token)) = ( self.activation_state.as_ref(), window.activation_token.clone(), @@ -1356,6 +1394,13 @@ impl InnerServerState { } } + drop(window); + drop(group); + drop(surface); + if let Some(mut b) = buf.flatten() { + b.run_on(&mut self.world); + } + ToplevelData { xdg: XdgSurfaceData { surface: xdg, @@ -1364,7 +1409,10 @@ impl InnerServerState { }, toplevel, fullscreen: false, - decoration, + decoration: DecorationsData { + wl: wl_decoration, + satellite: sat_decoration, + }, } } diff --git a/src/server/tests.rs b/src/server/tests.rs index 24da976..270979a 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -8,7 +8,7 @@ use std::io::Write; use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::sync::{Arc, Mutex}; -use testwl::SendDataForMimeFn; +use testwl::{SendDataForMimeFn, SurfaceRole}; use wayland_client::{ backend::{protocol::Message, Backend, ObjectData, ObjectId, WaylandError}, protocol::{ @@ -27,6 +27,7 @@ use wayland_client::{ }, Connection, Proxy, WEnum, }; +use wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1; use wayland_protocols::{ wp::{ linux_dmabuf::zv1::client::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, @@ -2600,6 +2601,83 @@ fn scaled_pointer_lock_position_hint() { ); } +#[test] +fn client_side_decorations() { + let (mut f, compositor) = TestFixture::new_with_compositor(); + let window = unsafe { Window::new(1) }; + let (_, id) = f.create_toplevel(&compositor, window); + f.testwl + .force_decoration_mode(id, zxdg_toplevel_decoration_v1::Mode::ClientSide); + f.run(); + + let subsurface_id = f.testwl.last_created_surface_id().unwrap(); + assert_ne!(subsurface_id, id); + let data = f.testwl.get_surface_data(subsurface_id).unwrap(); + let Some(SurfaceRole::Subsurface(subsurface)) = &data.role else { + panic!("surface was not a subsurface: {:?}", data.role); + }; + + assert_eq!(subsurface.position, testwl::Vec2 { x: 0, y: -25 }); + assert_eq!(subsurface.parent, id); + let subsurface = subsurface.subsurface.clone(); + + f.testwl + .force_decoration_mode(id, zxdg_toplevel_decoration_v1::Mode::ServerSide); + f.run(); + + assert!(f.testwl.get_surface_data(subsurface_id).is_none()); + assert!(!subsurface.is_alive()); +} + +#[test] +fn client_side_decorations_no_global() { + let mut f = TestFixture::new_pre_connect(|testwl| { + testwl.disable_decorations_global(); + }); + let compositor = f.compositor(); + let window = unsafe { Window::new(1) }; + let (buffer, surface) = compositor.create_surface(); + + let data = WindowData { + mapped: true, + dims: WindowDims { + x: 0, + y: 0, + width: 50, + height: 50, + }, + fullscreen: false, + }; + + f.new_window(window, false, data); + f.map_window(&compositor, window, &surface.obj, &buffer); + f.run(); + + let surfaces = f.testwl.created_surfaces(); + assert_eq!(surfaces.len(), 2); + let mut toplevel = None; + let mut subsurface_parent = None; + for id in surfaces { + let data = f.testwl.get_surface_data(*id).unwrap(); + match data + .role + .as_ref() + .expect("A surface was created without a role") + { + SurfaceRole::Toplevel(_) => { + toplevel = Some(*id); + } + SurfaceRole::Subsurface(sub) => { + assert_eq!(sub.position, testwl::Vec2 { x: 0, y: -25 }); + subsurface_parent = Some(sub.parent); + } + other => panic!("got surface with unexpected role: {other:?}"), + } + } + + assert_eq!(toplevel.unwrap(), subsurface_parent.unwrap()); +} + /// See Pointer::handle_event for an explanation. #[test] fn popup_pointer_motion_workaround() {} diff --git a/tests/integration.rs b/tests/integration.rs index a189d23..8a6f24c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1766,7 +1766,7 @@ fn xdg_decorations() { let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, window); let data = f.testwl.get_surface_data(surface).unwrap(); - // The default decoration mode in x11 is SDD + // The default decoration mode in x11 is SSD assert_eq!( data.toplevel() .decoration @@ -1782,8 +1782,7 @@ fn xdg_decorations() { connection.atoms.motif_wm_hints, &[2u32, 0, 0, 0, 0], ); - std::thread::sleep(std::time::Duration::from_millis(1)); - f.testwl.dispatch(); + f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert_eq!( data.toplevel() @@ -1800,8 +1799,7 @@ fn xdg_decorations() { connection.atoms.motif_wm_hints, &[2u32, 0, 1, 0, 0], ); - std::thread::sleep(std::time::Duration::from_millis(1)); - f.testwl.dispatch(); + f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert_eq!( data.toplevel() diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 899e0b4..742b7c5 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -59,6 +59,9 @@ use wayland_protocols::{ }, }, }; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_subcompositor::WlSubcompositor; +use wayland_server::protocol::wl_subsurface::WlSubsurface; use wayland_server::{ backend::{ protocol::{Interface, ProtocolError}, @@ -119,6 +122,7 @@ impl SurfaceData { match self.role.as_ref().expect("Surface missing role") { SurfaceRole::Toplevel(ref t) => &t.xdg, SurfaceRole::Popup(ref p) => &p.xdg, + SurfaceRole::Subsurface(_) => panic!("subsurface doesn't have an XdgSurface"), SurfaceRole::Cursor => panic!("cursor surface doesn't have an XdgSurface"), } } @@ -142,6 +146,7 @@ pub enum SurfaceRole { Toplevel(Toplevel), Popup(Popup), Cursor, + Subsurface(Subsurface), } #[derive(Debug, PartialEq, Eq)] @@ -169,6 +174,13 @@ pub struct Popup { pub positioner_state: PositionerState, } +#[derive(Debug, PartialEq, Eq)] +pub struct Subsurface { + pub subsurface: WlSubsurface, + pub position: Vec2, + pub parent: SurfaceId, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct Vec2 { pub x: i32, @@ -251,6 +263,7 @@ struct State { buffers: HashSet, begin: Instant, last_surface_id: Option, + created_surfaces: Vec, last_output: Option, callbacks: Vec, seat: Option, @@ -275,6 +288,7 @@ impl Default for State { fn default() -> Self { Self { surfaces: Default::default(), + created_surfaces: Default::default(), outputs: Default::default(), buffers: Default::default(), positioners: Default::default(), @@ -415,6 +429,7 @@ pub struct Server { dh: DisplayHandle, state: State, client: Option, + decorations_global: GlobalId, } pub trait SendDataForMimeFn: FnMut(&str, &mut Server) -> bool {} @@ -448,6 +463,7 @@ impl Server { }; } dh.create_global::(6, ()); + dh.create_global::(1, ()); dh.create_global::(1, ()); dh.create_global::(6, ()); dh.create_global::(5, ()); @@ -455,7 +471,7 @@ impl Server { dh.create_global::(1, ()); dh.create_global::(1, ()); dh.create_global::(1, ()); - dh.create_global::(1, ()); + let decorations_global = dh.create_global::(1, ()); dh.create_global::(1, ()); dh.create_global::(1, ()); global_noop!(ZwpLinuxDmabufV1); @@ -515,6 +531,7 @@ impl Server { dh, state: State::default(), client: None, + decorations_global, } } @@ -542,6 +559,7 @@ impl Server { } pub fn get_surface_data(&self, surface_id: SurfaceId) -> Option<&SurfaceData> { + println!("{:?}", self.state.surfaces); self.state.surfaces.get(&surface_id) } @@ -549,6 +567,10 @@ impl Server { self.state.last_surface_id } + pub fn created_surfaces(&self) -> &[SurfaceId] { + &self.state.created_surfaces + } + #[track_caller] pub fn last_created_output(&self) -> WlOutput { self.state @@ -893,6 +915,27 @@ impl Server { pub fn tablet(&mut self) -> &ZwpTabletV2 { self.state.tablet.as_ref().expect("No tablet created") } + + pub fn force_decoration_mode( + &mut self, + surface: SurfaceId, + mode: zxdg_toplevel_decoration_v1::Mode, + ) { + let toplevel = self.state.get_toplevel(surface); + toplevel + .decoration + .as_mut() + .expect("Missing toplevel decoration") + .0 + .configure(mode); + self.display.flush_clients().unwrap(); + } + + pub fn disable_decorations_global(&self) { + self.display + .handle() + .remove_global::(self.decorations_global.clone()); + } } #[derive(Clone, Eq, PartialEq, Debug)] @@ -930,6 +973,7 @@ impl Write for TransferFd { simple_global_dispatch!(WlShm); simple_global_dispatch!(WlCompositor); +simple_global_dispatch!(WlSubcompositor); simple_global_dispatch!(XdgWmBase); simple_global_dispatch!(ZxdgOutputManagerV1); simple_global_dispatch!(ZwpTabletManagerV2); @@ -1629,7 +1673,7 @@ impl Dispatch for State { || match data.role.as_ref().unwrap() { SurfaceRole::Toplevel(t) => t.toplevel.is_alive(), SurfaceRole::Popup(p) => p.popup.is_alive(), - SurfaceRole::Cursor => false, + _ => unreachable!() }; if role_alive { client.kill( @@ -1875,12 +1919,44 @@ impl Dispatch for State { }, ); state.last_surface_id = Some(SurfaceId(id)); + state.created_surfaces.push(SurfaceId(id)); } _ => unreachable!(), } } } +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlSubcompositor, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use proto::wl_subcompositor::Request::*; + match request { + GetSubsurface { + id, + surface, + parent, + } => { + let surface_id = SurfaceId::from(&surface); + let data = state.surfaces.get_mut(&surface_id).unwrap(); + data.role = Some(SurfaceRole::Subsurface(Subsurface { + parent: SurfaceId::from(&parent), + subsurface: data_init.init(id, surface_id), + position: Vec2::default(), + })); + } + Destroy => {} + other => todo!("unhandled subcompositor request {other:?}"), + } + } +} + impl Dispatch for State { fn request( state: &mut Self, @@ -1940,6 +2016,32 @@ impl Dispatch for State { } } +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlSubsurface, + request: ::Request, + surface_id: &SurfaceId, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + use proto::wl_subsurface::Request::*; + match request { + SetPosition { x, y } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let Some(SurfaceRole::Subsurface(subsurface)) = &mut data.role else { + unreachable!(); + }; + + subsurface.position = Vec2 { x, y }; + } + SetDesync | Destroy => {} + other => todo!("unhandled wl_subsurface request: {other:?}"), + } + } +} + impl Dispatch for State { fn request( _: &mut Self, @@ -2137,6 +2239,8 @@ impl Dispatch for State { .as_mut() .map(|(_, decoration)| decoration) .unwrap() = Some(mode); + + resource.configure(mode); } else { resource.post_error( zxdg_toplevel_decoration_v1::Error::Orphaned,