feat/fix: track output state in app-list

This commit is contained in:
Ryan Brue 2024-04-09 13:47:55 -05:00 committed by Ashley Wulber
parent 3c74e433dd
commit 27324f34e2
3 changed files with 136 additions and 78 deletions

View file

@ -3,15 +3,20 @@ use crate::config::AppListConfig;
use crate::config::APP_ID; use crate::config::APP_ID;
use crate::fl; use crate::fl;
use crate::wayland_subscription::wayland_subscription; use crate::wayland_subscription::wayland_subscription;
use crate::wayland_subscription::OutputUpdate;
use crate::wayland_subscription::ToplevelRequest; use crate::wayland_subscription::ToplevelRequest;
use crate::wayland_subscription::ToplevelUpdate; use crate::wayland_subscription::ToplevelUpdate;
use crate::wayland_subscription::WaylandImage; use crate::wayland_subscription::WaylandImage;
use crate::wayland_subscription::WaylandRequest; use crate::wayland_subscription::WaylandRequest;
use crate::wayland_subscription::WaylandUpdate; use crate::wayland_subscription::WaylandUpdate;
use cctk::sctk::output::OutputInfo;
use cctk::sctk::output::OutputState;
use cctk::sctk::reexports::calloop::channel::Sender; use cctk::sctk::reexports::calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo; use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction;
use cctk::wayland_client::protocol::wl_output::WlOutput;
use cctk::wayland_client::protocol::wl_seat::WlSeat; use cctk::wayland_client::protocol::wl_seat::WlSeat;
use cctk::wayland_client::Proxy;
use cosmic::applet::cosmic_panel_config::PanelSize; use cosmic::applet::cosmic_panel_config::PanelSize;
use cosmic::applet::Size; use cosmic::applet::Size;
use cosmic::cosmic_config::{Config, CosmicConfigEntry}; use cosmic::cosmic_config::{Config, CosmicConfigEntry};
@ -105,6 +110,40 @@ pub fn load_applications_for_app_ids_sorted<'a, 'b>(
ret ret
} }
#[derive(Debug, Clone)]
struct AppletIconData {
icon_size: u16,
icon_spacing: f32,
dot_size: f32,
dot_spacing: f32,
padding: [u16; 2],
}
impl AppletIconData {
fn new(applet: &Context) -> Self {
let suggested_size = applet.suggested_size().0;
let (icon_size, icon_spacing, dot_size, dot_spacing, p_padding) = match applet.size {
Size::PanelSize(PanelSize::XL) => (10 + suggested_size, 0.0, 2.0, 4.0, 5),
Size::PanelSize(PanelSize::L) => (10 + suggested_size, 0.0, 2.0, 4.0, 5),
Size::PanelSize(PanelSize::M) => (10 + suggested_size, 0.0, 2.0, 4.0, 5),
Size::PanelSize(PanelSize::S) => (16 + suggested_size, 0.0, 1.0, 2.0, 3),
Size::PanelSize(PanelSize::XS) => (8 + suggested_size, 0.0, 1.0, 2.0, 3),
Size::Hardcoded(_) => (10 + suggested_size, 4.0, 0.0, 4.0, 5),
};
let padding = match applet.anchor {
PanelAnchor::Bottom | PanelAnchor::Top => [0, p_padding],
PanelAnchor::Left | PanelAnchor::Right => [p_padding, 0],
};
AppletIconData {
icon_size,
icon_spacing,
dot_size,
dot_spacing,
padding,
}
}
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct DockItem { struct DockItem {
id: u32, id: u32,
@ -156,23 +195,14 @@ impl DockItem {
.. ..
} = self; } = self;
let (app_icon_size_modifier, dot_radius, p_padding) = match applet.size { let app_icon = AppletIconData::new(applet);
Size::PanelSize(PanelSize::XL) => (10, 2.0, 5),
Size::PanelSize(PanelSize::L) => (10, 2.0, 5),
Size::PanelSize(PanelSize::M) => (10, 2.0, 5),
Size::PanelSize(PanelSize::S) => (16, 1.0, 3),
Size::PanelSize(PanelSize::XS) => (8, 1.0, 3),
Size::Hardcoded(_) => (10, 2.0, 5),
};
let app_icon_size = applet.suggested_size().0 + app_icon_size_modifier; let cosmic_icon = desktop_info.icon.as_cosmic_icon().size(app_icon.icon_size);
let cosmic_icon = desktop_info.icon.as_cosmic_icon().size(app_icon_size);
let dot_spacer = (0..1) let dot_spacer = (0..1)
.map(|_| { .map(|_| {
container(vertical_space(Length::Fixed(0.0))) container(vertical_space(Length::Fixed(0.0)))
.padding(dot_radius) .padding(app_icon.dot_size)
.into() .into()
}) })
.collect_vec(); .collect_vec();
@ -181,7 +211,7 @@ impl DockItem {
(0..1) (0..1)
.map(|_| { .map(|_| {
container(vertical_space(Length::Fixed(0.0))) container(vertical_space(Length::Fixed(0.0)))
.padding(dot_radius) .padding(app_icon.dot_size)
.into() .into()
}) })
.collect_vec() .collect_vec()
@ -189,7 +219,7 @@ impl DockItem {
(0..min(toplevels.len(), 3)) (0..min(toplevels.len(), 3))
.map(|_| { .map(|_| {
container(vertical_space(Length::Fixed(0.0))) container(vertical_space(Length::Fixed(0.0)))
.padding(dot_radius) .padding(app_icon.dot_size)
.style(<Theme as container::StyleSheet>::Style::Custom(Box::new( .style(<Theme as container::StyleSheet>::Style::Custom(Box::new(
|theme| container::Appearance { |theme| container::Appearance {
text_color: Some(Color::TRANSPARENT), text_color: Some(Color::TRANSPARENT),
@ -212,55 +242,43 @@ impl DockItem {
let icon_wrapper: Element<_> = match applet.anchor { let icon_wrapper: Element<_> = match applet.anchor {
PanelAnchor::Left => row(vec![ PanelAnchor::Left => row(vec![
column(dots).spacing(4).into(), column(dots).spacing(app_icon.dot_spacing).into(),
cosmic_icon.into(), cosmic_icon.into(),
column(dot_spacer).spacing(4).into(), column(dot_spacer).spacing(app_icon.dot_spacing).into(),
]) ])
.align_items(iced::Alignment::Center) .align_items(iced::Alignment::Center)
.spacing(1) .spacing(1)
.into(), .into(),
PanelAnchor::Right => row(vec![ PanelAnchor::Right => row(vec![
column(dot_spacer).spacing(4).into(), column(dot_spacer).spacing(app_icon.dot_spacing).into(),
cosmic_icon.into(), cosmic_icon.into(),
column(dots).spacing(4).into(), column(dots).spacing(app_icon.dot_spacing).into(),
]) ])
.align_items(iced::Alignment::Center) .align_items(iced::Alignment::Center)
.spacing(1) .spacing(1)
.into(), .into(),
PanelAnchor::Top => column(vec![ PanelAnchor::Top => column(vec![
row(dots).spacing(4).into(), row(dots).spacing(app_icon.dot_spacing).into(),
cosmic_icon.into(), cosmic_icon.into(),
row(dot_spacer).spacing(4).into(), row(dot_spacer).spacing(app_icon.dot_spacing).into(),
]) ])
.align_items(iced::Alignment::Center) .align_items(iced::Alignment::Center)
.spacing(1) .spacing(1)
.into(), .into(),
PanelAnchor::Bottom => column(vec![ PanelAnchor::Bottom => column(vec![
row(dot_spacer).spacing(4).into(), row(dot_spacer).spacing(app_icon.dot_spacing).into(),
cosmic_icon.into(), cosmic_icon.into(),
row(dots).spacing(4).into(), row(dots).spacing(app_icon.dot_spacing).into(),
]) ])
.align_items(iced::Alignment::Center) .align_items(iced::Alignment::Center)
.spacing(1) .spacing(1)
.into(), .into(),
}; };
let icon_button = match applet.anchor { let icon_button = cosmic::widget::button(icon_wrapper)
PanelAnchor::Left => cosmic::widget::button(icon_wrapper) .padding(app_icon.padding)
.style(Button::Text) .selected(is_focused)
.padding([p_padding, 0]), .style(app_list_icon_style(is_focused));
PanelAnchor::Right => cosmic::widget::button(icon_wrapper)
.style(Button::Text)
.padding([p_padding, 0]),
PanelAnchor::Top => cosmic::widget::button(icon_wrapper)
.style(Button::Text)
.padding([0, p_padding]),
PanelAnchor::Bottom => cosmic::widget::button(icon_wrapper)
.style(Button::Text)
.padding([0, p_padding]),
}
.selected(is_focused)
.style(app_list_icon_style(is_focused));
let icon_button = if interaction_enabled { let icon_button = if interaction_enabled {
dnd_source( dnd_source(
@ -320,7 +338,8 @@ struct CosmicAppList {
dnd_offer: Option<DndOffer>, dnd_offer: Option<DndOffer>,
is_listening_for_dnd: bool, is_listening_for_dnd: bool,
gpus: Option<Vec<Gpu>>, gpus: Option<Vec<Gpu>>,
active_workspace: Option<ZcosmicWorkspaceHandleV1>, active_workspaces: Vec<ZcosmicWorkspaceHandleV1>,
output_list: HashMap<WlOutput, OutputInfo>,
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
@ -1093,9 +1112,18 @@ impl cosmic::Application for CosmicAppList {
} }
} }
}, },
WaylandUpdate::Workspace(handle) => { WaylandUpdate::Workspace(workspaces) => self.active_workspaces = workspaces,
self.active_workspace = Some(handle); WaylandUpdate::Output(event) => match event {
} OutputUpdate::Add(output, info) => {
self.output_list.insert(output, info);
}
OutputUpdate::Update(output, info) => {
self.output_list.insert(output, info);
}
OutputUpdate::Remove(output) => {
self.output_list.remove(&output);
}
},
WaylandUpdate::ActivationToken { WaylandUpdate::ActivationToken {
token, token,
exec, exec,
@ -1205,6 +1233,7 @@ impl cosmic::Application for CosmicAppList {
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let focused_item = self.currently_active_toplevel(); let focused_item = self.currently_active_toplevel();
let app_icon = AppletIconData::new(&self.core.applet);
let is_horizontal = match self.core.applet.anchor { let is_horizontal = match self.core.applet.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => true, PanelAnchor::Top | PanelAnchor::Bottom => true,
PanelAnchor::Left | PanelAnchor::Right => false, PanelAnchor::Left | PanelAnchor::Right => false,
@ -1274,20 +1303,18 @@ impl cosmic::Application for CosmicAppList {
( (
Length::Shrink, Length::Shrink,
Length::Shrink, Length::Shrink,
dnd_listener(row(favorites)), dnd_listener(row(favorites).spacing(app_icon.icon_spacing)),
row(active).into(), row(active).spacing(app_icon.icon_spacing).into(),
container(vertical_rule(1)) container(vertical_rule(1)).height(Length::Fill).into(),
.height(self.core.applet.suggested_size().1)
.into(),
) )
} else { } else {
( (
Length::Shrink, Length::Shrink,
Length::Shrink, Length::Shrink,
dnd_listener(column(favorites)), dnd_listener(column(favorites).spacing(app_icon.icon_spacing)),
column(active).into(), column(active).spacing(app_icon.icon_spacing).into(),
container(divider::horizontal::default()) container(divider::horizontal::default())
.width(self.core.applet.suggested_size().1) .width(Length::Fill)
.into(), .into(),
) )
}; };
@ -1342,14 +1369,14 @@ impl cosmic::Application for CosmicAppList {
let mut content = match &self.core.applet.anchor { let mut content = match &self.core.applet.anchor {
PanelAnchor::Left | PanelAnchor::Right => container( PanelAnchor::Left | PanelAnchor::Right => container(
Column::with_children(content_list) Column::with_children(content_list)
.spacing(4) .spacing(4.0)
.align_items(Alignment::Center) .align_items(Alignment::Center)
.height(h) .height(h)
.width(w), .width(w),
), ),
PanelAnchor::Top | PanelAnchor::Bottom => container( PanelAnchor::Top | PanelAnchor::Bottom => container(
Row::with_children(content_list) Row::with_children(content_list)
.spacing(4) .spacing(4.0)
.align_items(Alignment::Center) .align_items(Alignment::Center)
.height(h) .height(h)
.width(w), .width(w),
@ -1592,16 +1619,23 @@ impl cosmic::Application for CosmicAppList {
impl CosmicAppList { impl CosmicAppList {
fn currently_active_toplevel(&self) -> Vec<ZcosmicToplevelHandleV1> { fn currently_active_toplevel(&self) -> Vec<ZcosmicToplevelHandleV1> {
if self.active_workspace.is_none() { if self.active_workspaces.is_empty() {
println!("No active workspace?");
return Vec::new(); return Vec::new();
} }
let current_output = self.core.applet.output_name.clone();
let mut focused_toplevels: Vec<ZcosmicToplevelHandleV1> = Vec::new(); let mut focused_toplevels: Vec<ZcosmicToplevelHandleV1> = Vec::new();
let active_workspace = self.active_workspace.as_ref().unwrap().clone(); let active_workspaces = self.active_workspaces.clone();
for toplevel_list in self.active_list.iter().chain(self.favorite_list.iter()) { for toplevel_list in self.active_list.iter().chain(self.favorite_list.iter()) {
for (t_handle, t_info, _) in &toplevel_list.toplevels { for (t_handle, t_info, _) in &toplevel_list.toplevels {
if t_info.workspace.contains(&active_workspace) if t_info.state.contains(&State::Activated)
&& t_info.state.contains(&State::Activated) && active_workspaces
.iter()
.any(|workspace| t_info.workspace.contains(workspace))
&& t_info.output.iter().any(|x| {
self.output_list.get(x).is_some_and(|val| {
val.name.as_ref().is_some_and(|n| *n == current_output)
})
})
{ {
focused_toplevels.push(t_handle.clone()); focused_toplevels.push(t_handle.clone());
} }

View file

@ -1,5 +1,5 @@
use crate::wayland_subscription::{ use crate::wayland_subscription::{
ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate, OutputUpdate, ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
}; };
use std::{ use std::{
os::{ os::{
@ -58,6 +58,7 @@ struct AppData {
tx: UnboundedSender<WaylandUpdate>, tx: UnboundedSender<WaylandUpdate>,
conn: Connection, conn: Connection,
queue_handle: QueueHandle<Self>, queue_handle: QueueHandle<Self>,
output_state: OutputState,
workspace_state: WorkspaceState, workspace_state: WorkspaceState,
toplevel_info_state: ToplevelInfoState, toplevel_info_state: ToplevelInfoState,
toplevel_manager_state: ToplevelManagerState, toplevel_manager_state: ToplevelManagerState,
@ -66,7 +67,6 @@ struct AppData {
seat_state: SeatState, seat_state: SeatState,
shm_state: Shm, shm_state: Shm,
activation_state: Option<ActivationState>, activation_state: Option<ActivationState>,
output_state: OutputState,
} }
// Workspace and toplevel handling // Workspace and toplevel handling
@ -83,14 +83,30 @@ impl OutputHandler for AppData {
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
output: wl_output::WlOutput, output: wl_output::WlOutput,
) { ) {
if let Some(info) = self.output_state.info(&output) {
let _ = self
.tx
.unbounded_send(WaylandUpdate::Output(OutputUpdate::Add(
output.clone(),
info.clone(),
)));
}
} }
fn update_output( fn update_output(
&mut self, &mut self,
_conn: &Connection, _conn: &Connection,
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
_output: wl_output::WlOutput, output: wl_output::WlOutput,
) { ) {
if let Some(info) = self.output_state.info(&output) {
let _ = self
.tx
.unbounded_send(WaylandUpdate::Output(OutputUpdate::Update(
output.clone(),
info.clone(),
)));
}
} }
fn output_destroyed( fn output_destroyed(
@ -99,6 +115,9 @@ impl OutputHandler for AppData {
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
output: wl_output::WlOutput, output: wl_output::WlOutput,
) { ) {
let _ = self
.tx
.unbounded_send(WaylandUpdate::Output(OutputUpdate::Remove(output.clone())));
} }
} }
@ -108,19 +127,23 @@ impl WorkspaceHandler for AppData {
} }
fn done(&mut self) { fn done(&mut self) {
'workspaces_loop: for group in self.workspace_state.workspace_groups() { let active_workspaces = self
for workspace in &group.workspaces { .workspace_state
if workspace .workspace_groups()
.state .iter()
.contains(&WEnum::Value(WorkspaceUpdateState::Active)) .filter_map(|x| {
{ x.workspaces.iter().find(|w| {
let _ = self w.state
.tx .contains(&WEnum::Value(WorkspaceUpdateState::Active))
.unbounded_send(WaylandUpdate::Workspace(workspace.handle.clone())); })
break 'workspaces_loop; })
} .map(|workspace| workspace.handle.clone())
} .collect::<Vec<_>>();
} let _ = self
.tx
.unbounded_send(WaylandUpdate::Workspace(
active_workspaces.clone(),
));
} }
} }
@ -667,14 +690,14 @@ pub(crate) fn wayland_handler(
return; return;
} }
let registry_state = RegistryState::new(&globals); let registry_state = RegistryState::new(&globals);
let workspace_state = WorkspaceState::new(&registry_state, &qh); // Create before toplevel info state
let mut app_data = AppData { let mut app_data = AppData {
exit: false, exit: false,
tx, tx,
conn, conn,
queue_handle: qh.clone(), queue_handle: qh.clone(),
workspace_state, output_state: OutputState::new(&globals, &qh),
workspace_state: WorkspaceState::new(&registry_state, &qh),
toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh), toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh),
toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh), toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh),
screencopy_state: ScreencopyState::new(&globals, &qh), screencopy_state: ScreencopyState::new(&globals, &qh),
@ -682,7 +705,6 @@ pub(crate) fn wayland_handler(
seat_state: SeatState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh),
shm_state: Shm::bind(&globals, &qh).unwrap(), shm_state: Shm::bind(&globals, &qh).unwrap(),
activation_state: ActivationState::bind::<AppData>(&globals, &qh).ok(), activation_state: ActivationState::bind::<AppData>(&globals, &qh).ok(),
output_state: OutputState::new(&globals, &qh),
}; };
loop { loop {

View file

@ -2,8 +2,9 @@
//! //!
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. //! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
//! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`. //! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`.
use cctk::sctk::reexports::calloop; use cctk::sctk::{output::OutputInfo, reexports::calloop};
use cctk::toplevel_info::ToplevelInfo; use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_output::WlOutput;
use cosmic::iced; use cosmic::iced;
use cosmic::iced::subscription; use cosmic::iced::subscription;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
@ -99,7 +100,8 @@ pub enum WaylandUpdate {
Init(calloop::channel::Sender<WaylandRequest>), Init(calloop::channel::Sender<WaylandRequest>),
Finished, Finished,
Toplevel(ToplevelUpdate), Toplevel(ToplevelUpdate),
Workspace(ZcosmicWorkspaceHandleV1), Workspace(Vec<ZcosmicWorkspaceHandleV1>),
Output(OutputUpdate),
ActivationToken { ActivationToken {
token: Option<String>, token: Option<String>,
exec: String, exec: String,