From 56b63cc981099319e9317d36adf2c45ee1982bdf Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 1 Nov 2023 16:22:59 -0700 Subject: [PATCH] WIP subsurface --- examples/sctk_subsurface/Cargo.toml | 13 ++ examples/sctk_subsurface/src/main.rs | 108 +++++++++++ examples/sctk_subsurface/src/wayland.rs | 86 +++++++++ sctk/src/application.rs | 35 ++++ sctk/src/event_loop/mod.rs | 38 ++++ sctk/src/event_loop/state.rs | 4 + sctk/src/handlers/mod.rs | 1 + sctk/src/handlers/subcompositor.rs | 5 + sctk/src/lib.rs | 1 + sctk/src/sctk_event.rs | 21 ++- sctk/src/subsurface_widget.rs | 233 ++++++++++++++++++++++++ 11 files changed, 540 insertions(+), 5 deletions(-) create mode 100644 examples/sctk_subsurface/Cargo.toml create mode 100644 examples/sctk_subsurface/src/main.rs create mode 100644 examples/sctk_subsurface/src/wayland.rs create mode 100644 sctk/src/handlers/subcompositor.rs create mode 100644 sctk/src/subsurface_widget.rs diff --git a/examples/sctk_subsurface/Cargo.toml b/examples/sctk_subsurface/Cargo.toml new file mode 100644 index 0000000000..258a1cc234 --- /dev/null +++ b/examples/sctk_subsurface/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sctk_subsurface" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "a11y"] } +iced_runtime = { path = "../../runtime" } +iced_sctk = { path = "../../sctk" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" diff --git a/examples/sctk_subsurface/src/main.rs b/examples/sctk_subsurface/src/main.rs new file mode 100644 index 0000000000..0019d9f8e0 --- /dev/null +++ b/examples/sctk_subsurface/src/main.rs @@ -0,0 +1,108 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + event::wayland::Event as WaylandEvent, wayland::InitialSurface, + widget::text, window, Application, Command, Element, Length, Subscription, + Theme, +}; +use sctk::reexports::client::{ + protocol::wl_buffer::WlBuffer, Connection, Proxy, +}; + +mod wayland; + +fn main() { + let mut settings = iced::Settings::default(); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + connection: Option, + red_buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + WaylandEvent(WaylandEvent), + Wayland(wayland::Event), + Ignore, +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + type Theme = Theme; + + fn new(_flags: ()) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::WaylandEvent(evt) => match evt { + WaylandEvent::Output(_evt, output) => { + if self.connection.is_none() { + if let Some(backend) = output.backend().upgrade() { + self.connection = + Some(Connection::from_backend(backend)); + } + } + } + _ => {} + }, + Message::Wayland(evt) => match evt { + wayland::Event::RedBuffer(buffer) => { + self.red_buffer = Some(buffer); + } + }, + Message::Ignore => {} + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.red_buffer { + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + let mut subscriptions = + vec![iced::subscription::events_with(|evt, _| { + if let iced::Event::PlatformSpecific( + iced::event::PlatformSpecific::Wayland(evt), + ) = evt + { + Some(Message::WaylandEvent(evt)) + } else { + None + } + })]; + if let Some(connection) = &self.connection { + subscriptions + .push(wayland::subscription(connection).map(Message::Wayland)); + } + Subscription::batch(subscriptions) + } + + fn close_requested(&self, _id: window::Id) -> Self::Message { + Message::Ignore + } +} diff --git a/examples/sctk_subsurface/src/wayland.rs b/examples/sctk_subsurface/src/wayland.rs new file mode 100644 index 0000000000..f7a5ebcd89 --- /dev/null +++ b/examples/sctk_subsurface/src/wayland.rs @@ -0,0 +1,86 @@ +use futures_channel::mpsc; +use iced::futures::{FutureExt, SinkExt}; +use sctk::{ + reexports::{ + calloop_wayland_source::WaylandSource, + client::{ + delegate_noop, + globals::registry_queue_init, + protocol::{wl_buffer::WlBuffer, wl_shm}, + Connection, + }, + }, + registry::{ProvidesRegistryState, RegistryState}, + shm::{raw::RawPool, Shm, ShmHandler}, +}; +use std::thread; + +#[derive(Debug, Clone)] +pub enum Event { + RedBuffer(WlBuffer), +} + +struct AppData { + registry_state: RegistryState, + shm_state: Shm, +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers!(); +} + +impl ShmHandler for AppData { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +pub fn subscription(connection: &Connection) -> iced::Subscription { + let connection = connection.clone(); + iced::subscription::run_with_id( + "wayland-sub", + async { start(connection).await }.flatten_stream(), + ) +} + +async fn start(conn: Connection) -> mpsc::Receiver { + let (mut sender, receiver) = mpsc::channel(20); + + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + shm_state: Shm::bind(&globals, &qh).unwrap(), + }; + + let mut pool = RawPool::new(4, &app_data.shm_state).unwrap(); + let buffer = + pool.create_buffer(0, 1, 1, 4, wl_shm::Format::Xrgb8888, (), &qh); + let data = pool.mmap(); + data[0] = 0; + data[1] = 0; + data[2] = 255; + data[3] = 255; + let _ = sender.send(Event::RedBuffer(buffer)).await; + + thread::spawn(move || { + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + WaylandSource::new(conn, event_queue) + .insert(event_loop.handle()) + .unwrap(); + loop { + event_loop.dispatch(None, &mut app_data).unwrap(); + } + }); + + receiver +} + +delegate_noop!(AppData: ignore WlBuffer); +sctk::delegate_registry!(AppData); +sctk::delegate_shm!(AppData); diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 56491a3879..0796ba5def 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -71,6 +71,8 @@ use raw_window_handle::{ }; use std::mem::ManuallyDrop; +use crate::subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; + pub enum Event { /// A normal sctk event SctkEvent(IcedSctkEvent), @@ -364,6 +366,8 @@ where let mut interfaces = ManuallyDrop::new(HashMap::new()); let mut simple_clipboard = Clipboard::unconnected(); + let mut subsurface_state = None::>; + { run_command( &application, @@ -1373,6 +1377,11 @@ where state.viewport_changed = false; } + // Subsurface list should always be empty before `view` + assert!( + crate::subsurface_widget::take_subsurfaces().is_empty() + ); + debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -1385,6 +1394,17 @@ where state.cursor(), ); + // Update subsurfaces based on what view requested. + let subsurfaces = + crate::subsurface_widget::take_subsurfaces(); + if let Some(subsurface_state) = subsurface_state.as_ref() { + subsurface_state.update_subsurfaces( + &wrapper.wl_surface, + &mut state.subsurfaces, + &subsurfaces, + ); + } + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { mouse_interaction = new_mouse_interaction; @@ -1481,6 +1501,19 @@ where } } } + IcedSctkEvent::Subcompositor { + compositor, + subcompositor, + viewporter, + qh, + } => { + subsurface_state = Some(SubsurfaceState { + wl_compositor: compositor, + wl_subcompositor: subcompositor, + wp_viewporter: viewporter, + qh, + }); + } } } @@ -1612,6 +1645,7 @@ where first: bool, wp_viewport: Option, interface_state: user_interface::State, + subsurfaces: Vec, } impl State @@ -1645,6 +1679,7 @@ where first: true, wp_viewport: None, interface_state: user_interface::State::Outdated, + subsurfaces: Vec::new(), } } diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 8e2acd1dd3..9669213d05 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -33,6 +33,7 @@ use sctk::{ activation::{ActivationState, RequestData}, compositor::CompositorState, data_device_manager::DataDeviceManagerState, + globals::GlobalData, output::OutputState, reexports::{ calloop::{self, EventLoop, PostAction}, @@ -298,6 +299,43 @@ where &mut control_flow, ); + // XXX don't re-bind? + let compositor = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=6, GlobalData) + .unwrap(); + let subcompositor = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let viewporter = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + if let Ok(subcompositor) = subcompositor { + if let Ok(viewporter) = viewporter { + callback( + IcedSctkEvent::Subcompositor { + compositor, + subcompositor, + viewporter, + qh: self.state.queue_handle.clone(), + }, + &self.state, + &mut control_flow, + ); + } else { + tracing::warn!( + "No `wp_viewporter`. Subsurfaces not supported." + ); + } + } else { + tracing::warn!("No `wl_subcompositor`. Subsurfaces not supported."); + } + let mut sctk_event_sink_back_buffer = Vec::new(); let mut compositor_event_back_buffer = Vec::new(); let mut frame_event_back_buffer = Vec::new(); diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index e2b0e017c0..9f3fafaf0d 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -41,10 +41,12 @@ use sctk::{ reexports::{ calloop::{LoopHandle, RegistrationToken}, client::{ + delegate_noop, protocol::{ wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_seat::WlSeat, + wl_subsurface::WlSubsurface, wl_surface::{self, WlSurface}, wl_touch::WlTouch, }, @@ -842,3 +844,5 @@ where } } } + +delegate_noop!(@ SctkState: ignore WlSubsurface); diff --git a/sctk/src/handlers/mod.rs b/sctk/src/handlers/mod.rs index 332d296682..be989e6bf0 100644 --- a/sctk/src/handlers/mod.rs +++ b/sctk/src/handlers/mod.rs @@ -6,6 +6,7 @@ pub mod output; pub mod seat; pub mod session_lock; pub mod shell; +pub mod subcompositor; pub mod wp_fractional_scaling; pub mod wp_viewporter; diff --git a/sctk/src/handlers/subcompositor.rs b/sctk/src/handlers/subcompositor.rs new file mode 100644 index 0000000000..a5c9fdab3a --- /dev/null +++ b/sctk/src/handlers/subcompositor.rs @@ -0,0 +1,5 @@ +use crate::handlers::SctkState; +use sctk::delegate_subcompositor; +use std::fmt::Debug; + +delegate_subcompositor!(@ SctkState); diff --git a/sctk/src/lib.rs b/sctk/src/lib.rs index b90a64c788..ce477a9592 100644 --- a/sctk/src/lib.rs +++ b/sctk/src/lib.rs @@ -9,6 +9,7 @@ mod handlers; pub mod result; pub mod sctk_event; pub mod settings; +pub mod subsurface_widget; #[cfg(feature = "system")] pub mod system; pub mod util; diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 1062ba6769..9dc2b8be9b 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -5,6 +5,7 @@ use crate::{ pointer_button_to_native, }, dpi::PhysicalSize, + event_loop::state::SctkState, }; use iced_futures::core::event::{ @@ -22,13 +23,14 @@ use sctk::{ reexports::client::{ backend::ObjectId, protocol::{ - wl_data_device_manager::DndAction, wl_keyboard::WlKeyboard, - wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, - wl_surface::WlSurface, + wl_compositor::WlCompositor, wl_data_device_manager::DndAction, + wl_keyboard::WlKeyboard, wl_output::WlOutput, + wl_pointer::WlPointer, wl_seat::WlSeat, + wl_subcompositor::WlSubcompositor, wl_surface::WlSurface, }, Proxy, }, - reexports::csd_frame::WindowManagerCapabilities, + reexports::{client::QueueHandle, csd_frame::WindowManagerCapabilities}, seat::{ keyboard::{KeyEvent, Modifiers}, pointer::{PointerEvent, PointerEventKind}, @@ -41,7 +43,9 @@ use sctk::{ }, }; use std::{collections::HashMap, time::Instant}; -use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use wayland_protocols::wp::viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, +}; pub enum IcedSctkEvent { /// Emitted when new events arrive from the OS to be processed. @@ -121,6 +125,13 @@ pub enum IcedSctkEvent { /// Frame callback event Frame(WlSurface), + + Subcompositor { + compositor: WlCompositor, + subcompositor: WlSubcompositor, + viewporter: WpViewporter, + qh: QueueHandle>, + }, } #[derive(Debug, Clone)] diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs new file mode 100644 index 0000000000..066d2995d4 --- /dev/null +++ b/sctk/src/subsurface_widget.rs @@ -0,0 +1,233 @@ +// TODO z-order option? + +use crate::core::{ + layout::{self, Layout}, + mouse, renderer, + widget::{self, Widget}, + ContentFit, Element, Length, Rectangle, Size, +}; +use std::{cell::RefCell, mem}; + +use sctk::compositor::SurfaceData; +use sctk::reexports::client::{ + protocol::{ + wl_buffer::WlBuffer, wl_compositor::WlCompositor, + wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, + wl_surface::WlSurface, + }, + QueueHandle, +}; +use wayland_protocols::wp::viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, +}; + +use crate::event_loop::state::SctkState; + +pub(crate) struct SubsurfaceState { + pub wl_compositor: WlCompositor, + pub wl_subcompositor: WlSubcompositor, + pub wp_viewporter: WpViewporter, + pub qh: QueueHandle>, +} + +impl SubsurfaceState { + pub fn create_subsurface(&self, parent: &WlSurface) -> SubsurfaceInstance { + let wl_surface = self + .wl_compositor + .create_surface(&self.qh, SurfaceData::new(None, 1)); + let wl_subsurface = self.wl_subcompositor.get_subsurface( + &wl_surface, + parent, + &self.qh, + (), + ); + let wp_viewport = self.wp_viewporter.get_viewport( + &wl_surface, + &self.qh, + sctk::globals::GlobalData, + ); + SubsurfaceInstance { + wl_surface, + wl_subsurface, + wp_viewport, + } + } + + // Update `subsurfaces` from `view_subsurfaces` + pub fn update_subsurfaces( + &self, + parent: &WlSurface, + subsurfaces: &mut Vec, + view_subsurfaces: &[SubsurfaceInfo], + ) { + // If view requested fewer subsurfaces than there currently are, + // destroy excess. + if view_subsurfaces.len() < subsurfaces.len() { + subsurfaces.truncate(view_subsurfaces.len()); + } + // Create new subsurfaces if there aren't enough. + while subsurfaces.len() < view_subsurfaces.len() { + subsurfaces.push(self.create_subsurface(parent)); + } + // Attach buffers to subsurfaces, set viewports, and commit. + for (subsurface_data, subsurface) in + view_subsurfaces.iter().zip(subsurfaces.iter()) + { + subsurface.attach_and_commit(subsurface_data); + } + } +} + +pub(crate) struct SubsurfaceInstance { + wl_surface: WlSurface, + wl_subsurface: WlSubsurface, + wp_viewport: WpViewport, +} + +impl SubsurfaceInstance { + fn attach_and_commit(&self, info: &SubsurfaceInfo) { + // XXX scale factor? + self.wl_subsurface + .set_position(info.bounds.x as i32, info.bounds.y as i32); + self.wp_viewport.set_destination( + info.bounds.width as i32, + info.bounds.height as i32, + ); + self.wl_surface.attach(Some(&info.buffer), 0, 0); + self.wl_surface.commit(); + } +} + +impl Drop for SubsurfaceInstance { + fn drop(&mut self) { + self.wp_viewport.destroy(); + self.wl_subsurface.destroy(); + self.wl_surface.destroy(); + } +} + +pub(crate) struct SubsurfaceInfo { + pub buffer: WlBuffer, + pub bounds: Rectangle, +} + +thread_local! { + static SUBSURFACES: RefCell> = RefCell::new(Vec::new()); +} + +pub(crate) fn take_subsurfaces() -> Vec { + SUBSURFACES.with(|subsurfaces| mem::take(&mut *subsurfaces.borrow_mut())) +} + +#[must_use] +pub struct Subsurface<'a> { + buffer_size: Size, + buffer: &'a WlBuffer, + width: Length, + height: Length, + content_fit: ContentFit, +} + +impl<'a, Message, Renderer> Widget for Subsurface<'a> +where + Renderer: renderer::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + // Based on image widget + fn layout( + &self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let raw_size = limits + .width(self.width) + .height(self.height) + .resolve(self.buffer_size); + + let full_size = self.content_fit.fit(self.buffer_size, raw_size); + + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) + } + + fn draw( + &self, + _state: &widget::Tree, + _renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + // Instead of using renderer, we need to add surface to a list that is + // read by the iced-sctk shell. + SUBSURFACES.with(|subsurfaces| { + subsurfaces.borrow_mut().push(SubsurfaceInfo { + buffer: self.buffer.clone(), + bounds: layout.bounds(), + }) + }); + } +} + +impl<'a> Subsurface<'a> { + pub fn new( + buffer_width: u32, + buffer_height: u32, + buffer: &'a WlBuffer, + ) -> Self { + Self { + buffer_size: Size::new(buffer_width as f32, buffer_height as f32), + buffer, + // Matches defaults of image widget + width: Length::Shrink, + height: Length::Shrink, + content_fit: ContentFit::Contain, + } + } + + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: renderer::Renderer, +{ + fn from(subsurface: Subsurface<'a>) -> Self { + Self::new(subsurface) + } +}