feat: app-list click: zero and single toplevel behavior

This commit is contained in:
Ryan Brue 2024-03-30 13:55:12 -05:00 committed by Ashley Wulber
parent 71cb9e64ec
commit 5b78a9f22e
3 changed files with 209 additions and 42 deletions

View file

@ -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<u32>>,
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(<Theme as container::StyleSheet>::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(<Theme as container::StyleSheet>::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(<Theme as container::StyleSheet>::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<DndOffer>,
is_listening_for_dnd: bool,
gpus: Option<Vec<Gpu>>,
active_workspace: Option<ZcosmicWorkspaceHandleV1>,
}
// TODO DnD after sctk merges DnD
@ -319,6 +361,7 @@ enum Message {
CloseRequested(window::Id),
ClosePopup,
Activate(ZcosmicToplevelHandleV1),
Minimize(ZcosmicToplevelHandleV1),
Exec(String, Option<usize>),
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<Message> {
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<ZcosmicToplevelHandleV1> {
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
}
}

View file

@ -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<Self>,
output: wl_output::WlOutput,
) {
}
fn update_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
fn output_destroyed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
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(&registry_state, &qh),
toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh),
output_state: OutputState::new(&globals, &qh),
workspace_state: WorkspaceState::new(&registry_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);

View file

@ -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<WaylandRequest>),
Finished,
Toplevel(ToplevelUpdate),
Workspace(WorkspaceUpdate),
ActivationToken {
token: Option<String>,
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),
}