Add client side decorations to toplevels

These simple decorations will be rendered only when the host compositor doesn't
support server side decorations and the X11 window does not render its own
decorations.

Closes #31
This commit is contained in:
Shawn Wallace 2025-11-06 00:50:30 -05:00
parent 56256a1340
commit b39388d91a
10 changed files with 1087 additions and 49 deletions

180
Cargo.lock generated
View file

@ -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",

View file

@ -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 = []

BIN
OpenSans-Regular.ttf Normal file

Binary file not shown.

View file

@ -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<T> = <T as Proxy>::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<WlRegistry, GlobalListContents> for MyWorld {
fn event(
@ -236,6 +240,18 @@ impl Dispatch<WlCallback, server::wl_callback::WlCallback> for MyWorld {
}
}
impl Dispatch<WlSurface, DecorationMarker> for MyWorld {
fn event(
_: &mut Self,
_: &WlSurface,
_: <WlSurface as Proxy>::Event,
_: &DecorationMarker,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
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<P: Proxy> {
key: OnceLock<Entity>,

480
src/server/decoration.rs Normal file
View file

@ -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<ZxdgToplevelDecorationV1>,
// Boxed to avoid making ToplevelData so much bigger than PopupData
pub satellite: Option<Box<DecorationsDataSatellite>>,
}
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<String>,
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<impl X11Selection>,
parent: &WlSurface,
title: Option<&str>,
) -> Option<(Box<Self>, Option<CommandBuffer>)> {
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<Pixmap> {
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<Glyph>, PxScaleFont<&FontRef<'_>>) {
const TEXT_SIZE: f32 = 10.0;
const TEXT_MARGIN: f32 = 11.0;
static FONT: LazyLock<FontRef<'_>> = LazyLock::new(|| {
FontRef::try_from_slice(include_bytes!("../../OpenSans-Regular.ttf")).unwrap()
});
let mut ret = Vec::<Glyph>::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<hecs::RefMut<'_, Box<DecorationsDataSatellite>>> {
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<impl X11Selection>, parent: Entity) {
if let Some(mut decoration) = get_decoration(&state.world, parent) {
decoration.handle_leave(&state.world);
}
}
pub fn handle_pointer_motion(
state: &InnerServerState<impl X11Selection>,
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<impl XConnection>,
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);
}
}

View file

@ -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<C: XConnection>(self, target: Entity, state: &mut ServerState<C>) {
@ -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::<PendingEnter>(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 {
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::<PendingEnter>(target);
let _ = state.world.remove_one::<CurrentSurface>(target);
if !surface.is_alive() {
return;
}
debug!("leaving surface ({serial})");
if let Ok(CurrentSurface::Decoration(parent)) =
state.world.remove_one::<CurrentSurface>(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,

View file

@ -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<ZxdgToplevelDecorationV1>,
decoration: decoration::DecorationsData,
}
#[derive(Debug)]
@ -447,6 +448,9 @@ pub struct InnerServerState<S: X11Selection> {
last_hovered: Option<x::Window>,
xdg_wm_base: XdgWmBase,
compositor: client::wl_compositor::WlCompositor,
subcompositor: WlSubcompositor,
shm: client::wl_shm::WlShm,
viewporter: WpViewporter,
fractional_scale: Option<WpFractionalScaleManagerV1>,
decoration_manager: Option<ZxdgDecorationManagerV1>,
@ -482,6 +486,18 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
warn!("xdg_wm_base version 2 detected. Popup repositioning will not work, and some popups may not work correctly.");
}
let compositor = global_list
.bind::<client::wl_compositor::WlCompositor, _, _>(&qh, 4..=6, ())
.expect("Could not bind wl_compositor");
let subcompositor = global_list
.bind::<WlSubcompositor, _, _>(&qh, 1..=1, ())
.expect("Could not bind wl_subcompositor");
let shm = global_list
.bind::<client::wl_shm::WlShm, _, _>(&qh, 1..=1, ())
.expect("Could not bind wl_shm");
let viewporter = global_list
.bind::<WpViewporter, _, _>(&qh, 1..=1, ())
.expect("Could not bind wp_viewporter");
@ -511,6 +527,7 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
.contents()
.with_list(|globals| handle_globals::<S>(&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<S: X11Selection> ServerState<NoConnection<S>> {
last_focused_toplevel: None,
last_hovered: None,
xdg_wm_base,
compositor,
subcompositor,
shm,
viewporter,
fractional_scale,
selection_states,
@ -544,7 +564,7 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
updated_outputs: Vec::new(),
new_scale: None,
decoration_manager,
world: MyWorld::new(global_list),
world,
};
Self {
inner,
@ -778,9 +798,12 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
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<S: X11Selection + 'static> InnerServerState<S> {
}
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<S: X11Selection + 'static> InnerServerState<S> {
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<S: X11Selection + 'static> InnerServerState<S> {
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<S: X11Selection + 'static> InnerServerState<S> {
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<S: X11Selection + 'static> InnerServerState<S> {
}
}
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<S: X11Selection + 'static> InnerServerState<S> {
},
toplevel,
fullscreen: false,
decoration,
decoration: DecorationsData {
wl: wl_decoration,
satellite: sat_decoration,
},
}
}

View file

@ -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() {}

View file

@ -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()

View file

@ -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<WlBuffer>,
begin: Instant,
last_surface_id: Option<SurfaceId>,
created_surfaces: Vec<SurfaceId>,
last_output: Option<WlOutput>,
callbacks: Vec<WlCallback>,
seat: Option<WlSeat>,
@ -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<Client>,
decorations_global: GlobalId,
}
pub trait SendDataForMimeFn: FnMut(&str, &mut Server) -> bool {}
@ -448,6 +463,7 @@ impl Server {
};
}
dh.create_global::<State, WlCompositor, _>(6, ());
dh.create_global::<State, WlSubcompositor, _>(1, ());
dh.create_global::<State, WlShm, _>(1, ());
dh.create_global::<State, XdgWmBase, _>(6, ());
dh.create_global::<State, WlSeat, _>(5, ());
@ -455,7 +471,7 @@ impl Server {
dh.create_global::<State, ZwpPrimarySelectionDeviceManagerV1, _>(1, ());
dh.create_global::<State, ZwpTabletManagerV2, _>(1, ());
dh.create_global::<State, XdgActivationV1, _>(1, ());
dh.create_global::<State, ZxdgDecorationManagerV1, _>(1, ());
let decorations_global = dh.create_global::<State, ZxdgDecorationManagerV1, _>(1, ());
dh.create_global::<State, WpViewporter, _>(1, ());
dh.create_global::<State, ZwpPointerConstraintsV1, _>(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::<State>(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<XdgSurface, SurfaceId> 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<WlCompositor, ()> for State {
},
);
state.last_surface_id = Some(SurfaceId(id));
state.created_surfaces.push(SurfaceId(id));
}
_ => unreachable!(),
}
}
}
impl Dispatch<WlSubcompositor, ()> for State {
fn request(
state: &mut Self,
_: &Client,
_: &WlSubcompositor,
request: <WlSubcompositor as Resource>::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<WlSurface, ()> for State {
fn request(
state: &mut Self,
@ -1940,6 +2016,32 @@ impl Dispatch<WlSurface, ()> for State {
}
}
impl Dispatch<WlSubsurface, SurfaceId> for State {
fn request(
state: &mut Self,
_: &Client,
_: &WlSubsurface,
request: <WlSubsurface as Resource>::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<WlCallback, ()> for State {
fn request(
_: &mut Self,
@ -2137,6 +2239,8 @@ impl Dispatch<ZxdgToplevelDecorationV1, SurfaceId> for State {
.as_mut()
.map(|(_, decoration)| decoration)
.unwrap() = Some(mode);
resource.configure(mode);
} else {
resource.post_error(
zxdg_toplevel_decoration_v1::Error::Orphaned,