Files
ironbar-fork/src/popup.rs
Jake Stanger bea442ed96 refactor: update gtk/glib, remove glib channels
This is a major refactor which updates GTK, GLib and GTK Layer Shell to their latest versions.

GLib channels, previously used for receiving events on the GLib Main Context thread have been deprecated and a new method for running Futures on the main thread has been added instead. This commit also replaces all the deprecated code with this.

As part of the above, a bug was uncovered related to creating the GLib main context inside the Tokio runtime. Spawning of Tokio tasks has been refactored to fix this.
2023-12-18 22:09:21 +00:00

223 lines
6.8 KiB
Rust

use glib::Propagation;
use std::collections::HashMap;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{ApplicationWindow, Orientation};
use gtk_layer_shell::LayerShell;
use tracing::debug;
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::Ironbar;
#[derive(Debug, Clone)]
pub struct PopupCacheValue {
pub name: String,
pub content: ModulePopupParts,
}
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: HashMap<usize, PopupCacheValue>,
monitor: Monitor,
pos: BarPosition,
current_widget: Option<usize>,
}
impl Popup {
/// Creates a new popup window.
/// This includes setting up gtk-layer-shell
/// and an empty `gtk::Box` container.
pub fn new(module_info: &ModuleInfo, gap: i32) -> Self {
let pos = module_info.bar_position;
let orientation = pos.get_orientation();
let win = ApplicationWindow::builder()
.application(module_info.app)
.build();
win.init_layer_shell();
win.set_monitor(module_info.monitor);
win.set_layer(gtk_layer_shell::Layer::Overlay);
win.set_namespace(env!("CARGO_PKG_NAME"));
win.set_layer_shell_margin(
gtk_layer_shell::Edge::Top,
if pos == BarPosition::Top { gap } else { 0 },
);
win.set_layer_shell_margin(
gtk_layer_shell::Edge::Bottom,
if pos == BarPosition::Bottom { gap } else { 0 },
);
win.set_layer_shell_margin(
gtk_layer_shell::Edge::Left,
if pos == BarPosition::Left { gap } else { 0 },
);
win.set_layer_shell_margin(
gtk_layer_shell::Edge::Right,
if pos == BarPosition::Right { gap } else { 0 },
);
win.set_anchor(
gtk_layer_shell::Edge::Top,
pos == BarPosition::Top || orientation == Orientation::Vertical,
);
win.set_anchor(gtk_layer_shell::Edge::Bottom, pos == BarPosition::Bottom);
win.set_anchor(
gtk_layer_shell::Edge::Left,
pos == BarPosition::Left || orientation == Orientation::Horizontal,
);
win.set_anchor(gtk_layer_shell::Edge::Right, pos == BarPosition::Right);
win.connect_leave_notify_event(move |win, ev| {
const THRESHOLD: f64 = 3.0;
let (w, h) = win.size();
let (x, y) = ev.position();
// some child widgets trigger this event
// so check we're actually outside the window
let hide = match pos {
BarPosition::Top => {
x < THRESHOLD || y > f64::from(h) - THRESHOLD || x > f64::from(w) - THRESHOLD
}
BarPosition::Bottom => {
x < THRESHOLD || y < THRESHOLD || x > f64::from(w) - THRESHOLD
}
BarPosition::Left => {
y < THRESHOLD || x > f64::from(w) - THRESHOLD || y > f64::from(h) - THRESHOLD
}
BarPosition::Right => {
y < THRESHOLD || x < THRESHOLD || y > f64::from(h) - THRESHOLD
}
};
if hide {
win.hide();
}
Propagation::Proceed
});
Self {
window: win,
cache: HashMap::new(),
monitor: module_info.monitor.clone(),
pos,
current_widget: None,
}
}
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) {
debug!("Registered popup content for #{}", key);
for button in &content.buttons {
let id = Ironbar::unique_id();
button.set_tag("popup-id", id);
}
self.cache.insert(key, PopupCacheValue { name, content });
}
pub fn show(&mut self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
self.current_widget = Some(widget_id);
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
let button = content
.buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let orientation = self.pos.get_orientation();
let geometry = button.geometry(orientation);
self.set_pos(geometry);
}
}
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
self.set_pos(geometry);
}
}
fn clear_window(&self) {
let children = self.window.children();
for child in children {
self.window.remove(&child);
}
}
/// Hides the popover
pub fn hide(&mut self) {
self.current_widget = None;
self.window.hide();
}
/// Checks if the popup is currently visible
pub fn is_visible(&self) -> bool {
self.window.is_visible()
}
pub fn current_widget(&self) -> Option<usize> {
self.current_widget
}
/// Sets the popup's X/Y position relative to the left or border of the screen
/// (depending on orientation).
fn set_pos(&self, geometry: WidgetGeometry) {
let orientation = self.pos.get_orientation();
let mon_workarea = self.monitor.workarea();
let screen_size = if orientation == Orientation::Horizontal {
mon_workarea.width()
} else {
mon_workarea.height()
};
let (popup_width, popup_height) = self.window.size();
let popup_size = if orientation == Orientation::Horizontal {
popup_width
} else {
popup_height
};
let widget_center = f64::from(geometry.position) + f64::from(geometry.size) / 2.0;
let bar_offset = (f64::from(screen_size) - f64::from(geometry.bar_size)) / 2.0;
let mut offset = bar_offset + (widget_center - (f64::from(popup_size) / 2.0)).round();
if offset < 5.0 {
offset = 5.0;
} else if offset > f64::from(screen_size - popup_size) - 5.0 {
offset = f64::from(screen_size - popup_size) - 5.0;
}
let edge = if orientation == Orientation::Horizontal {
gtk_layer_shell::Edge::Left
} else {
gtk_layer_shell::Edge::Top
};
self.window.set_layer_shell_margin(edge, offset as i32);
}
}