feat(compositor): add weft-compositor crate

- Implement WeftCompositorState with all Wayland protocol globals:
 compositor, xdg-shell, layer-shell, shm, dmabuf, output, presentation,
 text-input, input-method, pointer-constraints, cursor-shape, seat.
- Implement process_input_event covering keyboard, pointer (relative +
 absolute), axis, touch, and all gesture types (swipe, pinch, hold).
- Implement Winit backend with damage-tracked rendering loop and frame
 callbacks.
- Add DRM/KMS backend skeleton: libseat session, udev device discovery,
 calloop integration (rendering path deferred).
- Add infra/systemd/weft-compositor.service (Type=notify).
- Split CI into cross-platform and linux-only jobs.
- Exclude weft-compositor from Windows check scripts.
This commit is contained in:
Marco Allegretti 2026-03-10 20:56:35 +01:00
parent 3db9541f72
commit feb69be199
13 changed files with 3775 additions and 10 deletions

View file

@ -7,7 +7,8 @@ on:
pull_request:
jobs:
rust:
# Crates that must compile on every supported host platform.
cross-platform:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@ -23,7 +24,34 @@ jobs:
components: rustfmt, clippy
- name: cargo fmt
run: cargo fmt --all --check
- name: cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: cargo test
run: cargo test --workspace
- name: cargo clippy (cross-platform crates)
run: cargo clippy --workspace --exclude weft-compositor --all-targets -- -D warnings
- name: cargo test (cross-platform crates)
run: cargo test --workspace --exclude weft-compositor
# Wayland compositor and other Linux-only system crates.
# These require libwayland-server and other Linux kernel interfaces.
linux-only:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
components: rustfmt, clippy
- name: install Linux system dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y --no-install-recommends \
libwayland-dev \
libxkbcommon-dev \
libegl-dev \
libgles2-mesa-dev \
libinput-dev \
libseat-dev \
libudev-dev \
pkg-config
- name: cargo clippy (weft-compositor)
run: cargo clippy -p weft-compositor --all-targets -- -D warnings
- name: cargo test (weft-compositor)
run: cargo test -p weft-compositor

2555
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
[workspace]
members = ["crates/weft-build-meta"]
members = ["crates/weft-build-meta", "crates/weft-compositor"]
resolver = "2"
[workspace.package]

View file

@ -0,0 +1,41 @@
[package]
name = "weft-compositor"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[lints]
workspace = true
[[bin]]
name = "weft-compositor"
path = "src/main.rs"
# Features available on all build platforms.
[dependencies]
smithay = { version = "0.7", default-features = false, features = [
"backend_egl",
"backend_winit",
"renderer_gl",
"wayland_frontend",
"desktop",
] }
calloop = { version = "0.14", features = ["executor"] }
calloop-wayland-source = "0.4"
wayland-server = "0.31"
wayland-protocols = { version = "0.32", features = ["server", "unstable"] }
wayland-protocols-wlr = { version = "0.3", features = ["server"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
anyhow = "1"
# DRM/KMS and hardware input depend on Linux kernel interfaces; compile only on Linux.
[target.'cfg(target_os = "linux")'.dependencies]
smithay = { version = "0.7", default-features = false, features = [
"backend_drm",
"backend_gbm",
"backend_libinput",
"backend_udev",
"backend_session_libseat",
] }

View file

@ -0,0 +1,94 @@
// Non-Linux: DRM/KMS backend is unavailable; callers must use --winit.
#[cfg(not(target_os = "linux"))]
pub fn run() -> anyhow::Result<()> {
anyhow::bail!("DRM/KMS backend requires Linux; pass --winit for development on other platforms")
}
// Linux DRM/KMS backend.
#[cfg(target_os = "linux")]
pub fn run() -> anyhow::Result<()> {
use std::time::Duration;
use smithay::{
backend::{
allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
drm::{DrmDevice, DrmDeviceFd, DrmNode, NodeType},
egl::EGLDevice,
libinput::{LibinputInputBackend, LibinputSessionInterface},
renderer::{
damage::OutputDamageTracker,
gles::GlesRenderer,
multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer},
},
session::{
libseat::{LibSeatSession, LibSeatSessionNotifier},
Session,
},
udev::{UdevBackend, UdevEvent},
},
desktop::{space::space_render_elements, Space, Window},
output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel},
reexports::{
calloop::{
timer::{TimeoutAction, Timer},
EventLoop, Interest, Mode, PostAction,
},
wayland_server::Display,
},
utils::Transform,
};
use crate::{input, state::WeftCompositorState};
let mut display: Display<WeftCompositorState> = Display::new()?;
let display_handle = display.handle();
let mut event_loop: EventLoop<'static, WeftCompositorState> = EventLoop::try_new()?;
let loop_handle = event_loop.handle();
let loop_signal = event_loop.get_signal();
// Open a libseat session to gain DRM device access without root.
let (session, notifier) = LibSeatSession::new()
.map_err(|e| anyhow::anyhow!("libseat session failed: {e}"))?;
// Discover GPU nodes via udev.
let udev_backend = UdevBackend::new(session.seat())?;
let mut state = WeftCompositorState::new(
display_handle,
loop_signal.clone(),
loop_handle.clone(),
session.seat(),
);
// Register the udev backend with calloop so device hotplug is handled.
loop_handle.insert_source(udev_backend, {
let signal = loop_signal.clone();
move |event, _, _state| match event {
UdevEvent::Added { device_id, path } => {
tracing::info!(?device_id, ?path, "GPU device added");
}
UdevEvent::Changed { device_id } => {
tracing::debug!(?device_id, "GPU device changed");
}
UdevEvent::Removed { device_id } => {
tracing::info!(?device_id, "GPU device removed");
signal.stop();
}
}
})?;
tracing::info!("DRM/KMS backend initialised; entering event loop");
loop {
display.dispatch_clients(&mut state)?;
display.flush_clients()?;
event_loop.dispatch(Some(Duration::from_millis(16)), &mut state)?;
if loop_signal.is_stopped() {
break;
}
}
Ok(())
}

View file

@ -0,0 +1,2 @@
pub mod drm;
pub mod winit;

View file

@ -0,0 +1,170 @@
use std::time::Duration;
use smithay::{
backend::{
renderer::{damage::OutputDamageTracker, gles::GlesRenderer},
winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend},
},
desktop::{space::space_render_elements, Space, Window},
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
reexports::{calloop::EventLoop, wayland_server::Display},
utils::Transform,
};
use crate::{input, state::WeftCompositorState};
pub fn run() -> anyhow::Result<()> {
let mut display: Display<WeftCompositorState> = Display::new()?;
let display_handle = display.handle();
let mut event_loop: EventLoop<'static, WeftCompositorState> = EventLoop::try_new()?;
let loop_handle = event_loop.handle();
let loop_signal = event_loop.get_signal();
let (mut winit_backend, mut winit_evt_loop) = winit::init::<GlesRenderer>()
.map_err(|e| anyhow::anyhow!("winit backend init failed: {e}"))?;
let initial_size = winit_backend.window_size();
let output = Output::new(
"WEFT-winit".to_string(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "WEFT".to_string(),
model: "Winit".to_string(),
},
);
let _wl_output_global = output.create_global::<WeftCompositorState>(&display_handle);
let initial_mode = OutputMode {
size: initial_size,
refresh: 60_000,
};
output.change_current_state(
Some(initial_mode),
Some(Transform::Flipped180),
None,
Some((0, 0).into()),
);
output.set_preferred(initial_mode);
let mut state = WeftCompositorState::new(
display_handle,
loop_signal,
loop_handle,
"seat-0".to_string(),
);
state.space.map_output(&output, (0, 0));
let mut damage_tracker = OutputDamageTracker::from_output(&output);
let start = std::time::Instant::now();
loop {
let dispatch_result = dispatch_winit_events(
&mut winit_evt_loop,
&mut state,
&output,
&mut damage_tracker,
);
if dispatch_result.is_err() || !state.running {
break;
}
display.dispatch_clients(&mut state)?;
render_frame(
&mut winit_backend,
&mut damage_tracker,
&mut state,
&output,
start.elapsed(),
)?;
display.flush_clients()?;
// Run any registered calloop sources (timers, signals) with a zero timeout so
// the loop stays responsive without blocking.
event_loop.dispatch(Some(Duration::ZERO), &mut state)?;
}
Ok(())
}
fn dispatch_winit_events(
evt_loop: &mut WinitEventLoop,
state: &mut WeftCompositorState,
output: &Output,
damage_tracker: &mut OutputDamageTracker,
) -> Result<(), ()> {
evt_loop
.dispatch_new_events(|event| match event {
WinitEvent::Resized { size, scale_factor } => {
let new_mode = OutputMode {
size,
refresh: 60_000,
};
output.change_current_state(
Some(new_mode),
None,
Some(Scale::Fractional(scale_factor)),
None,
);
output.set_preferred(new_mode);
state.space.map_output(output, (0, 0));
*damage_tracker = OutputDamageTracker::from_output(output);
}
WinitEvent::Input(input_event) => {
input::process_input_event(state, input_event);
}
WinitEvent::Focus(_focused) => {}
WinitEvent::Refresh => {}
WinitEvent::CloseRequested => {
state.running = false;
}
})
.map_err(|_| ())
}
fn render_frame(
backend: &mut WinitGraphicsBackend<GlesRenderer>,
damage_tracker: &mut OutputDamageTracker,
state: &mut WeftCompositorState,
output: &Output,
elapsed: Duration,
) -> anyhow::Result<()> {
backend
.bind()
.map_err(|e| anyhow::anyhow!("framebuffer bind failed: {e}"))?;
let age = backend.buffer_age().unwrap_or(0);
let renderer = backend.renderer();
let elements =
space_render_elements::<GlesRenderer, Window, &Space<Window>>(
renderer,
[&state.space],
output,
1.0_f64,
)
.map_err(|e| anyhow::anyhow!("render element collection failed: {e}"))?;
let result = damage_tracker
.render_output(renderer, age, &elements, [0.1_f32, 0.1, 0.1, 1.0])
.map_err(|e| anyhow::anyhow!("render_output failed: {e}"))?;
backend
.submit(result.damage.as_deref())
.map_err(|e| anyhow::anyhow!("buffer submit failed: {e}"))?;
// Notify clients that a new frame has been presented so they can submit the next buffer.
for window in state.space.elements() {
window.send_frame(
output,
elapsed,
Some(Duration::from_secs(1) / 60),
|_, _| Some(output.clone()),
);
}
Ok(())
}

View file

@ -0,0 +1,462 @@
use smithay::{
backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
GestureSwipeUpdateEvent, InputBackend, InputEvent, KeyState, KeyboardKeyEvent,
PointerAxisEvent, PointerButtonEvent, PointerMotionAbsoluteEvent, PointerMotionEvent,
TouchCancelEvent, TouchDownEvent, TouchFrameEvent, TouchMotionEvent, TouchUpEvent,
},
input::{
keyboard::{FilterResult, KeysymHandle, ModifiersState},
pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent},
},
reexports::wayland_server::protocol::wl_pointer,
utils::{Logical, Point, Serial, SERIAL_COUNTER},
};
use crate::state::WeftCompositorState;
pub fn process_input_event<B: InputBackend>(
state: &mut WeftCompositorState,
event: InputEvent<B>,
) {
match event {
InputEvent::Keyboard { event } => handle_keyboard(state, event),
InputEvent::PointerMotion { event } => handle_pointer_motion(state, event),
InputEvent::PointerMotionAbsolute { event } => {
handle_pointer_motion_absolute(state, event)
}
InputEvent::PointerButton { event } => handle_pointer_button(state, event),
InputEvent::PointerAxis { event } => handle_pointer_axis(state, event),
InputEvent::TouchDown { event } => handle_touch_down(state, event),
InputEvent::TouchUp { event } => handle_touch_up(state, event),
InputEvent::TouchMotion { event } => handle_touch_motion(state, event),
InputEvent::TouchFrame { event } => handle_touch_frame(state, event),
InputEvent::TouchCancel { event } => handle_touch_cancel(state, event),
InputEvent::GestureSwipeBegin { event } => handle_gesture_swipe_begin(state, event),
InputEvent::GestureSwipeUpdate { event } => handle_gesture_swipe_update(state, event),
InputEvent::GestureSwipeEnd { event } => handle_gesture_swipe_end(state, event),
InputEvent::GesturePinchBegin { event } => handle_gesture_pinch_begin(state, event),
InputEvent::GesturePinchUpdate { event } => handle_gesture_pinch_update(state, event),
InputEvent::GesturePinchEnd { event } => handle_gesture_pinch_end(state, event),
InputEvent::GestureHoldBegin { event } => handle_gesture_hold_begin(state, event),
InputEvent::GestureHoldEnd { event } => handle_gesture_hold_end(state, event),
// Device added/removed events are handled at the backend level.
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } => {}
_ => {}
}
}
fn handle_keyboard<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::KeyboardKeyEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
let time = event.time_msec();
let key_state = event.state();
if let Some(keyboard) = state.seat.get_keyboard() {
keyboard.input::<(), _>(
state,
event.key_code(),
key_state,
serial,
time,
|_state, _mods, _keysym| FilterResult::Forward,
);
}
}
fn handle_pointer_motion<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::PointerMotionEvent,
) {
let delta = event.delta();
state.pointer_location += delta;
clamp_pointer_to_output_space(state);
let serial = SERIAL_COUNTER.next_serial();
let pointer_location = state.pointer_location;
let under = surface_under(state, pointer_location);
if let Some(pointer) = state.seat.get_pointer() {
pointer.motion(
state,
under,
&MotionEvent {
location: pointer_location,
serial,
time: event.time_msec(),
},
);
pointer.frame(state);
}
}
fn handle_pointer_motion_absolute<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::PointerMotionAbsoluteEvent,
) {
let output = state.space.outputs().next().cloned();
if let Some(output) = output {
let output_geo = state
.space
.output_geometry(&output)
.unwrap_or_default();
let pos = event.position_transformed(output_geo.size);
state.pointer_location = output_geo.loc.to_f64() + pos;
}
let serial = SERIAL_COUNTER.next_serial();
let pointer_location = state.pointer_location;
let under = surface_under(state, pointer_location);
if let Some(pointer) = state.seat.get_pointer() {
pointer.motion(
state,
under,
&MotionEvent {
location: pointer_location,
serial,
time: event.time_msec(),
},
);
pointer.frame(state);
}
}
fn handle_pointer_button<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::PointerButtonEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
let button = event.button_code();
let button_state = event.state();
// On press: focus the surface under the pointer.
if button_state == ButtonState::Pressed {
let pointer_location = state.pointer_location;
if let Some((surface, _loc)) = surface_under(state, pointer_location) {
if let Some(keyboard) = state.seat.get_keyboard() {
keyboard.set_focus(state, Some(surface.clone()), serial);
}
} else if let Some(keyboard) = state.seat.get_keyboard() {
keyboard.set_focus(state, None, serial);
}
}
if let Some(pointer) = state.seat.get_pointer() {
pointer.button(
state,
&ButtonEvent {
button,
state: button_state,
serial,
time: event.time_msec(),
},
);
pointer.frame(state);
}
}
fn handle_pointer_axis<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::PointerAxisEvent,
) {
let horizontal = event.amount(Axis::Horizontal);
let vertical = event.amount(Axis::Vertical);
let h_discrete = event.amount_v120(Axis::Horizontal);
let v_discrete = event.amount_v120(Axis::Vertical);
let source = event.source();
let mut frame = AxisFrame::new(event.time_msec()).source(source);
if let Some(v) = horizontal {
if v != 0.0 {
frame = frame.value(Axis::Horizontal, v);
}
if let Some(d) = h_discrete {
frame = frame.v120(Axis::Horizontal, d as i32);
}
}
if let Some(v) = vertical {
if v != 0.0 {
frame = frame.value(Axis::Vertical, v);
}
if let Some(d) = v_discrete {
frame = frame.v120(Axis::Vertical, d as i32);
}
}
if source == AxisSource::Finger {
if event.amount(Axis::Horizontal).unwrap_or(0.0) == 0.0 {
frame = frame.stop(Axis::Horizontal);
}
if event.amount(Axis::Vertical).unwrap_or(0.0) == 0.0 {
frame = frame.stop(Axis::Vertical);
}
}
if let Some(pointer) = state.seat.get_pointer() {
pointer.axis(state, frame);
pointer.frame(state);
}
}
fn handle_touch_down<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::TouchDownEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
let output = state.space.outputs().next().cloned();
if let Some(output) = output {
let output_geo = state.space.output_geometry(&output).unwrap_or_default();
let pos = event.position_transformed(output_geo.size);
let location = output_geo.loc.to_f64() + pos;
let under = surface_under(state, location);
if let Some(touch) = state.seat.get_touch() {
touch.down(
state,
under,
&smithay::input::touch::DownEvent {
slot: event.slot(),
location,
serial,
time: event.time_msec(),
},
);
}
}
}
fn handle_touch_up<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::TouchUpEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(touch) = state.seat.get_touch() {
touch.up(
state,
&smithay::input::touch::UpEvent {
slot: event.slot(),
serial,
time: event.time_msec(),
},
);
}
}
fn handle_touch_motion<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::TouchMotionEvent,
) {
let output = state.space.outputs().next().cloned();
if let Some(output) = output {
let output_geo = state.space.output_geometry(&output).unwrap_or_default();
let pos = event.position_transformed(output_geo.size);
let location = output_geo.loc.to_f64() + pos;
let under = surface_under(state, location);
if let Some(touch) = state.seat.get_touch() {
touch.motion(
state,
under,
&smithay::input::touch::MotionEvent {
slot: event.slot(),
location,
time: event.time_msec(),
},
);
}
}
}
fn handle_touch_frame<B: InputBackend>(
state: &mut WeftCompositorState,
_event: B::TouchFrameEvent,
) {
if let Some(touch) = state.seat.get_touch() {
touch.frame(state);
}
}
fn handle_touch_cancel<B: InputBackend>(
state: &mut WeftCompositorState,
_event: B::TouchCancelEvent,
) {
if let Some(touch) = state.seat.get_touch() {
touch.cancel(state);
}
}
fn handle_gesture_swipe_begin<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GestureSwipeBeginEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_swipe_begin(
state,
&smithay::input::pointer::GestureSwipeBeginEvent {
serial,
time: event.time_msec(),
fingers: event.fingers(),
},
);
}
}
fn handle_gesture_swipe_update<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GestureSwipeUpdateEvent,
) {
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_swipe_update(
state,
&smithay::input::pointer::GestureSwipeUpdateEvent {
time: event.time_msec(),
delta: event.delta(),
},
);
}
}
fn handle_gesture_swipe_end<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GestureSwipeEndEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_swipe_end(
state,
&smithay::input::pointer::GestureSwipeEndEvent {
serial,
time: event.time_msec(),
cancelled: event.cancelled(),
},
);
}
}
fn handle_gesture_pinch_begin<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GesturePinchBeginEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_pinch_begin(
state,
&smithay::input::pointer::GesturePinchBeginEvent {
serial,
time: event.time_msec(),
fingers: event.fingers(),
},
);
}
}
fn handle_gesture_pinch_update<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GesturePinchUpdateEvent,
) {
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_pinch_update(
state,
&smithay::input::pointer::GesturePinchUpdateEvent {
time: event.time_msec(),
delta: event.delta(),
scale: event.scale(),
rotation: event.rotation(),
},
);
}
}
fn handle_gesture_pinch_end<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GesturePinchEndEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_pinch_end(
state,
&smithay::input::pointer::GesturePinchEndEvent {
serial,
time: event.time_msec(),
cancelled: event.cancelled(),
},
);
}
}
fn handle_gesture_hold_begin<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GestureHoldBeginEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_hold_begin(
state,
&smithay::input::pointer::GestureHoldBeginEvent {
serial,
time: event.time_msec(),
fingers: event.fingers(),
},
);
}
}
fn handle_gesture_hold_end<B: InputBackend>(
state: &mut WeftCompositorState,
event: B::GestureHoldEndEvent,
) {
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = state.seat.get_pointer() {
pointer.gesture_hold_end(
state,
&smithay::input::pointer::GestureHoldEndEvent {
serial,
time: event.time_msec(),
cancelled: event.cancelled(),
},
);
}
}
/// Returns the surface and its local coordinates under the given position.
pub fn surface_under(
state: &WeftCompositorState,
point: Point<f64, Logical>,
) -> Option<(smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, Point<f64, Logical>)> {
state
.space
.element_under(point)
.and_then(|(window, loc)| {
window
.surface_under(point - loc.to_f64(), smithay::desktop::WindowSurfaceType::ALL)
.map(|(surface, surface_loc)| (surface, (loc.to_f64() + surface_loc.to_f64())))
})
}
fn clamp_pointer_to_output_space(state: &mut WeftCompositorState) {
let bbox = state
.space
.outputs()
.filter_map(|o| state.space.output_geometry(o))
.fold(
smithay::utils::Rectangle::<i32, Logical>::default(),
|acc, r| acc.merge(r),
);
if bbox.size.w > 0 && bbox.size.h > 0 {
state.pointer_location.x = state
.pointer_location
.x
.clamp(bbox.loc.x as f64, (bbox.loc.x + bbox.size.w - 1) as f64);
state.pointer_location.y = state
.pointer_location
.y
.clamp(bbox.loc.y as f64, (bbox.loc.y + bbox.size.h - 1) as f64);
}
}

View file

@ -0,0 +1,26 @@
use tracing_subscriber::EnvFilter;
mod backend;
mod input;
mod state;
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let args: Vec<String> = std::env::args().collect();
let use_winit = args.iter().any(|a| a == "--winit")
|| std::env::var("DISPLAY").is_ok()
|| std::env::var("WAYLAND_DISPLAY").is_ok();
if use_winit {
tracing::info!("starting compositor with winit backend");
backend::winit::run()
} else {
tracing::info!("starting compositor with DRM/KMS backend");
backend::drm::run()
}
}

View file

@ -0,0 +1,366 @@
use smithay::{
backend::renderer::utils::on_commit_buffer_handler,
delegate_compositor, delegate_cursor_shape, delegate_dmabuf, delegate_input_method_manager,
delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_presentation,
delegate_seat, delegate_shm, delegate_text_input_manager, delegate_xdg_shell,
desktop::{
layer_map_for_output, PopupKind, PopupManager, Space, Window, WindowSurfaceType,
},
input::{
keyboard::XkbConfig,
pointer::CursorImageStatus,
Seat, SeatHandler, SeatState,
},
output::Output,
reexports::{
calloop::{LoopHandle, LoopSignal},
wayland_server::{
backend::{ClientData, ClientId, DisconnectReason},
protocol::{wl_output::WlOutput, wl_surface::WlSurface},
Client, DisplayHandle,
},
},
utils::{Logical, Point, Rectangle},
wayland::{
compositor::{CompositorClientState, CompositorHandler, CompositorState},
cursor_shape::{CursorShapeHandler, CursorShapeManagerState},
dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier},
input_method::{InputMethodHandler, InputMethodManagerState},
output::OutputManagerState,
pointer_constraints::{PointerConstraintsHandler, PointerConstraintsState},
presentation::{PresentationHandler, PresentationState},
seat::WaylandFocus,
shell::{
wlr_layer::{Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState},
xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState},
},
shm::{ShmHandler, ShmState},
text_input::{TextInputHandler, TextInputManagerState},
},
};
// Per-client state that Smithay compositor protocol handlers need.
#[derive(Default)]
pub struct WeftClientState {
pub compositor_state: CompositorClientState,
}
impl ClientData for WeftClientState {
fn initialized(&self, _client_id: ClientId) {}
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
}
pub struct WeftCompositorState {
pub display_handle: DisplayHandle,
pub loop_signal: LoopSignal,
pub loop_handle: LoopHandle<'static, WeftCompositorState>,
// Wayland protocol globals
pub compositor_state: CompositorState,
pub xdg_shell_state: XdgShellState,
pub layer_shell_state: WlrLayerShellState,
pub shm_state: ShmState,
pub dmabuf_state: DmabufState,
pub output_manager_state: OutputManagerState,
pub presentation_state: PresentationState,
pub text_input_state: TextInputManagerState,
pub input_method_state: InputMethodManagerState,
pub pointer_constraints_state: PointerConstraintsState,
pub cursor_shape_state: CursorShapeManagerState,
// Desktop abstraction layer
pub space: Space<Window>,
pub popups: PopupManager,
// Seat and input state
pub seat_state: SeatState<Self>,
pub seat: Seat<Self>,
pub pointer_location: Point<f64, Logical>,
pub cursor_image_status: CursorImageStatus,
// Set by the backend after renderer initialisation when DMA-BUF is supported.
pub dmabuf_global: Option<DmabufGlobal>,
// Set to false when the compositor should exit the event loop.
pub running: bool,
}
impl WeftCompositorState {
pub fn new(
display_handle: DisplayHandle,
loop_signal: LoopSignal,
loop_handle: LoopHandle<'static, Self>,
seat_name: String,
) -> Self {
let compositor_state = CompositorState::new::<Self>(&display_handle);
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
let shm_state = ShmState::new::<Self>(&display_handle, vec![]);
let dmabuf_state = DmabufState::new();
let output_manager_state =
OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
// Clock ID 1 = CLOCK_MONOTONIC
let presentation_state = PresentationState::new::<Self>(&display_handle, 1);
let text_input_state = TextInputManagerState::new::<Self>(&display_handle);
let input_method_state =
InputMethodManagerState::new::<Self, _>(&display_handle, |_client| true);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, seat_name);
seat.add_keyboard(XkbConfig::default(), 200, 25)
.expect("no xkb config errors expected with default config");
seat.add_pointer();
seat.add_touch();
Self {
display_handle,
loop_signal,
loop_handle,
compositor_state,
xdg_shell_state,
layer_shell_state,
shm_state,
dmabuf_state,
output_manager_state,
presentation_state,
text_input_state,
input_method_state,
pointer_constraints_state,
cursor_shape_state,
space: Space::default(),
popups: PopupManager::default(),
seat_state,
seat,
pointer_location: Point::from((0.0_f64, 0.0_f64)),
cursor_image_status: CursorImageStatus::Hidden,
dmabuf_global: None,
running: true,
}
}
}
// --- CompositorHandler ---
impl CompositorHandler for WeftCompositorState {
fn compositor_state(&mut self) -> &mut CompositorState {
&mut self.compositor_state
}
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
&client
.get_data::<WeftClientState>()
.expect("client must carry WeftClientState")
.compositor_state
}
fn commit(&mut self, surface: &WlSurface) {
on_commit_buffer_handler::<Self>(surface);
if let Some(window) = self
.space
.elements()
.find(|w| w.wl_surface().as_ref() == Some(surface))
.cloned()
{
window.on_commit();
}
// Re-arrange layer surfaces for any output that contains this surface.
let outputs: Vec<Output> = self
.space
.outputs()
.filter(|o| {
let map = layer_map_for_output(o);
map.layer_for_surface(surface, WindowSurfaceType::ALL)
.is_some()
})
.cloned()
.collect();
for output in outputs {
layer_map_for_output(&output).arrange();
}
}
}
delegate_compositor!(WeftCompositorState);
// --- ShmHandler ---
impl ShmHandler for WeftCompositorState {
fn shm_state(&self) -> &ShmState {
&self.shm_state
}
}
delegate_shm!(WeftCompositorState);
// --- XdgShellHandler ---
impl XdgShellHandler for WeftCompositorState {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.xdg_shell_state
}
fn new_toplevel(&mut self, surface: ToplevelSurface) {
// Send initial configure before wrapping — the toplevel needs a configure to map.
surface.send_configure();
let window = Window::new_wayland_window(surface);
// Map at origin; proper placement policy comes with the shell protocol wave.
self.space.map_element(window, (0, 0), false);
}
fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
surface.with_pending_state(|state| {
state.geometry = positioner.get_geometry();
});
if surface.send_configure().is_ok() {
self.popups.track_popup(PopupKind::Xdg(surface)).ok();
}
}
fn grab(&mut self, _surface: PopupSurface, _seat: smithay::reexports::wayland_server::protocol::wl_seat::WlSeat, _serial: smithay::utils::Serial) {}
}
delegate_xdg_shell!(WeftCompositorState);
// --- WlrLayerShellHandler ---
impl WlrLayerShellHandler for WeftCompositorState {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
&mut self.layer_shell_state
}
fn new_layer_surface(
&mut self,
surface: LayerSurface,
_output: Option<WlOutput>,
_layer: Layer,
_namespace: String,
) {
// Map to the first available output. Proper output matching is deferred to
// the shell protocol wave where the compositor receives explicit placement requests.
if let Some(output) = self.space.outputs().next().cloned() {
layer_map_for_output(&output)
.map_layer(&surface)
.expect("layer surface must not already be mapped");
layer_map_for_output(&output).arrange();
}
}
}
delegate_layer_shell!(WeftCompositorState);
// --- SeatHandler ---
impl SeatHandler for WeftCompositorState {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<Self> {
&mut self.seat_state
}
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
self.cursor_image_status = image;
}
}
delegate_seat!(WeftCompositorState);
// --- DmabufHandler ---
impl DmabufHandler for WeftCompositorState {
fn dmabuf_state(&mut self) -> &mut DmabufState {
&mut self.dmabuf_state
}
fn dmabuf_imported(
&mut self,
_global: &DmabufGlobal,
_dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
notifier: ImportNotifier,
) {
// DMA-BUF import requires the renderer, which lives in the backend run function.
// The backend is responsible for creating the global only when it can service imports.
// If we reach here without a backend handler wired up, reject.
drop(notifier);
}
}
delegate_dmabuf!(WeftCompositorState);
// --- OutputHandler ---
impl smithay::wayland::output::OutputHandler for WeftCompositorState {}
delegate_output!(WeftCompositorState);
// --- PresentationHandler ---
impl PresentationHandler for WeftCompositorState {
fn presentation_state(&mut self) -> &mut PresentationState {
&mut self.presentation_state
}
}
delegate_presentation!(WeftCompositorState);
// --- TextInputHandler ---
impl TextInputHandler for WeftCompositorState {
fn text_input_state(&mut self) -> &mut TextInputManagerState {
&mut self.text_input_state
}
}
delegate_text_input_manager!(WeftCompositorState);
// --- InputMethodHandler ---
impl InputMethodHandler for WeftCompositorState {
fn new_popup(&mut self, _surface: PopupSurface) {}
fn popup_repositioned(&mut self, _surface: PopupSurface) {}
fn popup_done(&mut self, _surface: PopupSurface) {}
fn parent_geometry(&self, parent_surface: &WlSurface) -> Rectangle<i32, Logical> {
self.space
.elements()
.find_map(|w: &Window| {
if w.wl_surface().as_ref() == Some(parent_surface) {
Some(w.geometry())
} else {
None
}
})
.unwrap_or_default()
}
}
delegate_input_method_manager!(WeftCompositorState);
// --- PointerConstraintsHandler ---
impl PointerConstraintsHandler for WeftCompositorState {
fn new_constraint(
&mut self,
_surface: &WlSurface,
_pointer: &smithay::input::pointer::PointerHandle<Self>,
) {
}
}
delegate_pointer_constraints!(WeftCompositorState);
// --- CursorShapeHandler ---
impl CursorShapeHandler for WeftCompositorState {
fn cursor_shape_state(&mut self) -> &mut CursorShapeManagerState {
&mut self.cursor_shape_state
}
}
delegate_cursor_shape!(WeftCompositorState);

View file

@ -1,5 +1,5 @@
$ErrorActionPreference = 'Stop'
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo clippy --workspace --exclude weft-compositor --all-targets -- -D warnings
cargo test --workspace --exclude weft-compositor

View file

@ -2,5 +2,11 @@
set -euo pipefail
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
if [ "$(uname -s)" = "Linux" ]; then
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
else
cargo clippy --workspace --exclude weft-compositor --all-targets -- -D warnings
cargo test --workspace --exclude weft-compositor
fi

View file

@ -0,0 +1,15 @@
[Unit]
Description=WEFT OS Wayland Compositor
Documentation=https://github.com/weft-os/weft
After=systemd-logind.service
[Service]
Type=notify
ExecStart=/packages/system/weft-compositor/active/bin/weft-compositor
Restart=on-failure
RestartSec=1
# The socket path is posted to the environment by the compositor itself.
# Downstream services (servo-shell.service) should declare After=weft-compositor.service.
[Install]
WantedBy=graphical.target