refactor: use separate lists for active & favorites on the app-list

This commit is contained in:
Ashley Wulber 2023-04-06 15:39:59 -04:00 • committed by Ashley Wulber
parent 903a5589a3
commit 020e76ad8a

View file

@ -73,27 +73,114 @@ pub fn run() -> cosmic::iced::Result {
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct Toplevel { struct DockItem {
id: u32, id: usize,
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>, toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
desktop_info: DesktopInfo, desktop_info: DesktopInfo,
popup: Option<window::Id>, }
impl DockItem {
fn new(
id: usize,
toplevel: (ZcosmicToplevelHandleV1, ToplevelInfo),
desktop_info: DesktopInfo,
) -> Self {
Self {
id,
toplevels: vec![toplevel],
desktop_info,
}
}
fn as_icon(&self, applet_helper: &CosmicAppletHelper, rectangle_tracker: Option<&RectangleTracker<usize>>, has_popup: bool) -> Element<'_, Message> {
let DockItem {
toplevels,
desktop_info,
id,
..
} = self;
let cosmic_icon = cosmic::widget::icon(
Path::new(&desktop_info.icon),
applet_helper.suggested_size().0,
);
let dot_radius = 2;
let dots = (0..toplevels.len())
.into_iter()
.map(|_| {
container(vertical_space(Length::Units(0)))
.padding(dot_radius)
.style(<<CosmicAppList as cosmic::iced::Application>::Theme as container::StyleSheet>::Style::Custom(
|theme| container::Appearance {
text_color: Some(Color::TRANSPARENT),
background: Some(Background::Color(
theme.cosmic().on_bg_color().into(),
)),
border_radius: 4.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
))
.into()
})
.collect_vec();
let icon_wrapper = match applet_helper.anchor {
PanelAnchor::Left => row(vec![column(dots).spacing(4).into(), cosmic_icon.into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Right => row(vec![cosmic_icon.into(), column(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Top => column(vec![row(dots).spacing(4).into(), cosmic_icon.into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Bottom => column(vec![cosmic_icon.into(), row(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
};
let mut icon_button = cosmic::widget::button(Button::Text)
.custom(vec![icon_wrapper])
.padding(8);
if !has_popup {
icon_button = icon_button.on_press(
toplevels
.first()
.map(|t| Message::Activate(t.0.clone()))
.unwrap_or_else(|| Message::Exec(desktop_info.exec.clone())),
);
}
// TODO tooltip on hover
let icon_button = mouse_listener(icon_button.width(Length::Shrink).height(Length::Shrink))
.on_right_release(Message::Popup(desktop_info.id.clone()));
if let Some(tracker) = rectangle_tracker {
tracker.container(*id, icon_button).into()
} else {
icon_button.into()
}
}
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct CosmicAppList { struct CosmicAppList {
theme: Theme, theme: Theme,
popup: Option<window::Id>, popup: Option<(window::Id, DockItem)>,
surface_id_ctr: u32, surface_id_ctr: u32,
subscription_ctr: u32, subscription_ctr: u32,
toplevel_ctr: u32, active_list: Vec<DockItem>,
toplevel_list: Vec<Toplevel>, favorite_list: Vec<DockItem>,
dnd_preview: Option<(bool, DockItem)>, // TODO allow non-toplevels to be dragged
config: AppListConfig, config: AppListConfig,
toplevel_sender: Option<Sender<ToplevelRequest>>, toplevel_sender: Option<Sender<ToplevelRequest>>,
applet_helper: CosmicAppletHelper, applet_helper: CosmicAppletHelper,
seat: Option<WlSeat>, seat: Option<WlSeat>,
rectangle_tracker: Option<RectangleTracker<u32>>, rectangle_tracker: Option<RectangleTracker<usize>>,
rectangles: HashMap<u32, iced::Rectangle>, rectangles: HashMap<usize, iced::Rectangle>,
} }
// TODO DnD after sctk merges DnD // TODO DnD after sctk merges DnD
@ -107,11 +194,10 @@ enum Message {
Activate(ZcosmicToplevelHandleV1), Activate(ZcosmicToplevelHandleV1),
Exec(String), Exec(String),
Quit(String), Quit(String),
Errored(String),
Ignore, Ignore,
NewSeat(WlSeat), NewSeat(WlSeat),
RemovedSeat(WlSeat), RemovedSeat(WlSeat),
Rectangle(RectangleUpdate<u32>), Rectangle(RectangleUpdate<usize>),
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -160,6 +246,24 @@ fn desktop_info_for_app_ids(mut app_ids: Vec<String>) -> Vec<DesktopInfo> {
ret ret
} }
fn split_toplevel_favorites(
toplevel_list: Vec<DockItem>,
existing_favorites: &mut Vec<DockItem>,
) -> Vec<DockItem> {
let mut active_list = Vec::new();
for toplevel in toplevel_list {
if let Some(favorite) = existing_favorites.iter_mut().find(|f| {
f.desktop_info.name == toplevel.desktop_info.id
|| f.desktop_info.id == toplevel.desktop_info.id
}) {
favorite.toplevels = toplevel.toplevels;
} else {
active_list.push(toplevel);
}
}
active_list
}
impl Application for CosmicAppList { impl Application for CosmicAppList {
type Message = Message; type Message = Message;
type Theme = Theme; type Theme = Theme;
@ -168,22 +272,17 @@ impl Application for CosmicAppList {
fn new(_flags: ()) -> (Self, Command<Message>) { fn new(_flags: ()) -> (Self, Command<Message>) {
let config = config::AppListConfig::load().unwrap_or_default(); let config = config::AppListConfig::load().unwrap_or_default();
let mut toplevel_ctr = 0;
let self_ = CosmicAppList { let self_ = CosmicAppList {
toplevel_list: desktop_info_for_app_ids(config.favorites.clone()) favorite_list: desktop_info_for_app_ids(config.favorites.clone())
.into_iter() .into_iter()
.map(|e| { .enumerate()
toplevel_ctr += 1; .map(|(i, e)| DockItem {
Toplevel { id: i,
id: toplevel_ctr, toplevels: Default::default(),
toplevels: Default::default(), desktop_info: e,
desktop_info: e,
popup: None,
}
}) })
.collect(), .collect(),
config, config,
toplevel_ctr,
..Default::default() ..Default::default()
}; };
@ -196,19 +295,16 @@ impl Application for CosmicAppList {
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::Errored(_) => {
// TODO log errors
}
Message::Popup(id) => { Message::Popup(id) => {
if let Some((popup_id, _toplevel)) = self.popup.take() {
return destroy_popup(popup_id);
}
if let Some(toplevel_group) = self if let Some(toplevel_group) = self
.toplevel_list .active_list
.iter_mut() .iter()
.chain(self.favorite_list.iter())
.find(|t| t.desktop_info.id == id) .find(|t| t.desktop_info.id == id)
{ {
if let Some(p) = self.popup.take() {
toplevel_group.popup.take();
return destroy_popup(p);
}
let rectangle = match self.rectangles.get(&toplevel_group.id) { let rectangle = match self.rectangles.get(&toplevel_group.id) {
Some(r) => r, Some(r) => r,
None => return Command::none(), None => return Command::none(),
@ -216,8 +312,7 @@ impl Application for CosmicAppList {
self.surface_id_ctr += 1; self.surface_id_ctr += 1;
let new_id = window::Id::new(self.surface_id_ctr); let new_id = window::Id::new(self.surface_id_ctr);
self.popup.replace(new_id); self.popup = Some((new_id, toplevel_group.clone()));
toplevel_group.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id::new(0),
@ -245,11 +340,18 @@ impl Application for CosmicAppList {
let _ = self.config.add_favorite(id); let _ = self.config.add_favorite(id);
} }
Message::UnFavorite(id) => { Message::UnFavorite(id) => {
let _ = self.config.remove_favorite(id); let _ = self.config.remove_favorite(id.clone());
self.toplevel_list.retain(|t| { if let Some(i) = self
self.config.favorites.contains(&t.desktop_info.id) .favorite_list
|| self.config.favorites.contains(&t.desktop_info.name) .iter()
}) .position(|t| t.desktop_info.id == id)
{
let entry = self.favorite_list.remove(i);
self.rectangles.remove(&entry.id);
if !entry.toplevels.is_empty() {
self.active_list.push(entry);
}
}
} }
Message::Activate(handle) => { Message::Activate(handle) => {
if let (Some(tx), Some(seat)) = (self.toplevel_sender.as_ref(), self.seat.as_ref()) if let (Some(tx), Some(seat)) = (self.toplevel_sender.as_ref(), self.seat.as_ref())
@ -258,8 +360,11 @@ impl Application for CosmicAppList {
} }
} }
Message::Quit(id) => { Message::Quit(id) => {
if let Some(toplevel_group) = if let Some(toplevel_group) = self
self.toplevel_list.iter().find(|t| t.desktop_info.id == id) .active_list
.iter()
.chain(self.favorite_list.iter())
.find(|t| t.desktop_info.id == id)
{ {
for (handle, _) in &toplevel_group.toplevels { for (handle, _) in &toplevel_group.toplevels {
if let Some(tx) = self.toplevel_sender.as_ref() { if let Some(tx) = self.toplevel_sender.as_ref() {
@ -274,19 +379,22 @@ impl Application for CosmicAppList {
if info.app_id.is_empty() { if info.app_id.is_empty() {
return Command::none(); return Command::none();
} }
if let Some(i) = self.toplevel_list.iter().position( if let Some(t) = self
|Toplevel { desktop_info, .. }| desktop_info.id == info.app_id, .active_list
) { .iter_mut()
self.toplevel_list[i].toplevels.push((handle, info)); .chain(self.favorite_list.iter_mut())
.find(|DockItem { desktop_info, .. }| {
desktop_info.id == info.app_id || desktop_info.name == info.app_id
})
{
t.toplevels.push((handle, info));
} else { } else {
let desktop_info = let desktop_info =
desktop_info_for_app_ids(vec![info.app_id.clone()]).remove(0); desktop_info_for_app_ids(vec![info.app_id.clone()]).remove(0);
self.toplevel_ctr += 1; self.active_list.push(DockItem {
self.toplevel_list.push(Toplevel { id: self.active_list.len(),
id: self.toplevel_ctr,
toplevels: vec![(handle, info)], toplevels: vec![(handle, info)],
desktop_info, desktop_info,
popup: None,
}); });
} }
} }
@ -295,36 +403,31 @@ impl Application for CosmicAppList {
} }
ToplevelUpdate::Finished => { ToplevelUpdate::Finished => {
self.subscription_ctr += 1; self.subscription_ctr += 1;
for t in &mut self.toplevel_list { for t in &mut self.favorite_list {
t.toplevels.clear(); t.toplevels.clear();
} }
self.active_list.clear();
} }
ToplevelUpdate::RemoveToplevel(handle) => { ToplevelUpdate::RemoveToplevel(handle) => {
if let Some(i) = self.toplevel_list.iter_mut().position( for t in self
|Toplevel { .active_list
toplevels, .iter_mut()
desktop_info, .chain(self.favorite_list.iter_mut())
.. {
}| { t.toplevels.retain(|(t_handle, _)| t_handle != &handle);
if let Some(ret) = toplevels.iter().position(|t| t.0 == handle) {
toplevels.remove(ret);
toplevels.is_empty()
&& !self.config.favorites.contains(&desktop_info.id)
&& !self.config.favorites.contains(&desktop_info.name)
} else {
false
}
},
) {
self.toplevel_list.remove(i);
} }
self.active_list.retain(|t| !t.toplevels.is_empty());
} }
ToplevelUpdate::UpdateToplevel(handle, info) => { ToplevelUpdate::UpdateToplevel(handle, info) => {
// TODO probably want to make sure it is removed // TODO probably want to make sure it is removed
if info.app_id.is_empty() { if info.app_id.is_empty() {
return Command::none(); return Command::none();
} }
'toplevel_loop: for toplevel_list in &mut self.toplevel_list { 'toplevel_loop: for toplevel_list in self
.active_list
.iter_mut()
.chain(self.favorite_list.iter_mut())
{
for (t_handle, t_info) in &mut toplevel_list.toplevels { for (t_handle, t_info) in &mut toplevel_list.toplevels {
if &handle == t_handle { if &handle == t_handle {
*t_info = info; *t_info = info;
@ -366,12 +469,7 @@ impl Application for CosmicAppList {
Message::Ignore => {} Message::Ignore => {}
Message::ClosePopup => { Message::ClosePopup => {
if let Some(p) = self.popup.take() { if let Some(p) = self.popup.take() {
if let Some(toplevel_group) = return destroy_popup(p.0);
self.toplevel_list.iter_mut().find(|t| t.popup == Some(p))
{
toplevel_group.popup.take();
}
return destroy_popup(p);
} }
} }
} }
@ -379,11 +477,14 @@ impl Application for CosmicAppList {
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self, id: window::Id) -> Element<Message> {
if let Some(Toplevel { if let Some((
toplevels, popup_id,
desktop_info, DockItem {
.. toplevels,
}) = self.toplevel_list.iter().find(|t| t.popup == Some(id)) desktop_info,
..
},
)) = self.popup.as_ref().filter(|p| id == p.0)
{ {
let is_favorite = self.config.favorites.contains(&desktop_info.id) let is_favorite = self.config.favorites.contains(&desktop_info.id)
|| self.config.favorites.contains(&desktop_info.name); || self.config.favorites.contains(&desktop_info.name);
@ -450,97 +551,24 @@ impl Application for CosmicAppList {
return self.applet_helper.popup_container(content).into(); return self.applet_helper.popup_container(content).into();
} }
let (favorites, running) = self.toplevel_list.iter().fold( let favorites = self
(Vec::new(), Vec::new()), .favorite_list
|(mut favorites, mut running), .iter()
Toplevel { .map(
id, |dock_item| {
toplevels, dock_item.as_icon(&self.applet_helper, self.rectangle_tracker.as_ref(), self.popup.is_some())
desktop_info, },
.. )
}| { .collect();
let cosmic_icon = cosmic::widget::icon( let active = self
Path::new(&desktop_info.icon), .active_list
self.applet_helper.suggested_size().0, .iter()
); .map(
|dock_item| {
let dot_radius = 2; dock_item.as_icon(&self.applet_helper, self.rectangle_tracker.as_ref(), self.popup.is_some())
let dots = (0..toplevels.len()) },
.into_iter() )
.map(|_| { .collect();
container(vertical_space(Length::Units(0)))
.padding(dot_radius)
.style(<Self::Theme as container::StyleSheet>::Style::Custom(
|theme| container::Appearance {
text_color: Some(Color::TRANSPARENT),
background: Some(Background::Color(
theme.cosmic().on_bg_color().into(),
)),
border_radius: 4.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
))
.into()
})
.collect_vec();
let icon_wrapper = match &self.applet_helper.anchor {
PanelAnchor::Left => {
row(vec![column(dots).spacing(4).into(), cosmic_icon.into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into()
}
PanelAnchor::Right => {
row(vec![cosmic_icon.into(), column(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into()
}
PanelAnchor::Top => {
column(vec![row(dots).spacing(4).into(), cosmic_icon.into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into()
}
PanelAnchor::Bottom => {
column(vec![cosmic_icon.into(), row(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into()
}
};
let mut icon_button = cosmic::widget::button(Button::Text)
.custom(vec![icon_wrapper])
.padding(8);
if self.popup.is_none() {
icon_button = icon_button.on_press(
toplevels
.first()
.map(|t| Message::Activate(t.0.clone()))
.unwrap_or_else(|| Message::Exec(desktop_info.exec.clone())),
);
}
// TODO tooltip on hover
let icon_button =
mouse_listener(icon_button.width(Length::Shrink).height(Length::Shrink))
.on_right_release(Message::Popup(desktop_info.id.clone()));
let icon_button = if let Some(tracker) = self.rectangle_tracker.as_ref() {
tracker.container(*id, icon_button).into()
} else {
icon_button.into()
};
if self.config.favorites.contains(&desktop_info.id)
|| self.config.favorites.contains(&desktop_info.name)
{
favorites.push(icon_button)
} else {
running.push(icon_button);
}
(favorites, running)
},
);
let (w, h) = match self.applet_helper.anchor { let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => (Length::Shrink, Length::Fill), PanelAnchor::Top | PanelAnchor::Bottom => (Length::Shrink, Length::Fill),
@ -552,7 +580,7 @@ impl Application for CosmicAppList {
column![ column![
column(favorites), column(favorites),
divider::horizontal::light(), divider::horizontal::light(),
column(running) column(active)
] ]
.spacing(4) .spacing(4)
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -560,7 +588,7 @@ impl Application for CosmicAppList {
.width(w), .width(w),
), ),
PanelAnchor::Top | PanelAnchor::Bottom => container( PanelAnchor::Top | PanelAnchor::Bottom => container(
row![row(favorites), vertical_rule(1), row(running)] row![row(favorites), vertical_rule(1), row(active)]
.spacing(4) .spacing(4)
.align_items(Alignment::Center) .align_items(Alignment::Center)
.height(h) .height(h)