1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 14:51:04 +02:00
ironbar/src/modules/launcher/item.rs

323 lines
8.7 KiB
Rust
Raw Normal View History

use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle;
use crate::config::BarPosition;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::{read_lock, try_send};
use color_eyre::{Report, Result};
2022-08-14 14:30:13 +01:00
use gtk::prelude::*;
use gtk::{Button, IconTheme};
use indexmap::IndexMap;
2022-08-14 14:30:13 +01:00
use std::rc::Rc;
use std::sync::RwLock;
use tokio::sync::mpsc::Sender;
use tracing::error;
use wayland_client::protocol::wl_seat::WlSeat;
2022-08-14 14:30:13 +01:00
#[derive(Debug, Clone)]
pub struct Item {
2022-08-14 14:30:13 +01:00
pub app_id: String,
pub favorite: bool,
pub open_state: OpenState,
pub windows: IndexMap<usize, Window>,
pub name: String,
2022-08-14 14:30:13 +01:00
}
impl Item {
pub fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
Self {
app_id,
favorite,
open_state,
windows: IndexMap::new(),
name: String::new(),
}
}
2022-08-14 14:30:13 +01:00
/// Merges the provided node into this launcher item
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
if self.windows.is_empty() {
self.name = info.title;
}
2022-08-14 14:30:13 +01:00
let window = Window::try_from(handle)?;
self.windows.insert(id, window.clone());
self.recalculate_open_state();
2022-08-14 14:30:13 +01:00
Ok(window)
2022-08-14 14:30:13 +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-08-14 14:30:13 +01:00
pub fn set_window_name(&mut self, window_id: usize, name: String) {
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
pub fn set_window_focused(&mut self, window_id: usize, focused: bool) {
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
self.recalculate_open_state();
}
}
2022-08-14 14:30:13 +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()
.map(|(_, win)| &win.open_state)
.collect::<Vec<_>>(),
);
self.open_state = new_state;
}
}
2022-08-14 14:30:13 +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
let mut windows = IndexMap::new();
let window = Window::try_from(handle)?;
windows.insert(info.id, window);
2022-08-14 14:30:13 +01:00
Ok(Self {
app_id,
favorite: false,
open_state,
windows,
name,
})
2022-08-14 14:30:13 +01:00
}
}
2022-08-14 14:30:13 +01:00
#[derive(Clone, Debug)]
pub struct Window {
pub id: usize,
pub name: String,
pub open_state: OpenState,
handle: ToplevelHandle,
}
impl TryFrom<ToplevelHandle> for Window {
type Error = Report;
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,
open_state,
handle,
})
}
}
impl Window {
pub fn focus(&self, seat: &WlSeat) {
self.handle.focus(seat);
2022-08-14 14:30:13 +01:00
}
}
2022-08-14 14:30:13 +01:00
pub struct MenuState {
pub num_windows: usize,
}
2022-08-14 14:30:13 +01:00
pub struct ItemButton {
pub button: Button,
pub persistent: bool,
pub show_names: bool,
pub menu_state: Rc<RwLock<MenuState>>,
}
#[derive(Clone, Copy)]
pub struct AppearanceOptions {
pub show_names: bool,
pub show_icons: bool,
pub icon_size: i32,
}
impl ItemButton {
pub fn new(
item: &Item,
appearance: AppearanceOptions,
icon_theme: &IconTheme,
bar_position: BarPosition,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>,
) -> Self {
let mut button = Button::builder();
if appearance.show_names {
button = button.label(&item.name);
2022-08-14 14:30:13 +01:00
}
let button = button.build();
if appearance.show_icons {
let gtk_image = gtk::Image::new();
let image =
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
if let Some(image) = image {
button.set_image(Some(&gtk_image));
button.set_always_show_image(true);
if let Err(err) = image.load_into_image(gtk_image) {
error!("{err:?}");
}
};
2022-08-14 14:30:13 +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
{
let app_id = item.app_id.clone();
let tx = controller_tx.clone();
button.connect_clicked(move |button| {
// lazy check :| TODO: Improve this
let style_context = button.style_context();
if style_context.has_class("open") {
try_send!(tx, ItemEvent::FocusItem(app_id.clone()));
} else {
try_send!(tx, ItemEvent::OpenItem(app_id.clone()));
}
});
2022-08-14 14:30:13 +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, _| {
let menu_state = read_lock!(menu_state);
if menu_state.num_windows > 1 {
try_send!(
tx,
ModuleUpdateEvent::Update(LauncherUpdate::Hover(app_id.clone(),))
);
try_send!(
tx,
ModuleUpdateEvent::OpenPopupAt(
button.geometry(bar_position.get_orientation())
)
);
} else {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
}
Inhibit(false)
});
}
{
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,
BarPosition::Bottom => y > 0.0,
BarPosition::Left => x + THRESHOLD < alloc.width() as f64,
BarPosition::Right => x > THRESHOLD,
};
if close {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
}
Inhibit(false)
});
}
button.show_all();
Self {
button,
persistent: item.favorite,
show_names: appearance.show_names,
menu_state,
}
}
pub fn set_open(&self, open: bool) {
self.update_class("open", open);
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-14 14:30:13 +01:00
}