1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-02 11:11:04 +02:00

Merge branch 'master' into feat/networkmanager

This commit is contained in:
Reinout Meliesie 2024-04-18 01:38:16 +02:00
commit b860f5b603
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
57 changed files with 2450 additions and 858 deletions

View file

@ -1,7 +1,5 @@
use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation};
use crate::popup::Popup;
use crate::Ironbar;
use color_eyre::Result;
@ -350,57 +348,10 @@ fn add_modules(
ironbar: &Rc<Ironbar>,
popup: &Rc<Popup>,
) -> Result<()> {
let orientation = info.bar_position.orientation();
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("common config to exist");
let widget_parts = create_module(
*$module,
$id,
ironbar.clone(),
common.name.clone(),
&info,
&Rc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation);
content.add(&container);
}};
}
let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into();
for config in modules {
let id = Ironbar::unique_id();
match config {
#[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
#[cfg(feature = "clock")]
ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id),
#[cfg(feature = "focused")]
ModuleConfig::Focused(mut module) => add_module!(module, id),
ModuleConfig::Label(mut module) => add_module!(module, id),
#[cfg(feature = "launcher")]
ModuleConfig::Launcher(mut module) => add_module!(module, id),
#[cfg(feature = "music")]
ModuleConfig::Music(mut module) => add_module!(module, id),
#[cfg(feature = "networkmanager")]
ModuleConfig::NetworkManager(mut module) => add_module!(module, id),
#[cfg(feature = "notifications")]
ModuleConfig::Notifications(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
#[cfg(feature = "tray")]
ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
#[cfg(feature = "volume")]
ModuleConfig::Volume(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
}
config.create(&module_factory, content, info)?;
}
Ok(())

View file

@ -1,4 +1,4 @@
use crate::{await_sync, register_client};
use crate::{await_sync, register_fallible_client};
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use std::fmt::{Debug, Display, Formatter};
@ -141,4 +141,4 @@ pub trait WorkspaceClient: Debug + Send + Sync {
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
}
register_client!(dyn WorkspaceClient, workspaces);
register_fallible_client!(dyn WorkspaceClient, workspaces);

View file

@ -1,7 +1,7 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn};
use color_eyre::{Report, Result};
use futures_util::StreamExt;
use futures_lite::StreamExt;
use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver, Sender};

41
src/clients/lua.rs Normal file
View file

@ -0,0 +1,41 @@
use mlua::Lua;
use std::ops::Deref;
use std::path::Path;
use tracing::{debug, error};
/// Wrapper around Lua instance
/// to create a singleton and handle initialization.
#[derive(Debug)]
pub struct LuaEngine {
lua: Lua,
}
impl LuaEngine {
pub fn new(config_dir: &Path) -> Self {
let lua = unsafe { Lua::unsafe_new() };
let user_init = config_dir.join("init.lua");
if user_init.exists() {
debug!("loading user init script");
if let Err(err) = lua.load(user_init).exec() {
error!("{err:?}");
}
}
debug!("loading internal init script");
if let Err(err) = lua.load(include_str!("../../lua/init.lua")).exec() {
error!("{err:?}");
}
Self { lua }
}
}
impl Deref for LuaEngine {
type Target = Lua;
fn deref(&self) -> &Self::Target {
&self.lua
}
}

View file

@ -1,10 +1,15 @@
use crate::Ironbar;
use crate::{await_sync, Ironbar};
use color_eyre::Result;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "networkmanager")]
@ -28,6 +33,8 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "networkmanager")]
@ -42,6 +49,8 @@ pub struct Clients {
volume: Option<Arc<volume::Client>>,
}
pub type ClientResult<T> = Result<Arc<T>>;
impl Clients {
pub(crate) fn new() -> Self {
Self::default()
@ -63,12 +72,23 @@ impl Clients {
}
#[cfg(feature = "workspaces")]
pub fn workspaces(&mut self) -> Arc<dyn compositor::WorkspaceClient> {
// TODO: Error handling here isn't great - should throw a user-friendly error & exit
self.workspaces
.get_or_insert_with(|| {
compositor::Compositor::create_workspace_client().expect("to be valid compositor")
})
pub fn workspaces(&mut self) -> ClientResult<dyn compositor::WorkspaceClient> {
let client = match &self.workspaces {
Some(workspaces) => workspaces.clone(),
None => {
let client = compositor::Compositor::create_workspace_client()?;
self.workspaces.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
.get_or_insert_with(|| Rc::new(lua::LuaEngine::new(config_dir)))
.clone()
}
@ -88,29 +108,35 @@ impl Clients {
}
#[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> Arc<swaync::Client> {
self.notifications
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async { swaync::Client::new().await }))
})
.clone()
pub fn notifications(&mut self) -> ClientResult<swaync::Client> {
let client = match &self.notifications {
Some(client) => client.clone(),
None => {
let client = await_sync(async { swaync::Client::new().await })?;
let client = Arc::new(client);
self.notifications.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<tray::Client> {
// TODO: Error handling here isn't great - should throw a user-friendly error
self.tray
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async {
let service_name =
format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
pub fn tray(&mut self) -> ClientResult<tray::Client> {
let client = match &self.tray {
Some(client) => client.clone(),
None => {
let service_name = format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
tray::Client::new(&service_name)
.await
.expect("to be able to start client")
}))
})
.clone()
let client = await_sync(async { tray::Client::new(&service_name).await })?;
let client = Arc::new(client);
self.tray.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "upower")]
@ -137,6 +163,14 @@ pub trait ProvidesClient<T: ?Sized> {
fn provide(&self) -> Arc<T>;
}
/// Types implementing this trait
/// indicate that they provide a singleton client instance of type `T`,
/// which may fail to be created.
pub trait ProvidesFallibleClient<T: ?Sized> {
/// Returns a singleton client instance of type `T`.
fn try_provide(&self) -> ClientResult<T>;
}
/// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type.
///
@ -159,3 +193,26 @@ macro_rules! register_client {
}
};
}
/// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type.
///
/// The implementation calls `$method` (second argument)
/// on the `Clients` struct to obtain the client instance.
///
/// # Example
/// `register_client!(Client, clipboard);`
#[macro_export]
macro_rules! register_fallible_client {
($ty:ty, $method:ident) => {
impl<TSend, TReceive> $crate::clients::ProvidesFallibleClient<$ty>
for $crate::modules::WidgetContext<TSend, TReceive>
where
TSend: Clone,
{
fn try_provide(&self) -> color_eyre::Result<std::sync::Arc<$ty>> {
self.ironbar.clients.borrow_mut().$method()
}
}
};
}

View file

@ -1,6 +1,6 @@
mod dbus;
use crate::{register_client, send, spawn};
use crate::{register_fallible_client, send, spawn};
use color_eyre::{Report, Result};
use dbus::SwayNcProxy;
use serde::Deserialize;
@ -24,9 +24,9 @@ type GetSubscribeData = (bool, bool, u32, bool);
impl From<GetSubscribeData> for Event {
fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self {
Self {
count,
dnd,
cc_open,
count,
inhibited,
}
}
@ -40,15 +40,13 @@ pub struct Client {
}
impl Client {
pub async fn new() -> Self {
let dbus = Box::pin(zbus::Connection::session())
.await
.expect("failed to create connection to system bus");
pub async fn new() -> Result<Self> {
let dbus = Box::pin(zbus::Connection::session()).await?;
let proxy = SwayNcProxy::new(&dbus).await.unwrap();
let proxy = SwayNcProxy::new(&dbus).await?;
let (tx, rx) = broadcast::channel(8);
let mut stream = proxy.receive_subscribe_v2().await.unwrap();
let mut stream = proxy.receive_subscribe_v2().await?;
{
let tx = tx.clone();
@ -62,7 +60,7 @@ impl Client {
});
}
Self { proxy, tx, _rx: rx }
Ok(Self { proxy, tx, _rx: rx })
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
@ -85,4 +83,4 @@ impl Client {
}
}
register_client!(Client, notifications);
register_fallible_client!(Client, notifications);

View file

@ -1,4 +1,4 @@
use crate::register_client;
use crate::register_fallible_client;
pub use system_tray::client::Client;
register_client!(Client, tray);
register_fallible_client!(Client, tray);

View file

@ -27,6 +27,8 @@ pub struct CommonConfig {
pub on_mouse_exit: Option<ScriptInput>,
pub tooltip: Option<String>,
#[serde(default)]
pub disable_popup: bool,
}
#[derive(Debug, Deserialize, Clone)]
@ -38,6 +40,34 @@ pub enum TransitionType {
SlideEnd,
}
#[derive(Debug, Default, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ModuleOrientation {
#[default]
#[serde(alias = "h")]
Horizontal,
#[serde(alias = "v")]
Vertical,
}
impl ModuleOrientation {
pub const fn to_angle(self) -> f64 {
match self {
Self::Horizontal => 0.0,
Self::Vertical => 90.0,
}
}
}
impl From<ModuleOrientation> for Orientation {
fn from(o: ModuleOrientation) -> Self {
match o {
ModuleOrientation::Horizontal => Self::Horizontal,
ModuleOrientation::Vertical => Self::Vertical,
}
}
}
impl TransitionType {
pub const fn to_revealer_transition_type(
&self,

View file

@ -2,6 +2,8 @@ mod common;
mod r#impl;
mod truncate;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
@ -29,16 +31,21 @@ use crate::modules::upower::UpowerModule;
use crate::modules::volume::VolumeModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use crate::modules::{AnyModuleFactory, ModuleFactory, ModuleInfo};
use cfg_if::cfg_if;
use color_eyre::Result;
use serde::Deserialize;
use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType};
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
@ -68,6 +75,51 @@ pub enum ModuleConfig {
Workspaces(Box<WorkspacesModule>),
}
impl ModuleConfig {
pub fn create(
self,
module_factory: &AnyModuleFactory,
container: &gtk::Box,
info: &ModuleInfo,
) -> Result<()> {
macro_rules! create {
($module:expr) => {
module_factory.create(*$module, container, info)
};
}
match self {
#[cfg(feature = "cairo")]
Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")]
Self::Clock(module) => create!(module),
Self::Custom(module) => create!(module),
#[cfg(feature = "focused")]
Self::Focused(module) => create!(module),
Self::Label(module) => create!(module),
#[cfg(feature = "launcher")]
Self::Launcher(module) => create!(module),
#[cfg(feature = "music")]
Self::Music(module) => create!(module),
#[cfg(feature = "notifications")]
Self::Notifications(module) => create!(module),
Self::Script(module) => create!(module),
#[cfg(feature = "sys_info")]
Self::SysInfo(module) => create!(module),
#[cfg(feature = "tray")]
Self::Tray(module) => create!(module),
#[cfg(feature = "upower")]
Self::Upower(module) => create!(module),
#[cfg(feature = "volume")]
Self::Volume(module) => create!(module),
#[cfg(feature = "workspaces")]
Self::Workspaces(module) => create!(module),
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub enum BarEntryConfig {
Single(BarConfig),

View file

@ -32,6 +32,9 @@ pub enum Command {
key: Box<str>,
},
/// Gets the current value of all `ironvar`s.
List,
/// Load an additional CSS stylesheet.
/// The sheet is automatically hot-reloaded.
LoadCss {

View file

@ -129,7 +129,7 @@ impl Ipc {
ironbar.reload_config();
for output in outputs {
match crate::load_output_bars(ironbar, application, output) {
match crate::load_output_bars(ironbar, application, &output) {
Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars),
Err(err) => error!("{err:?}"),
}
@ -153,6 +153,20 @@ impl Ipc {
None => Response::error("Variable not found"),
}
}
Command::List => {
let variable_manager = Ironbar::variable_manager();
let mut values = read_lock!(variable_manager)
.get_all()
.iter()
.map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default()))
.collect::<Vec<_>>();
values.sort();
let value = values.join("\n");
Response::OkValue { value }
}
Command::LoadCss { path } => {
if path.exists() {
load_css(path);
@ -172,7 +186,7 @@ impl Ipc {
popup.hide();
let data = popup
.cache
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
@ -209,7 +223,7 @@ impl Ipc {
popup.hide();
let data = popup
.cache
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)

View file

@ -46,6 +46,10 @@ impl VariableManager {
self.variables.get(key).and_then(IronVar::get)
}
pub fn get_all(&self) -> &HashMap<Box<str>, IronVar> {
&self.variables
}
/// Subscribes to an `ironvar`, creating it if it does not exist.
/// Any time the var is set, its value is sent on the channel.
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
@ -66,7 +70,7 @@ impl VariableManager {
/// Ironbar dynamic variable representation.
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
#[derive(Debug)]
struct IronVar {
pub struct IronVar {
value: Option<String>,
tx: broadcast::Sender<Option<String>>,
_rx: broadcast::Receiver<Option<String>>,
@ -82,7 +86,7 @@ impl IronVar {
/// Gets the current variable value.
/// Prefer to subscribe to changes where possible.
fn get(&self) -> Option<String> {
pub fn get(&self) -> Option<String> {
self.value.clone()
}

View file

@ -1,3 +1,31 @@
/// Provides implementations of methods required by the `Module` trait
/// which cannot be included as part of the trait.
///
/// This removes the need to add the same boilerplate method definitions
/// to every module implementation.
///
/// # Usage:
///
/// ```rs
/// impl Module for ClockModule {
/// type SendMessage = DateTime<Local>;
/// type ReceiveMessage = ();
///
/// module_impl!("clock");
/// }
#[macro_export]
macro_rules! module_impl {
($name:literal) => {
fn name() -> &'static str {
$name
}
fn take_common(&mut self) -> $crate::config::CommonConfig {
self.common.take().unwrap_or_default()
}
};
}
/// Sends a message on an asynchronous `Sender` using `send()`
/// Panics if the message cannot be sent.
///

View file

@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>,
config_dir: PathBuf,
}
impl Ironbar {
fn new() -> Self {
let (config, config_dir) = load_config();
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
config: Rc::new(RefCell::new(load_config())),
config: Rc::new(RefCell::new(config)),
config_dir,
}
}
@ -192,7 +196,7 @@ impl Ironbar {
while let Ok(event) = rx_outputs.recv().await {
match event.event_type {
OutputEventType::New => {
match load_output_bars(&instance, &app, event.output) {
match load_output_bars(&instance, &app, &event.output) {
Ok(mut new_bars) => {
instance.bars.borrow_mut().append(&mut new_bars);
}
@ -260,7 +264,7 @@ impl Ironbar {
/// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")]
fn reload_config(&self) {
self.config.replace(load_config());
self.config.replace(load_config().0);
}
}
@ -270,20 +274,37 @@ fn start_ironbar() {
}
/// Loads the config file from disk.
fn load_config() -> Config {
let mut config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
fn load_config() -> (Config, PathBuf) {
let config_path = env::var("IRONBAR_CONFIG");
Config::default()
});
let (config, directory) = if let Ok(config_path) = config_path {
let path = PathBuf::from(config_path);
(
ConfigLoader::load(&path),
path.parent()
.map(PathBuf::from)
.ok_or_else(|| Report::msg("Specified path has no parent")),
)
} else {
let config_loader = ConfigLoader::new("ironbar");
(
config_loader.find_and_load(),
config_loader.config_dir().map_err(Report::new),
)
};
let mut config = config.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
let directory = directory
.and_then(|dir| dir.canonicalize().map_err(Report::new))
.unwrap_or_else(|_| env::current_dir().expect("to have current working directory"));
debug!("Loaded config file");
@ -297,7 +318,7 @@ fn load_config() -> Config {
}
}
config
(config, directory)
}
/// Gets the GDK `Display` instance.
@ -316,7 +337,7 @@ fn get_display() -> Display {
fn load_output_bars(
ironbar: &Rc<Ironbar>,
app: &Application,
output: OutputInfo,
output: &OutputInfo,
) -> Result<Vec<Bar>> {
let Some(monitor_name) = &output.name else {
return Err(Report::msg("Output missing monitor name"));

198
src/modules/cairo.rs Normal file
View file

@ -0,0 +1,198 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, spawn, try_send};
use cairo::{Format, ImageSurface};
use glib::translate::IntoGlibPtr;
use glib::Propagation;
use gtk::prelude::*;
use gtk::DrawingArea;
use mlua::{Error, Function, LightUserData};
use notify::event::ModifyKind;
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc::Receiver;
use tokio::time::sleep;
use tracing::{debug, error};
#[derive(Debug, Clone, Deserialize)]
pub struct CairoModule {
path: PathBuf,
#[serde(default = "default_frequency")]
frequency: u64,
#[serde(default = "default_size")]
width: u32,
#[serde(default = "default_size")]
height: u32,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_size() -> u32 {
42
}
const fn default_frequency() -> u64 {
200
}
impl Module<gtk::Box> for CairoModule {
type SendMessage = ();
type ReceiveMessage = ();
module_impl!("cairo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let path = self.path.to_path_buf();
let tx = context.tx.clone();
spawn(async move {
let parent = path.parent().expect("to have parent path");
let mut watcher = recommended_watcher({
let path = path.clone();
move |res: notify::Result<Event>| match res {
Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => {
debug!("{event:?}");
if event.paths.first().is_some_and(|p| p == &path) {
try_send!(tx, ModuleUpdateEvent::Update(()));
}
}
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
_ => {}
}
})
.expect("Failed to create lua file watcher");
watcher
.watch(parent, RecursiveMode::NonRecursive)
.expect("Failed to start lua file watcher");
// avoid watcher from dropping
loop {
sleep(Duration::from_secs(1)).await;
}
});
// Lua needs to run synchronously with the GTK updates,
// so the controller does not handle the script engine.
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<gtk::Box>>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let id = context.id.to_string();
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let surface = ImageSurface::create(Format::ARgb32, self.width as i32, self.height as i32)?;
let area = DrawingArea::new();
let lua = context
.ironbar
.clients
.borrow_mut()
.lua(&context.ironbar.config_dir);
// this feels kinda dirty,
// but it keeps draw functions separate in the global scope
let script = fs::read_to_string(&self.path)?
.replace("function draw", format!("function __draw_{id}").as_str());
lua.load(&script).exec()?;
{
let lua = lua.clone();
let id = id.clone();
let path = self.path.clone();
area.connect_draw(move |_, cr| {
let function: Function = lua
.load(include_str!("../../lua/draw.lua"))
.eval()
.expect("to be valid");
if let Err(err) = cr.set_source_surface(&surface, 0.0, 0.0) {
error!("{err}");
return Propagation::Stop;
}
let ptr = unsafe { cr.clone().into_glib_ptr().cast() };
// mlua needs a valid return type, even if we don't return anything
if let Err(err) =
function.call::<_, Option<bool>>((id.as_str(), LightUserData(ptr)))
{
match err {
Error::RuntimeError(message) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua runtime error] {}:{message}", path.display())
}
_ => error!("{err}"),
}
return Propagation::Stop;
}
Propagation::Proceed
});
}
area.set_size_request(self.width as i32, self.height as i32);
container.add(&area);
glib::spawn_future_local(async move {
loop {
area.queue_draw();
glib::timeout_future(Duration::from_millis(self.frequency)).await;
}
});
glib_recv!(context.subscribe(), _ev => {
let res = fs::read_to_string(&self.path)
.map(|s| s.replace("function draw", format!("function __draw_{id}").as_str()));
match res {
Ok(script) => {
match lua.load(&script).exec() {
Ok(_) => {},
Err(Error::SyntaxError { message, ..}) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua syntax error] {}:{message}", self.path.display())
},
Err(err) => error!("lua error: {err:?}")
}
},
Err(err) => error!("{err:?}")
}
});
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View file

@ -5,7 +5,7 @@ use crate::image::new_icon_button;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, spawn, try_send};
use crate::{glib_recv, module_impl, spawn, try_send};
use glib::Propagation;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
@ -13,7 +13,6 @@ use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error};
@ -65,9 +64,7 @@ impl Module<Button> for ClipboardModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = UIEvent;
fn name() -> &'static str {
"clipboard"
}
module_impl!("clipboard");
fn spawn_controller(
&self,
@ -78,7 +75,7 @@ impl Module<Button> for ClipboardModule {
let max_items = self.max_items;
let tx = context.tx.clone();
let client: Arc<clipboard::Client> = context.client();
let client = context.client::<clipboard::Client>();
// listen to clipboard events
spawn(async move {
@ -137,7 +134,7 @@ impl Module<Button> for ClipboardModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -147,6 +144,7 @@ impl Module<Button> for ClipboardModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -8,12 +8,12 @@ use serde::Deserialize;
use tokio::sync::{broadcast, mpsc};
use tokio::time::sleep;
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
#[derive(Debug, Deserialize, Clone)]
pub struct ClockModule {
@ -31,6 +31,9 @@ pub struct ClockModule {
#[serde(default = "default_locale")]
locale: String,
#[serde(default)]
orientation: ModuleOrientation,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -41,6 +44,7 @@ impl Default for ClockModule {
format: default_format(),
format_popup: default_popup_format(),
locale: default_locale(),
orientation: ModuleOrientation::Horizontal,
common: Some(CommonConfig::default()),
}
}
@ -71,9 +75,7 @@ impl Module<Button> for ClockModule {
type SendMessage = DateTime<Local>;
type ReceiveMessage = ();
fn name() -> &'static str {
"clock"
}
module_impl!("clock");
fn spawn_controller(
&self,
@ -100,7 +102,7 @@ impl Module<Button> for ClockModule {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let label = Label::builder()
.angle(info.bar_position.get_angle())
.angle(self.orientation.to_angle())
.use_markup(true)
.build();
button.add(&label);
@ -120,7 +122,12 @@ impl Module<Button> for ClockModule {
});
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -130,6 +137,7 @@ impl Module<Button> for ClockModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0);

View file

@ -1,15 +1,15 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::config::ModuleOrientation;
use crate::modules::custom::WidgetConfig;
use gtk::prelude::*;
use gtk::Orientation;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct BoxWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
orientation: Option<ModuleOrientation>,
widgets: Option<Vec<WidgetConfig>>,
}
@ -20,9 +20,7 @@ impl CustomWidget for BoxWidget {
let container = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
container.set_orientation(
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
);
container.set_orientation(orientation.into());
}
if let Some(widgets) = self.widgets {

View file

@ -1,12 +1,13 @@
use gtk::prelude::*;
use gtk::{Button, Label};
use gtk::{Button, Label, Orientation};
use serde::Deserialize;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton;
use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig};
#[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget {
@ -14,6 +15,9 @@ pub struct ButtonWidget {
class: Option<String>,
label: Option<String>,
on_click: Option<String>,
widgets: Option<Vec<WidgetConfig>>,
#[serde(default)]
orientation: ModuleOrientation,
}
impl CustomWidget for ButtonWidget {
@ -23,9 +27,20 @@ impl CustomWidget for ButtonWidget {
let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone());
if let Some(text) = self.label {
if let Some(widgets) = self.widgets {
let container = gtk::Box::new(Orientation::Horizontal, 0);
for widget in widgets {
widget.widget.add_to(&container, &context, widget.common);
}
button.add(&container);
} else if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
label.set_angle(self.orientation.to_angle());
button.add(&label);
dynamic_string(&text, move |string| {

View file

@ -3,6 +3,7 @@ use gtk::Label;
use serde::Deserialize;
use crate::build;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
@ -12,6 +13,8 @@ pub struct LabelWidget {
name: Option<String>,
class: Option<String>,
label: String,
#[serde(default)]
orientation: ModuleOrientation,
}
impl CustomWidget for LabelWidget {
@ -20,6 +23,8 @@ impl CustomWidget for LabelWidget {
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
let label = build!(self, Self::Widget);
label.set_angle(self.orientation.to_angle());
label.set_use_markup(true);
{

View file

@ -9,15 +9,16 @@ use self::image::ImageWidget;
use self::label::LabelWidget;
use self::r#box::BoxWidget;
use self::slider::SliderWidget;
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleConfig};
use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
wrap_widget, AnyModuleFactory, BarModuleFactory, Module, ModuleInfo, ModuleParts, ModulePopup,
ModuleUpdateEvent, PopupButton, PopupModuleFactory, WidgetContext,
};
use crate::script::Script;
use crate::{send_async, spawn};
use color_eyre::{Report, Result};
use crate::{module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use serde::Deserialize;
@ -40,11 +41,18 @@ pub struct CustomModule {
#[derive(Debug, Deserialize, Clone)]
pub struct WidgetConfig {
#[serde(flatten)]
widget: Widget,
widget: WidgetOrModule,
#[serde(flatten)]
common: CommonConfig,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum WidgetOrModule {
Widget(Widget),
Module(ModuleConfig),
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Widget {
@ -58,10 +66,12 @@ pub enum Widget {
#[derive(Clone)]
struct CustomWidgetContext<'a> {
info: &'a ModuleInfo<'a>,
tx: &'a mpsc::Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
module_factory: AnyModuleFactory,
}
trait CustomWidget {
@ -103,14 +113,16 @@ pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orient
};
}
/// Attempts to parse an `Orientation` from `String`.
/// Will accept `horizontal`, `vertical`, `h` or `v`.
/// Ignores case.
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
match orientation.to_lowercase().as_str() {
"horizontal" | "h" => Ok(Orientation::Horizontal),
"vertical" | "v" => Ok(Orientation::Vertical),
_ => Err(Report::msg("Invalid orientation string in config")),
impl WidgetOrModule {
fn add_to(self, parent: &gtk::Box, context: &CustomWidgetContext, common: CommonConfig) {
match self {
WidgetOrModule::Widget(widget) => widget.add_to(parent, context, common),
WidgetOrModule::Module(config) => {
if let Err(err) = config.create(&context.module_factory, parent, context.info) {
error!("{err:?}");
}
}
}
}
}
@ -151,9 +163,7 @@ impl Module<gtk::Box> for CustomModule {
type SendMessage = ();
type ReceiveMessage = ExecEvent;
fn name() -> &'static str {
"custom"
}
module_impl!("custom");
fn spawn_controller(
&self,
@ -191,7 +201,7 @@ impl Module<gtk::Box> for CustomModule {
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let orientation = info.bar_position.orientation();
@ -200,10 +210,13 @@ impl Module<gtk::Box> for CustomModule {
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
let custom_context = CustomWidgetContext {
info,
tx: &context.controller_tx,
bar_orientation: orientation,
icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
module_factory: BarModuleFactory::new(context.ironbar.clone(), context.popup.clone())
.into(),
};
self.bar.clone().into_iter().for_each(|widget| {
@ -212,8 +225,22 @@ impl Module<gtk::Box> for CustomModule {
.add_to(&container, &custom_context, widget.common);
});
for button in popup_buttons.borrow().iter() {
button.ensure_popup_id();
}
context.button_id = popup_buttons
.borrow()
.first()
.map_or(usize::MAX, PopupButton::popup_id);
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts_owned(popup_buttons.take());
Ok(ModuleParts {
@ -226,6 +253,7 @@ impl Module<gtk::Box> for CustomModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
@ -235,10 +263,17 @@ impl Module<gtk::Box> for CustomModule {
if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext {
info,
tx: &tx,
bar_orientation: info.bar_position.orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
module_factory: PopupModuleFactory::new(
context.ironbar,
context.popup,
context.button_id,
)
.into(),
};
for widget in popup {

View file

@ -4,18 +4,20 @@ use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
#[serde(default)]
orientation: ModuleOrientation,
label: Option<String>,
value: Option<ScriptInput>,
#[serde(default = "default_max")]
@ -33,11 +35,7 @@ impl CustomWidget for ProgressWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let progress = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
progress.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
progress.set_orientation(self.orientation.into());
if let Some(length) = self.length {
set_length(&progress, length, context.bar_orientation);

View file

@ -8,17 +8,19 @@ use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::config::ModuleOrientation;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
#[serde(default)]
orientation: ModuleOrientation,
value: Option<ScriptInput>,
on_change: Option<String>,
#[serde(default = "default_min")]
@ -45,11 +47,7 @@ impl CustomWidget for SliderWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let scale = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
scale.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
scale.set_orientation(self.orientation.into());
if let Some(length) = self.length {
set_length(&scale, length, context.bar_orientation);

View file

@ -3,7 +3,7 @@ use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -50,9 +50,7 @@ impl Module<gtk::Box> for FocusedModule {
type SendMessage = Option<(String, String)>;
type ReceiveMessage = ();
fn name() -> &'static str {
"focused"
}
module_impl!("focused");
fn spawn_controller(
&self,

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, try_send};
use crate::{glib_recv, module_impl, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -29,9 +29,7 @@ impl Module<Label> for LabelModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"label"
}
module_impl!("label");
fn spawn_controller(
&self,

View file

@ -7,7 +7,7 @@ use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, Wid
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file;
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock};
use color_eyre::{Help, Report};
use gtk::prelude::*;
use gtk::{Button, Orientation};
@ -80,9 +80,7 @@ impl Module<gtk::Box> for LauncherModule {
type SendMessage = LauncherUpdate;
type ReceiveMessage = ItemEvent;
fn name() -> &'static str {
"launcher"
}
module_impl!("launcher");
fn spawn_controller(
&self,
@ -401,7 +399,7 @@ impl Module<gtk::Box> for LauncherModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
@ -414,6 +412,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
controller_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250;

View file

@ -10,12 +10,14 @@ use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widge
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
use crate::clients::ProvidesClient;
use crate::clients::{ClientResult, ProvidesClient, ProvidesFallibleClient};
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "cairo")]
pub mod cairo;
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
@ -56,6 +58,8 @@ pub enum ModuleLocation {
Center,
Right,
}
#[derive(Clone)]
pub struct ModuleInfo<'a> {
pub app: &'a Application,
pub location: ModuleLocation,
@ -87,10 +91,16 @@ where
{
pub id: usize,
pub ironbar: Rc<Ironbar>,
pub popup: Rc<Popup>,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>,
// TODO: Don't like this - need some serious refactoring to deal with it
// This is a hack to be able to pass data from module -> popup creation
// for custom widget only.
pub button_id: usize,
_update_rx: broadcast::Receiver<TSend>,
}
@ -109,6 +119,13 @@ where
ProvidesClient::provide(self)
}
pub fn try_client<T: ?Sized>(&self) -> ClientResult<T>
where
WidgetContext<TSend, TReceive>: ProvidesFallibleClient<T>,
{
ProvidesFallibleClient::try_provide(self)
}
/// Subscribes to events sent from this widget.
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe()
@ -124,6 +141,32 @@ impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
pub fn setup_identifiers(&self, common: &CommonConfig) {
if let Some(ref name) = common.name {
self.widget.set_widget_name(name);
if let Some(ref popup) = self.popup {
popup.container.set_widget_name(&format!("popup-{name}"));
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
self.widget.style_context().add_class(part);
}
if let Some(ref popup) = self.popup {
for part in class.split(' ') {
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
}
}
}
}
#[derive(Debug, Clone)]
@ -152,11 +195,24 @@ impl ModulePopup for Option<gtk::Box> {
}
pub trait PopupButton {
fn ensure_popup_id(&self) -> usize;
fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button,
/// or creates a new one if it does not exist.
fn ensure_popup_id(&self) -> usize {
if let Some(id) = self.try_popup_id() {
id
} else {
let id = Ironbar::unique_id();
self.set_tag("popup-id", id);
id
}
}
/// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> {
@ -203,165 +259,290 @@ where
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
<Self as Module<W>>::SendMessage: Clone,
{
None
}
fn take_common(&mut self) -> CommonConfig;
}
/// Creates a module and sets it up.
/// This setup includes widget/popup content and event channels.
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
ironbar: Rc<Ironbar>,
name: Option<String>,
info: &ModuleInfo,
popup: &Rc<Popup>,
) -> Result<ModuleParts<TWidget>>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static,
{
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64);
pub trait ModuleFactory {
fn create<TModule, TWidget, TSend, TRev>(
&self,
mut module: TModule,
container: &gtk::Box,
info: &ModuleInfo,
) -> Result<()>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRev>,
TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static,
{
let id = Ironbar::unique_id();
let common = module.take_common();
let (tx, rx) = broadcast::channel(64);
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRev>(64);
let context = WidgetContext {
id,
ironbar,
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,
_update_rx: rx,
};
let (tx, rx) = broadcast::channel(64);
module.spawn_controller(info, &context, controller_rx)?;
let context = WidgetContext {
id,
ironbar: self.ironbar().clone(),
popup: self.popup().clone(),
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,
_update_rx: rx,
button_id: usize::MAX, // hack :(
};
let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
module.spawn_controller(info, &context, controller_rx)?;
let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
let module_name = TModule::name();
let instance_name = common
.name
.clone()
.unwrap_or_else(|| module_name.to_string());
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{module_name}"));
let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
popup.register_content(id, instance_name, popup_content);
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{module_name}"));
self.popup()
.register_content(id, instance_name, popup_content);
}
self.setup_receiver(tx, ui_rx, module_name, id, common.disable_popup);
module_parts.setup_identifiers(&common);
let ev_container = wrap_widget(
&module_parts.widget,
common,
info.bar_position.orientation(),
);
container.add(&ev_container);
Ok(())
}
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static;
Ok(module_parts)
fn ironbar(&self) -> &Rc<Ironbar>;
fn popup(&self) -> &Rc<Popup>;
}
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
/// Handles opening/closing popups
/// and communicating update messages between controllers and widgets/popups.
fn setup_receiver<TSend>(
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
#[derive(Clone)]
pub struct BarModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
name: &'static str,
id: usize,
) where
TSend: Debug + Clone + Send + 'static,
{
// some rare cases can cause the popup to incorrectly calculate its size on first open.
// we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false;
}
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
if popup.is_visible() {
popup.hide();
} else {
popup.show(id, button_id);
impl BarModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>) -> Self {
Self { ironbar, popup }
}
}
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
impl ModuleFactory for BarModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
let popup = self.popup.clone();
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide();
} else {
popup.show(id, button_id);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
ModuleUpdateEvent::OpenPopup(button_id) if !disable_popup => {
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
popup.hide();
popup.show(id, button_id);
has_popup_opened = true;
}
}
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.hide();
popup.show_at(id, geometry);
has_popup_opened = true;
}
ModuleUpdateEvent::ClosePopup if !disable_popup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
},
_ => {}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
}
}
});
});
}
fn ironbar(&self) -> &Rc<Ironbar> {
&self.ironbar
}
fn popup(&self) -> &Rc<Popup> {
&self.popup
}
}
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleParts<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
#[derive(Clone)]
pub struct PopupModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
button_id: usize,
}
if let Some(ref popup) = widget_parts.popup {
popup.container.set_widget_name(&format!("popup-{name}"));
impl PopupModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>, button_id: usize) -> Self {
Self {
ironbar,
popup,
button_id,
}
}
}
impl ModuleFactory for PopupModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
let popup = self.popup.clone();
let button_id = self.button_id;
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide();
} else {
popup.show(id, button_id);
}
}
ModuleUpdateEvent::OpenPopup(_) if !disable_popup => {
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
popup.hide();
popup.show(id, button_id);
}
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show_at(id, geometry);
}
ModuleUpdateEvent::ClosePopup if !disable_popup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
},
_ => {}
}
});
}
fn ironbar(&self) -> &Rc<Ironbar> {
&self.ironbar
}
fn popup(&self) -> &Rc<Popup> {
&self.popup
}
}
#[derive(Clone)]
pub enum AnyModuleFactory {
Bar(BarModuleFactory),
Popup(PopupModuleFactory),
}
impl ModuleFactory for AnyModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
match self {
AnyModuleFactory::Bar(bar) => bar.setup_receiver(tx, rx, name, id, disable_popup),
AnyModuleFactory::Popup(popup) => popup.setup_receiver(tx, rx, name, id, disable_popup),
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
widget_parts.widget.style_context().add_class(part);
fn ironbar(&self) -> &Rc<Ironbar> {
match self {
AnyModuleFactory::Bar(bar) => bar.ironbar(),
AnyModuleFactory::Popup(popup) => popup.ironbar(),
}
}
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
fn popup(&self) -> &Rc<Popup> {
match self {
AnyModuleFactory::Bar(bar) => bar.popup(),
AnyModuleFactory::Popup(popup) => popup.popup(),
}
}
}
impl From<BarModuleFactory> for AnyModuleFactory {
fn from(value: BarModuleFactory) -> Self {
Self::Bar(value)
}
}
impl From<PopupModuleFactory> for AnyModuleFactory {
fn from(value: PopupModuleFactory) -> Self {
Self::Popup(value)
}
}
/// Takes a widget and adds it into a new `gtk::EventBox`.
/// The event box container is returned.
pub fn wrap_widget<W: IsA<Widget>>(

View file

@ -22,7 +22,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
pub use self::config::MusicModule;
use self::config::PlayerType;
@ -87,9 +87,7 @@ impl Module<Button> for MusicModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = PlayerCommand;
fn name() -> &'static str {
"music"
}
module_impl!("music");
fn spawn_controller(
&self,
@ -255,7 +253,7 @@ impl Module<Button> for MusicModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -265,6 +263,7 @@ impl Module<Button> for MusicModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;

View file

@ -2,7 +2,7 @@ use crate::clients::swaync;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use gtk::prelude::*;
use gtk::{Align, Button, Label, Overlay};
use serde::Deserialize;
@ -75,7 +75,7 @@ fn default_icon_open_dnd() -> String {
}
impl Icons {
fn icon(&self, value: &swaync::Event) -> &str {
fn icon(&self, value: swaync::Event) -> &str {
match (value.cc_open, value.count > 0, value.dnd) {
(true, _, true) => &self.open_dnd,
(true, true, false) => &self.open_some,
@ -97,9 +97,7 @@ impl Module<Overlay> for NotificationsModule {
type SendMessage = swaync::Event;
type ReceiveMessage = UiEvent;
fn name() -> &'static str {
"notifications"
}
module_impl!("notifications");
fn spawn_controller(
&self,
@ -110,7 +108,7 @@ impl Module<Overlay> for NotificationsModule {
where
<Self as Module<Overlay>>::SendMessage: Clone,
{
let client = context.client::<swaync::Client>();
let client = context.try_client::<swaync::Client>()?;
{
let client = client.clone();
@ -174,7 +172,7 @@ impl Module<Overlay> for NotificationsModule {
let button = button.clone();
glib_recv!(context.subscribe(), ev => {
let icon = self.icons.icon(&ev);
let icon = self.icons.icon(ev);
button.set_label(icon);
label.set_label(&ev.count.to_string());

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode};
use crate::{glib_recv, spawn, try_send};
use crate::{glib_recv, module_impl, spawn, try_send};
use color_eyre::{Help, Report, Result};
use gtk::prelude::*;
use gtk::Label;
@ -48,9 +48,7 @@ impl Module<Label> for ScriptModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"script"
}
module_impl!("script");
fn spawn_controller(
&self,

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn};
use crate::{glib_recv, module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -21,6 +21,11 @@ pub struct SysInfoModule {
#[serde(default = "Interval::default")]
interval: Interval,
#[serde(default)]
orientation: ModuleOrientation,
direction: Option<ModuleOrientation>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -116,9 +121,7 @@ impl Module<gtk::Box> for SysInfoModule {
type SendMessage = HashMap<String, String>;
type ReceiveMessage = ();
fn name() -> &'static str {
"sysinfo"
}
module_impl!("sysinfo");
fn spawn_controller(
&self,
@ -184,11 +187,16 @@ impl Module<gtk::Box> for SysInfoModule {
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
_info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.orientation(), 10);
let layout = match self.direction {
Some(orientation) => orientation,
None => self.orientation,
};
let container = gtk::Box::new(layout.into(), 10);
let mut labels = Vec::new();
@ -196,7 +204,7 @@ impl Module<gtk::Box> for SysInfoModule {
let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item");
label.set_angle(info.bar_position.get_angle());
label.set_angle(self.orientation.to_angle());
container.add(&label);
labels.push(label);

View file

@ -6,7 +6,7 @@ use crate::clients::tray;
use crate::config::CommonConfig;
use crate::modules::tray::diff::get_diffs;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, lock, send_async, spawn};
use crate::{glib_recv, lock, module_impl, send_async, spawn};
use color_eyre::{Report, Result};
use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar};
@ -57,9 +57,7 @@ impl Module<MenuBar> for TrayModule {
type SendMessage = Event;
type ReceiveMessage = ActivateRequest;
fn name() -> &'static str {
"tray"
}
module_impl!("tray");
fn spawn_controller(
&self,
@ -69,14 +67,14 @@ impl Module<MenuBar> for TrayModule {
) -> Result<()> {
let tx = context.tx.clone();
let client = context.client::<tray::Client>();
let client = context.try_client::<tray::Client>()?;
let mut tray_rx = client.subscribe();
let initial_items = lock!(client.items()).clone();
// listen to tray updates
spawn(async move {
for (key, (item, menu)) in initial_items.into_iter() {
for (key, (item, menu)) in initial_items {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
@ -91,7 +89,7 @@ impl Module<MenuBar> for TrayModule {
}
while let Ok(message) = tray_rx.recv().await {
send_async!(tx, ModuleUpdateEvent::Update(message))
send_async!(tx, ModuleUpdateEvent::Update(message));
}
});
@ -161,12 +159,11 @@ fn on_update(
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
container.add(&menu_item.widget);
match icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
Ok(image) => menu_item.set_image(&image),
Err(_) => {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label)
}
if let Ok(image) = icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
menu_item.set_image(&image);
} else {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label);
};
menu_item.widget.show();

View file

@ -15,7 +15,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
@ -54,9 +54,7 @@ impl Module<gtk::Button> for UpowerModule {
type SendMessage = UpowerProperties;
type ReceiveMessage = ();
fn name() -> &'static str {
"upower"
}
module_impl!("upower");
fn spawn_controller(
&self,
@ -183,7 +181,6 @@ impl Module<gtk::Button> for UpowerModule {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
});
label.set_angle(info.bar_position.get_angle());
let format = self.format.clone();
let rx = context.subscribe();
@ -211,7 +208,7 @@ impl Module<gtk::Button> for UpowerModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -221,6 +218,7 @@ impl Module<gtk::Button> for UpowerModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -4,7 +4,7 @@ use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, lock, send_async, spawn, try_send};
use crate::{glib_recv, lock, module_impl, send_async, spawn, try_send};
use glib::Propagation;
use gtk::pango::EllipsizeMode;
use gtk::prelude::*;
@ -99,9 +99,7 @@ impl Module<Button> for VolumeModule {
type SendMessage = Event;
type ReceiveMessage = Update;
fn name() -> &'static str {
"volume"
}
module_impl!("volume");
fn spawn_controller(
&self,
@ -208,7 +206,12 @@ impl Module<Button> for VolumeModule {
}
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -218,6 +221,7 @@ impl Module<Button> for VolumeModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: tokio::sync::broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -2,7 +2,7 @@ use crate::clients::compositor::{Visibility, Workspace, WorkspaceClient, Workspa
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme};
@ -144,9 +144,7 @@ impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String;
fn name() -> &'static str {
"workspaces"
}
module_impl!("workspaces");
fn spawn_controller(
&self,
@ -155,7 +153,7 @@ impl Module<gtk::Box> for WorkspacesModule {
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces();
let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events
spawn(async move {
let mut srx = client.subscribe_workspace_change();
@ -168,7 +166,7 @@ impl Module<gtk::Box> for WorkspacesModule {
}
});
let client = context.client::<dyn WorkspaceClient>();
let client = context.try_client::<dyn WorkspaceClient>()?;
// Change workspace focus
spawn(async move {

View file

@ -12,7 +12,7 @@ use tracing::{debug, trace};
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::Ironbar;
use crate::rc_mut;
#[derive(Debug, Clone)]
pub struct PopupCacheValue {
@ -23,7 +23,8 @@ pub struct PopupCacheValue {
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
pub container_cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
pub button_cache: Rc<RefCell<Vec<Button>>>,
monitor: Monitor,
pos: BarPosition,
current_widget: Rc<RefCell<Option<(usize, usize)>>>,
@ -106,10 +107,11 @@ impl Popup {
Self {
window: win,
cache: Rc::new(RefCell::new(HashMap::new())),
container_cache: rc_mut!(HashMap::new()),
button_cache: rc_mut!(vec![]),
monitor: module_info.monitor.clone(),
pos,
current_widget: Rc::new(RefCell::new(None)),
current_widget: rc_mut!(None),
}
}
@ -117,8 +119,7 @@ impl Popup {
debug!("Registered popup content for #{}", key);
for button in &content.buttons {
let id = Ironbar::unique_id();
button.set_tag("popup-id", id);
button.ensure_popup_id();
}
let orientation = self.pos.orientation();
@ -126,7 +127,8 @@ impl Popup {
let window = self.window.clone();
let current_widget = self.current_widget.clone();
let cache = self.cache.clone();
let cache = self.container_cache.clone();
let button_cache = self.button_cache.clone();
content
.container
@ -135,11 +137,9 @@ impl Popup {
trace!("Resized: {}x{}", rect.width(), rect.height());
if let Some((widget_id, button_id)) = *current_widget.borrow() {
if let Some(PopupCacheValue { content, .. }) =
cache.borrow().get(&widget_id)
{
if let Some(PopupCacheValue { .. }) = cache.borrow().get(&widget_id) {
Self::set_position(
&content.buttons,
&button_cache.borrow(),
button_id,
orientation,
&monitor,
@ -150,7 +150,11 @@ impl Popup {
}
});
self.cache
self.button_cache
.borrow_mut()
.append(&mut content.buttons.clone());
self.container_cache
.borrow_mut()
.insert(key, PopupCacheValue { name, content });
}
@ -158,16 +162,17 @@ impl Popup {
pub fn show(&self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
if let Some(PopupCacheValue { content, .. }) = self.container_cache.borrow().get(&widget_id)
{
*self.current_widget.borrow_mut() = Some((widget_id, button_id));
content.container.style_context().add_class("popup");
content.container.add_class("popup");
self.window.add(&content.container);
self.window.show();
Self::set_position(
&content.buttons,
&self.button_cache.borrow(),
button_id,
self.pos.orientation(),
&self.monitor,
@ -179,8 +184,9 @@ impl Popup {
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
content.container.style_context().add_class("popup");
if let Some(PopupCacheValue { content, .. }) = self.container_cache.borrow().get(&widget_id)
{
content.container.add_class("popup");
self.window.add(&content.container);
self.window.show();
@ -216,7 +222,7 @@ impl Popup {
}
}
/// Hides the popover
/// Hides the popup
pub fn hide(&self) {
*self.current_widget.borrow_mut() = None;
self.window.hide();