diff --git a/cosmic-app-list/src/app.rs b/cosmic-app-list/src/app.rs index 614de8be..3ef42fbb 100755 --- a/cosmic-app-list/src/app.rs +++ b/cosmic-app-list/src/app.rs @@ -7,6 +7,7 @@ use crate::wayland_subscription::ToplevelRequest; use crate::wayland_subscription::ToplevelUpdate; use crate::wayland_subscription::WaylandRequest; use crate::wayland_subscription::WaylandUpdate; +use crate::wayland_subscription::WorkspaceUpdate; use cctk::sctk::reexports::calloop::channel::Sender; use cctk::toplevel_info::ToplevelInfo; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; @@ -48,7 +49,9 @@ use cosmic::{ Command, }; use cosmic::{Element, Theme}; +use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::State; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; +use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1; use futures::future::pending; use iced::widget::container; use iced::Alignment; @@ -134,6 +137,7 @@ impl DockItem { applet: &Context, rectangle_tracker: Option<&RectangleTracker>, interaction_enabled: bool, + focused: bool, gpus: Option<&[Gpu]>, ) -> Element<'_, Message> { let Self { @@ -166,28 +170,53 @@ impl DockItem { }) .collect_vec() } else { - (0..min(toplevels.len(), 3)) - .map(|_| { - container(vertical_space(Length::Fixed(0.0))) - .padding(dot_radius) - .style(::Style::Custom(Box::new( - |theme| container::Appearance { - text_color: Some(Color::TRANSPARENT), - background: Some(Background::Color( - theme.cosmic().on_bg_color().into(), - )), - border: Border { - radius: 4.0.into(), - width: 0.0, - color: Color::TRANSPARENT, + if focused { + (0..min(toplevels.len(), 3)) + .map(|_| { + container(vertical_space(Length::Fixed(0.0))) + .padding(dot_radius) + .style(::Style::Custom(Box::new( + |theme| container::Appearance { + text_color: Some(Color::TRANSPARENT), + background: Some(Background::Color( + theme.cosmic().accent_color().into(), + )), + border: Border { + radius: 4.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Shadow::default(), + icon_color: Some(Color::TRANSPARENT), }, - shadow: Shadow::default(), - icon_color: Some(Color::TRANSPARENT), - }, - ))) - .into() - }) - .collect_vec() + ))) + .into() + }) + .collect_vec() + } else { + (0..min(toplevels.len(), 3)) + .map(|_| { + container(vertical_space(Length::Fixed(0.0))) + .padding(dot_radius) + .style(::Style::Custom(Box::new( + |theme| container::Appearance { + text_color: Some(Color::TRANSPARENT), + background: Some(Background::Color( + theme.cosmic().on_bg_color().into(), + )), + border: Border { + radius: 4.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Shadow::default(), + icon_color: Some(Color::TRANSPARENT), + }, + ))) + .into() + }) + .collect_vec() + } }; let icon_wrapper: Element<_> = match applet.anchor { @@ -244,25 +273,37 @@ impl DockItem { dnd_source( mouse_area( icon_button - .on_press_maybe( - toplevels - .first() - .map(|t| Message::Activate(t.0.clone())) - .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) - } - }); + .on_press_maybe(if toplevels.is_empty() { + 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)) - }), - ) + desktop_info + .exec + .clone() + .map(|exec| Message::Exec(exec, gpu_idx)) + } else if toplevels.len() == 1 { + toplevels.first().map(|t| { + if focused { + Message::Minimize(t.0.clone()) + } else { + Message::Activate(t.0.clone()) + } + }) + } else { + // TODO: Change this + toplevels.first().map(|t| { + if focused { + Message::Minimize(t.0.clone()) + } else { + Message::Activate(t.0.clone()) + } + }) + }) .width(Length::Shrink) .height(Length::Shrink), ) @@ -306,6 +347,7 @@ struct CosmicAppList { dnd_offer: Option, is_listening_for_dnd: bool, gpus: Option>, + active_workspace: Option, } // TODO DnD after sctk merges DnD @@ -319,6 +361,7 @@ enum Message { CloseRequested(window::Id), ClosePopup, Activate(ZcosmicToplevelHandleV1), + Minimize(ZcosmicToplevelHandleV1), Exec(String, Option), Quit(String), Ignore, @@ -553,6 +596,14 @@ impl cosmic::Application for CosmicAppList { return destroy_popup(p.0); } } + Message::Minimize(handle) => { + if let Some(tx) = self.wayland_sender.as_ref() { + let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Minimize(handle))); + } + if let Some(p) = self.popup.take() { + return destroy_popup(p.0); + } + } Message::Quit(id) => { if let Some(toplevel_group) = self .active_list @@ -825,6 +876,12 @@ impl cosmic::Application for CosmicAppList { } } }, + WaylandUpdate::Workspace(event) => match event { + WorkspaceUpdate::Enter(handle) => { + self.active_workspace = Some(handle); + } + _ => {} + }, WaylandUpdate::ActivationToken { token, exec, @@ -933,6 +990,7 @@ impl cosmic::Application for CosmicAppList { } fn view(&self) -> Element { + let active_toplevel = self.currently_active_toplevel(); let is_horizontal = match self.core.applet.anchor { PanelAnchor::Top | PanelAnchor::Bottom => true, PanelAnchor::Left | PanelAnchor::Right => false, @@ -945,6 +1003,9 @@ impl cosmic::Application for CosmicAppList { &self.core.applet, self.rectangle_tracker.as_ref(), self.popup.is_none(), + active_toplevel + .as_ref() + .is_some_and(|x| dock_item.toplevels.iter().any(|y| y.0 == *x)), self.gpus.as_deref(), ) }) @@ -957,7 +1018,15 @@ impl cosmic::Application for CosmicAppList { { favorites.insert( index, - item.as_icon(&self.core.applet, None, false, self.gpus.as_deref()), + item.as_icon( + &self.core.applet, + None, + false, + active_toplevel + .as_ref() + .is_some_and(|x| item.toplevels.iter().any(|y| y.0 == *x)), + self.gpus.as_deref(), + ), ); } else if self.is_listening_for_dnd && self.favorite_list.is_empty() { // show star indicating favorite_list is drag target @@ -979,6 +1048,9 @@ impl cosmic::Application for CosmicAppList { &self.core.applet, self.rectangle_tracker.as_ref(), self.popup.is_none(), + active_toplevel + .as_ref() + .is_some_and(|x| dock_item.toplevels.iter().any(|y| y.0 == *x)), self.gpus.as_deref(), ) }) @@ -1252,3 +1324,22 @@ impl cosmic::Application for CosmicAppList { Some(Message::CloseRequested(id)) } } + +impl CosmicAppList { + fn currently_active_toplevel(&self) -> Option { + if self.active_workspace.is_none() { + return None; + } + let active_workspace = self.active_workspace.as_ref().unwrap().clone(); + for toplevel_list in self.active_list.iter().chain(self.favorite_list.iter()) { + for (t_handle, t_info) in &toplevel_list.toplevels { + if t_info.workspace.contains(&active_workspace) + && t_info.state.contains(&State::Activated) + { + return Some(t_handle.clone()); + } + } + } + None + } +} diff --git a/cosmic-app-list/src/wayland_handler.rs b/cosmic-app-list/src/wayland_handler.rs index 5f80861d..8d985a7d 100644 --- a/cosmic-app-list/src/wayland_handler.rs +++ b/cosmic-app-list/src/wayland_handler.rs @@ -1,4 +1,6 @@ -use crate::wayland_subscription::{ToplevelRequest, ToplevelUpdate, WaylandRequest, WaylandUpdate}; +use crate::wayland_subscription::{ + ToplevelRequest, ToplevelUpdate, WaylandRequest, WaylandUpdate, WorkspaceUpdate, +}; use std::os::{ fd::{FromRawFd, RawFd}, unix::net::UnixStream, @@ -8,6 +10,7 @@ use cctk::{ sctk::{ self, activation::{RequestData, RequestDataExt}, + output::{OutputHandler, OutputState}, reexports::{calloop, calloop_wayland_source::WaylandSource}, seat::{SeatHandler, SeatState}, }, @@ -15,13 +18,15 @@ use cctk::{ toplevel_management::{ToplevelManagerHandler, ToplevelManagerState}, wayland_client::{ self, - protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + protocol::{wl_output, wl_seat::WlSeat, wl_surface::WlSurface}, WEnum, }, + workspace::{WorkspaceHandler, WorkspaceState}, }; use cosmic_protocols::{ toplevel_info::v1::client::zcosmic_toplevel_handle_v1, toplevel_management::v1::client::zcosmic_toplevel_manager_v1, + workspace::v1::client::zcosmic_workspace_handle_v1::State, }; use futures::channel::mpsc::UnboundedSender; use sctk::{ @@ -39,6 +44,60 @@ struct AppData { toplevel_info_state: ToplevelInfoState, toplevel_manager_state: ToplevelManagerState, seat_state: SeatState, + workspace_state: WorkspaceState, + output_state: OutputState, +} + +// Need to bind output globals just so workspace can get output events +impl OutputHandler for AppData { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + output: wl_output::WlOutput, + ) { + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + output: wl_output::WlOutput, + ) { + } +} + +impl WorkspaceHandler for AppData { + fn workspace_state(&mut self) -> &mut WorkspaceState { + &mut self.workspace_state + } + + fn done(&mut self) { + 'workspaces_loop: for group in self.workspace_state.workspace_groups() { + for workspace in &group.workspaces { + if workspace.state.contains(&WEnum::Value(State::Active)) { + let _ = + self.tx + .unbounded_send(WaylandUpdate::Workspace(WorkspaceUpdate::Enter( + workspace.handle.clone(), + ))); + break 'workspaces_loop; + } + } + } + } } impl ProvidesRegistryState for AppData { @@ -211,6 +270,10 @@ pub(crate) fn wayland_handler( manager.activate(&handle, &seat); } } + ToplevelRequest::Minimize(handle) => { + let manager = &state.toplevel_manager_state.manager; + manager.set_minimized(&handle); + } ToplevelRequest::Quit(handle) => { let manager = &state.toplevel_manager_state.manager; manager.close(&handle); @@ -264,6 +327,8 @@ pub(crate) fn wayland_handler( seat_state: SeatState::new(&globals, &qh), toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh), toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh), + output_state: OutputState::new(&globals, &qh), + workspace_state: WorkspaceState::new(®istry_state, &qh), registry_state, }; @@ -280,3 +345,6 @@ sctk::delegate_seat!(AppData); sctk::delegate_registry!(AppData); cctk::delegate_toplevel_info!(AppData); cctk::delegate_toplevel_manager!(AppData); + +sctk::delegate_output!(AppData); +cctk::delegate_workspace!(AppData); diff --git a/cosmic-app-list/src/wayland_subscription.rs b/cosmic-app-list/src/wayland_subscription.rs index c9f13d79..20607299 100644 --- a/cosmic-app-list/src/wayland_subscription.rs +++ b/cosmic-app-list/src/wayland_subscription.rs @@ -7,6 +7,7 @@ use cctk::toplevel_info::ToplevelInfo; use cosmic::iced; use cosmic::iced::subscription; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; +use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1; use futures::{ channel::mpsc::{unbounded, UnboundedReceiver}, SinkExt, StreamExt, @@ -79,6 +80,7 @@ pub enum WaylandUpdate { Init(calloop::channel::Sender), Finished, Toplevel(ToplevelUpdate), + Workspace(WorkspaceUpdate), ActivationToken { token: Option, exec: String, @@ -93,6 +95,11 @@ pub enum ToplevelUpdate { Remove(ZcosmicToplevelHandleV1), } +#[derive(Clone, Debug)] +pub enum WorkspaceUpdate { + Enter(ZcosmicWorkspaceHandleV1), +} + #[derive(Clone, Debug)] pub enum WaylandRequest { Toplevel(ToplevelRequest), @@ -106,5 +113,6 @@ pub enum WaylandRequest { #[derive(Debug, Clone)] pub enum ToplevelRequest { Activate(ZcosmicToplevelHandleV1), + Minimize(ZcosmicToplevelHandleV1), Quit(ZcosmicToplevelHandleV1), }