use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig}; use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation}; use crate::popup::Popup; use crate::Ironbar; use color_eyre::Result; use glib::Propagation; use gtk::gdk::Monitor; use gtk::prelude::*; use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType}; use gtk_layer_shell::LayerShell; use std::rc::Rc; use std::time::Duration; use tracing::{debug, info}; #[derive(Debug, Clone)] enum Inner { New { config: Option }, Loaded { popup: Rc }, } #[derive(Debug, Clone)] pub struct Bar { name: String, monitor_name: String, position: BarPosition, ironbar: Rc, window: ApplicationWindow, content: gtk::Box, start: gtk::Box, center: gtk::Box, end: gtk::Box, inner: Inner, } impl Bar { pub fn new( app: &Application, monitor_name: String, config: BarConfig, ironbar: Rc, ) -> Self { let window = ApplicationWindow::builder() .application(app) .type_(WindowType::Toplevel) .build(); let name = config .name .clone() .unwrap_or_else(|| format!("bar-{}", Ironbar::unique_id())); window.set_widget_name(&name); let position = config.position; let orientation = position.orientation(); let content = gtk::Box::builder() .orientation(orientation) .spacing(0) .hexpand(false) .name("bar"); let content = if orientation == Orientation::Horizontal { content.height_request(config.height) } else { content.width_request(config.height) } .build(); content.style_context().add_class("container"); let start = create_container("start", orientation); let center = create_container("center", orientation); let end = create_container("end", orientation); content.add(&start); content.set_center_widget(Some(¢er)); content.pack_end(&end, false, false, 0); window.add(&content); window.connect_destroy_event(|_, _| { info!("Shutting down"); gtk::main_quit(); Propagation::Proceed }); Self { name, monitor_name, position, ironbar, window, content, start, center, end, inner: Inner::New { config: Some(config), }, } } pub fn init(mut self, monitor: &Monitor) -> Result { let Inner::New { ref mut config } = self.inner else { return Ok(self); }; let Some(config) = config.take() else { return Ok(self); }; info!( "Initializing bar '{}' on '{}'", self.name, self.monitor_name ); self.setup_layer_shell( &self.window, true, config.anchor_to_edges, config.margin, monitor, ); let start_hidden = config .start_hidden .unwrap_or_else(|| config.autohide.is_some()); if let Some(autohide) = config.autohide { let hotspot_window = Window::new(WindowType::Toplevel); Self::setup_autohide(&self.window, &hotspot_window, autohide); self.setup_layer_shell( &hotspot_window, false, config.anchor_to_edges, config.margin, monitor, ); if start_hidden { hotspot_window.show(); } } let load_result = self.load_modules(config, monitor)?; self.show(!start_hidden); self.inner = Inner::Loaded { popup: load_result.popup, }; Ok(self) } /// Sets up GTK layer shell for a provided application window. fn setup_layer_shell( &self, win: &impl IsA, exclusive_zone: bool, anchor_to_edges: bool, margin: MarginConfig, monitor: &Monitor, ) { let position = self.position; win.init_layer_shell(); win.set_monitor(monitor); win.set_layer(gtk_layer_shell::Layer::Top); win.set_namespace(env!("CARGO_PKG_NAME")); if exclusive_zone { win.auto_exclusive_zone_enable(); } win.set_layer_shell_margin(gtk_layer_shell::Edge::Top, margin.top); win.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, margin.bottom); win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left); win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right); let bar_orientation = position.orientation(); win.set_anchor( gtk_layer_shell::Edge::Top, position == BarPosition::Top || (bar_orientation == Orientation::Vertical && anchor_to_edges), ); win.set_anchor( gtk_layer_shell::Edge::Bottom, position == BarPosition::Bottom || (bar_orientation == Orientation::Vertical && anchor_to_edges), ); win.set_anchor( gtk_layer_shell::Edge::Left, position == BarPosition::Left || (bar_orientation == Orientation::Horizontal && anchor_to_edges), ); win.set_anchor( gtk_layer_shell::Edge::Right, position == BarPosition::Right || (bar_orientation == Orientation::Horizontal && anchor_to_edges), ); } fn setup_autohide(window: &ApplicationWindow, hotspot_window: &Window, timeout: u64) { hotspot_window.hide(); hotspot_window.set_opacity(0.0); hotspot_window.set_decorated(false); hotspot_window.set_size_request(0, 1); { let hotspot_window = hotspot_window.clone(); window.connect_leave_notify_event(move |win, _| { let win = win.clone(); let hotspot_window = hotspot_window.clone(); glib::timeout_add_local_once(Duration::from_millis(timeout), move || { win.hide(); hotspot_window.show(); }); Propagation::Proceed }); } { let win = window.clone(); hotspot_window.connect_enter_notify_event(move |hotspot_win, _| { hotspot_win.hide(); win.show(); Propagation::Proceed }); } } /// Loads the configured modules onto a bar. fn load_modules(&self, config: BarConfig, monitor: &Monitor) -> Result { let icon_theme = IconTheme::new(); if let Some(ref theme) = config.icon_theme { icon_theme.set_custom_theme(Some(theme)); } let app = &self.window.application().expect("to exist"); macro_rules! info { ($location:expr) => { ModuleInfo { app, bar_position: config.position, monitor, output_name: &self.monitor_name, location: $location, icon_theme: &icon_theme, } }; } // popup ignores module location so can bodge this for now let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap); let popup = Rc::new(popup); if let Some(modules) = config.start { let info = info!(ModuleLocation::Left); add_modules(&self.start, modules, &info, &self.ironbar, &popup)?; } if let Some(modules) = config.center { let info = info!(ModuleLocation::Center); add_modules(&self.center, modules, &info, &self.ironbar, &popup)?; } if let Some(modules) = config.end { let info = info!(ModuleLocation::Right); add_modules(&self.end, modules, &info, &self.ironbar, &popup)?; } let result = BarLoadResult { popup }; Ok(result) } fn show(&self, include_window: bool) { debug!("Showing bar: {}", self.name); // show each box but do not use `show_all`. // this ensures `show_if` option works as intended. self.start.show(); self.center.show(); self.end.show(); self.content.show(); if include_window { self.window.show(); } } pub fn name(&self) -> &str { &self.name } /// The name of the output the bar is displayed on. pub fn monitor_name(&self) -> &str { &self.monitor_name } pub fn popup(&self) -> Rc { match &self.inner { Inner::New { .. } => { panic!("Attempted to get popup of uninitialized bar. This is a serious bug!") } Inner::Loaded { popup } => popup.clone(), } } pub fn visible(&self) -> bool { self.window.is_visible() } /// Sets the window visibility status pub fn set_visible(&self, visible: bool) { self.window.set_visible(visible) } } /// Creates a `gtk::Box` container to place widgets inside. fn create_container(name: &str, orientation: Orientation) -> gtk::Box { let container = gtk::Box::builder() .orientation(orientation) .spacing(0) .name(name) .build(); container.style_context().add_class("container"); container } #[derive(Debug)] struct BarLoadResult { popup: Rc, } /// Adds modules into a provided GTK box, /// which should be one of its left, center or right containers. fn add_modules( content: >k::Box, modules: Vec, info: &ModuleInfo, ironbar: &Rc, popup: &Rc, ) -> Result<()> { let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into(); for config in modules { config.create(&module_factory, content, info)?; } Ok(()) } pub fn create_bar( app: &Application, monitor: &Monitor, monitor_name: String, config: BarConfig, ironbar: Rc, ) -> Result { let bar = Bar::new(app, monitor_name, config, ironbar); bar.init(monitor) }