diff --git a/cosmic-settings/src/app.rs b/cosmic-settings/src/app.rs index 157445a..3a500f3 100644 --- a/cosmic-settings/src/app.rs +++ b/cosmic-settings/src/app.rs @@ -489,7 +489,12 @@ impl cosmic::Application for SettingsApp { #[cfg(feature = "page-legacy-applications")] crate::pages::Message::LegacyApplications(message) => { - page::update!(self.pages, message, applications::legacy_applications::Page); + if let Some(page) = self + .pages + .page_mut::() + { + return page.update(message).map(Into::into); + } } #[cfg(feature = "page-input")] diff --git a/cosmic-settings/src/pages/applications/legacy_applications.rs b/cosmic-settings/src/pages/applications/legacy_applications.rs index d2cff93..7afa9e2 100644 --- a/cosmic-settings/src/pages/applications/legacy_applications.rs +++ b/cosmic-settings/src/pages/applications/legacy_applications.rs @@ -1,27 +1,46 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +use std::{ + process::ExitStatus, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + use cosmic::{ - Apply, Element, + Apply, Element, Task, cosmic_config::{self, ConfigGet, ConfigSet}, iced::Length, - widget::{self, text}, + surface, + widget::{self, dropdown, text}, }; use cosmic_comp_config::{EavesdroppingKeyboardMode, XwaylandDescaling, XwaylandEavesdropping}; +use cosmic_randr_shell::List; use cosmic_settings_page::Section; use cosmic_settings_page::{self as page, section}; use slab::Slab; use slotmap::SlotMap; +use tokio::sync::oneshot; use tracing::error; #[derive(Clone, Debug)] pub enum Message { + RandrUpdate(Arc>), + RandrResult(Arc>), SetXwaylandDescaling(XwaylandDescaling), SetXwaylandKeyboardMode(EavesdroppingKeyboardMode), SetXwaylandMouseButtonMode(bool), + SetXwaylandPrimaryOutput(usize), + Surface(surface::Action), } pub struct Page { + refresh_pending: Arc, + randr_handle: Option<(oneshot::Sender<()>, cosmic::iced::task::Handle)>, + output_options: Vec, + output_options_selected: usize, comp_config: cosmic_config::Config, comp_config_descale_xwayland: XwaylandDescaling, comp_config_xwayland_eavesdropping: XwaylandEavesdropping, @@ -48,7 +67,12 @@ impl Default for Page { Default::default() }); + let no_display = fl!("legacy-app-scaling", "no-display"); Self { + refresh_pending: Arc::new(AtomicBool::new(false)), + randr_handle: None, + output_options: vec![no_display], + output_options_selected: 0, comp_config, comp_config_descale_xwayland, comp_config_xwayland_eavesdropping, @@ -75,13 +99,110 @@ impl page::Page for Page { .title(fl!("legacy-applications")) .description(fl!("legacy-applications", "desc")) } + + fn on_enter(&mut self) -> Task { + let mut tasks = Vec::new(); + + tasks.push(cosmic::task::future(on_enter())); + + let refresh_pending = self.refresh_pending.clone(); + let (tx, mut rx) = tachyonix::channel(4); + let (canceller, cancelled) = oneshot::channel::<()>(); + let runtime = tokio::runtime::Handle::current(); + + // Spawns a background service to monitor for display state changes. + // This must be spawned onto its own thread because `*mut wayland_sys::client::wl_display` is not Send-able. + tokio::task::spawn_blocking(move || { + let dispatcher = std::pin::pin!(async move { + let Ok((mut context, mut event_queue)) = cosmic_randr::connect(tx) else { + return; + }; + + loop { + if context.dispatch(&mut event_queue).await.is_err() { + return; + } + } + }); + + runtime.block_on(futures::future::select(cancelled, dispatcher)); + }); + + // Forward messages from another thread to prevent the monitoring thread from blocking. + let (randr_task, randr_handle) = + Task::stream(async_fn_stream::fn_stream(|emitter| async move { + while let Ok(message) = rx.recv().await { + if let cosmic_randr::Message::ManagerDone = message { + if !refresh_pending.swap(true, Ordering::SeqCst) { + _ = emitter.emit(on_enter().await).await; + } + } + } + })) + .abortable(); + + tasks.push(randr_task); + self.randr_handle = Some((canceller, randr_handle)); + + cosmic::task::batch(tasks) + } + + fn on_leave(&mut self) -> Task { + if let Some((canceller, handle)) = self.randr_handle.take() { + _ = canceller.send(()); + handle.abort(); + } + + Task::none() + } +} + +pub async fn on_enter() -> crate::pages::Message { + let randr_fut = cosmic_randr_shell::list(); + + crate::pages::Message::LegacyApplications(Message::RandrUpdate(Arc::new(randr_fut.await))) } impl page::AutoBind for Page {} impl Page { - pub fn update(&mut self, message: Message) { + pub fn update(&mut self, message: Message) -> Task { match message { + Message::RandrUpdate(randr) => { + match Arc::into_inner(randr) { + Some(Ok(outputs)) => { + let output_options_selected = outputs + .outputs + .values() + .position(|o| o.xwayland_primary.is_some_and(std::convert::identity)) + .map(|x| x + 1) + .unwrap_or(0); + let mut output_options = vec![fl!("legacy-app-scaling", "no-display")]; + output_options.extend( + outputs + .outputs + .values() + .flat_map(|o| o.xwayland_primary.is_some().then(|| o.name.clone())), + ); + + self.output_options_selected = output_options_selected; + self.output_options = output_options; + } + + Some(Err(why)) => { + tracing::error!(why = why.to_string(), "error fetching displays"); + } + + None => (), + } + + self.refresh_pending.store(false, Ordering::SeqCst); + } + Message::RandrResult(result) => { + if let Some(Err(why)) = Arc::into_inner(result) { + tracing::error!(why = why.to_string(), "cosmic-randr error"); + } + } Message::SetXwaylandDescaling(descale) => { self.comp_config_descale_xwayland = descale; if let Err(err) = self @@ -109,7 +230,28 @@ impl Page { error!(?err, "Failed to set config 'xwayland_eavesdropping'"); } } + Message::SetXwaylandPrimaryOutput(idx) => { + let mut task = tokio::process::Command::new("cosmic-randr"); + task.arg("xwayland"); + if idx == 0 { + task.arg("--no-primary"); + } else { + task.arg("--primary").arg(&self.output_options[idx]); + } + + return cosmic::task::future(async move { + tracing::debug!(?task, "executing"); + crate::app::Message::PageMessage(crate::pages::Message::LegacyApplications( + Message::RandrResult(Arc::new(task.status().await)), + )) + }); + } + Message::Surface(a) => { + return cosmic::task::message(crate::app::Message::Surface(a)); + } } + + Task::none() } } @@ -243,6 +385,21 @@ pub fn legacy_application_scaling() -> Section { .width(Length::Fill) .into(), ])) + .add(widget::settings::item( + &descriptions[preferred_display], + dropdown::popup_dropdown( + &page.output_options, + Some(page.output_options_selected), + Message::SetXwaylandPrimaryOutput, + cosmic::iced::window::Id::RESERVED, + Message::Surface, + |a| { + crate::app::Message::PageMessage( + crate::pages::Message::LegacyApplications(a), + ) + }, + ), + )) .apply(Element::from) .map(crate::pages::Message::LegacyApplications) }) diff --git a/cosmic-settings/src/pages/display/mod.rs b/cosmic-settings/src/pages/display/mod.rs index b8edbaa..a67e532 100644 --- a/cosmic-settings/src/pages/display/mod.rs +++ b/cosmic-settings/src/pages/display/mod.rs @@ -407,6 +407,7 @@ impl page::Page for Page { current: Some(test_mode), adaptive_sync: None, adaptive_sync_availability: None, + xwayland_primary: None, }); randr.outputs.insert(cosmic_randr_shell::Output { @@ -423,6 +424,7 @@ impl page::Page for Page { current: Some(test_mode), adaptive_sync: Some(AdaptiveSyncState::Disabled), adaptive_sync_availability: Some(AdaptiveSyncAvailability::Supported), + xwayland_primary: None, }); crate::pages::Message::Displays(Message::Update {