use crate::AUTO_SELECT_FILL_COLOUR; use crate::AUTO_SELECT_TEXT_COLOUR; use crate::BAR_HEIGHT; use crate::DEFAULT_PADDING; use crate::KomorebiEvent; use crate::MAX_LABEL_WIDTH; use crate::MONITOR_LEFT; use crate::MONITOR_RIGHT; use crate::MONITOR_TOP; use crate::config::KomobarConfig; use crate::config::KomobarTheme; use crate::config::MonitorConfigOrIndex; use crate::config::Position; use crate::config::PositionConfig; use crate::config::get_individual_spacing; use crate::process_hwnd; use crate::render::Color32Ext; use crate::render::Grouping; use crate::render::RenderConfig; use crate::render::RenderExt; use crate::take_widget_clicked; use crate::widgets::komorebi::Komorebi; use crate::widgets::komorebi::MonitorInfo; use crate::widgets::widget::BarWidget; use crate::widgets::widget::WidgetConfig; use color_eyre::eyre; use crossbeam_channel::Receiver; use crossbeam_channel::TryRecvError; use eframe::egui::Align; use eframe::egui::Align2; use eframe::egui::Area; use eframe::egui::CentralPanel; use eframe::egui::Color32; use eframe::egui::Context; use eframe::egui::FontData; use eframe::egui::FontDefinitions; use eframe::egui::FontFamily; use eframe::egui::FontId; use eframe::egui::Frame; use eframe::egui::Id; use eframe::egui::Layout; use eframe::egui::Margin; use eframe::egui::PointerButton; use eframe::egui::Rgba; use eframe::egui::Style; use eframe::egui::TextStyle; use eframe::egui::Vec2; use eframe::egui::Visuals; use font_loader::system_fonts; use font_loader::system_fonts::FontPropertyBuilder; use komorebi_client::Colour; use komorebi_client::MonitorNotification; use komorebi_client::NotificationEvent; use komorebi_client::PathExt; use komorebi_client::SocketMessage; use komorebi_client::VirtualDesktopNotification; use komorebi_themes::Base16Wrapper; use komorebi_themes::Catppuccin; use komorebi_themes::KomobarThemeBase16; use komorebi_themes::KomobarThemeCatppuccin; use komorebi_themes::KomobarThemeCustom; use komorebi_themes::catppuccin_egui; use lazy_static::lazy_static; use parking_lot::Mutex; use std::cell::RefCell; use std::collections::HashMap; use std::io::Error; use std::io::ErrorKind; use std::io::Write; use std::os::windows::process::CommandExt; use std::path::PathBuf; use std::process::ChildStdin; use std::process::Command; use std::process::Stdio; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::Ordering; const CREATE_NO_WINDOW: u32 = 0x0800_0000; lazy_static! { static ref SESSION_STDIN: Mutex> = Mutex::new(None); } fn start_powershell() -> eyre::Result<()> { // found running session, do nothing if SESSION_STDIN.lock().as_mut().is_some() { tracing::debug!("PowerShell session already started"); return Ok(()); } tracing::debug!("Starting PowerShell session"); let mut child = Command::new("powershell.exe") .args(["-NoLogo", "-NoProfile", "-Command", "-"]) .stdin(Stdio::piped()) .creation_flags(CREATE_NO_WINDOW) .spawn()?; let stdin = child.stdin.take().expect("stdin piped"); // Store stdin for later commands let mut session_stdin = SESSION_STDIN.lock(); *session_stdin = Option::from(stdin); Ok(()) } fn stop_powershell() -> eyre::Result<()> { tracing::debug!("Stopping PowerShell session"); if let Some(mut session_stdin) = SESSION_STDIN.lock().take() { if let Err(e) = session_stdin.write_all(b"exit\n") { tracing::error!(error = %e, "failed to write exit command to PowerShell stdin"); return Err(e.into()); } if let Err(e) = session_stdin.flush() { tracing::error!(error = %e, "failed to flush PowerShell stdin"); return Err(e.into()); } tracing::debug!("PowerShell session stopped"); } else { tracing::debug!("PowerShell session already stopped"); } Ok(()) } pub fn exec_powershell(cmd: &str) -> eyre::Result<()> { if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() { if let Err(e) = writeln!(session_stdin, "{cmd}") { tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin"); return Err(e.into()); } if let Err(e) = session_stdin.flush() { tracing::error!(error = %e, "failed to flush PowerShell stdin"); return Err(e.into()); } return Ok(()); } Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into()) } pub struct Komobar { pub hwnd: Option, pub monitor_index: Option, pub disabled: bool, pub config: KomobarConfig, pub render_config: Rc>, pub monitor_info: Option>>, pub left_widgets: Vec>, pub center_widgets: Vec>, pub right_widgets: Vec>, pub rx_gui: Receiver, pub rx_config: Receiver, pub bg_color: Rc>, pub bg_color_with_alpha: Rc>, pub scale_factor: f32, pub size_rect: komorebi_client::Rect, pub work_area_offset: komorebi_client::Rect, applied_theme_on_first_frame: bool, mouse_follows_focus: bool, input_config: InputConfig, } struct InputConfig { accumulated_scroll_delta: Vec2, act_on_vertical_scroll: bool, act_on_horizontal_scroll: bool, vertical_scroll_threshold: f32, horizontal_scroll_threshold: f32, vertical_scroll_max_threshold: f32, horizontal_scroll_max_threshold: f32, } pub fn apply_theme( ctx: &Context, theme: KomobarTheme, bg_color: Rc>, bg_color_with_alpha: Rc>, transparency_alpha: Option, grouping: Option, render_config: Rc>, ) { let (auto_select_fill, auto_select_text) = match theme { KomobarTheme::Catppuccin(KomobarThemeCatppuccin { name: catppuccin, accent: catppuccin_value, auto_select_fill: catppuccin_auto_select_fill, auto_select_text: catppuccin_auto_select_text, }) => { match catppuccin { Catppuccin::Frappe => { catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE); let catppuccin_value = catppuccin_value.unwrap_or_default(); let accent = catppuccin_value.color32(catppuccin.as_theme()); ctx.style_mut(|style| { style.visuals.selection.stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent; style.visuals.override_text_color = None; }); bg_color.replace(catppuccin_egui::FRAPPE.base); } Catppuccin::Latte => { catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE); let catppuccin_value = catppuccin_value.unwrap_or_default(); let accent = catppuccin_value.color32(catppuccin.as_theme()); ctx.style_mut(|style| { style.visuals.selection.stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent; style.visuals.override_text_color = None; }); bg_color.replace(catppuccin_egui::LATTE.base); } Catppuccin::Macchiato => { catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO); let catppuccin_value = catppuccin_value.unwrap_or_default(); let accent = catppuccin_value.color32(catppuccin.as_theme()); ctx.style_mut(|style| { style.visuals.selection.stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent; style.visuals.override_text_color = None; }); bg_color.replace(catppuccin_egui::MACCHIATO.base); } Catppuccin::Mocha => { catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA); let catppuccin_value = catppuccin_value.unwrap_or_default(); let accent = catppuccin_value.color32(catppuccin.as_theme()); ctx.style_mut(|style| { style.visuals.selection.stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent; style.visuals.override_text_color = None; }); bg_color.replace(catppuccin_egui::MOCHA.base); } } ( catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())), catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())), ) } KomobarTheme::Base16(KomobarThemeBase16 { name: base16, accent: base16_value, auto_select_fill: base16_auto_select_fill, auto_select_text: base16_auto_select_text, }) => { ctx.set_style(base16.style()); let base16_value = base16_value.unwrap_or_default(); let accent = base16_value.color32(Base16Wrapper::Base16(base16)); ctx.style_mut(|style| { style.visuals.selection.stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent; }); bg_color.replace(base16.background()); ( base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Base16(base16))), base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))), ) } KomobarTheme::Custom(KomobarThemeCustom { colours, accent: base16_value, auto_select_fill: base16_auto_select_fill, auto_select_text: base16_auto_select_text, }) => { let background = colours.background(); ctx.set_style(colours.style()); let base16_value = base16_value.unwrap_or_default(); let accent = base16_value.color32(Base16Wrapper::Custom(colours.clone())); ctx.style_mut(|style| { style.visuals.selection.stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent; }); bg_color.replace(background); ( base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))), base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))), ) } }; AUTO_SELECT_FILL_COLOUR.store( auto_select_fill.map_or(0, |c| Colour::from(c).into()), Ordering::SeqCst, ); AUTO_SELECT_TEXT_COLOUR.store( auto_select_text.map_or(0, |c| Colour::from(c).into()), Ordering::SeqCst, ); // Apply transparency_alpha let theme_color = *bg_color.borrow(); bg_color_with_alpha.replace(theme_color.try_apply_alpha(transparency_alpha)); // apply rounding to the widgets if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) = &grouping && let Some(rounding) = config.rounding { ctx.style_mut(|style| { style.visuals.widgets.noninteractive.corner_radius = rounding.into(); style.visuals.widgets.inactive.corner_radius = rounding.into(); style.visuals.widgets.hovered.corner_radius = rounding.into(); style.visuals.widgets.active.corner_radius = rounding.into(); style.visuals.widgets.open.corner_radius = rounding.into(); }); } // Update RenderConfig's background_color so that widgets will have the new color render_config.borrow_mut().background_color = *bg_color.borrow(); } impl Komobar { pub fn apply_config( &mut self, ctx: &Context, previous_monitor_info: Option>>, ) { MAX_LABEL_WIDTH.store( self.config.max_label_width.unwrap_or(400.0) as i32, Ordering::SeqCst, ); if let Some(font_family) = &self.config.font_family { tracing::info!("attempting to add custom font family: {font_family}"); Self::add_custom_font(ctx, font_family); } // Update the `size_rect` so that the bar position can be changed on the EGUI update // function self.update_size_rect(); self.try_apply_theme(ctx); if let Some(font_size) = &self.config.font_size { tracing::info!("attempting to set custom font size: {font_size}"); Self::set_font_size(ctx, *font_size); } self.render_config.replace((&self.config).new_renderconfig( ctx, *self.bg_color.borrow(), self.config.icon_scale, )); let mut monitor_info = previous_monitor_info; let mut komorebi_widgets = Vec::new(); for (idx, widget_config) in self.config.left_widgets.iter().enumerate() { if let WidgetConfig::Komorebi(config) = widget_config { komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Left)); } } if let Some(center_widgets) = &self.config.center_widgets { for (idx, widget_config) in center_widgets.iter().enumerate() { if let WidgetConfig::Komorebi(config) = widget_config { komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Center)); } } } for (idx, widget_config) in self.config.right_widgets.iter().enumerate() { if let WidgetConfig::Komorebi(config) = widget_config { komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Right)); } } let mut left_widgets = self .config .left_widgets .iter() .filter(|config| config.enabled()) .map(|config| config.as_boxed_bar_widget()) .collect::>>(); let mut center_widgets = match &self.config.center_widgets { Some(center_widgets) => center_widgets .iter() .filter(|config| config.enabled()) .map(|config| config.as_boxed_bar_widget()) .collect::>>(), None => vec![], }; let mut right_widgets = self .config .right_widgets .iter() .filter(|config| config.enabled()) .map(|config| config.as_boxed_bar_widget()) .collect::>>(); if !komorebi_widgets.is_empty() { komorebi_widgets .into_iter() .for_each(|(mut widget, idx, side)| { match monitor_info { None => { monitor_info = Some(widget.monitor_info.clone()); } Some(ref previous) => { if widget.workspaces.is_some() { previous .borrow_mut() .update_from_self(&widget.monitor_info.borrow()); } widget.monitor_info = previous.clone(); } } let boxed: Box = Box::new(widget); match side { Alignment::Left => left_widgets[idx] = boxed, Alignment::Center => center_widgets[idx] = boxed, Alignment::Right => right_widgets[idx] = boxed, } }); } right_widgets.reverse(); self.left_widgets = left_widgets; self.center_widgets = center_widgets; self.right_widgets = right_widgets; let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor { Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { (monitor_config.index, monitor_config.work_area_offset) } Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None), None => (0, None), }; let mapped_info = self.monitor_info.as_ref().map(|info| { let monitor = info.borrow(); ( monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(), monitor.mouse_follows_focus, ) }); if let Some(info) = mapped_info { self.monitor_index = info.0; self.mouse_follows_focus = info.1; } if let Some(monitor_index) = self.monitor_index { if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset) { if new_rect != prev_rect { self.work_area_offset = *new_rect; if let Err(error) = komorebi_client::send_message( &SocketMessage::MonitorWorkAreaOffset(monitor_index, *new_rect), ) { tracing::error!( "error applying work area offset to monitor '{}': {}", monitor_index, error, ); } else { tracing::info!("work area offset applied to monitor: {}", monitor_index); } } } else if let Some(height) = self.config.height.or(Some(BAR_HEIGHT)) { // We only add the `bottom_margin` to the work_area_offset since the top margin is // already considered on the `size_rect.top` let bottom_margin = self .config .margin .as_ref() .map_or(0, |v| v.to_individual(0.0).bottom as i32); let new_rect = komorebi_client::Rect { left: 0, top: (height as i32) + (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst)) + bottom_margin, right: 0, bottom: (height as i32) + (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst)) + bottom_margin, }; if new_rect != self.work_area_offset { self.work_area_offset = new_rect; if let Err(error) = komorebi_client::send_message( &SocketMessage::MonitorWorkAreaOffset(monitor_index, new_rect), ) { tracing::error!( "error applying work area offset to monitor '{monitor_index}': {error}" ); } else { tracing::info!("work area offset applied to monitor: {monitor_index}",); } } } } else if self.monitor_info.is_some() && !self.disabled { tracing::warn!( "couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..." ); self.disabled = true; } else { tracing::warn!( "couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi." ); self.disabled = true; } if let Some(mouse) = &self.config.mouse { self.input_config.act_on_vertical_scroll = mouse.on_scroll_up.is_some() || mouse.on_scroll_down.is_some(); self.input_config.act_on_horizontal_scroll = mouse.on_scroll_left.is_some() || mouse.on_scroll_right.is_some(); self.input_config.vertical_scroll_threshold = mouse .vertical_scroll_threshold .unwrap_or(30.0) .clamp(10.0, 300.0); self.input_config.horizontal_scroll_threshold = mouse .horizontal_scroll_threshold .unwrap_or(30.0) .clamp(10.0, 300.0); // limit how many "ticks" can be accumulated self.input_config.vertical_scroll_max_threshold = self.input_config.vertical_scroll_threshold * 3.0; self.input_config.horizontal_scroll_max_threshold = self.input_config.horizontal_scroll_threshold * 3.0; if mouse.has_command() { start_powershell().unwrap_or_else(|_| { tracing::error!("failed to start powershell session"); }); } else { stop_powershell().unwrap_or_else(|_| { tracing::error!("failed to stop powershell session"); }); } } tracing::info!("widget configuration options applied"); self.monitor_info = monitor_info; } /// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not fn update_size_rect(&mut self) { let position = self.config.position.clone().unwrap_or(PositionConfig { start: Some(Position { x: MONITOR_LEFT.load(Ordering::SeqCst) as f32, y: MONITOR_TOP.load(Ordering::SeqCst) as f32, }), end: Some(Position { x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32, y: BAR_HEIGHT, }), }); let mut start = position.start.unwrap_or(Position { x: MONITOR_LEFT.load(Ordering::SeqCst) as f32, y: MONITOR_TOP.load(Ordering::SeqCst) as f32, }); let mut end = position.end.unwrap_or(Position { x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32, y: BAR_HEIGHT, }); if let Some(height) = self.config.height { end.y = height; } let margin = get_individual_spacing(0.0, &self.config.margin); start.y += margin.top; start.x += margin.left; end.x -= margin.left + margin.right; if end.y == 0.0 { tracing::warn!( "position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default" ) } self.size_rect = komorebi_client::Rect { left: start.x as i32, top: start.y as i32, right: end.x as i32, bottom: end.y as i32, }; } fn try_apply_theme(&mut self, ctx: &Context) { match &self.config.theme { Some(theme) => { apply_theme( ctx, theme.clone(), self.bg_color.clone(), self.bg_color_with_alpha.clone(), self.config.transparency_alpha, self.config.grouping, self.render_config.clone(), ); } None => { let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else( |_| dirs::home_dir().expect("there is no home directory"), |home_path| { let home = home_path.replace_env(); assert!( home.is_dir(), "$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory" ); home }, ); let bar_transparency_alpha = self.config.transparency_alpha; let bar_grouping = self.config.grouping; let config = home_dir.join("komorebi.json"); match komorebi_client::StaticConfig::read(&config) { Ok(config) => { if let Some(theme) = config.theme { apply_theme( ctx, KomobarTheme::from(theme), self.bg_color.clone(), self.bg_color_with_alpha.clone(), bar_transparency_alpha, bar_grouping, self.render_config.clone(), ); } } Err(_) => { ctx.set_style(Style::default()); self.bg_color.replace(Style::default().visuals.panel_fill); // apply rounding to the widgets since we didn't call `apply_theme` if let Some( Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config), ) = &bar_grouping && let Some(rounding) = config.rounding { ctx.style_mut(|style| { style.visuals.widgets.noninteractive.corner_radius = rounding.into(); style.visuals.widgets.inactive.corner_radius = rounding.into(); style.visuals.widgets.hovered.corner_radius = rounding.into(); style.visuals.widgets.active.corner_radius = rounding.into(); style.visuals.widgets.open.corner_radius = rounding.into(); }); } } } } } } pub fn new( cc: &eframe::CreationContext<'_>, rx_gui: Receiver, rx_config: Receiver, config: KomobarConfig, ) -> Self { let mut komobar = Self { hwnd: process_hwnd(), monitor_index: None, disabled: false, config, render_config: Rc::new(RefCell::new(RenderConfig::new())), monitor_info: None, left_widgets: vec![], center_widgets: vec![], right_widgets: vec![], rx_gui, rx_config, bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)), bg_color_with_alpha: Rc::new(RefCell::new(Style::default().visuals.panel_fill)), scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0), size_rect: komorebi_client::Rect::default(), work_area_offset: komorebi_client::Rect::default(), applied_theme_on_first_frame: false, mouse_follows_focus: false, input_config: InputConfig { accumulated_scroll_delta: Vec2::new(0.0, 0.0), act_on_vertical_scroll: false, act_on_horizontal_scroll: false, vertical_scroll_threshold: 0.0, horizontal_scroll_threshold: 0.0, vertical_scroll_max_threshold: 0.0, horizontal_scroll_max_threshold: 0.0, }, }; komobar.apply_config(&cc.egui_ctx, None); // needs a double apply the first time for some reason komobar.apply_config(&cc.egui_ctx, None); komobar } fn set_font_size(ctx: &Context, font_size: f32) { ctx.style_mut(|style| { style.text_styles = [ (TextStyle::Small, FontId::new(9.0, FontFamily::Proportional)), ( TextStyle::Body, FontId::new(font_size, FontFamily::Proportional), ), ( TextStyle::Button, FontId::new(font_size, FontFamily::Proportional), ), ( TextStyle::Heading, FontId::new(18.0, FontFamily::Proportional), ), ( TextStyle::Monospace, FontId::new(font_size, FontFamily::Monospace), ), ] .into(); }); } fn add_custom_font(ctx: &Context, name: &str) { let mut fonts = FontDefinitions::default(); egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); let mut fallbacks = HashMap::new(); fallbacks.insert("Microsoft YaHei", "C:\\Windows\\Fonts\\msyh.ttc"); // chinese fallbacks.insert("Malgun Gothic", "C:\\Windows\\Fonts\\malgun.ttf"); // korean fallbacks.insert("Leelawadee UI", "C:\\Windows\\Fonts\\LeelawUI.ttf"); // thai for (name, path) in fallbacks { if let Ok(bytes) = std::fs::read(path) { fonts .font_data .insert(name.to_owned(), Arc::from(FontData::from_owned(bytes))); for family in [FontFamily::Proportional, FontFamily::Monospace] { fonts .families .entry(family) .or_default() .insert(0, name.to_owned()); } } } let property = FontPropertyBuilder::new().family(name).build(); if let Some((font, _)) = system_fonts::get(&property) { fonts .font_data .insert(name.to_owned(), Arc::new(FontData::from_owned(font))); for family in [FontFamily::Proportional, FontFamily::Monospace] { fonts .families .entry(family) .or_default() .insert(0, name.to_owned()); } } // Tell egui to use these fonts: ctx.set_fonts(fonts); } pub fn position_bar(&self) { if let Some(hwnd) = self.hwnd { let window = komorebi_client::Window::from(hwnd); match window.set_position(&self.size_rect, false) { Ok(_) => { tracing::info!("updated bar position"); } Err(error) => { tracing::error!("{error}") } } } } fn update_monitor_coordinates(&mut self, monitor_size: &komorebi_client::Rect) { // Store the new monitor coordinates MONITOR_TOP.store(monitor_size.top, Ordering::SeqCst); MONITOR_LEFT.store(monitor_size.left, Ordering::SeqCst); MONITOR_RIGHT.store(monitor_size.right, Ordering::SeqCst); // Since the `config.position` is changed on `main.rs` we need to update it here. // If the user had set up some `start` position, that will be overriden here // since we have no way to know what was that value since it might have been // changed on `main.rs`. However if the users use the new configs this won't be // a problem for them. if let Some(start) = self.config.position.as_mut().and_then(|p| p.start.as_mut()) { start.x = monitor_size.left as f32; start.y = monitor_size.top as f32; } } } impl eframe::App for Komobar { // Needed for transparency fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] { Rgba::TRANSPARENT.to_array() } fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { if self.hwnd.is_none() { self.hwnd = process_hwnd(); } if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) { self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0); self.apply_config(ctx, self.monitor_info.clone()); } if let Ok(updated_config) = self.rx_config.try_recv() { self.config = updated_config; self.apply_config(ctx, self.monitor_info.clone()); } match self.rx_gui.try_recv() { Err(error) => match error { TryRecvError::Empty => {} TryRecvError::Disconnected => { tracing::error!( "failed to receive komorebi notification on gui thread: {error}" ); } }, Ok(KomorebiEvent::Notification(notification)) => { let state = ¬ification.state; let usr_monitor_index = match &self.config.monitor { Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { monitor_config.index } Some(MonitorConfigOrIndex::Index(idx)) => *idx, None => 0, }; let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied(); self.monitor_index = monitor_index; let mut should_apply_config = false; match notification.event { NotificationEvent::VirtualDesktop( VirtualDesktopNotification::EnteredAssociatedVirtualDesktop, ) => { tracing::debug!( "back on komorebi's associated virtual desktop - restoring bar" ); if let Some(hwnd) = self.hwnd { komorebi_client::WindowsApi::restore_window(hwnd); } } NotificationEvent::VirtualDesktop( VirtualDesktopNotification::LeftAssociatedVirtualDesktop, ) => { tracing::debug!( "no longer on komorebi's associated virtual desktop - minimizing bar" ); if let Some(hwnd) = self.hwnd { komorebi_client::WindowsApi::minimize_window(hwnd); } } _ => {} } if self.monitor_index.is_none() || self .monitor_index .is_some_and(|idx| idx >= state.monitors.elements().len()) { if !self.disabled { // Monitor for this bar got disconnected lets disable the bar until it // reconnects self.disabled = true; tracing::warn!( "This bar's monitor got disconnected. The bar will be disabled until it reconnects..." ); } return; } else { if self.disabled { tracing::info!("Found this bar's monitor. The bar will be enabled!"); // Restore the bar in case it has been minimized when the monitor // disconnected if let Some(hwnd) = self.hwnd { let window = komorebi_client::Window::from(hwnd); if window.is_miminized() { komorebi_client::WindowsApi::restore_window(hwnd); } } // Reset the current `work_area_offset` so that it gets recalculated and // properly applied again, since if the monitor has connected for the first // time it won't have the work_area_offset applied but the bar thinks it // does. self.work_area_offset = komorebi_client::Rect::default(); should_apply_config = true; } self.disabled = false; } if matches!( notification.event, NotificationEvent::Monitor(MonitorNotification::DisplayConnectionChange) ) { let monitor_index = self.monitor_index.expect("should have a monitor index"); let monitor_size = state.monitors.elements()[monitor_index].size; self.update_monitor_coordinates(&monitor_size); should_apply_config = true; } if self.disabled { return; } // Check if monitor coordinates/size has changed if let Some(monitor_index) = self.monitor_index { let monitor_size = state.monitors.elements()[monitor_index].size; let top = MONITOR_TOP.load(Ordering::SeqCst); let left = MONITOR_LEFT.load(Ordering::SeqCst); let right = MONITOR_RIGHT.load(Ordering::SeqCst); let rect = komorebi_client::Rect { top, left, bottom: monitor_size.bottom, right, }; if monitor_size != rect { tracing::info!( "Monitor coordinates/size has changed, storing new coordinates: {:#?}", monitor_size ); self.update_monitor_coordinates(&monitor_size); should_apply_config = true; } } if let Some(monitor_info) = &self.monitor_info { monitor_info.borrow_mut().update( self.monitor_index, notification.state, self.render_config.borrow().show_all_icons, ); handle_notification( ctx, notification.event, self.bg_color.clone(), self.bg_color_with_alpha.clone(), self.config.transparency_alpha, self.config.grouping, self.config.theme.clone(), self.render_config.clone(), ); } if should_apply_config { self.apply_config(ctx, self.monitor_info.clone()); // Reposition the Bar self.position_bar(); } } Ok(KomorebiEvent::Reconnect) => { if let Some(monitor_index) = self.monitor_index { if let Err(error) = komorebi_client::send_message( &SocketMessage::MonitorWorkAreaOffset(monitor_index, self.work_area_offset), ) { tracing::error!( "error applying work area offset to monitor '{}': {}", monitor_index, error, ); } else { tracing::info!("work area offset applied to monitor: {}", monitor_index); } } } } if self.disabled { // The check for disabled is performed above, if we get here and the bar is still // disabled then we should return without drawing anything. return; } if !self.applied_theme_on_first_frame { self.try_apply_theme(ctx); self.applied_theme_on_first_frame = true; } // Check if egui's Window size is the expected one, if not, update it if let Some(current_rect) = ctx.input(|i| i.viewport().outer_rect) { // Get the correct size according to scale factor let current_rect = komorebi_client::Rect { left: (current_rect.min.x * self.scale_factor) as i32, top: (current_rect.min.y * self.scale_factor) as i32, right: ((current_rect.max.x - current_rect.min.x) * self.scale_factor) as i32, bottom: ((current_rect.max.y - current_rect.min.y) * self.scale_factor) as i32, }; if self.size_rect != current_rect { self.position_bar(); } } let frame = match &self.config.padding { None => { if let Some(frame) = &self.config.frame { Frame::NONE .inner_margin(Margin::symmetric( frame.inner_margin.x as i8, frame.inner_margin.y as i8, )) .fill(*self.bg_color_with_alpha.borrow()) } else { Frame::NONE .inner_margin(Margin::same(0)) .fill(*self.bg_color_with_alpha.borrow()) } } Some(padding) => { let padding = padding.to_individual(DEFAULT_PADDING); Frame::NONE .inner_margin(Margin { top: padding.top as i8, bottom: padding.bottom as i8, left: padding.left as i8, right: padding.right as i8, }) .fill(*self.bg_color_with_alpha.borrow()) } }; let mut render_config = self.render_config.borrow_mut(); let frame = render_config.change_frame_on_bar(frame, &ctx.style()); CentralPanel::default().frame(frame).show(ctx, |ui| { // Variable to store command to execute after widgets are rendered // This allows widgets to mark clicks as consumed before bar processes them let mut pending_command: Option = None; if let Some(mouse_config) = &self.config.mouse { let command = if ui .input(|i| i.pointer.button_double_clicked(PointerButton::Primary)) { tracing::debug!("Input: primary button double clicked"); &mouse_config.on_primary_double_click } else if ui.input(|i| i.pointer.button_clicked(PointerButton::Secondary)) { tracing::debug!("Input: secondary button clicked"); &mouse_config.on_secondary_click } else if ui.input(|i| i.pointer.button_clicked(PointerButton::Middle)) { tracing::debug!("Input: middle button clicked"); &mouse_config.on_middle_click } else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra1)) { tracing::debug!("Input: extra1 button clicked"); &mouse_config.on_extra1_click } else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra2)) { tracing::debug!("Input: extra2 button clicked"); &mouse_config.on_extra2_click } else if self.input_config.act_on_vertical_scroll || self.input_config.act_on_horizontal_scroll { let scroll_delta = ui.input(|input| input.smooth_scroll_delta); self.input_config.accumulated_scroll_delta += scroll_delta; if scroll_delta.y != 0.0 && self.input_config.act_on_vertical_scroll { // Do not store more than the max threshold self.input_config.accumulated_scroll_delta.y = self.input_config.accumulated_scroll_delta.y.clamp( -self.input_config.vertical_scroll_max_threshold, self.input_config.vertical_scroll_max_threshold, ); // When the accumulated scroll passes the threshold, trigger a tick. if self.input_config.accumulated_scroll_delta.y.abs() >= self.input_config.vertical_scroll_threshold { let direction_command = if self.input_config.accumulated_scroll_delta.y > 0.0 { &mouse_config.on_scroll_up } else { &mouse_config.on_scroll_down }; // Remove one tick's worth of scroll from the accumulator, preserving any excess. self.input_config.accumulated_scroll_delta.y -= self.input_config.vertical_scroll_threshold * self.input_config.accumulated_scroll_delta.y.signum(); tracing::debug!( "Input: vertical scroll ticked. excess: {} | threshold: {}", self.input_config.accumulated_scroll_delta.y, self.input_config.vertical_scroll_threshold ); direction_command } else { &None } } else if scroll_delta.x != 0.0 && self.input_config.act_on_horizontal_scroll { // Do not store more than the max threshold self.input_config.accumulated_scroll_delta.x = self.input_config.accumulated_scroll_delta.x.clamp( -self.input_config.horizontal_scroll_max_threshold, self.input_config.horizontal_scroll_max_threshold, ); // When the accumulated scroll passes the threshold, trigger a tick. if self.input_config.accumulated_scroll_delta.x.abs() >= self.input_config.horizontal_scroll_threshold { let direction_command = if self.input_config.accumulated_scroll_delta.x > 0.0 { &mouse_config.on_scroll_left } else { &mouse_config.on_scroll_right }; // Remove one tick's worth of scroll from the accumulator, preserving any excess. self.input_config.accumulated_scroll_delta.x -= self.input_config.horizontal_scroll_threshold * self.input_config.accumulated_scroll_delta.x.signum(); tracing::debug!( "Input: horizontal scroll ticked. excess: {} | threshold: {}", self.input_config.accumulated_scroll_delta.x, self.input_config.horizontal_scroll_threshold ); direction_command } else { &None } } else { &None } } else { &None }; // Store the command to execute after widgets are rendered // This allows widgets to mark clicks as consumed pending_command = command.clone(); } // Apply grouping logic for the bar as a whole let area_frame = if let Some(frame) = &self.config.frame { Frame::NONE .inner_margin(Margin::symmetric(0, frame.inner_margin.y as i8)) .outer_margin(Margin::same(0)) } else { Frame::NONE .inner_margin(Margin::same(0)) .outer_margin(Margin::same(0)) }; let available_height = ui.max_rect().max.y; ctx.style_mut(|style| { style.spacing.interact_size.y = available_height; }); if !self.left_widgets.is_empty() { // Left-aligned widgets layout Area::new(Id::new("left_panel")) .anchor(Align2::LEFT_CENTER, [0.0, 0.0]) // Align in the left center of the window .show(ctx, |ui| { let mut left_area_frame = area_frame; if let Some(padding) = self .config .padding .as_ref() .map(|s| s.to_individual(DEFAULT_PADDING)) { left_area_frame.inner_margin.left = padding.left as i8; left_area_frame.inner_margin.top = padding.top as i8; left_area_frame.inner_margin.bottom = padding.bottom as i8; } else if let Some(frame) = &self.config.frame { left_area_frame.inner_margin.left = frame.inner_margin.x as i8; left_area_frame.inner_margin.top = frame.inner_margin.y as i8; left_area_frame.inner_margin.bottom = frame.inner_margin.y as i8; } left_area_frame.show(ui, |ui| { ui.horizontal(|ui| { let mut render_conf = render_config.clone(); render_conf.alignment = Some(Alignment::Left); render_config.apply_on_alignment(ui, |ui| { for w in &mut self.left_widgets { w.render(ctx, ui, &mut render_conf); } }); }); }); }); } if !self.right_widgets.is_empty() { // Right-aligned widgets layout Area::new(Id::new("right_panel")) .anchor(Align2::RIGHT_CENTER, [0.0, 0.0]) // Align in the right center of the window .show(ctx, |ui| { let mut right_area_frame = area_frame; if let Some(padding) = self .config .padding .as_ref() .map(|s| s.to_individual(DEFAULT_PADDING)) { right_area_frame.inner_margin.right = padding.right as i8; right_area_frame.inner_margin.top = padding.top as i8; right_area_frame.inner_margin.bottom = padding.bottom as i8; } else if let Some(frame) = &self.config.frame { right_area_frame.inner_margin.right = frame.inner_margin.x as i8; right_area_frame.inner_margin.top = frame.inner_margin.y as i8; right_area_frame.inner_margin.bottom = frame.inner_margin.y as i8; } right_area_frame.show(ui, |ui| { let initial_size = Vec2 { x: ui.available_size_before_wrap().x, y: ui.spacing().interact_size.y, }; ui.allocate_ui_with_layout( initial_size, Layout::right_to_left(Align::Center), |ui| { let mut render_conf = render_config.clone(); render_conf.alignment = Some(Alignment::Right); render_config.apply_on_alignment(ui, |ui| { for w in &mut self.right_widgets { w.render(ctx, ui, &mut render_conf); } }); }, ); }); }); } if !self.center_widgets.is_empty() { // Floating center widgets Area::new(Id::new("center_panel")) .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window .show(ctx, |ui| { let mut center_area_frame = area_frame; if let Some(padding) = self .config .padding .as_ref() .map(|s| s.to_individual(DEFAULT_PADDING)) { center_area_frame.inner_margin.top = padding.top as i8; center_area_frame.inner_margin.bottom = padding.bottom as i8; } else if let Some(frame) = &self.config.frame { center_area_frame.inner_margin.top = frame.inner_margin.y as i8; center_area_frame.inner_margin.bottom = frame.inner_margin.y as i8; } center_area_frame.show(ui, |ui| { ui.horizontal(|ui| { let mut render_conf = render_config.clone(); render_conf.alignment = Some(Alignment::Center); render_config.apply_on_alignment(ui, |ui| { for w in &mut self.center_widgets { w.render(ctx, ui, &mut render_conf); } }); }); }); }); } // Execute the deferred mouse command only if no widget consumed the click if let Some(command) = pending_command && !take_widget_clicked() { command.execute(self.mouse_follows_focus); } }); } } #[derive(Copy, Clone)] pub enum Alignment { Left, Center, Right, } #[allow(clippy::too_many_arguments)] fn handle_notification( ctx: &Context, event: komorebi_client::NotificationEvent, bg_color: Rc>, bg_color_with_alpha: Rc>, transparency_alpha: Option, grouping: Option, default_theme: Option, render_config: Rc>, ) { if let NotificationEvent::Socket(message) = event { match message { SocketMessage::ReloadStaticConfiguration(path) => { if let Ok(config) = komorebi_client::StaticConfig::read(&path) { if let Some(theme) = config.theme { apply_theme( ctx, KomobarTheme::from(theme), bg_color.clone(), bg_color_with_alpha.clone(), transparency_alpha, grouping, render_config, ); tracing::info!("applied theme from updated komorebi.json"); } else if let Some(default_theme) = default_theme { apply_theme( ctx, default_theme, bg_color.clone(), bg_color_with_alpha.clone(), transparency_alpha, grouping, render_config, ); tracing::info!( "removed theme from updated komorebi.json and applied default theme" ); } else { tracing::warn!( "theme was removed from updated komorebi.json but there was no default theme to apply" ); } } } SocketMessage::Theme(theme) => { apply_theme( ctx, KomobarTheme::from(*theme), bg_color, bg_color_with_alpha.clone(), transparency_alpha, grouping, render_config, ); tracing::info!("applied theme from komorebi socket message"); } _ => {} } } }