2022-09-25 22:49:00 +01:00
|
|
|
use super::open_state::OpenState;
|
2023-04-29 22:08:02 +01:00
|
|
|
use crate::clients::wayland::ToplevelHandle;
|
2023-08-13 14:23:05 +01:00
|
|
|
use crate::config::BarPosition;
|
2023-07-16 18:57:00 +01:00
|
|
|
use crate::gtk_helpers::IronbarGtkExt;
|
2023-01-29 17:46:02 +00:00
|
|
|
use crate::image::ImageProvider;
|
2022-09-25 22:49:00 +01:00
|
|
|
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
|
|
|
use crate::modules::ModuleUpdateEvent;
|
2022-12-11 22:45:52 +00:00
|
|
|
use crate::{read_lock, try_send};
|
2023-04-29 22:08:02 +01:00
|
|
|
use color_eyre::{Report, Result};
|
2022-08-14 14:30:13 +01:00
|
|
|
use gtk::prelude::*;
|
2023-08-13 14:23:05 +01:00
|
|
|
use gtk::{Button, IconTheme};
|
2022-11-05 17:32:01 +00:00
|
|
|
use indexmap::IndexMap;
|
2022-08-14 14:30:13 +01:00
|
|
|
use std::rc::Rc;
|
2022-09-25 22:49:00 +01:00
|
|
|
use std::sync::RwLock;
|
|
|
|
use tokio::sync::mpsc::Sender;
|
2023-01-29 17:46:02 +00:00
|
|
|
use tracing::error;
|
2023-04-29 22:08:02 +01:00
|
|
|
use wayland_client::protocol::wl_seat::WlSeat;
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
2022-09-25 22:49:00 +01:00
|
|
|
pub struct Item {
|
2022-08-14 14:30:13 +01:00
|
|
|
pub app_id: String,
|
|
|
|
pub favorite: bool,
|
2022-08-25 23:42:57 +01:00
|
|
|
pub open_state: OpenState,
|
2022-11-05 17:32:01 +00:00
|
|
|
pub windows: IndexMap<usize, Window>,
|
2022-10-10 20:15:24 +01:00
|
|
|
pub name: String,
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
impl Item {
|
2022-11-05 17:32:01 +00:00
|
|
|
pub fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
|
2022-09-25 22:49:00 +01:00
|
|
|
Self {
|
|
|
|
app_id,
|
|
|
|
favorite,
|
|
|
|
open_state,
|
2022-11-05 17:32:01 +00:00
|
|
|
windows: IndexMap::new(),
|
2022-10-10 20:15:24 +01:00
|
|
|
name: String::new(),
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
/// Merges the provided node into this launcher item
|
2023-04-29 22:08:02 +01:00
|
|
|
pub fn merge_toplevel(&mut self, handle: ToplevelHandle) -> Result<Window> {
|
|
|
|
let info = handle
|
|
|
|
.info()
|
|
|
|
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
|
|
|
|
|
|
|
let id = info.id;
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
if self.windows.is_empty() {
|
2023-04-29 22:08:02 +01:00
|
|
|
self.name = info.title;
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
let window = Window::try_from(handle)?;
|
2022-09-25 22:49:00 +01:00
|
|
|
self.windows.insert(id, window.clone());
|
|
|
|
|
|
|
|
self.recalculate_open_state();
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
Ok(window)
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
pub fn unmerge_toplevel(&mut self, handle: &ToplevelHandle) {
|
|
|
|
if let Some(info) = handle.info() {
|
|
|
|
self.windows.remove(&info.id);
|
|
|
|
self.recalculate_open_state();
|
|
|
|
}
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-10-10 20:15:24 +01:00
|
|
|
pub fn set_window_name(&mut self, window_id: usize, name: String) {
|
2022-09-25 22:49:00 +01:00
|
|
|
if let Some(window) = self.windows.get_mut(&window_id) {
|
|
|
|
if let OpenState::Open { focused: true, .. } = window.open_state {
|
|
|
|
self.name = name.clone();
|
|
|
|
}
|
|
|
|
|
|
|
|
window.name = name;
|
|
|
|
}
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-10-10 20:15:24 +01:00
|
|
|
pub fn set_window_focused(&mut self, window_id: usize, focused: bool) {
|
2022-09-25 22:49:00 +01:00
|
|
|
if let Some(window) = self.windows.get_mut(&window_id) {
|
|
|
|
window.open_state =
|
|
|
|
OpenState::merge_states(&[&window.open_state, &OpenState::focused(focused)]);
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
self.recalculate_open_state();
|
|
|
|
}
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
/// Sets this item's open state
|
|
|
|
/// to the merged result of its windows' open states
|
|
|
|
fn recalculate_open_state(&mut self) {
|
|
|
|
let new_state = OpenState::merge_states(
|
|
|
|
&self
|
|
|
|
.windows
|
|
|
|
.iter()
|
2022-11-05 17:32:01 +00:00
|
|
|
.map(|(_, win)| &win.open_state)
|
2022-09-25 22:49:00 +01:00
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
);
|
|
|
|
self.open_state = new_state;
|
|
|
|
}
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
impl TryFrom<ToplevelHandle> for Item {
|
|
|
|
type Error = Report;
|
|
|
|
|
|
|
|
fn try_from(handle: ToplevelHandle) -> std::result::Result<Self, Self::Error> {
|
|
|
|
let info = handle
|
|
|
|
.info()
|
|
|
|
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
|
|
|
|
|
|
|
let name = info.title.clone();
|
|
|
|
let app_id = info.app_id.clone();
|
|
|
|
let open_state = OpenState::from(&info);
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-11-05 17:32:01 +00:00
|
|
|
let mut windows = IndexMap::new();
|
2023-04-29 22:08:02 +01:00
|
|
|
let window = Window::try_from(handle)?;
|
|
|
|
windows.insert(info.id, window);
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
Ok(Self {
|
2022-09-25 22:49:00 +01:00
|
|
|
app_id,
|
|
|
|
favorite: false,
|
|
|
|
open_state,
|
|
|
|
windows,
|
|
|
|
name,
|
2023-04-29 22:08:02 +01:00
|
|
|
})
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct Window {
|
2022-10-10 20:15:24 +01:00
|
|
|
pub id: usize,
|
|
|
|
pub name: String,
|
2022-09-25 22:49:00 +01:00
|
|
|
pub open_state: OpenState,
|
2023-04-29 22:08:02 +01:00
|
|
|
handle: ToplevelHandle,
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
impl TryFrom<ToplevelHandle> for Window {
|
|
|
|
type Error = Report;
|
2022-09-25 22:49:00 +01:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
fn try_from(handle: ToplevelHandle) -> Result<Self, Self::Error> {
|
|
|
|
let info = handle
|
|
|
|
.info()
|
|
|
|
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
|
|
|
let open_state = OpenState::from(&info);
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
id: info.id,
|
|
|
|
name: info.title,
|
2022-09-25 22:49:00 +01:00
|
|
|
open_state,
|
2023-04-29 22:08:02 +01:00
|
|
|
handle,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Window {
|
|
|
|
pub fn focus(&self, seat: &WlSeat) {
|
|
|
|
self.handle.focus(seat);
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
pub struct MenuState {
|
|
|
|
pub num_windows: usize,
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
pub struct ItemButton {
|
|
|
|
pub button: Button,
|
|
|
|
pub persistent: bool,
|
|
|
|
pub show_names: bool,
|
|
|
|
pub menu_state: Rc<RwLock<MenuState>>,
|
|
|
|
}
|
|
|
|
|
2023-04-22 22:18:36 +01:00
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
pub struct AppearanceOptions {
|
|
|
|
pub show_names: bool,
|
|
|
|
pub show_icons: bool,
|
|
|
|
pub icon_size: i32,
|
|
|
|
}
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
impl ItemButton {
|
|
|
|
pub fn new(
|
|
|
|
item: &Item,
|
2023-04-22 22:18:36 +01:00
|
|
|
appearance: AppearanceOptions,
|
2022-09-25 22:49:00 +01:00
|
|
|
icon_theme: &IconTheme,
|
2023-08-13 14:23:05 +01:00
|
|
|
bar_position: BarPosition,
|
2022-09-25 22:49:00 +01:00
|
|
|
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
|
|
|
controller_tx: &Sender<ItemEvent>,
|
|
|
|
) -> Self {
|
|
|
|
let mut button = Button::builder();
|
|
|
|
|
2023-04-22 22:18:36 +01:00
|
|
|
if appearance.show_names {
|
2022-10-10 20:15:24 +01:00
|
|
|
button = button.label(&item.name);
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
|
2023-01-29 17:46:02 +00:00
|
|
|
let button = button.build();
|
|
|
|
|
2023-04-22 22:18:36 +01:00
|
|
|
if appearance.show_icons {
|
2023-01-29 17:46:02 +00:00
|
|
|
let gtk_image = gtk::Image::new();
|
2023-04-22 22:18:36 +01:00
|
|
|
let image =
|
2023-07-26 21:49:45 +01:00
|
|
|
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
|
2023-05-20 14:36:04 +01:00
|
|
|
if let Some(image) = image {
|
|
|
|
button.set_image(Some(>k_image));
|
|
|
|
button.set_always_show_image(true);
|
|
|
|
|
|
|
|
if let Err(err) = image.load_into_image(gtk_image) {
|
|
|
|
error!("{err:?}");
|
2023-01-29 17:46:02 +00:00
|
|
|
}
|
|
|
|
};
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
let style_context = button.style_context();
|
|
|
|
style_context.add_class("item");
|
|
|
|
|
|
|
|
if item.favorite {
|
|
|
|
style_context.add_class("favorite");
|
|
|
|
}
|
|
|
|
if item.open_state.is_open() {
|
|
|
|
style_context.add_class("open");
|
|
|
|
}
|
|
|
|
if item.open_state.is_focused() {
|
|
|
|
style_context.add_class("focused");
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
{
|
|
|
|
let app_id = item.app_id.clone();
|
|
|
|
let tx = controller_tx.clone();
|
|
|
|
button.connect_clicked(move |button| {
|
2022-12-11 22:45:52 +00:00
|
|
|
// lazy check :| TODO: Improve this
|
2022-09-25 22:49:00 +01:00
|
|
|
let style_context = button.style_context();
|
|
|
|
if style_context.has_class("open") {
|
2022-12-11 22:45:52 +00:00
|
|
|
try_send!(tx, ItemEvent::FocusItem(app_id.clone()));
|
2022-09-25 22:49:00 +01:00
|
|
|
} else {
|
2022-12-11 22:45:52 +00:00
|
|
|
try_send!(tx, ItemEvent::OpenItem(app_id.clone()));
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
|
|
|
});
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
2022-08-25 23:42:57 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
let menu_state = Rc::new(RwLock::new(MenuState {
|
|
|
|
num_windows: item.windows.len(),
|
|
|
|
}));
|
|
|
|
|
|
|
|
{
|
|
|
|
let app_id = item.app_id.clone();
|
|
|
|
let tx = tx.clone();
|
|
|
|
let menu_state = menu_state.clone();
|
|
|
|
|
|
|
|
button.connect_enter_notify_event(move |button, _| {
|
2022-12-11 22:45:52 +00:00
|
|
|
let menu_state = read_lock!(menu_state);
|
2022-08-25 23:42:57 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
if menu_state.num_windows > 1 {
|
2022-12-11 22:45:52 +00:00
|
|
|
try_send!(
|
|
|
|
tx,
|
|
|
|
ModuleUpdateEvent::Update(LauncherUpdate::Hover(app_id.clone(),))
|
|
|
|
);
|
|
|
|
|
|
|
|
try_send!(
|
|
|
|
tx,
|
2023-08-13 14:23:05 +01:00
|
|
|
ModuleUpdateEvent::OpenPopupAt(
|
|
|
|
button.geometry(bar_position.get_orientation())
|
|
|
|
)
|
2022-12-11 22:45:52 +00:00
|
|
|
);
|
2022-09-25 22:49:00 +01:00
|
|
|
} else {
|
2022-12-11 22:45:52 +00:00
|
|
|
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
2022-09-25 22:49:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Inhibit(false)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-08-13 14:23:05 +01:00
|
|
|
{
|
|
|
|
let tx = tx.clone();
|
|
|
|
|
|
|
|
button.connect_leave_notify_event(move |button, ev| {
|
|
|
|
const THRESHOLD: f64 = 5.0;
|
|
|
|
|
|
|
|
let alloc = button.allocation();
|
|
|
|
|
|
|
|
let (x, y) = ev.position();
|
|
|
|
|
|
|
|
let close = match bar_position {
|
|
|
|
BarPosition::Top => y + THRESHOLD < alloc.height() as f64,
|
2023-08-13 20:33:08 +01:00
|
|
|
BarPosition::Bottom => y > THRESHOLD,
|
2023-08-13 14:23:05 +01:00
|
|
|
BarPosition::Left => x + THRESHOLD < alloc.width() as f64,
|
|
|
|
BarPosition::Right => x > THRESHOLD,
|
|
|
|
};
|
|
|
|
|
|
|
|
if close {
|
|
|
|
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
|
|
|
}
|
|
|
|
|
|
|
|
Inhibit(false)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
button.show_all();
|
|
|
|
|
|
|
|
Self {
|
|
|
|
button,
|
|
|
|
persistent: item.favorite,
|
2023-04-22 22:18:36 +01:00
|
|
|
show_names: appearance.show_names,
|
2022-09-25 22:49:00 +01:00
|
|
|
menu_state,
|
2022-08-25 23:42:57 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
pub fn set_open(&self, open: bool) {
|
|
|
|
self.update_class("open", open);
|
2022-08-25 23:42:57 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
if !open {
|
|
|
|
self.set_focused(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_focused(&self, focused: bool) {
|
|
|
|
self.update_class("focused", focused);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds or removes a class to the button based on `toggle`.
|
|
|
|
fn update_class(&self, class: &str, toggle: bool) {
|
|
|
|
let style_context = self.button.style_context();
|
|
|
|
|
|
|
|
if toggle {
|
|
|
|
style_context.add_class(class);
|
|
|
|
} else {
|
|
|
|
style_context.remove_class(class);
|
|
|
|
}
|
2022-08-25 23:42:57 +01:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|