diff --git a/examples/sctk_subsurface/Cargo.toml b/examples/sctk_subsurface/Cargo.toml new file mode 100644 index 0000000000..a896a237d5 --- /dev/null +++ b/examples/sctk_subsurface/Cargo.toml @@ -0,0 +1,14 @@ +[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" +rustix = { version = "0.38.30", features = ["fs", "shm"] } diff --git a/examples/sctk_subsurface/src/main.rs b/examples/sctk_subsurface/src/main.rs new file mode 100644 index 0000000000..c26034134a --- /dev/null +++ b/examples/sctk_subsurface/src/main.rs @@ -0,0 +1,100 @@ +// 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 iced_sctk::subsurface_widget::SubsurfaceBuffer; +use sctk::reexports::client::{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), +} + +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, _id: window::Id) -> 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); + } + }, + } + 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::event::listen_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) + } +} diff --git a/examples/sctk_subsurface/src/wayland.rs b/examples/sctk_subsurface/src/wayland.rs new file mode 100644 index 0000000000..c3ba0b46e1 --- /dev/null +++ b/examples/sctk_subsurface/src/wayland.rs @@ -0,0 +1,125 @@ +use futures_channel::mpsc; +use iced::futures::{FutureExt, SinkExt}; +use iced_sctk::subsurface_widget::{Shmbuf, SubsurfaceBuffer}; +use rustix::{io::Errno, shm::ShmOFlags}; +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::{Shm, ShmHandler}, +}; +use std::{ + os::fd::OwnedFd, + sync::Arc, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Clone)] +pub enum Event { + RedBuffer(SubsurfaceBuffer), +} + +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 fd = create_memfile().unwrap(); + rustix::io::write(&fd, &[0, 0, 255, 255]).unwrap(); + + let shmbuf = Shmbuf { + fd, + offset: 0, + width: 1, + height: 1, + stride: 4, + format: wl_shm::Format::Xrgb8888, + }; + + let buffer = SubsurfaceBuffer::new(Arc::new(shmbuf.into())).0; + 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 +} + +fn create_memfile() -> rustix::io::Result { + loop { + let flags = ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR; + + let time = SystemTime::now(); + let name = format!( + "/iced-sctk-{}", + time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + + match rustix::io::retry_on_intr(|| { + rustix::shm::shm_open(&name, flags, 0600.into()) + }) { + Ok(fd) => match rustix::shm::shm_unlink(&name) { + Ok(_) => return Ok(fd), + Err(errno) => { + return Err(errno.into()); + } + }, + Err(Errno::EXIST) => { + continue; + } + Err(err) => return Err(err.into()), + } + } +} + +delegate_noop!(AppData: ignore WlBuffer); +sctk::delegate_registry!(AppData); +sctk::delegate_shm!(AppData); diff --git a/examples/sctk_subsurface_gst/Cargo.toml b/examples/sctk_subsurface_gst/Cargo.toml new file mode 100644 index 0000000000..f83edd45a0 --- /dev/null +++ b/examples/sctk_subsurface_gst/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sctk_subsurface_gst" +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" +gst = { package = "gstreamer", version = "0.21.3" } +gst-app = { package = "gstreamer-app", version = "0.21.2" } +gst-video = { package = "gstreamer-video", version = "0.21.2" } +gst-allocators = { package = "gstreamer-allocators", version = "0.21.2" } +drm-fourcc = "2.2.0" diff --git a/examples/sctk_subsurface_gst/src/main.rs b/examples/sctk_subsurface_gst/src/main.rs new file mode 100644 index 0000000000..09119117bb --- /dev/null +++ b/examples/sctk_subsurface_gst/src/main.rs @@ -0,0 +1,84 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + wayland::InitialSurface, widget::text, window, Application, Command, + Element, Length, Subscription, Theme, +}; +use iced_sctk::subsurface_widget::SubsurfaceBuffer; +use std::{env, path::Path}; + +mod pipewire; + +fn main() { + let args = env::args(); + if args.len() != 2 { + eprintln!("usage: sctk_subsurface_gst [h264 mp4 path]"); + return; + } + let path = args.skip(1).next().unwrap(); + if !Path::new(&path).exists() { + eprintln!("File `{path}` not found."); + return; + } + let mut settings = iced::Settings::with_flags(path); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + path: String, + buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + Pipewire(pipewire::Event), +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = String; + type Theme = Theme; + + fn new(flags: String) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + path: flags, + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::Pipewire(evt) => match evt { + pipewire::Event::Frame(subsurface_buffer) => { + self.buffer = Some(subsurface_buffer); + } + }, + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.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 { + pipewire::subscription(&self.path).map(Message::Pipewire) + } +} diff --git a/examples/sctk_subsurface_gst/src/pipewire.rs b/examples/sctk_subsurface_gst/src/pipewire.rs new file mode 100644 index 0000000000..302f19c4b9 --- /dev/null +++ b/examples/sctk_subsurface_gst/src/pipewire.rs @@ -0,0 +1,111 @@ +use drm_fourcc::{DrmFourcc, DrmModifier}; +use gst::prelude::*; +use iced::futures::{executor::block_on, SinkExt}; +use iced_sctk::subsurface_widget::{Dmabuf, Plane, SubsurfaceBuffer}; +use std::{os::unix::io::BorrowedFd, sync::Arc, thread}; + +#[derive(Debug, Clone)] +pub enum Event { + Frame(SubsurfaceBuffer), +} + +pub fn subscription(path: &str) -> iced::Subscription { + let path = path.to_string(); + iced::subscription::channel("pw", 16, |sender| async { + thread::spawn(move || pipewire_thread(&path, sender)); + std::future::pending().await + }) +} + +fn pipewire_thread( + path: &str, + mut sender: futures_channel::mpsc::Sender, +) { + gst::init().unwrap(); + + // `vapostproc` can be added to convert color format + // TODO had issue on smithay using NV12? + let pipeline = gst::parse_launch(&format!( + "filesrc location={path} ! + qtdemux ! + h264parse ! + vah264dec ! + vapostproc ! + video/x-raw(memory:DMABuf),format=BGRA ! + appsink name=sink", + )) + .unwrap() + .dynamic_cast::() + .unwrap(); + + let appsink = pipeline + .by_name("sink") + .unwrap() + .dynamic_cast::() + .unwrap(); + + let mut subsurface_release = None; + + appsink.set_callbacks( + gst_app::AppSinkCallbacks::builder() + .new_sample(move |appsink| { + let sample = + appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + + let buffer = sample.buffer().unwrap(); + let meta = buffer.meta::().unwrap(); + + let planes = (0..meta.n_planes()) + .map(|plane_idx| { + let memory = buffer + .memory(plane_idx) + .unwrap() + .downcast_memory::() + .unwrap(); + + // TODO avoid dup? + let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) } + .try_clone_to_owned() + .unwrap(); + + Plane { + fd, + plane_idx, + offset: meta.offset()[plane_idx as usize] as u32, + stride: meta.stride()[plane_idx as usize] as u32, + } + }) + .collect(); + + let dmabuf = Dmabuf { + width: meta.width() as i32, + height: meta.height() as i32, + planes, + // TODO should use dmabuf protocol to get supported formats, + // convert if needed. + format: DrmFourcc::Argb8888 as u32, + //format: DrmFourcc::Nv12 as u32, + // TODO modifier negotiation + modifier: DrmModifier::Linear.into(), + }; + + let (buffer, new_subsurface_release) = + SubsurfaceBuffer::new(Arc::new(dmabuf.into())); + block_on(sender.send(Event::Frame(buffer))).unwrap(); + + // Wait for server to release other buffer + // TODO is gstreamer using triple buffering? + if let Some(release) = subsurface_release.take() { + block_on(release); + } + subsurface_release = Some(new_subsurface_release); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline.set_state(gst::State::Playing).unwrap(); + let bus = pipeline.bus().unwrap(); + for _msg in bus.iter_timed(gst::ClockTime::NONE) {} +} diff --git a/sctk/src/application.rs b/sctk/src/application.rs index d3660a2e5b..065f386d94 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -80,6 +80,8 @@ use raw_window_handle::{ }; use std::mem::ManuallyDrop; +use crate::subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; + pub enum Event { /// A normal sctk event SctkEvent(IcedSctkEvent), @@ -363,6 +365,8 @@ where let mut interfaces = ManuallyDrop::new(HashMap::new()); let mut simple_clipboard = Clipboard::unconnected(); + let mut subsurface_state = None::>; + { run_command( &application, @@ -893,6 +897,9 @@ where ); state.synchronize(&application); + // Subsurface list should always be empty before `view` + assert!(crate::subsurface_widget::take_subsurfaces().is_empty()); + // just draw here immediately and never again for dnd icons // TODO handle scale factor? let _new_mouse_interaction = user_interface.draw( @@ -906,6 +913,15 @@ where state.cursor(), ); + let subsurfaces = crate::subsurface_widget::take_subsurfaces(); + if let Some(subsurface_state) = subsurface_state.as_ref() { + subsurface_state.update_subsurfaces( + &state.wrapper.wl_surface, + &mut state.subsurfaces, + &subsurfaces, + ); + } + let _ = compositor.present( &mut renderer, state.surface.as_mut().unwrap(), @@ -1354,6 +1370,12 @@ where debug.layout_finished(); 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, @@ -1366,6 +1388,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( + &state.wrapper.wl_surface, + &mut state.subsurfaces, + &subsurfaces, + ); + } + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { mouse_interaction = new_mouse_interaction; @@ -1462,6 +1495,9 @@ where } } } + IcedSctkEvent::Subcompositor(state) => { + subsurface_state = Some(state); + } } } @@ -1598,6 +1634,7 @@ where interface_state: user_interface::State, surface: Option, wrapper: SurfaceDisplayWrapper, + subsurfaces: Vec, } impl State @@ -1636,6 +1673,7 @@ where interface_state: user_interface::State::Outdated, surface: None, wrapper, + subsurfaces: Vec::new(), } } diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 8e2acd1dd3..e728dd7b95 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -20,6 +20,7 @@ use crate::{ PopupEventVariant, SctkEvent, StartCause, WindowEventVariant, }, settings, + subsurface_widget::SubsurfaceState, }; use iced_futures::core::window::Mode; use iced_runtime::command::platform_specific::{ @@ -33,6 +34,7 @@ use sctk::{ activation::{ActivationState, RequestData}, compositor::CompositorState, data_device_manager::DataDeviceManagerState, + globals::GlobalData, output::OutputState, reexports::{ calloop::{self, EventLoop, PostAction}, @@ -298,6 +300,55 @@ where &mut control_flow, ); + // XXX don't re-bind? + let wl_compositor = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=6, GlobalData) + .unwrap(); + let wl_subcompositor = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let wp_viewporter = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let wl_shm = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=1, GlobalData) + .unwrap(); + let wp_dmabuf = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 2..=4, GlobalData) + .ok(); + if let Ok(wl_subcompositor) = wl_subcompositor { + if let Ok(wp_viewporter) = wp_viewporter { + callback( + IcedSctkEvent::Subcompositor(SubsurfaceState { + wl_compositor, + wl_subcompositor, + wp_viewporter, + wl_shm, + wp_dmabuf, + 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 48520f8889..beed0700d6 100644 --- a/sctk/src/lib.rs +++ b/sctk/src/lib.rs @@ -10,6 +10,7 @@ pub mod keymap; 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 f2ddaa51d6..f67cd8e241 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -5,6 +5,7 @@ use crate::{ }, dpi::PhysicalSize, keymap::{self, keysym_to_key}, + subsurface_widget::SubsurfaceState, }; use iced_futures::core::event::{ @@ -122,6 +123,8 @@ pub enum IcedSctkEvent { /// Frame callback event Frame(WlSurface), + + Subcompositor(SubsurfaceState), } #[derive(Debug, Clone)] diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs new file mode 100644 index 0000000000..699ae9e5a6 --- /dev/null +++ b/sctk/src/subsurface_widget.rs @@ -0,0 +1,519 @@ +// TODO z-order option? + +use crate::core::{ + layout::{self, Layout}, + mouse, renderer, + widget::{self, Widget}, + ContentFit, Element, Length, Rectangle, Size, +}; +use std::{ + cell::RefCell, + future::Future, + mem, + os::unix::io::{AsFd, OwnedFd}, + pin::Pin, + sync::Arc, + task, +}; + +use futures::channel::oneshot; +use sctk::{ + compositor::SurfaceData, + globals::GlobalData, + reexports::client::{ + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_compositor::WlCompositor, + wl_shm::{self, WlShm}, + wl_shm_pool::{self, WlShmPool}, + wl_subcompositor::WlSubcompositor, + wl_subsurface::WlSubsurface, + wl_surface::WlSurface, + }, + Connection, Dispatch, Proxy, QueueHandle, + }, +}; +use wayland_protocols::wp::{ + linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, + }, + viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, + }, +}; + +use crate::event_loop::state::SctkState; + +#[derive(Debug)] +pub struct Plane { + pub fd: OwnedFd, + pub plane_idx: u32, + pub offset: u32, + pub stride: u32, +} + +#[derive(Debug)] +pub struct Dmabuf { + pub width: i32, + pub height: i32, + pub planes: Vec, + pub format: u32, + pub modifier: u64, +} + +#[derive(Debug)] +pub struct Shmbuf { + pub fd: OwnedFd, + pub offset: i32, + pub width: i32, + pub height: i32, + pub stride: i32, + pub format: wl_shm::Format, +} + +#[derive(Debug)] +pub enum BufferSource { + Shm(Shmbuf), + Dma(Dmabuf), +} + +impl From for BufferSource { + fn from(buf: Shmbuf) -> Self { + Self::Shm(buf) + } +} + +impl From for BufferSource { + fn from(buf: Dmabuf) -> Self { + Self::Dma(buf) + } +} + +#[derive(Debug)] +struct SubsurfaceBufferInner { + source: Arc, + _sender: oneshot::Sender<()>, +} + +/// Refcounted type containing a `BufferSource` with a sender that is signaled +/// all references are dropped and `wl_buffer`s created from the source are +/// released. +#[derive(Clone, Debug)] +pub struct SubsurfaceBuffer(Arc); + +pub struct BufferData { + source: SubsurfaceBuffer, +} + +/// Future signalled when subsurface buffer is released +pub struct SubsurfaceBufferRelease(oneshot::Receiver<()>); + +impl SubsurfaceBufferRelease { + /// Non-blocking check if buffer is released yet, without awaiting + pub fn released(&mut self) -> bool { + self.0.try_recv() == Ok(None) + } +} + +impl Future for SubsurfaceBufferRelease { + type Output = (); + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut task::Context, + ) -> task::Poll<()> { + Pin::new(&mut self.0).poll(cx).map(|_| ()) + } +} + +impl SubsurfaceBuffer { + pub fn new(source: Arc) -> (Self, SubsurfaceBufferRelease) { + let (_sender, receiver) = oneshot::channel(); + let subsurface_buffer = + SubsurfaceBuffer(Arc::new(SubsurfaceBufferInner { + source, + _sender, + })); + (subsurface_buffer, SubsurfaceBufferRelease(receiver)) + } + + // Behavior of `wl_buffer::released` is undefined if attached to multiple surfaces. To allow + // things like that, create a new `wl_buffer` each time. + fn create_buffer( + &self, + shm: &WlShm, + dmabuf: Option<&ZwpLinuxDmabufV1>, + qh: &QueueHandle>, + ) -> Option { + // create reference to source, that is dropped on release + match self.0.source.as_ref() { + BufferSource::Shm(buf) => { + let pool = shm.create_pool( + buf.fd.as_fd(), + buf.offset + buf.height * buf.stride, + qh, + GlobalData, + ); + let buffer = pool.create_buffer( + buf.offset, + buf.width, + buf.height, + buf.stride, + buf.format, + qh, + BufferData { + source: self.clone(), + }, + ); + pool.destroy(); + Some(buffer) + } + BufferSource::Dma(buf) => { + if let Some(dmabuf) = dmabuf { + let params = dmabuf.create_params(qh, GlobalData); + for plane in &buf.planes { + let modifier_hi = (buf.modifier >> 32) as u32; + let modifier_lo = (buf.modifier & 0xffffffff) as u32; + params.add( + plane.fd.as_fd(), + plane.plane_idx, + plane.offset, + plane.stride, + modifier_hi, + modifier_lo, + ); + } + // Will cause protocol error if format is not supported + Some(params.create_immed( + buf.width, + buf.height, + buf.format, + zwp_linux_buffer_params_v1::Flags::empty(), + qh, + BufferData { + source: self.clone(), + }, + )) + } else { + None + } + } + } + } + + fn for_buffer(buffer: &WlBuffer) -> Option<&Self> { + Some(&buffer.data::()?.source) + } +} + +impl PartialEq for SubsurfaceBuffer { + fn eq(&self, rhs: &Self) -> bool { + Arc::ptr_eq(&self.0, &rhs.0) + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlShmPool, + _: wl_shm_pool::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + unreachable!() + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxDmabufV1, + _: zwp_linux_dmabuf_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxBufferParamsV1, + _: zwp_linux_buffer_params_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlBuffer, + event: wl_buffer::Event, + _: &BufferData, + _: &Connection, + _: &QueueHandle>, + ) { + match event { + wl_buffer::Event::Release => {} + _ => unreachable!(), + } + } +} + +// create wl_buffer from BufferSource (avoid create_immed?) +// release +#[doc(hidden)] +pub struct SubsurfaceState { + pub wl_compositor: WlCompositor, + pub wl_subcompositor: WlSubcompositor, + pub wp_viewporter: WpViewporter, + pub wl_shm: WlShm, + pub wp_dmabuf: Option, + pub qh: QueueHandle>, +} + +impl SubsurfaceState { + 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, + wl_buffer: None, + } + } + + // Update `subsurfaces` from `view_subsurfaces` + pub(crate) 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_mut()) + { + subsurface.attach_and_commit( + subsurface_data, + &self.wl_shm, + self.wp_dmabuf.as_ref(), + &self.qh, + ); + } + } +} + +pub(crate) struct SubsurfaceInstance { + wl_surface: WlSurface, + wl_subsurface: WlSubsurface, + wp_viewport: WpViewport, + wl_buffer: Option, +} + +impl SubsurfaceInstance { + // TODO correct damage? no damage/commit if unchanged? + fn attach_and_commit( + &mut self, + info: &SubsurfaceInfo, + shm: &WlShm, + dmabuf: Option<&ZwpLinuxDmabufV1>, + qh: &QueueHandle>, + ) { + let buffer = match self.wl_buffer.take() { + Some(buffer) + if SubsurfaceBuffer::for_buffer(&buffer) + == Some(&info.buffer) => + { + // Same buffer is already attached to this subsurface. Don't create new `wl_buffer`. + buffer + } + buffer => { + if let Some(buffer) = buffer { + buffer.destroy(); + } + if let Some(buffer) = info.buffer.create_buffer(shm, dmabuf, qh) + { + buffer + } else { + // TODO log error + self.wl_surface.attach(None, 0, 0); + return; + } + } + }; + + // 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(&buffer), 0, 0); + self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); + self.wl_surface.commit(); + + self.wl_buffer = Some(buffer); + } +} + +impl Drop for SubsurfaceInstance { + fn drop(&mut self) { + self.wp_viewport.destroy(); + self.wl_subsurface.destroy(); + self.wl_surface.destroy(); + if let Some(wl_buffer) = self.wl_buffer.as_ref() { + wl_buffer.destroy(); + } + } +} + +pub(crate) struct SubsurfaceInfo { + pub buffer: SubsurfaceBuffer, + 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 SubsurfaceBuffer, + width: Length, + height: Length, + content_fit: ContentFit, +} + +impl<'a, Message, Theme, Renderer> Widget + for Subsurface<'a> +where + Renderer: renderer::Renderer, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + // Based on image widget + fn layout( + &self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let raw_size = + limits.resolve(self.width, self.height, 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: &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 SubsurfaceBuffer, + ) -> 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, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Renderer: renderer::Renderer, +{ + fn from(subsurface: Subsurface<'a>) -> Self { + Self::new(subsurface) + } +}