xwayland-satellite/src/server/decoration.rs
Shawn Wallace 64c70be855 cargo fmt
2026-01-18 14:46:11 -05:00

519 lines
15 KiB
Rust

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::borrow::Cow;
use std::sync::LazyLock;
use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Stroke, Transform};
use tiny_skia::{ColorU8, Rect};
use wayland_client::Proxy;
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_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,
should_draw: bool,
remove_buffer: bool,
}
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_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(),
should_draw: true,
remove_buffer: false,
}
.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();
}
#[must_use]
pub fn will_draw_decorations(&self, width: i32) -> bool {
width > 0 && self.should_draw
}
pub fn draw_decorations(&mut self, world: &World, width: i32, parent_scale_factor: f32) {
if !self.will_draw_decorations(width) {
if self.remove_buffer {
self.surface.attach(None, 0, 0);
self.surface.commit();
self.remove_buffer = false;
}
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());
if !self.should_draw {
return;
}
// 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);
}
pub fn handle_fullscreen(&mut self, fullscreen: bool) {
if self.should_draw == fullscreen {
self.should_draw = !fullscreen;
self.remove_buffer = fullscreen;
}
}
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))?
.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)
}
static FONT_DATA: LazyLock<Cow<'static, [u8]>> = LazyLock::new(|| {
#[cfg(feature = "fontconfig")]
{
let fc = fontconfig::Fontconfig::new().expect("Failed to initialize fontconfig.");
let font = fc
.find("opensans", None)
.expect("Failed to load Open Sans Regular.");
let data = std::fs::read(font.path).expect("Failed to read font from disk.");
Cow::Owned(data)
}
#[cfg(not(feature = "fontconfig"))]
{
Cow::Borrowed(include_bytes!("../../OpenSans-Regular.ttf"))
}
});
static FONT: LazyLock<FontRef<'_>> =
LazyLock::new(|| FontRef::try_from_slice(FONT_DATA.as_ref()).unwrap());
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;
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);
}
}