diff --git a/Cargo.lock b/Cargo.lock index c9cba79f..9a4b6829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -806,12 +806,14 @@ dependencies = [ "rust-embed 6.8.1", "rust-embed-utils 7.8.1", "serde", + "switcheroo-control", "tokio", "tracing", "tracing-log", "tracing-subscriber", "url", "xdg", + "zbus", ] [[package]] @@ -1085,7 +1087,7 @@ dependencies = [ [[package]] name = "cosmic-dbus-networkmanager" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#3644bc909984842f35adb1057ef6e0a277c1aa6a" +source = "git+https://github.com/pop-os/dbus-settings-bindings#5dea929b730460f883935357a1a8fb9736f36f95" dependencies = [ "bitflags 2.4.2", "derive_builder", @@ -3687,7 +3689,7 @@ dependencies = [ [[package]] name = "mpris2-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#3644bc909984842f35adb1057ef6e0a277c1aa6a" +source = "git+https://github.com/pop-os/dbus-settings-bindings#5dea929b730460f883935357a1a8fb9736f36f95" dependencies = [ "serde", "thiserror", @@ -5143,6 +5145,14 @@ dependencies = [ "zeno", ] +[[package]] +name = "switcheroo-control" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#5dea929b730460f883935357a1a8fb9736f36f95" +dependencies = [ + "zbus", +] + [[package]] name = "syn" version = "1.0.109" @@ -6163,7 +6173,7 @@ dependencies = [ "js-sys", "log", "naga", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "profiling", "raw-window-handle", "smallvec", @@ -6188,7 +6198,7 @@ dependencies = [ "codespan-reporting", "log", "naga", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "profiling", "raw-window-handle", "rustc-hash", @@ -6228,7 +6238,7 @@ dependencies = [ "naga", "objc", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "profiling", "range-alloc", "raw-window-handle", diff --git a/cosmic-app-list/Cargo.toml b/cosmic-app-list/Cargo.toml index d51e58b0..598b2cfb 100644 --- a/cosmic-app-list/Cargo.toml +++ b/cosmic-app-list/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" cctk.workspace = true cosmic-protocols.workspace = true libcosmic.workspace = true +zbus.workspace = true # libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio", "applet"] } ron = "0.8" futures = "0.3" @@ -29,3 +30,4 @@ rust-embed = "6.3" url = "2.3.1" rust-embed-utils = "7.5.0" rand = "0.8.5" +switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } \ No newline at end of file diff --git a/cosmic-app-list/i18n/de/cosmic_app_list.ftl b/cosmic-app-list/i18n/de/cosmic_app_list.ftl index 763860f2..6537a383 100644 --- a/cosmic-app-list/i18n/de/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/de/cosmic_app_list.ftl @@ -3,4 +3,7 @@ favorite = Favorisieren unfavorite = Entfavorisieren quit = Beenden quit-all = Alle beenden -new-window = Neues Fenster \ No newline at end of file +new-window = Neues Fenster +run = Ausführen +run-on = Ausführen auf {$gpu} +run-on-default = (Standard) \ No newline at end of file diff --git a/cosmic-app-list/i18n/en/cosmic_app_list.ftl b/cosmic-app-list/i18n/en/cosmic_app_list.ftl index d3bffb22..0a722919 100644 --- a/cosmic-app-list/i18n/en/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/en/cosmic_app_list.ftl @@ -3,4 +3,7 @@ favorite = Favorite unfavorite = Un-Favorite quit = Quit quit-all = Quit All -new-window = New Window \ No newline at end of file +new-window = New Window +run = Run +run-on = Run on {$gpu} +run-on-default = (Default) \ No newline at end of file diff --git a/cosmic-app-list/src/app.rs b/cosmic-app-list/src/app.rs index 1620e469..3e0fea6c 100755 --- a/cosmic-app-list/src/app.rs +++ b/cosmic-app-list/src/app.rs @@ -11,7 +11,7 @@ use cctk::sctk::reexports::calloop::channel::Sender; use cctk::toplevel_info::ToplevelInfo; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; use cctk::wayland_client::protocol::wl_seat::WlSeat; -use cosmic::cosmic_config::{self, Config, CosmicConfigEntry}; +use cosmic::cosmic_config::{Config, CosmicConfigEntry}; use cosmic::desktop::{load_applications_for_app_ids, DesktopEntryData}; use cosmic::iced; use cosmic::iced::event::listen_with; @@ -25,6 +25,7 @@ use cosmic::iced::widget::vertical_space; use cosmic::iced::widget::{column, dnd_source, mouse_area, row, Column, Row}; use cosmic::iced::Color; use cosmic::iced::{window, Subscription}; +use cosmic::iced_core::Padding; use cosmic::iced_runtime::core::alignment::Horizontal; use cosmic::iced_runtime::core::event; use cosmic::iced_sctk::commands::data_device::accept_mime_type; @@ -55,6 +56,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use switcheroo_control::Gpu; use tokio::time::sleep; use url::Url; @@ -105,6 +107,7 @@ impl DockItem { applet: &Context, rectangle_tracker: Option<&RectangleTracker>, interaction_enabled: bool, + gpus: Option<&[Gpu]>, ) -> Element<'_, Message> { let Self { toplevels, @@ -168,7 +171,20 @@ impl DockItem { toplevels .first() .map(|t| Message::Activate(t.0.clone())) - .or_else(|| desktop_info.exec.clone().map(Message::Exec)), + .or_else(|| { + let gpu_idx = gpus.map(|gpus| { + if desktop_info.prefers_dgpu { + gpus.iter().position(|gpu| !gpu.default).unwrap_or(0) + } else { + gpus.iter().position(|gpu| gpu.default).unwrap_or(0) + } + }); + + desktop_info + .exec + .clone() + .map(|exec| Message::Exec(exec, gpu_idx)) + }), ) .width(Length::Shrink) .height(Length::Shrink), @@ -212,6 +228,7 @@ struct CosmicAppList { rectangles: HashMap, dnd_offer: Option, is_listening_for_dnd: bool, + gpus: Option>, } // TODO DnD after sctk merges DnD @@ -221,10 +238,11 @@ enum Message { Favorite(String), UnFavorite(String), Popup(String), + GpuRequest(Option>), CloseRequested(window::Id), ClosePopup, Activate(ZcosmicToplevelHandleV1), - Exec(String), + Exec(String, Option), Quit(String), Ignore, NewSeat(WlSeat), @@ -282,6 +300,39 @@ fn index_in_list( } } +async fn try_get_gpus() -> Option> { + let connection = zbus::Connection::system().await.ok()?; + let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) + .await + .ok()?; + + if !proxy.has_dual_gpu().await.ok()? { + return None; + } + + let gpus = proxy.get_gpus().await.ok()?; + if gpus.is_empty() { + return None; + } + + Some(gpus) +} + +pub fn menu_button<'a, Message>( + content: impl Into>, +) -> cosmic::widget::Button<'a, Message, cosmic::Renderer> { + cosmic::widget::Button::new(content) + .style(Button::AppletMenu) + .padding(menu_control_padding()) + .width(Length::Fill) +} + +pub fn menu_control_padding() -> Padding { + let theme = cosmic::theme::active(); + let cosmic = theme.cosmic(); + [cosmic.space_xxs(), cosmic.space_m()].into() +} + impl cosmic::Application for CosmicAppList { type Message = Message; type Executor = cosmic::SingleThreadExecutor; @@ -316,7 +367,12 @@ impl cosmic::Application for CosmicAppList { }; self_.item_ctr = self_.favorite_list.len() as u32; - (self_, Command::none()) + ( + self_, + Command::perform(try_get_gpus(), |gpus| { + cosmic::app::Message::App(Message::GpuRequest(gpus)) + }), + ) } fn core(&self) -> &cosmic::app::Core { @@ -369,7 +425,11 @@ impl cosmic::Application for CosmicAppList { width: width as i32, height: height as i32, }; - return get_popup(popup_settings); + + let gpu_update = Command::perform(try_get_gpus(), |gpus| { + cosmic::app::Message::App(Message::GpuRequest(gpus)) + }); + return Command::batch([gpu_update, get_popup(popup_settings)]); } } Message::Favorite(id) => { @@ -689,11 +749,23 @@ impl cosmic::Application for CosmicAppList { } } }, - WaylandUpdate::ActivationToken { token, exec } => { + WaylandUpdate::ActivationToken { + token, + exec, + gpu_idx, + } => { let mut envs = Vec::new(); if let Some(token) = token { - envs.push(("XDG_ACTIVATION_TOKEN", token.clone())); - envs.push(("DESKTOP_STARTUP_ID", token)); + envs.push(("XDG_ACTIVATION_TOKEN".to_string(), token.clone())); + envs.push(("DESKTOP_STARTUP_ID".to_string(), token)); + } + if let (Some(gpus), Some(idx)) = (self.gpus.as_ref(), gpu_idx) { + envs.extend( + gpus[idx] + .environment + .iter() + .map(|(k, v)| (k.clone(), v.clone())), + ); } tokio::task::spawn_blocking(|| { cosmic::desktop::spawn_desktop_exec(exec, envs); @@ -707,11 +779,12 @@ impl cosmic::Application for CosmicAppList { Message::RemovedSeat(_) => { self.seat.take(); } - Message::Exec(exec) => { + Message::Exec(exec, gpu_idx) => { if let Some(tx) = self.wayland_sender.as_ref() { let _ = tx.send(WaylandRequest::TokenRequest { app_id: Self::APP_ID.to_string(), exec, + gpu_idx, }); } } @@ -775,6 +848,9 @@ impl cosmic::Application for CosmicAppList { self.popup = None; } } + Message::GpuRequest(gpus) => { + self.gpus = gpus; + } } Command::none() @@ -793,6 +869,7 @@ impl cosmic::Application for CosmicAppList { &self.core.applet, self.rectangle_tracker.as_ref(), self.popup.is_none(), + self.gpus.as_deref(), ) }) .collect(); @@ -802,7 +879,10 @@ impl cosmic::Application for CosmicAppList { .as_ref() .and_then(|o| o.dock_item.as_ref().map(|item| (item, o.preview_index))) { - favorites.insert(index, item.as_icon(&self.core.applet, None, false)); + favorites.insert( + index, + item.as_icon(&self.core.applet, None, false, self.gpus.as_deref()), + ); } else if self.is_listening_for_dnd && self.favorite_list.is_empty() { // show star indicating favorite_list is drag target favorites.push( @@ -823,6 +903,7 @@ impl cosmic::Application for CosmicAppList { &self.core.applet, self.rectangle_tracker.as_ref(), self.popup.is_none(), + self.gpus.as_deref(), ) }) .collect(); @@ -949,19 +1030,47 @@ impl cosmic::Application for CosmicAppList { .as_ref() .is_some_and(|wm_class| self.config.favorites.contains(wm_class)); - let mut content = column![ - iced::widget::text(&desktop_info.name).horizontal_alignment(Horizontal::Center), - ] - .padding(8) - .spacing(4) + let mut content = column![container( + iced::widget::text(&desktop_info.name).horizontal_alignment(Horizontal::Center) + ) + .padding(menu_control_padding()),] + .padding([8, 0]) .align_items(Alignment::Center); if let Some(exec) = desktop_info.exec.clone() { - content = content.push( - cosmic::widget::button(iced::widget::text(fl!("new-window"))) - .style(Button::Text) - .on_press(Message::Exec(exec)), - ); + if !toplevels.is_empty() { + content = content.push( + menu_button(iced::widget::text(fl!("new-window"))) + .on_press(Message::Exec(exec, None)), + ); + } else { + if let Some(gpus) = self.gpus.as_ref() { + let default_idx = if desktop_info.prefers_dgpu { + gpus.iter().position(|gpu| !gpu.default).unwrap_or(0) + } else { + gpus.iter().position(|gpu| gpu.default).unwrap_or(0) + }; + for (i, gpu) in gpus.iter().enumerate() { + content = content.push( + menu_button(iced::widget::text(format!( + "{} {}", + fl!("run-on", gpu = gpu.name.clone()), + if i == default_idx { + fl!("run-on-default") + } else { + String::new() + } + ))) + .on_press(Message::Exec(exec.clone(), Some(i))), + ); + } + } else { + content = content.push( + menu_button(iced::widget::text(fl!("run"))) + .on_press(Message::Exec(exec, None)), + ); + } + } content = content.push(divider::horizontal::default()); } @@ -974,8 +1083,7 @@ impl cosmic::Application for CosmicAppList { info.title.clone() }; list_col = list_col.push( - cosmic::widget::button(iced::widget::text(title)) - .style(Button::Text) + menu_button(iced::widget::text(title)) .on_press(Message::Activate(handle.clone())), ); } @@ -983,25 +1091,21 @@ impl cosmic::Application for CosmicAppList { content = content.push(divider::horizontal::default()); } content = content.push(if is_favorite { - cosmic::widget::button(iced::widget::text(fl!("unfavorite"))) - .style(Button::Text) + menu_button(iced::widget::text(fl!("unfavorite"))) .on_press(Message::UnFavorite(desktop_info.id.clone())) } else { - cosmic::widget::button(iced::widget::text(fl!("favorite"))) - .style(Button::Text) + menu_button(iced::widget::text(fl!("favorite"))) .on_press(Message::Favorite(desktop_info.id.clone())) }); content = match toplevels.len() { 0 => content, 1 => content.push( - cosmic::widget::button(iced::widget::text(fl!("quit"))) - .style(Button::Text) + menu_button(iced::widget::text(fl!("quit"))) .on_press(Message::Quit(desktop_info.id.clone())), ), _ => content.push( - cosmic::widget::button(iced::widget::text(&fl!("quit-all"))) - .style(Button::Text) + menu_button(iced::widget::text(&fl!("quit-all"))) .on_press(Message::Quit(desktop_info.id.clone())), ), }; diff --git a/cosmic-app-list/src/wayland_handler.rs b/cosmic-app-list/src/wayland_handler.rs index 33a44d7a..447eb186 100644 --- a/cosmic-app-list/src/wayland_handler.rs +++ b/cosmic-app-list/src/wayland_handler.rs @@ -52,6 +52,7 @@ impl ProvidesRegistryState for AppData { struct ExecRequestData { data: RequestData, exec: String, + gpu_idx: Option, } impl RequestDataExt for ExecRequestData { @@ -74,6 +75,7 @@ impl ActivationHandler for AppData { let _ = self.tx.unbounded_send(WaylandUpdate::ActivationToken { token: Some(token), exec: data.exec.clone(), + gpu_idx: data.gpu_idx.clone(), }); } } @@ -217,7 +219,11 @@ pub(crate) fn wayland_handler( state.exit = true; } }, - WaylandRequest::TokenRequest { app_id, exec } => { + WaylandRequest::TokenRequest { + app_id, + exec, + gpu_idx, + } => { if let Some(activation_state) = state.activation_state.as_ref() { activation_state.request_token_with_data( &state.queue_handle, @@ -232,12 +238,15 @@ pub(crate) fn wayland_handler( surface: None, }, exec, + gpu_idx, }, ); } else { - let _ = state - .tx - .unbounded_send(WaylandUpdate::ActivationToken { token: None, exec }); + let _ = state.tx.unbounded_send(WaylandUpdate::ActivationToken { + token: None, + exec, + gpu_idx, + }); } } }, diff --git a/cosmic-app-list/src/wayland_subscription.rs b/cosmic-app-list/src/wayland_subscription.rs index a348573e..c5658db4 100644 --- a/cosmic-app-list/src/wayland_subscription.rs +++ b/cosmic-app-list/src/wayland_subscription.rs @@ -79,7 +79,11 @@ pub enum WaylandUpdate { Init(calloop::channel::Sender), Finished, Toplevel(ToplevelUpdate), - ActivationToken { token: Option, exec: String }, + ActivationToken { + token: Option, + exec: String, + gpu_idx: Option, + }, } #[derive(Clone, Debug)] @@ -92,7 +96,11 @@ pub enum ToplevelUpdate { #[derive(Clone, Debug)] pub enum WaylandRequest { Toplevel(ToplevelRequest), - TokenRequest { app_id: String, exec: String }, + TokenRequest { + app_id: String, + exec: String, + gpu_idx: Option, + }, } #[derive(Debug, Clone)]