diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 64f741bd588d2227198fda13c0a8fbf5fdb4337c..eb9e3c8d86b7b9de9df66f7ac9798426db2df6c1 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -19,7 +19,8 @@ test-support = [ "gpui/test-support", "livekit_client/test-support", "project/test-support", - "util/test-support" + "util/test-support", + "workspace/test-support" ] [dependencies] @@ -51,5 +52,6 @@ gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } livekit_client = { workspace = true, features = ["test-support"] } diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 117789e233ab6fbc101b28e5e9f485ec17c1f79d..5f8eeb965cbc9d5665e8316cf1dde329b4277260 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -1529,6 +1529,84 @@ impl Room { }) } + #[cfg(target_os = "linux")] + pub fn share_screen_wayland(&mut self, cx: &mut Context) -> Task> { + log::info!("will screenshare on wayland"); + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + if self.is_sharing_screen() { + return Task::ready(Err(anyhow!("screen was already shared"))); + } + + let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { + let publish_id = post_inc(&mut live_kit.next_publish_id); + live_kit.screen_track = LocalTrack::Pending { publish_id }; + cx.notify(); + (live_kit.room.local_participant(), publish_id) + } else { + return Task::ready(Err(anyhow!("live-kit was not initialized"))); + }; + + cx.spawn(async move |this, cx| { + let publication = participant.publish_screenshare_track_wayland(cx).await; + + this.update(cx, |this, cx| { + let live_kit = this + .live_kit + .as_mut() + .context("live-kit was not initialized")?; + + let canceled = if let LocalTrack::Pending { + publish_id: cur_publish_id, + } = &live_kit.screen_track + { + *cur_publish_id != publish_id + } else { + true + }; + + match publication { + Ok((publication, stream, failure_rx)) => { + if canceled { + cx.spawn(async move |_, cx| { + participant.unpublish_track(publication.sid(), cx).await + }) + .detach() + } else { + cx.spawn(async move |this, cx| { + if failure_rx.await.is_ok() { + log::warn!("Wayland capture died, auto-unsharing screen"); + let _ = + this.update(cx, |this, cx| this.unshare_screen(false, cx)); + } + }) + .detach(); + + live_kit.screen_track = LocalTrack::Published { + track_publication: publication, + _stream: stream, + }; + cx.notify(); + } + + Audio::play_sound(Sound::StartScreenshare, cx); + Ok(()) + } + Err(error) => { + if canceled { + Ok(()) + } else { + live_kit.screen_track = LocalTrack::None; + cx.notify(); + Err(error) + } + } + } + })? + }) + } + pub fn toggle_mute(&mut self, cx: &mut Context) { if let Some(live_kit) = self.live_kit.as_mut() { // When unmuting, undeafen if the user was deafened before. diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index 8c817c1fc7cc9bb7d33c01ba467d13c971453ac3..965e791102a373b718f36e92ea55a5753bbe32c7 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -6596,6 +6596,151 @@ async fn test_join_call_after_screen_was_shared( }); } +#[cfg(target_os = "linux")] +#[gpui::test(iterations = 10)] +async fn test_share_screen_wayland( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + // User A calls user B. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + + // User B accepts. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + executor.run_until_parked(); + incoming_call_b.next().await.unwrap().unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + executor.run_until_parked(); + + // User A shares their screen via the Wayland path. + let events_b = active_call_events(cx_b); + active_call_a + .update(cx_a, |call, cx| { + call.room() + .unwrap() + .update(cx, |room, cx| room.share_screen_wayland(cx)) + }) + .await + .unwrap(); + + executor.run_until_parked(); + + // Room A is sharing and has a nonzero synthetic screen ID. + room_a.read_with(cx_a, |room, _| { + assert!(room.is_sharing_screen()); + let screen_id = room.shared_screen_id(); + assert!(screen_id.is_some(), "shared_screen_id should be Some"); + assert_ne!(screen_id.unwrap(), 0, "synthetic ID must be nonzero"); + }); + + // User B observes the remote screen sharing track. + assert_eq!(events_b.borrow().len(), 1); + if let call::room::Event::RemoteVideoTracksChanged { participant_id } = + events_b.borrow().first().unwrap() + { + assert_eq!(*participant_id, client_a.peer_id().unwrap()); + room_b.read_with(cx_b, |room, _| { + assert_eq!( + room.remote_participants()[&client_a.user_id().unwrap()] + .video_tracks + .len(), + 1 + ); + }); + } else { + panic!("expected RemoteVideoTracksChanged event"); + } +} + +#[cfg(target_os = "linux")] +#[gpui::test(iterations = 10)] +async fn test_unshare_screen_wayland( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + // User A calls user B. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + + // User B accepts. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + executor.run_until_parked(); + incoming_call_b.next().await.unwrap().unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + executor.run_until_parked(); + + // User A shares their screen via the Wayland path. + active_call_a + .update(cx_a, |call, cx| { + call.room() + .unwrap() + .update(cx, |room, cx| room.share_screen_wayland(cx)) + }) + .await + .unwrap(); + executor.run_until_parked(); + + room_a.read_with(cx_a, |room, _| { + assert!(room.is_sharing_screen()); + }); + + // User A stops sharing. + room_a + .update(cx_a, |room, cx| room.unshare_screen(true, cx)) + .unwrap(); + executor.run_until_parked(); + + // Room A is no longer sharing, screen ID is gone. + room_a.read_with(cx_a, |room, _| { + assert!(!room.is_sharing_screen()); + assert!(room.shared_screen_id().is_none()); + }); +} + #[gpui::test] async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) { let mut server = TestServer::start(cx.executor().clone()).await; diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 7541e5871bf0699f93e842dbfed610ee59d1d13b..34595e9440f518a23128e4a00ba909cec055b1e2 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -171,6 +171,7 @@ pub fn init(cx: &mut App) { }); }); }); + // TODO(jk): Is this action ever triggered? workspace.register_action(|_, _: &ScreenShare, window, cx| { let room = ActiveCall::global(cx).read(cx).room().cloned(); if let Some(room) = room { @@ -179,19 +180,32 @@ pub fn init(cx: &mut App) { if room.is_sharing_screen() { room.unshare_screen(true, cx).ok(); } else { - let sources = cx.screen_capture_sources(); - - cx.spawn(async move |room, cx| { - let sources = sources.await??; - let first = sources.into_iter().next(); - if let Some(first) = first { - room.update(cx, |room, cx| room.share_screen(first, cx))? - .await - } else { - Ok(()) + #[cfg(target_os = "linux")] + let is_wayland = gpui::guess_compositor() == "Wayland"; + #[cfg(not(target_os = "linux"))] + let is_wayland = false; + + #[cfg(target_os = "linux")] + { + if is_wayland { + room.share_screen_wayland(cx).detach_and_log_err(cx); } - }) - .detach_and_log_err(cx); + } + if !is_wayland { + let sources = cx.screen_capture_sources(); + + cx.spawn(async move |room, cx| { + let sources = sources.await??; + let first = sources.into_iter().next(); + if let Some(first) = first { + room.update(cx, |room, cx| room.share_screen(first, cx))? + .await + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); + } }; }); }); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 885dad0d96dc50993a7098b5d48509e4749894ec..cd0b74a2c5d2f7d0233aec18509aa0f9f5e5c3a2 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -78,6 +78,7 @@ pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream} #[cfg(all(target_os = "macos", any(test, feature = "test-support")))] pub use visual_test::VisualTestPlatform; +// TODO(jk): return an enum instead of a string /// Return which compositor we're guessing we'll use. /// Does not attempt to connect to the given compositor. #[cfg(any(target_os = "linux", target_os = "freebsd"))] diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index cb46fa028727a24a481f2ca275368af0cf841e93..633e0245602cb54c5066c67a1730c4554dfb5960 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -57,7 +57,7 @@ pub(crate) trait LinuxClient { #[cfg(feature = "screen-capture")] fn is_screen_capture_supported(&self) -> bool { - false + true } #[cfg(feature = "screen-capture")] diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index ce49fca37232f256e570f584272519d8d6f34dd8..49e6e835508e1511771656bdd3b52dcfb86cfaa3 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -702,11 +702,6 @@ impl LinuxClient for WaylandClient { None } - #[cfg(feature = "screen-capture")] - fn is_screen_capture_supported(&self) -> bool { - false - } - #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 31a13e64e7dff17f0d1a36662761cdd2c51de4e7..57b7f7c42e9f684497d508d7404a69ebc4fb6666 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -9,6 +9,8 @@ use playback::capture_local_video_track; use settings::Settings; use std::sync::{Arc, atomic::AtomicU64}; +#[cfg(target_os = "linux")] +mod linux; mod playback; use crate::{ @@ -231,6 +233,33 @@ impl LocalParticipant { .map(LocalTrackPublication) .context("unpublishing a track") } + + #[cfg(target_os = "linux")] + pub async fn publish_screenshare_track_wayland( + &self, + cx: &mut AsyncApp, + ) -> Result<( + LocalTrackPublication, + Box, + futures::channel::oneshot::Receiver<()>, + )> { + let (track, stop_flag, feed_task, failure_rx) = + linux::start_wayland_desktop_capture(cx).await?; + let options = livekit::options::TrackPublishOptions { + source: livekit::track::TrackSource::Screenshare, + video_codec: livekit::options::VideoCodec::VP8, + ..Default::default() + }; + let publication = self + .publish_track(livekit::track::LocalTrack::Video(track.0), options, cx) + .await?; + + Ok(( + publication, + Box::new(linux::WaylandScreenCaptureStream::new(stop_flag, feed_task)), + failure_rx, + )) + } } impl LocalTrackPublication { diff --git a/crates/livekit_client/src/livekit_client/linux.rs b/crates/livekit_client/src/livekit_client/linux.rs new file mode 100644 index 0000000000000000000000000000000000000000..6c6768980181c3abb2137417e94a64f4c8e2efc1 --- /dev/null +++ b/crates/livekit_client/src/livekit_client/linux.rs @@ -0,0 +1,206 @@ +use anyhow::Result; +use futures::StreamExt as _; +use futures::channel::oneshot; +use gpui::{AsyncApp, ScreenCaptureStream}; +use livekit::track; +use livekit::webrtc::{ + video_frame::{VideoFrame, VideoRotation}, + video_source::{RtcVideoSource, VideoResolution, native::NativeVideoSource}, +}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, + }, + time::Duration, +}; + +static NEXT_WAYLAND_SHARE_ID: AtomicU64 = AtomicU64::new(1); + +pub struct WaylandScreenCaptureStream { + id: u64, + stop_flag: Arc, + _feed_task: gpui::Task<()>, +} + +impl WaylandScreenCaptureStream { + pub fn new(stop_flag: Arc, feed_task: gpui::Task<()>) -> Self { + Self { + id: NEXT_WAYLAND_SHARE_ID.fetch_add(1, Ordering::Relaxed), + stop_flag, + _feed_task: feed_task, + } + } +} + +impl ScreenCaptureStream for WaylandScreenCaptureStream { + fn metadata(&self) -> Result { + Ok(gpui::SourceMetadata { + id: self.id, + label: None, + is_main: None, + resolution: gpui::size(gpui::DevicePixels(1), gpui::DevicePixels(1)), + }) + } +} + +impl Drop for WaylandScreenCaptureStream { + fn drop(&mut self) { + self.stop_flag.store(true, Ordering::Relaxed); + } +} + +struct CapturedFrame { + width: u32, + height: u32, + stride: u32, + data: Vec, +} + +fn desktop_frame_to_nv12(frame: &CapturedFrame) -> livekit::webrtc::prelude::NV12Buffer { + use libwebrtc::native::yuv_helper::argb_to_nv12; + use livekit::webrtc::prelude::NV12Buffer; + + let mut buffer = NV12Buffer::new(frame.width, frame.height); + let (stride_y, stride_uv) = buffer.strides(); + let (data_y, data_uv) = buffer.data_mut(); + argb_to_nv12( + &frame.data, + frame.stride, + data_y, + stride_y, + data_uv, + stride_uv, + frame.width as i32, + frame.height as i32, + ); + buffer +} + +pub(crate) async fn start_wayland_desktop_capture( + cx: &mut AsyncApp, +) -> Result<( + crate::LocalVideoTrack, + Arc, + gpui::Task<()>, + oneshot::Receiver<()>, +)> { + use futures::channel::mpsc; + use gpui::FutureExt as _; + use libwebrtc::desktop_capturer::{ + CaptureError, DesktopCaptureSourceType, DesktopCapturer, DesktopCapturerOptions, + }; + + let (frame_tx, mut frame_rx) = mpsc::channel::(2); + let stop_flag = Arc::new(AtomicBool::new(false)); + let stop = stop_flag.clone(); + + let permanent_error = Arc::new(AtomicBool::new(false)); + let permanent_error_cb = permanent_error.clone(); + + let executor = cx.background_executor().clone(); + + let capture_executor = executor.clone(); + executor + .spawn(async move { + let mut options = DesktopCapturerOptions::new(DesktopCaptureSourceType::Generic); + options.set_include_cursor(true); + + let Some(mut capturer) = DesktopCapturer::new(options) else { + log::error!( + "Failed to create Wayland desktop capturer. Is xdg-desktop-portal running?" + ); + return; + }; + + let frame_tx_cb = parking_lot::Mutex::new(frame_tx.clone()); + capturer.start_capture(None, move |result| match result { + Ok(frame) => { + let captured = CapturedFrame { + width: frame.width() as u32, + height: frame.height() as u32, + stride: frame.stride(), + data: frame.data().to_vec(), + }; + frame_tx_cb.lock().try_send(captured).ok(); + } + Err(CaptureError::Temporary) => { + // Expected before the portal picker completes + } + Err(CaptureError::Permanent) => { + permanent_error_cb.store(true, Ordering::Relaxed); + log::error!("Wayland desktop capture encountered a permanent error"); + } + }); + + while !stop.load(Ordering::Relaxed) { + capturer.capture_frame(); + if permanent_error.load(Ordering::Relaxed) { + break; + } + capture_executor.timer(Duration::from_millis(33)).await; + } + + drop(frame_tx); + }) + .detach(); + let first_frame = frame_rx + .next() + .with_timeout(Duration::from_secs(15), &executor) + .await + .map_err(|_| { + stop_flag.store(true, Ordering::Relaxed); + anyhow::anyhow!( + "Screen sharing timed out waiting for the first frame. \ + Check that xdg-desktop-portal and PipeWire are running, \ + and that your portal backend matches your compositor." + ) + })? + .ok_or_else(|| { + anyhow::anyhow!( + "Screen sharing was canceled or the portal denied permission. \ + You can try again from the screen share button." + ) + })?; + + let width = first_frame.width; + let height = first_frame.height; + let video_source = gpui_tokio::Tokio::spawn(cx, async move { + NativeVideoSource::new(VideoResolution { width, height }, true) + }) + .await?; + + let nv12 = desktop_frame_to_nv12(&first_frame); + video_source.capture_frame(&VideoFrame { + rotation: VideoRotation::VideoRotation0, + timestamp_us: 0, + buffer: nv12, + }); + + let track = super::LocalVideoTrack(track::LocalVideoTrack::create_video_track( + "screen share", + RtcVideoSource::Native(video_source.clone()), + )); + + let (failure_tx, failure_rx) = oneshot::channel::<()>(); + let feed_stop = stop_flag.clone(); + let feed_task = cx.background_executor().spawn(async move { + while let Some(frame) = frame_rx.next().await { + if feed_stop.load(Ordering::Relaxed) { + break; + } + let nv12 = desktop_frame_to_nv12(&frame); + video_source.capture_frame(&VideoFrame { + rotation: VideoRotation::VideoRotation0, + timestamp_us: 0, + buffer: nv12, + }); + } + if !feed_stop.load(Ordering::Relaxed) { + log::error!("Wayland screen capture ended unexpectedly"); + let _ = failure_tx.send(()); + } + }); + + Ok((track, stop_flag, feed_task, failure_rx)) +} diff --git a/crates/livekit_client/src/mock_client/participant.rs b/crates/livekit_client/src/mock_client/participant.rs index be8cd7f2d38ebcda00dc58300ef98adb6b7340f9..d3f720c5d8a07a99459943078aeaafbdfabec79f 100644 --- a/crates/livekit_client/src/mock_client/participant.rs +++ b/crates/livekit_client/src/mock_client/participant.rs @@ -99,6 +99,31 @@ impl LocalParticipant { Box::new(TestScreenCaptureStream {}), )) } + + #[cfg(target_os = "linux")] + pub async fn publish_screenshare_track_wayland( + &self, + _cx: &mut AsyncApp, + ) -> Result<( + LocalTrackPublication, + Box, + futures::channel::oneshot::Receiver<()>, + )> { + let (_failure_tx, failure_rx) = futures::channel::oneshot::channel(); + let this = self.clone(); + let server = this.room.test_server(); + let sid = server + .publish_video_track(this.room.token(), LocalVideoTrack {}) + .await?; + Ok(( + LocalTrackPublication { + room: self.room.downgrade(), + sid, + }, + Box::new(TestWaylandScreenCaptureStream::new()), + failure_rx, + )) + } } impl RemoteParticipant { @@ -166,3 +191,32 @@ impl ScreenCaptureStream for TestScreenCaptureStream { }) } } + +#[cfg(target_os = "linux")] +static NEXT_TEST_WAYLAND_SHARE_ID: AtomicU64 = AtomicU64::new(1); + +#[cfg(target_os = "linux")] +struct TestWaylandScreenCaptureStream { + id: u64, +} + +#[cfg(target_os = "linux")] +impl TestWaylandScreenCaptureStream { + fn new() -> Self { + Self { + id: NEXT_TEST_WAYLAND_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), + } + } +} + +#[cfg(target_os = "linux")] +impl ScreenCaptureStream for TestWaylandScreenCaptureStream { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + id: self.id, + is_main: None, + label: None, + resolution: size(DevicePixels(1), DevicePixels(1)), + }) + } +} diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 40ea36e815c7a0dd001b8da648cde9c0cfc36b35..d740dd90984cd3cbbfd058f7a00a07bb7326f0cd 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -548,6 +548,11 @@ impl TitleBar { ); if can_use_microphone && screen_sharing_supported { + #[cfg(target_os = "linux")] + let is_wayland = gpui::guess_compositor() == "Wayland"; + #[cfg(not(target_os = "linux"))] + let is_wayland = false; + let trigger = IconButton::new("screen-share", IconName::Screen) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) @@ -564,28 +569,56 @@ impl TitleBar { .room() .is_some_and(|room| !room.read(cx).is_sharing_screen()); - window - .spawn(cx, async move |cx| { - let screen = if should_share { - cx.update(|_, cx| pick_default_screen(cx))?.await - } else { - Ok(None) - }; - cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; + #[cfg(target_os = "linux")] + { + if is_wayland + && let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() + { + let task = room.update(cx, |room, cx| { + if should_share { + room.share_screen_wayland(cx) + } else { + room.unshare_screen(true, cx) + .map(|()| Task::ready(Ok(()))) + .unwrap_or_else(|e| Task::ready(Err(e))) + } + }); + task.detach_and_prompt_err( + "Sharing Screen Failed", + window, + cx, + |e, _, _| Some(format!("{e:?}")), + ); + } + } + if !is_wayland { + window + .spawn(cx, async move |cx| { + let screen = if should_share { + cx.update(|_, cx| pick_default_screen(cx))?.await + } else { + Ok(None) + }; + cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; - Result::<_, anyhow::Error>::Ok(()) - }) - .detach(); + Result::<_, anyhow::Error>::Ok(()) + }) + .detach(); + } }); - children.push( - SplitButton::new( - trigger.render(window, cx), - self.render_screen_list().into_any_element(), - ) - .style(SplitButtonStyle::Transparent) - .into_any_element(), - ); + if is_wayland { + children.push(trigger.into_any_element()); + } else { + children.push( + SplitButton::new( + trigger.render(window, cx), + self.render_screen_list().into_any_element(), + ) + .style(SplitButtonStyle::Transparent) + .into_any_element(), + ); + } } children.push(div().pr_2().into_any_element()); diff --git a/script/linux b/script/linux index 808841aeb39262f148399c643cc17314a9727fef..1eda7909b9580e95882f9de5ec9881f83acbcb13 100755 --- a/script/linux +++ b/script/linux @@ -50,6 +50,8 @@ if [[ -n $apt ]]; then musl-tools musl-dev build-essential + pipewire + xdg-desktop-portal ) if (grep -qP 'PRETTY_NAME="(Debian|Raspbian).+13' /etc/os-release); then # libstdc++-14-dev is in build-essential @@ -110,6 +112,8 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then libzstd-devel vulkan-loader sqlite-devel + pipewire + xdg-desktop-portal jq git tar @@ -185,6 +189,8 @@ if [[ -n $zyp ]]; then tar wayland-devel xcb-util-devel + pipewire + xdg-desktop-portal ) $maysudo "$zyp" install -y "${deps[@]}" finalize @@ -213,6 +219,8 @@ if [[ -n $pacman ]]; then pkgconf mold sqlite + pipewire + xdg-desktop-portal jq git ) @@ -244,6 +252,8 @@ if [[ -n $xbps ]]; then vulkan-loader mold sqlite-devel + pipewire + xdg-desktop-portal ) $maysudo "$xbps" -Syu "${deps[@]}" finalize @@ -269,6 +279,8 @@ if [[ -n $emerge ]]; then x11-libs/libxkbcommon sys-devel/mold dev-db/sqlite + media-video/pipewire + sys-apps/xdg-desktop-portal ) $maysudo "$emerge" -u "${deps[@]}" finalize