feat: subsurfaces

This commit is contained in:
Ashley Wulber 2025-03-14 11:16:25 -04:00 committed by Ashley Wulber
parent 0f37c9922d
commit 93bc4bbd88
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
33 changed files with 1898 additions and 2651 deletions

View file

@ -162,6 +162,7 @@ impl Window {
scale_input: "1.0".to_string(),
current_scale: 1.0,
theme: Theme::ALL[count % Theme::ALL.len()].clone(),
input_id: text_input::Id::unique(),
}
}

View file

@ -15,6 +15,6 @@ iced = { path = "../..", default-features = false, features = [
iced_runtime = { path = "../../runtime" }
env_logger = "0.10"
futures-channel = "0.3.29"
calloop = "0.12.3"
calloop = "0.13"
rustix = { version = "0.38.30", features = ["fs", "shm"] }
cctk.workspace = true

View file

@ -1,14 +1,24 @@
// Shows a subsurface with a 1x1 px red buffer, stretch to window size
use cctk::sctk::reexports::{
client::{Connection, Proxy},
protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity},
};
use iced::platform_specific::shell::commands::subsurface::get_subsurface;
use iced::{
event::wayland::Event as WaylandEvent,
platform_specific::shell::subsurface_widget::{self, SubsurfaceBuffer},
widget::text,
platform_specific::{
runtime::wayland::subsurface::SctkSubsurfaceSettings,
shell::subsurface_widget::{self, SubsurfaceBuffer},
},
widget::{button, column, text, text_input},
window::{self, Id, Settings},
Element, Length, Subscription, Task,
};
use cctk::sctk::reexports::client::{Connection, Proxy};
use std::sync::{Arc, Mutex};
mod subsurface_container;
mod wayland;
fn main() -> iced::Result {
@ -23,8 +33,11 @@ fn main() -> iced::Result {
#[derive(Debug, Clone, Default)]
struct SubsurfaceApp {
text: Arc<Mutex<String>>,
counter: Arc<Mutex<u32>>,
connection: Option<Connection>,
red_buffer: Option<SubsurfaceBuffer>,
green_buffer: Option<SubsurfaceBuffer>,
}
#[derive(Debug, Clone)]
@ -33,6 +46,8 @@ pub enum Message {
Wayland(wayland::Event),
Pressed(&'static str),
Id(Id),
Inc,
Text(String),
}
impl SubsurfaceApp {
@ -55,47 +70,106 @@ impl SubsurfaceApp {
fn update(&mut self, message: Message) -> Task<Message> {
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::WaylandEvent(evt) => {
dbg!(&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);
}
wayland::Event::GreenBuffer(buffer) => {
self.green_buffer = Some(buffer);
}
},
Message::Pressed(side) => println!("{side} surface pressed"),
Message::Id(_) => {}
Message::Id(id) => {
let my_text = self.text.clone();
let my_counter = self.counter.clone();
return get_subsurface(SctkSubsurfaceSettings {
id: window::Id::unique(),
parent: id,
loc: iced::Point::new(100., 200.),
size: Some(iced::Size::new(100., 100.)),
z: 1000,
steal_keyboard_focus: false,
gravity: Gravity::BottomRight,
input_zone: None,
offset: (0, 0),
});
}
Message::Inc => {
let mut guard = self.counter.lock().unwrap();
*guard += 1;
}
Message::Text(s) => {
let mut guard = self.text.lock().unwrap();
*guard = s;
}
}
Task::none()
}
fn view(&self, _id: window::Id) -> Element<Message> {
if let Some(buffer) = &self.red_buffer {
iced::widget::row![
iced::widget::button(
subsurface_widget::Subsurface::new(1, 1, buffer)
.width(Length::Fill)
.height(Length::Fill)
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Pressed("left")),
iced::widget::button(
subsurface_widget::Subsurface::new(1, 1, buffer)
.width(Length::Fill)
.height(Length::Fill)
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Pressed("right"))
fn view(&self, id: window::Id) -> Element<Message> {
let my_text_guard = self.text.lock().unwrap();
if let Some((red_buffer, green_buffer)) =
self.red_buffer.iter().zip(self.green_buffer.iter()).next()
{
column![
iced::widget::row![
iced::widget::button(
subsurface_container::SubsurfaceContainer::new()
.width(Length::Fill)
.height(Length::Fill)
.push(
subsurface_widget::Subsurface::new(
red_buffer.clone()
)
.width(Length::Fill)
.height(Length::Fill)
.z(0)
)
.push(
subsurface_widget::Subsurface::new(
green_buffer.clone()
)
.width(Length::Fixed(1920.))
.height(Length::Fixed(200.))
.z(1)
)
.push(
subsurface_widget::Subsurface::new(
red_buffer.clone()
)
.width(Length::Fill)
.height(Length::Fixed(100.))
.z(2)
)
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Pressed("left")),
iced::widget::button(
subsurface_widget::Subsurface::new(red_buffer.clone())
.width(Length::Fill)
.height(Length::Fill)
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Pressed("right"))
],
text_input("asdf", &my_text_guard).on_input(Message::Text)
]
.into()
} else {
@ -106,10 +180,13 @@ impl SubsurfaceApp {
fn subscription(&self) -> Subscription<Message> {
let mut subscriptions = vec![iced::event::listen_with(|evt, _, _| {
if let iced::Event::PlatformSpecific(
iced::event::PlatformSpecific::Wayland(evt),
iced::event::PlatformSpecific::Wayland(WaylandEvent::Output(
evt,
output,
)),
) = evt
{
Some(Message::WaylandEvent(evt))
Some(Message::WaylandEvent(WaylandEvent::Output(evt, output)))
} else {
None
}

View file

@ -0,0 +1,430 @@
//! Distribute content vertically.
use iced::core::alignment::{self, Alignment};
use iced::core::event::{self, Event};
use iced::core::layout;
use iced::core::mouse;
use iced::core::overlay;
use iced::core::renderer;
use iced::core::widget::{Operation, Tree};
use iced::core::{
Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell,
Size, Vector, Widget,
};
/// A container that distributes its contents vertically.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::{button, SubsurfaceContainer};
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// SubsurfaceContainer![
/// "I am on top!",
/// button("I am in the center!"),
/// "I am below.",
/// ].into()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct SubsurfaceContainer<
'a,
Message,
Theme = iced::Theme,
Renderer = iced::Renderer,
> {
spacing: f32,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
impl<'a, Message, Theme, Renderer>
SubsurfaceContainer<'a, Message, Theme, Renderer>
where
Renderer: iced::core::Renderer,
{
/// Creates an empty [`SubsurfaceContainer`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`SubsurfaceContainer`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`SubsurfaceContainer`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`SubsurfaceContainer`] from an already allocated [`Vec`].
///
/// Keep in mind that the [`SubsurfaceContainer`] will not inspect the [`Vec`], which means
/// it won't automatically adapt to the sizing strategy of its contents.
///
/// If any of the children have a [`Length::Fill`] strategy, you will need to
/// call [`SubsurfaceContainer::width`] or [`SubsurfaceContainer::height`] accordingly.
pub fn from_vec(
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
align: Alignment::Start,
clip: false,
children,
}
}
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the [`Padding`] of the [`SubsurfaceContainer`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the width of the [`SubsurfaceContainer`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`SubsurfaceContainer`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the maximum width of the [`SubsurfaceContainer`].
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = max_width.into().0;
self
}
/// Sets the horizontal alignment of the contents of the [`SubsurfaceContainer`] .
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
self.align = Alignment::from(align.into());
self
}
/// Sets whether the contents of the [`SubsurfaceContainer`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Adds an element to the [`SubsurfaceContainer`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
self.children.push(child);
self
}
/// Adds an element to the [`SubsurfaceContainer`], if `Some`.
pub fn push_maybe(
self,
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
) -> Self {
if let Some(child) = child {
self.push(child)
} else {
self
}
}
/// Extends the [`SubsurfaceContainer`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
}
impl<'a, Message, Renderer> Default
for SubsurfaceContainer<'a, Message, Renderer>
where
Renderer: iced::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Theme, Renderer: iced::core::Renderer>
FromIterator<Element<'a, Message, Theme, Renderer>>
for SubsurfaceContainer<'a, Message, Theme, Renderer>
{
fn from_iter<
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
>(
iter: T,
) -> Self {
Self::with_children(iter)
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for SubsurfaceContainer<'a, Message, Theme, Renderer>
where
Renderer: iced::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(self.children.as_mut_slice());
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.max_width(self.max_width);
let nodes = self
.children
.iter()
.zip(tree.children.iter_mut())
.map(|c| {
let size = c.0.as_widget().size();
layout::positioned(
&limits.max_width(self.max_width),
size.width,
size.height,
self.padding,
|limits| c.0.as_widget().layout(c.1, renderer, limits),
|content, size| {
content.align(self.align, Alignment::Start, size)
},
)
})
.collect();
let size = limits.resolve(self.width, self.height, Size::ZERO);
layout::Node::with_children(size, nodes)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.operate(state, layout, renderer, operation);
});
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
state, layout, cursor, viewport, renderer,
)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(viewport))
{
child.as_widget().draw(
state, renderer, theme, style, layout, cursor, viewport,
);
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
translation,
)
}
#[cfg(feature = "a11y")]
/// get the a11y nodes for the widget
fn a11y_nodes(
&self,
layout: Layout<'_>,
state: &Tree,
cursor: mouse::Cursor,
) -> iced_accessibility::A11yTree {
use iced_accessibility::A11yTree;
A11yTree::join(
self.children
.iter()
.zip(layout.children())
.zip(state.children.iter())
.map(|((c, c_layout), state)| {
c.as_widget().a11y_nodes(c_layout, state, cursor)
}),
)
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles,
) {
for ((e, layout), state) in self
.children
.iter()
.zip(layout.children())
.zip(state.children.iter())
{
e.as_widget().drag_destinations(
state,
layout,
renderer,
dnd_rectangles,
);
}
}
}
impl<'a, Message, Theme, Renderer>
From<SubsurfaceContainer<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: iced::core::Renderer + 'a,
{
fn from(
SubsurfaceContainer: SubsurfaceContainer<'a, Message, Theme, Renderer>,
) -> Self {
Self::new(SubsurfaceContainer)
}
}

View file

@ -1,11 +1,5 @@
use futures_channel::mpsc;
use iced::{
futures::{FutureExt, SinkExt},
platform_specific::shell::subsurface_widget::{Shmbuf, SubsurfaceBuffer},
};
use iced_runtime::futures::subscription;
use rustix::{io::Errno, shm::ShmOFlags};
use cctk::sctk::{
self,
reexports::{
calloop_wayland_source::WaylandSource,
client::{
@ -18,16 +12,24 @@ use cctk::sctk::{
registry::{ProvidesRegistryState, RegistryState},
shm::{Shm, ShmHandler},
};
use futures_channel::mpsc;
use iced::{
futures::{FutureExt, SinkExt},
platform_specific::shell::subsurface_widget::{Shmbuf, SubsurfaceBuffer},
};
use iced_runtime::futures::subscription;
use rustix::{io::Errno, shm::ShmOFlags};
use std::{
os::fd::OwnedFd,
sync::Arc,
thread,
time::{SystemTime, UNIX_EPOCH},
time::{Duration, SystemTime, UNIX_EPOCH},
};
#[derive(Debug, Clone)]
pub enum Event {
RedBuffer(SubsurfaceBuffer),
GreenBuffer(SubsurfaceBuffer),
}
struct AppData {
@ -80,6 +82,20 @@ async fn start(conn: Connection) -> mpsc::Receiver<Event> {
format: wl_shm::Format::Xrgb8888,
};
let buffer = SubsurfaceBuffer::new(Arc::new(shmbuf.into())).0;
let _ = sender.send(Event::GreenBuffer(buffer)).await;
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;
@ -90,6 +106,7 @@ async fn start(conn: Connection) -> mpsc::Receiver<Event> {
.unwrap();
loop {
event_loop.dispatch(None, &mut app_data).unwrap();
std::thread::sleep(Duration::from_millis(500));
}
});