feat: add nested Phase 0 compositor

This commit is contained in:
Samuel Onoja committed 2026-09-24 05:38:18 +01:00
1 parent aab447ef25
commit 475d433ec4
8 files changed
+2710 -2

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
use smithay::{
backend::renderer::utils::on_commit_buffer_handler,
reexports::wayland_server::{
Client,
protocol::{wl_buffer, wl_surface::WlSurface},
},
wayland::{
buffer::BufferHandler,
compositor::{
CompositorClientState, CompositorHandler, CompositorState, get_parent,
is_sync_subsurface,
},
shm::{ShmHandler, ShmState},
},
};
use super::xdg_shell;
use crate::{Ferese, state::ClientState};
impl CompositorHandler for Ferese {
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::<ClientState>()
.expect("all clients have Ferese client state")
.compositor_state
}
fn commit(&mut self, surface: &WlSurface) {
on_commit_buffer_handler::<Self>(surface);
if !is_sync_subsurface(surface) {
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
if let Some(window) = self.space.elements().find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
}) {
window.on_commit();
}
}
xdg_shell::handle_commit(&mut self.popups, &self.space, surface);
}
}
impl BufferHandler for Ferese {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
impl ShmHandler for Ferese {
fn shm_state(&self) -> &ShmState {
&self.shm_state
}
}
+30
View File
@@ -0,0 +1,30 @@
mod compositor;
mod xdg_shell;
use smithay::{
input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus},
reexports::wayland_server::protocol::wl_surface::WlSurface,
wayland::output::OutputHandler,
};
use crate::Ferese;
impl SeatHandler for Ferese {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<Self> {
&mut self.seat_state
}
fn cursor_image(&mut self, _seat: &Seat<Self>, _image: CursorImageStatus) {}
}
impl OutputHandler for Ferese {}
smithay::delegate_compositor!(Ferese);
smithay::delegate_output!(Ferese);
smithay::delegate_seat!(Ferese);
smithay::delegate_shm!(Ferese);
smithay::delegate_xdg_shell!(Ferese);
+129
View File
@@ -0,0 +1,129 @@
use smithay::{
desktop::{
PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords,
},
reexports::wayland_server::protocol::{wl_seat, wl_surface::WlSurface},
utils::Serial,
wayland::{
compositor::with_states,
shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
XdgToplevelSurfaceData,
},
},
};
use crate::Ferese;
impl XdgShellHandler for Ferese {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.xdg_shell_state
}
fn new_toplevel(&mut self, surface: ToplevelSurface) {
self.space
.map_element(Window::new_wayland_window(surface), (0, 0), true);
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
self.unconstrain_popup(&surface);
if let Err(error) = self.popups.track_popup(PopupKind::Xdg(surface)) {
tracing::warn!(?error, "failed to track xdg popup");
}
}
fn reposition_request(
&mut self,
surface: PopupSurface,
positioner: PositionerState,
token: u32,
) {
surface.with_pending_state(|state| {
state.geometry = positioner.get_geometry();
state.positioner = positioner;
});
self.unconstrain_popup(&surface);
surface.send_repositioned(token);
}
fn move_request(&mut self, _: ToplevelSurface, _: wl_seat::WlSeat, _: Serial) {}
fn resize_request(
&mut self,
_: ToplevelSurface,
_: wl_seat::WlSeat,
_: Serial,
_: smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge,
) {
}
fn grab(&mut self, _: PopupSurface, _: wl_seat::WlSeat, _: Serial) {}
}
pub fn handle_commit(popups: &mut PopupManager, space: &Space<Window>, surface: &WlSurface) {
if let Some(window) = space
.elements()
.find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
})
.cloned()
{
let configured = with_states(surface, |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("xdg toplevel state exists")
.lock()
.expect("xdg toplevel state is not poisoned")
.initial_configure_sent
});
if !configured {
window
.toplevel()
.expect("mapped window has a toplevel")
.send_configure();
}
}
popups.commit(surface);
if let Some(PopupKind::Xdg(popup)) = popups.find_popup(surface)
&& !popup.is_initial_configure_sent()
{
popup
.send_configure()
.expect("initial popup configure is valid");
}
}
impl Ferese {
fn unconstrain_popup(&self, popup: &PopupSurface) {
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
return;
};
let Some(window) = self.space.elements().find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
}) else {
return;
};
let Some(output) = self.space.outputs().next() else {
return;
};
let Some(output_geometry) = self.space.output_geometry(output) else {
return;
};
let Some(window_geometry) = self.space.element_geometry(window) else {
return;
};
let mut target = output_geometry;
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
target.loc -= window_geometry.loc;
popup.with_pending_state(|state| {
state.geometry = state.positioner.get_unconstrained_geometry(target);
});
}
}
+125
View File
@@ -0,0 +1,125 @@
use smithay::{
backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent,
},
input::{
keyboard::FilterResult,
pointer::{AxisFrame, ButtonEvent, MotionEvent},
},
reexports::wayland_server::protocol::wl_surface::WlSurface,
utils::{SERIAL_COUNTER, Serial},
};
use crate::Ferese;
impl Ferese {
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
match event {
InputEvent::Keyboard { event, .. } => {
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
keyboard.input::<(), _>(
self,
event.key_code(),
event.state(),
SERIAL_COUNTER.next_serial(),
Event::time(&event) as u32,
|_, _, _| FilterResult::Forward,
);
}
InputEvent::PointerMotionAbsolute { event, .. } => {
let Some(output) = self.space.outputs().next() else {
return;
};
let Some(geometry) = self.space.output_geometry(output) else {
return;
};
let position = event.position_transformed(geometry.size) + geometry.loc.to_f64();
let pointer = self.seat.get_pointer().expect("seat has a pointer");
pointer.motion(
self,
self.surface_under(position),
&MotionEvent {
location: position,
serial: SERIAL_COUNTER.next_serial(),
time: event.time() as u32,
},
);
pointer.frame(self);
}
InputEvent::PointerButton { event, .. } => {
let pointer = self.seat.get_pointer().expect("seat has a pointer");
let serial = SERIAL_COUNTER.next_serial();
if event.state() == ButtonState::Pressed && !pointer.is_grabbed() {
self.focus_window_under_pointer(serial);
}
pointer.button(
self,
&ButtonEvent {
button: event.button_code(),
state: event.state(),
serial,
time: event.time() as u32,
},
);
pointer.frame(self);
}
InputEvent::PointerAxis { event, .. } => {
let source = event.source();
let horizontal = event.amount(Axis::Horizontal).unwrap_or_else(|| {
event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0
});
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
});
let mut frame = AxisFrame::new(event.time() as u32).source(source);
if horizontal != 0.0 {
frame = frame.value(Axis::Horizontal, horizontal);
if let Some(value) = event.amount_v120(Axis::Horizontal) {
frame = frame.v120(Axis::Horizontal, value as i32);
}
}
if vertical != 0.0 {
frame = frame.value(Axis::Vertical, vertical);
if let Some(value) = event.amount_v120(Axis::Vertical) {
frame = frame.v120(Axis::Vertical, value as i32);
}
}
if source == AxisSource::Finger {
if event.amount(Axis::Horizontal) == Some(0.0) {
frame = frame.stop(Axis::Horizontal);
}
if event.amount(Axis::Vertical) == Some(0.0) {
frame = frame.stop(Axis::Vertical);
}
}
let pointer = self.seat.get_pointer().expect("seat has a pointer");
pointer.axis(self, frame);
pointer.frame(self);
}
_ => {}
}
}
fn focus_window_under_pointer(&mut self, serial: Serial) {
let pointer = self.seat.get_pointer().expect("seat has a pointer");
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
if let Some((window, _)) = self.space.element_under(pointer.current_location()) {
let window = window.clone();
self.space.raise_element(&window, true);
let surface = window
.toplevel()
.expect("mapped window has a toplevel")
.wl_surface()
.clone();
keyboard.set_focus(self, Some(surface), serial);
} else {
keyboard.set_focus(self, Option::<WlSurface>::None, serial);
}
self.space.elements().for_each(|window| {
if let Some(toplevel) = window.toplevel() {
toplevel.send_pending_configure();
}
});
}
}
+48 -2
View File
@@ -1,3 +1,49 @@
fn main() {
println!("Ferese Phase 0 scaffold");
#![allow(irrefutable_let_patterns)]
mod handlers;
mod input;
mod state;
mod winit;
use std::{error::Error, process::Command};
use smithay::reexports::{calloop::EventLoop, wayland_server::Display};
pub use state::Ferese;
use tracing::{info, warn};
fn main() -> Result<(), Box<dyn Error>> {
init_logging();
let mut event_loop: EventLoop<Ferese> = EventLoop::try_new()?;
let display: Display<Ferese> = Display::new()?;
let mut state = Ferese::new(&mut event_loop, display)?;
winit::init(&mut event_loop, &mut state)?;
// SAFETY: the backend has already read the host display and Ferese is
// single-threaded before the event loop and optional child start.
unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) };
info!(socket = ?state.socket_name, "Ferese is accepting Wayland clients");
spawn_client_from_args();
event_loop.run(None, &mut state, |_| {})?;
Ok(())
}
fn init_logging() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("ferese=info"));
tracing_subscriber::fmt().with_env_filter(filter).init();
}
fn spawn_client_from_args() {
let mut args = std::env::args_os().skip(1);
let Some(program) = args.next() else {
info!("no client requested; pass one after `--`, for example `-- foot`");
return;
};
match Command::new(&program).args(args).spawn() {
Ok(child) => info!(program = ?program, pid = child.id(), "spawned nested client"),
Err(error) => warn!(program = ?program, %error, "failed to spawn nested client"),
}
}
+122
View File
@@ -0,0 +1,122 @@
use std::{error::Error, ffi::OsString, sync::Arc, time::Instant};
use smithay::{
desktop::{PopupManager, Space, Window, WindowSurfaceType},
input::{Seat, SeatState},
reexports::{
calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic},
wayland_server::{
Display, DisplayHandle,
backend::{ClientData, ClientId, DisconnectReason},
protocol::wl_surface::WlSurface,
},
},
utils::{Logical, Point},
wayland::{
compositor::{CompositorClientState, CompositorState},
output::OutputManagerState,
shell::xdg::XdgShellState,
shm::ShmState,
socket::ListeningSocketSource,
},
};
pub struct Ferese {
pub start_time: Instant,
pub socket_name: OsString,
pub display_handle: DisplayHandle,
pub loop_signal: LoopSignal,
pub space: Space<Window>,
pub popups: PopupManager,
pub seat: Seat<Self>,
pub compositor_state: CompositorState,
pub output_manager_state: OutputManagerState,
pub seat_state: SeatState<Self>,
pub shm_state: ShmState,
pub xdg_shell_state: XdgShellState,
}
impl Ferese {
pub fn new(
event_loop: &mut EventLoop<Self>,
display: Display<Self>,
) -> Result<Self, Box<dyn Error>> {
let display_handle = display.handle();
let compositor_state = CompositorState::new::<Self>(&display_handle);
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit");
seat.add_keyboard(Default::default(), 200, 25)?;
seat.add_pointer();
let socket_name = Self::init_wayland_listener(display, event_loop)?;
Ok(Self {
start_time: Instant::now(),
socket_name,
display_handle,
loop_signal: event_loop.get_signal(),
space: Space::default(),
popups: PopupManager::default(),
seat,
compositor_state,
output_manager_state,
seat_state,
shm_state,
xdg_shell_state,
})
}
fn init_wayland_listener(
display: Display<Self>,
event_loop: &mut EventLoop<Self>,
) -> Result<OsString, Box<dyn Error>> {
let listening_socket = ListeningSocketSource::new_auto()?;
let socket_name = listening_socket.socket_name().to_os_string();
let loop_handle = event_loop.handle();
loop_handle.insert_source(listening_socket, |client_stream, _, state| {
if let Err(error) = state
.display_handle
.insert_client(client_stream, Arc::new(ClientState::default()))
{
tracing::warn!(%error, "failed to register Wayland client");
}
})?;
loop_handle.insert_source(
Generic::new(display, Interest::READ, Mode::Level),
|_, display, state| {
// SAFETY: this event source owns the display for the loop lifetime.
unsafe { display.get_mut().dispatch_clients(state)? };
Ok(PostAction::Continue)
},
)?;
Ok(socket_name)
}
pub fn surface_under(
&self,
position: Point<f64, Logical>,
) -> Option<(WlSurface, Point<f64, Logical>)> {
self.space
.element_under(position)
.and_then(|(window, location)| {
window
.surface_under(position - location.to_f64(), WindowSurfaceType::ALL)
.map(|(surface, surface_location)| {
(surface, (surface_location + location).to_f64())
})
})
}
}
#[derive(Default)]
pub struct ClientState {
pub compositor_state: CompositorClientState,
}
impl ClientData for ClientState {
fn initialized(&self, _client_id: ClientId) {}
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
}
+106
View File
@@ -0,0 +1,106 @@
use std::{error::Error, time::Duration};
use smithay::{
backend::{
renderer::{
damage::OutputDamageTracker, element::surface::WaylandSurfaceRenderElement,
gles::GlesRenderer,
},
winit::{self, WinitEvent},
},
output::{Mode, Output, PhysicalProperties, Subpixel},
reexports::calloop::EventLoop,
utils::{Rectangle, Transform},
};
use crate::Ferese;
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
let (mut backend, event_source) = winit::init()?;
let mode = Mode {
size: backend.window_size(),
refresh: 60_000,
};
let output = Output::new(
"ferese-winit".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "Ferese".into(),
model: "Nested".into(),
},
);
output.create_global::<Ferese>(&state.display_handle);
output.change_current_state(
Some(mode),
Some(Transform::Flipped180),
None,
Some((0, 0).into()),
);
output.set_preferred(mode);
state.space.map_output(&output, (0, 0));
let mut damage_tracker = OutputDamageTracker::from_output(&output);
event_loop
.handle()
.insert_source(event_source, move |event, _, state| match event {
WinitEvent::Resized { size, .. } => output.change_current_state(
Some(Mode {
size,
refresh: 60_000,
}),
None,
None,
None,
),
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Redraw => {
let damage = Rectangle::from_size(backend.window_size());
let rendered = (|| -> Result<(), Box<dyn Error>> {
{
let (renderer, mut framebuffer) = backend.bind()?;
smithay::desktop::space::render_output::<
_,
WaylandSurfaceRenderElement<GlesRenderer>,
_,
_,
>(
&output,
renderer,
&mut framebuffer,
1.0,
0,
[&state.space],
&[],
&mut damage_tracker,
[0.035, 0.04, 0.055, 1.0],
)?;
}
backend.submit(Some(&[damage]))?;
Ok(())
})();
if let Err(error) = rendered {
tracing::error!(%error, "nested renderer failed");
state.loop_signal.stop();
return;
}
state.space.elements().for_each(|window| {
window.send_frame(
&output,
state.start_time.elapsed(),
Some(Duration::ZERO),
|_, _| Some(output.clone()),
);
});
state.space.refresh();
state.popups.cleanup();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush clients");
}
backend.window().request_redraw();
}
WinitEvent::CloseRequested => state.loop_signal.stop(),
_ => {}
})?;
Ok(())
}