feat: app-list click: zero and single toplevel behavior
This commit is contained in:
parent
71cb9e64ec
commit
5b78a9f22e
3 changed files with 209 additions and 42 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(®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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue