From 219fa8e14f2006685516b5d0396dced89ce96504 Mon Sep 17 00:00:00 2001 From: CtByte <165908630+CtByte@users.noreply.github.com> Date: Tue, 19 Nov 2024 04:55:21 +0100 Subject: [PATCH] feat(bar): add widget grouping options This commit adds various widget grouping and transparency options to komorebi-bar, and is comprised of the individual commits listed below, worked on in PR #1108, squashed into one. e8f5952abb47608ca5f63fb627956f51b2b19142 * adding RenderConfig, and some test frames on widgets 0a5e0a4c0aa7b7b2826b18922ef648a741e9470a * no clone a5a7d6906cee7cd3810f59a26261437d596399f7 * comment 6a91dd46cd28b5f16a74c9bbcda7c453d17a5d86 * ignore unused 80f0214e4717f4224811a9e2e9082e046144517c * Group enum, Copy RenderConfig fbe5e2c1f794694888540fdb596dbcec825d41a9 * Group -> Grouping ce49b433f9787a9360954e9c8bc2c90e08542a75 * GroupingConfig f446a6a45f3bfbfea7088288a1194aee58769de3 * "fmt --check" fix (thanks VS) d188222be71adea7fcb6e9c32e16e6b3fd156657 * added widget grouping and group module 1008ec2031ab5c9f9ee9cec48883eaeabb904fb4 * rounding from settings, and apply_on_side 7fff6d29a9d5faabbbb355fa99b2197f818349bb * dereferencing 655e8ce4c16117f51cb115597dd73d4bff6c7c9c * AlphaColour, transparency, bar background, more grouping config options cba0fcd8821751ac4221e483377dd3dc9c460aa6 * added RoundingConfig ec5f7dc82db1f52f8c401586e3232860d76b536c * handling grouping edge case for komorebi focus window 12117b832b2d3c7a75882ab1714b82f2283a0267 * changed default values 645c46beb86793225ed58db0ed417ad13dae6802 * background color using theme color, AlphaColour.to_color32_or, updating json format for Grouping and RoundingConfig 10d2ab21c7b77dbff97052f6800ff9c78863747a * hot-reload on grouping d88774328a83dac64ee7ddc1cd3dd159ec1500a8 * grouping correction on init 2cd237fd0d3e78cbf2f3570e16d1a0849e3021df * added shadow to grouping, optional width on grouping stroke 4f4b617f2681f02e07554bc4bc66572c4e9b59f9 * grouping on bar, converting AlphaColour from_rgba_unmultiplied, simplified grouping 3808fcec8fd3ab2709def38338413773a0573659 * widget rounding based on grouping, atomic background color, simplified config, style on grouping be45d14f6d4854c35ef1c1e9d5f6bc8ee5cea409 * renamed Side to Alignment, group spacing be45d14f6d4854c35ef1c1e9d5f6bc8ee5cea409 * proper widget spacing based on alignment b43a5bda691278a4449bad980bfdbb2d1e15bc19 * added widget_spacing to config c18e5f4dbefa4c3b0b6d6e1e7471395b462011a2 * test commit cba2b2f7acd9a022b7acf30973cea829b9ceaacc * refactoring of render and grouping, widget spacing WIP 9311cb00ec4adaeb7c130d32fc635303dc4c6f82 * simplify no_spacing 36c267246bc7c5c2e5526c259bf460c1ad3e97e8 * correct spacing on komorebi and network widgets (WIP) 85a41bf5b2a395a17265eaa393edeb2e25a2f1e3 * correct widget spacing on all widgets 50b49ccf6965aa371ea15213ab5d2a29b21543b8 * refactoring widget spacing 9ec67ad98813a5ea79e6f29663486de1df1b7500 * account for ui item_spacing when setting the widget_spacing e88a2fd9c08dcd7fac1a301129be80993181ca30 * format --- komorebi-bar/src/bar.rs | 91 ++++++-- komorebi-bar/src/battery.rs | 18 +- komorebi-bar/src/config.rs | 7 + komorebi-bar/src/cpu.rs | 31 +-- komorebi-bar/src/date.rs | 28 +-- komorebi-bar/src/komorebi.rs | 439 ++++++++++++++++++----------------- komorebi-bar/src/main.rs | 5 +- komorebi-bar/src/media.rs | 42 ++-- komorebi-bar/src/memory.rs | 31 +-- komorebi-bar/src/network.rs | 42 ++-- komorebi-bar/src/render.rs | 263 +++++++++++++++++++++ komorebi-bar/src/storage.rs | 42 ++-- komorebi-bar/src/time.rs | 28 +-- komorebi-bar/src/widget.rs | 3 +- komorebi/src/colour.rs | 12 + 15 files changed, 719 insertions(+), 363 deletions(-) create mode 100644 komorebi-bar/src/render.rs diff --git a/komorebi-bar/src/bar.rs b/komorebi-bar/src/bar.rs index 698a94a8f..321e33417 100644 --- a/komorebi-bar/src/bar.rs +++ b/komorebi-bar/src/bar.rs @@ -5,6 +5,10 @@ use crate::config::PositionConfig; use crate::komorebi::Komorebi; use crate::komorebi::KomorebiNotificationState; use crate::process_hwnd; +use crate::render::Color32Ext; +use crate::render::Grouping; +use crate::render::RenderConfig; +use crate::render::RenderExt; use crate::widget::BarWidget; use crate::widget::WidgetConfig; use crate::BAR_HEIGHT; @@ -24,8 +28,10 @@ use eframe::egui::FontId; use eframe::egui::Frame; use eframe::egui::Layout; use eframe::egui::Margin; +use eframe::egui::Rgba; use eframe::egui::Style; use eframe::egui::TextStyle; +use eframe::egui::Visuals; use font_loader::system_fonts; use font_loader::system_fonts::FontPropertyBuilder; use komorebi_client::KomorebiTheme; @@ -41,6 +47,7 @@ use std::sync::Arc; pub struct Komobar { pub config: Arc, + pub render_config: Rc>, pub komorebi_notification_state: Option>>, pub left_widgets: Vec>, pub right_widgets: Vec>, @@ -237,6 +244,30 @@ impl Komobar { } } + // apply rounding to the widgets + if let Some( + Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config), + ) = &config.grouping + { + if let Some(rounding) = config.rounding { + ctx.style_mut(|style| { + style.visuals.widgets.noninteractive.rounding = rounding.into(); + style.visuals.widgets.inactive.rounding = rounding.into(); + style.visuals.widgets.hovered.rounding = rounding.into(); + style.visuals.widgets.active.rounding = rounding.into(); + style.visuals.widgets.open.rounding = rounding.into(); + }); + } + } + + let theme_color = *self.bg_color.borrow(); + + self.render_config + .replace(config.new_renderconfig(theme_color)); + + self.bg_color + .replace(theme_color.try_apply_alpha(self.config.transparency_alpha)); + if let Some(font_size) = &config.font_size { tracing::info!("attempting to set custom font size: {font_size}"); Self::set_font_size(ctx, *font_size); @@ -251,7 +282,7 @@ impl Komobar { if let WidgetConfig::Komorebi(config) = widget_config { komorebi_widget = Some(Komorebi::from(config)); komorebi_widget_idx = Some(idx); - side = Some(Side::Left); + side = Some(Alignment::Left); } } @@ -259,7 +290,7 @@ impl Komobar { if let WidgetConfig::Komorebi(config) = widget_config { komorebi_widget = Some(Komorebi::from(config)); komorebi_widget_idx = Some(idx); - side = Some(Side::Right); + side = Some(Alignment::Right); } } @@ -293,8 +324,8 @@ impl Komobar { let boxed: Box = Box::new(widget); match side { - Side::Left => left_widgets[idx] = boxed, - Side::Right => right_widgets[idx] = boxed, + Alignment::Left => left_widgets[idx] = boxed, + Alignment::Right => right_widgets[idx] = boxed, } } @@ -307,6 +338,7 @@ impl Komobar { self.komorebi_notification_state = komorebi_notification_state; } + pub fn new( cc: &eframe::CreationContext<'_>, rx_gui: Receiver, @@ -315,6 +347,7 @@ impl Komobar { ) -> Self { let mut komobar = Self { config: config.clone(), + render_config: Rc::new(RefCell::new(RenderConfig::new())), komorebi_notification_state: None, left_widgets: vec![], right_widgets: vec![], @@ -385,13 +418,10 @@ impl Komobar { } } impl eframe::App for Komobar { - // TODO: I think this is needed for transparency?? - // fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] { - // egui::Rgba::TRANSPARENT.to_array() - // let mut background = Color32::from_gray(18).to_normalized_gamma_f32(); - // background[3] = 0.9; - // background - // } + // 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.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) { @@ -433,18 +463,35 @@ impl eframe::App for Komobar { Frame::none().fill(*self.bg_color.borrow()) }; + let mut render_config = self.render_config.borrow_mut(); + CentralPanel::default().frame(frame).show(ctx, |ui| { - ui.horizontal_centered(|ui| { - ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - for w in &mut self.left_widgets { - w.render(ctx, ui); - } - }); + // Apply grouping logic for the bar as a whole + render_config.clone().apply_on_bar(ui, |ui| { + ui.horizontal_centered(|ui| { + // Left-aligned widgets layout + ui.with_layout(Layout::left_to_right(Align::Center), |ui| { + let mut render_conf = *render_config; + 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); + } + }); + }); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - for w in &mut self.right_widgets { - w.render(ctx, ui); - } + // Right-aligned widgets layout + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let mut render_conf = *render_config; + 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); + } + }); + }) }) }) }); @@ -452,7 +499,7 @@ impl eframe::App for Komobar { } #[derive(Copy, Clone)] -enum Side { +pub enum Alignment { Left, Right, } diff --git a/komorebi-bar/src/battery.rs b/komorebi-bar/src/battery.rs index 3f1e198ee..1aac1e6cc 100644 --- a/komorebi-bar/src/battery.rs +++ b/komorebi-bar/src/battery.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -115,7 +115,7 @@ impl Battery { } impl BarWidget for Battery { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let output = self.output(); if !output.is_empty() { @@ -147,14 +147,14 @@ impl BarWidget for Battery { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - ui.add( - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()), - ); + config.apply_on_widget(true, ui, |ui| { + ui.add( + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()), + ); + }); } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/config.rs b/komorebi-bar/src/config.rs index c3e13ad2c..fe8355b4e 100644 --- a/komorebi-bar/src/config.rs +++ b/komorebi-bar/src/config.rs @@ -1,3 +1,4 @@ +use crate::render::Grouping; use crate::widget::WidgetConfig; use eframe::egui::Pos2; use eframe::egui::TextBuffer; @@ -28,6 +29,12 @@ pub struct KomobarConfig { pub max_label_width: Option, /// Theme pub theme: Option, + /// Alpha value for the color transparency [[0-255]] (default: 200) + pub transparency_alpha: Option, + /// Spacing between widgets (default: 10.0) + pub widget_spacing: Option, + /// Visual grouping for widgets + pub grouping: Option, /// Left side widgets (ordered left-to-right) pub left_widgets: Vec, /// Right side widgets (ordered left-to-right) diff --git a/komorebi-bar/src/cpu.rs b/komorebi-bar/src/cpu.rs index 1a2c1d4d3..7c8187f68 100644 --- a/komorebi-bar/src/cpu.rs +++ b/komorebi-bar/src/cpu.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -70,7 +70,7 @@ impl Cpu { } impl BarWidget for Cpu { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let output = self.output(); if !output.is_empty() { @@ -99,22 +99,23 @@ impl BarWidget for Cpu { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - if ui - .add( - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn() + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() { - eprintln!("{}", error) + if let Err(error) = + Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn() + { + eprintln!("{}", error) + } } - } + }); } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/date.rs b/komorebi-bar/src/date.rs index 666fb2f13..479b8e7cb 100644 --- a/komorebi-bar/src/date.rs +++ b/komorebi-bar/src/date.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -86,7 +86,7 @@ impl Date { } impl BarWidget for Date { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let mut output = self.output(); if !output.is_empty() { @@ -119,19 +119,19 @@ impl BarWidget for Date { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - if ui - .add( - Label::new(WidgetText::LayoutJob(layout_job.clone())) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - self.format.next() - } + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(WidgetText::LayoutJob(layout_job.clone())) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() + { + self.format.next() + } + }); } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/komorebi.rs b/komorebi-bar/src/komorebi.rs index c3467e556..ad95c0989 100644 --- a/komorebi-bar/src/komorebi.rs +++ b/komorebi-bar/src/komorebi.rs @@ -1,9 +1,9 @@ use crate::bar::apply_theme; use crate::config::KomobarTheme; +use crate::render::RenderConfig; use crate::ui::CustomUi; use crate::widget::BarWidget; use crate::MAX_LABEL_WIDTH; -use crate::WIDGET_SPACING; use crossbeam_channel::Receiver; use crossbeam_channel::TryRecvError; use eframe::egui::text::LayoutJob; @@ -122,102 +122,123 @@ pub struct Komorebi { } impl BarWidget for Komorebi { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut(); if self.workspaces.enable { let mut update = None; - for (i, (ws, should_show)) in komorebi_notification_state.workspaces.iter().enumerate() - { - if *should_show - && ui - .add(SelectableLabel::new( - komorebi_notification_state.selected_workspace.eq(ws), - ws.to_string(), - )) - .clicked() + // NOTE: There should always be at least one workspace if the bar is connected to komorebi. + config.apply_on_widget(false, ui, |ui| { + for (i, (ws, should_show)) in + komorebi_notification_state.workspaces.iter().enumerate() { - update = Some(ws.to_string()); - let mut proceed = true; - - if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false)) - .is_err() + if *should_show + && ui + .add(SelectableLabel::new( + komorebi_notification_state.selected_workspace.eq(ws), + ws.to_string(), + )) + .clicked() { - tracing::error!("could not send message to komorebi: MouseFollowsFocus"); - proceed = false; - } + update = Some(ws.to_string()); + let mut proceed = true; - if proceed - && komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i)) + if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false)) .is_err() - { - tracing::error!("could not send message to komorebi: FocusWorkspaceNumber"); - proceed = false; - } + { + tracing::error!( + "could not send message to komorebi: MouseFollowsFocus" + ); + proceed = false; + } - if proceed - && komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( - komorebi_notification_state.mouse_follows_focus, - )) - .is_err() - { - tracing::error!("could not send message to komorebi: MouseFollowsFocus"); - proceed = false; - } + if proceed + && komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber( + i, + )) + .is_err() + { + tracing::error!( + "could not send message to komorebi: FocusWorkspaceNumber" + ); + proceed = false; + } - if proceed - && komorebi_client::send_message(&SocketMessage::RetileWithResizeDimensions) + if proceed + && komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( + komorebi_notification_state.mouse_follows_focus, + )) .is_err() - { - tracing::error!("could not send message to komorebi: Retile"); + { + tracing::error!( + "could not send message to komorebi: MouseFollowsFocus" + ); + proceed = false; + } + + if proceed + && komorebi_client::send_message( + &SocketMessage::RetileWithResizeDimensions, + ) + .is_err() + { + tracing::error!("could not send message to komorebi: Retile"); + } } } - } + }); if let Some(update) = update { komorebi_notification_state.selected_workspace = update; } - - ui.add_space(WIDGET_SPACING); } if let Some(layout) = self.layout { if layout.enable { - if ui - .add( - Label::new(komorebi_notification_state.layout.to_string()) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - match komorebi_notification_state.layout { - KomorebiLayout::Default(_) => { - if komorebi_client::send_message(&SocketMessage::CycleLayout( - CycleDirection::Next, - )) - .is_err() - { - tracing::error!("could not send message to komorebi: CycleLayout"); + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(komorebi_notification_state.layout.to_string()) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() + { + match komorebi_notification_state.layout { + KomorebiLayout::Default(_) => { + if komorebi_client::send_message(&SocketMessage::CycleLayout( + CycleDirection::Next, + )) + .is_err() + { + tracing::error!( + "could not send message to komorebi: CycleLayout" + ); + } } - } - KomorebiLayout::Floating => { - if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() - { - tracing::error!("could not send message to komorebi: ToggleTiling"); + KomorebiLayout::Floating => { + if komorebi_client::send_message(&SocketMessage::ToggleTiling) + .is_err() + { + tracing::error!( + "could not send message to komorebi: ToggleTiling" + ); + } } - } - KomorebiLayout::Paused => { - if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() { - tracing::error!("could not send message to komorebi: TogglePause"); + KomorebiLayout::Paused => { + if komorebi_client::send_message(&SocketMessage::TogglePause) + .is_err() + { + tracing::error!( + "could not send message to komorebi: TogglePause" + ); + } } + KomorebiLayout::Custom => {} } - KomorebiLayout::Custom => {} } - } - - ui.add_space(WIDGET_SPACING); + }); } } @@ -225,170 +246,174 @@ impl BarWidget for Komorebi { if configuration_switcher.enable { for (name, location) in configuration_switcher.configurations.iter() { let path = PathBuf::from(location); - if path.is_file() - && ui + if path.is_file() { + config.apply_on_widget(true, ui,|ui|{ + if ui .add(Label::new(name).selectable(false).sense(Sense::click())) .clicked() - { - let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path); - let mut proceed = true; - if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration( - canonicalized, - )) - .is_err() { - tracing::error!( - "could not send message to komorebi: ReplaceConfiguration" - ); - proceed = false; - } + let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path); + let mut proceed = true; + if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration( + canonicalized, + )) + .is_err() + { + tracing::error!( + "could not send message to komorebi: ReplaceConfiguration" + ); + proceed = false; + } - if let Some(rect) = komorebi_notification_state.work_area_offset { - if proceed { - match komorebi_client::send_query(&SocketMessage::Query( - komorebi_client::StateQuery::FocusedMonitorIndex, - )) { - Ok(idx) => { - if let Ok(monitor_idx) = idx.parse::() { - if komorebi_client::send_message( - &SocketMessage::MonitorWorkAreaOffset( - monitor_idx, - rect, - ), - ) - .is_err() - { - tracing::error!( + if let Some(rect) = komorebi_notification_state.work_area_offset { + if proceed { + match komorebi_client::send_query(&SocketMessage::Query( + komorebi_client::StateQuery::FocusedMonitorIndex, + )) { + Ok(idx) => { + if let Ok(monitor_idx) = idx.parse::() { + if komorebi_client::send_message( + &SocketMessage::MonitorWorkAreaOffset( + monitor_idx, + rect, + ), + ) + .is_err() + { + tracing::error!( "could not send message to komorebi: MonitorWorkAreaOffset" ); + } } } - } - Err(_) => { - tracing::error!( - "could not send message to komorebi: Query" - ); + Err(_) => { + tracing::error!( + "could not send message to komorebi: Query" + ); + } } } } - } + }}); } } - - ui.add_space(WIDGET_SPACING); } } if let Some(focused_window) = self.focused_window { if focused_window.enable { let titles = &komorebi_notification_state.focused_container_information.0; - let icons = &komorebi_notification_state.focused_container_information.1; - let focused_window_idx = - komorebi_notification_state.focused_container_information.2; - - let iter = titles.iter().zip(icons.iter()); - - for (i, (title, icon)) in iter.enumerate() { - if focused_window.show_icon { - if let Some(img) = icon { - ui.add( - Image::from(&img_to_texture(ctx, img)) - .maintain_aspect_ratio(true) - .max_height(15.0), - ); - } - } - - if i == focused_window_idx { - let font_id = ctx - .style() - .text_styles - .get(&TextStyle::Body) - .cloned() - .unwrap_or_else(FontId::default); - - let layout_job = LayoutJob::simple( - title.to_string(), - font_id.clone(), - komorebi_notification_state - .stack_accent - .unwrap_or(ctx.style().visuals.selection.stroke.color), - 100.0, - ); - - if titles.len() > 1 { - let available_height = ui.available_height(); - let mut custom_ui = CustomUi(ui); - custom_ui.add_sized_left_to_right( - Vec2::new( - MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, - available_height, - ), - Label::new(layout_job).selectable(false).truncate(), - ); - } else { - let available_height = ui.available_height(); - let mut custom_ui = CustomUi(ui); - custom_ui.add_sized_left_to_right( - Vec2::new( - MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, - available_height, - ), - Label::new(title).selectable(false).truncate(), - ); - } - } else { - let available_height = ui.available_height(); - let mut custom_ui = CustomUi(ui); - - if custom_ui - .add_sized_left_to_right( - Vec2::new( - MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, - available_height, - ), - Label::new(title) - .selectable(false) - .sense(Sense::click()) - .truncate(), - ) - .clicked() - { - if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( - false, - )) - .is_err() - { - tracing::error!( - "could not send message to komorebi: MouseFollowsFocus" - ); + if !titles.is_empty() { + config.apply_on_widget(true, ui, |ui| { + let icons = &komorebi_notification_state.focused_container_information.1; + let focused_window_idx = + komorebi_notification_state.focused_container_information.2; + + let iter = titles.iter().zip(icons.iter()); + + for (i, (title, icon)) in iter.enumerate() { + if focused_window.show_icon { + if let Some(img) = icon { + ui.add( + Image::from(&img_to_texture(ctx, img)) + .maintain_aspect_ratio(true) + .max_height(15.0), + ); + } } - if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i)) - .is_err() - { - tracing::error!( - "could not send message to komorebi: FocusStackWindow" + if i == focused_window_idx { + let font_id = ctx + .style() + .text_styles + .get(&TextStyle::Body) + .cloned() + .unwrap_or_else(FontId::default); + + let layout_job = LayoutJob::simple( + title.to_string(), + font_id.clone(), + komorebi_notification_state + .stack_accent + .unwrap_or(ctx.style().visuals.selection.stroke.color), + 100.0, ); - } - if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( - komorebi_notification_state.mouse_follows_focus, - )) - .is_err() - { - tracing::error!( - "could not send message to komorebi: MouseFollowsFocus" - ); + if titles.len() > 1 { + let available_height = ui.available_height(); + let mut custom_ui = CustomUi(ui); + custom_ui.add_sized_left_to_right( + Vec2::new( + MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, + available_height, + ), + Label::new(layout_job).selectable(false).truncate(), + ); + } else { + let available_height = ui.available_height(); + let mut custom_ui = CustomUi(ui); + custom_ui.add_sized_left_to_right( + Vec2::new( + MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, + available_height, + ), + Label::new(title).selectable(false).truncate(), + ); + } + } else { + let available_height = ui.available_height(); + let mut custom_ui = CustomUi(ui); + + if custom_ui + .add_sized_left_to_right( + Vec2::new( + MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, + available_height, + ), + Label::new(title) + .selectable(false) + .sense(Sense::click()) + .truncate(), + ) + .clicked() + { + if komorebi_client::send_message( + &SocketMessage::MouseFollowsFocus(false), + ) + .is_err() + { + tracing::error!( + "could not send message to komorebi: MouseFollowsFocus" + ); + } + + if komorebi_client::send_message( + &SocketMessage::FocusStackWindow(i), + ) + .is_err() + { + tracing::error!( + "could not send message to komorebi: FocusStackWindow" + ); + } + + if komorebi_client::send_message( + &SocketMessage::MouseFollowsFocus( + komorebi_notification_state.mouse_follows_focus, + ), + ) + .is_err() + { + tracing::error!( + "could not send message to komorebi: MouseFollowsFocus" + ); + } + } } } - } - - ui.add_space(WIDGET_SPACING); + }); } } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/main.rs b/komorebi-bar/src/main.rs index f32c5937b..4d30c4d78 100644 --- a/komorebi-bar/src/main.rs +++ b/komorebi-bar/src/main.rs @@ -7,6 +7,7 @@ mod komorebi; mod media; mod memory; mod network; +mod render; mod storage; mod time; mod ui; @@ -42,8 +43,6 @@ use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows; use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; -pub static WIDGET_SPACING: f32 = 10.0; - pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400); pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0); pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0); @@ -266,7 +265,7 @@ fn main() -> color_eyre::Result<()> { let viewport_builder = ViewportBuilder::default() .with_decorations(false) - // .with_transparent(config.transparent) + .with_transparent(config.transparency_alpha.is_some()) .with_taskbar(false); let native_options = eframe::NativeOptions { diff --git a/komorebi-bar/src/media.rs b/komorebi-bar/src/media.rs index 23f8f9595..c8dda4f91 100644 --- a/komorebi-bar/src/media.rs +++ b/komorebi-bar/src/media.rs @@ -1,7 +1,7 @@ +use crate::render::RenderConfig; use crate::ui::CustomUi; use crate::widget::BarWidget; use crate::MAX_LABEL_WIDTH; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -78,7 +78,7 @@ impl Media { } impl BarWidget for Media { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let output = self.output(); if !output.is_empty() { @@ -102,26 +102,26 @@ impl BarWidget for Media { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - let available_height = ui.available_height(); - let mut custom_ui = CustomUi(ui); + config.apply_on_widget(true, ui, |ui| { + let available_height = ui.available_height(); + let mut custom_ui = CustomUi(ui); - if custom_ui - .add_sized_left_to_right( - Vec2::new( - MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, - available_height, - ), - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()) - .truncate(), - ) - .clicked() - { - self.toggle(); - } - - ui.add_space(WIDGET_SPACING); + if custom_ui + .add_sized_left_to_right( + Vec2::new( + MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, + available_height, + ), + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()) + .truncate(), + ) + .clicked() + { + self.toggle(); + } + }); } } } diff --git a/komorebi-bar/src/memory.rs b/komorebi-bar/src/memory.rs index f5261b804..c6cce6142 100644 --- a/komorebi-bar/src/memory.rs +++ b/komorebi-bar/src/memory.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -73,7 +73,7 @@ impl Memory { } impl BarWidget for Memory { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let output = self.output(); if !output.is_empty() { @@ -102,22 +102,23 @@ impl BarWidget for Memory { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - if ui - .add( - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn() + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() { - eprintln!("{}", error) + if let Err(error) = + Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn() + { + eprintln!("{}", error) + } } - } + }); } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/network.rs b/komorebi-bar/src/network.rs index 6f41fcb73..bea6f961f 100644 --- a/komorebi-bar/src/network.rs +++ b/komorebi-bar/src/network.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -317,21 +317,21 @@ impl Network { } impl BarWidget for Network { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.show_total_data_transmitted { for output in self.total_data_transmitted() { - ui.add(Label::new(output).selectable(false)); + config.apply_on_widget(true, ui, |ui| { + ui.add(Label::new(output).selectable(false)); + }); } - - ui.add_space(WIDGET_SPACING); } if self.show_network_activity { for output in self.network_activity() { - ui.add(Label::new(output).selectable(false)); + config.apply_on_widget(true, ui, |ui| { + ui.add(Label::new(output).selectable(false)); + }); } - - ui.add_space(WIDGET_SPACING); } if self.enable { @@ -367,21 +367,21 @@ impl BarWidget for Network { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - if ui - .add( - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() { - eprintln!("{}", error) + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() + { + if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() { + eprintln!("{}", error) + } } - } + }); } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/render.rs b/komorebi-bar/src/render.rs new file mode 100644 index 000000000..797d55e7b --- /dev/null +++ b/komorebi-bar/src/render.rs @@ -0,0 +1,263 @@ +use crate::bar::Alignment; +use crate::config::KomobarConfig; +use eframe::egui::Color32; +use eframe::egui::Frame; +use eframe::egui::InnerResponse; +use eframe::egui::Margin; +use eframe::egui::Rounding; +use eframe::egui::Shadow; +use eframe::egui::Ui; +use eframe::egui::Vec2; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "kind")] +pub enum Grouping { + /// No grouping is applied + None, + /// Widgets are grouped as a whole + Bar(GroupingConfig), + /// Widgets are grouped by alignment + Alignment(GroupingConfig), + /// Widgets are grouped individually + Widget(GroupingConfig), +} + +#[derive(Copy, Clone)] +pub struct RenderConfig { + /// Spacing between widgets + pub spacing: f32, + /// Sets how widgets are grouped + pub grouping: Grouping, + /// Background color + pub background_color: Color32, + /// Alignment of the widgets + pub alignment: Option, + /// Add more inner margin when adding a widget group + pub more_inner_margin: bool, + /// Set to true after the first time the apply_on_widget was called on an alignment + pub applied_on_widget: bool, +} + +pub trait RenderExt { + fn new_renderconfig(&self, background_color: Color32) -> RenderConfig; +} + +impl RenderExt for &KomobarConfig { + fn new_renderconfig(&self, background_color: Color32) -> RenderConfig { + RenderConfig { + spacing: self.widget_spacing.unwrap_or(10.0), + grouping: self.grouping.unwrap_or(Grouping::None), + background_color, + alignment: None, + more_inner_margin: false, + applied_on_widget: false, + } + } +} + +impl RenderConfig { + pub fn new() -> Self { + Self { + spacing: 0.0, + grouping: Grouping::None, + background_color: Color32::BLACK, + alignment: None, + more_inner_margin: false, + applied_on_widget: false, + } + } + + pub fn apply_on_bar( + &mut self, + ui: &mut Ui, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + self.alignment = None; + + if let Grouping::Bar(config) = self.grouping { + return self.define_group(None, config, ui, add_contents); + } + + Self::fallback_group(ui, add_contents) + } + + pub fn apply_on_alignment( + &mut self, + ui: &mut Ui, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + self.alignment = None; + + if let Grouping::Alignment(config) = self.grouping { + return self.define_group(None, config, ui, add_contents); + } + + Self::fallback_group(ui, add_contents) + } + + pub fn apply_on_widget( + &mut self, + more_inner_margin: bool, + ui: &mut Ui, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + self.more_inner_margin = more_inner_margin; + let outer_margin = self.widget_outer_margin(ui); + + if let Grouping::Widget(config) = self.grouping { + return self.define_group(Some(outer_margin), config, ui, add_contents); + } + + self.fallback_widget_group(Some(outer_margin), ui, add_contents) + } + + fn fallback_group(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { + InnerResponse { + inner: add_contents(ui), + response: ui.response().clone(), + } + } + + fn fallback_widget_group( + &mut self, + outer_margin: Option, + ui: &mut Ui, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + Frame::none() + .outer_margin(outer_margin.unwrap_or(Margin::ZERO)) + .inner_margin(match self.more_inner_margin { + true => Margin::symmetric(5.0, 0.0), + false => Margin::same(0.0), + }) + .show(ui, add_contents) + } + + fn define_group( + &mut self, + outer_margin: Option, + config: GroupingConfig, + ui: &mut Ui, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + Frame::group(ui.style_mut()) + .outer_margin(outer_margin.unwrap_or(Margin::ZERO)) + .inner_margin(match self.more_inner_margin { + true => Margin::symmetric(8.0, 3.0), + false => Margin::symmetric(3.0, 3.0), + }) + .stroke(ui.style().visuals.widgets.noninteractive.bg_stroke) + .rounding(match config.rounding { + Some(rounding) => rounding.into(), + None => ui.style().visuals.widgets.noninteractive.rounding, + }) + .fill( + self.background_color + .try_apply_alpha(config.transparency_alpha), + ) + .shadow(match config.style { + Some(style) => match style { + // new styles can be added if needed here + GroupingStyle::Default => Shadow::NONE, + GroupingStyle::DefaultWithShadow => Shadow { + blur: 4.0, + offset: Vec2::new(1.0, 1.0), + spread: 3.0, + color: Color32::BLACK.try_apply_alpha(config.transparency_alpha), + }, + }, + None => Shadow::NONE, + }) + .show(ui, add_contents) + } + + fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin { + let spacing = if self.applied_on_widget { + // Remove the default item spacing from the margin + self.spacing - ui.spacing().item_spacing.x + } else { + 0.0 + }; + + if !self.applied_on_widget { + self.applied_on_widget = true; + } + + Margin { + left: match self.alignment { + Some(align) => match align { + Alignment::Left => spacing, + Alignment::Right => 0.0, + }, + None => 0.0, + }, + right: match self.alignment { + Some(align) => match align { + Alignment::Left => 0.0, + Alignment::Right => spacing, + }, + None => 0.0, + }, + top: 0.0, + bottom: 0.0, + } + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct GroupingConfig { + /// Styles for the grouping + pub style: Option, + /// Alpha value for the color transparency [[0-255]] (default: 200) + pub transparency_alpha: Option, + /// Rounding values for the 4 corners. Can be a single or 4 values. + pub rounding: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub enum GroupingStyle { + Default, + /// A black shadow is added under the default group + DefaultWithShadow, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum RoundingConfig { + /// All 4 corners are the same + Same(f32), + /// All 4 corners are custom. Order: NW, NE, SW, SE + Individual([f32; 4]), +} + +impl From for Rounding { + fn from(value: RoundingConfig) -> Self { + match value { + RoundingConfig::Same(value) => Rounding::same(value), + RoundingConfig::Individual(values) => Self { + nw: values[0], + ne: values[1], + sw: values[2], + se: values[3], + }, + } + } +} + +pub trait Color32Ext { + fn try_apply_alpha(self, transparency_alpha: Option) -> Self; +} + +impl Color32Ext for Color32 { + /// Tries to apply the alpha value to the Color32 + fn try_apply_alpha(self, transparency_alpha: Option) -> Self { + if let Some(alpha) = transparency_alpha { + return Color32::from_rgba_unmultiplied(self.r(), self.g(), self.b(), alpha); + } + + self + } +} diff --git a/komorebi-bar/src/storage.rs b/komorebi-bar/src/storage.rs index a4498dc51..a63fab94b 100644 --- a/komorebi-bar/src/storage.rs +++ b/komorebi-bar/src/storage.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -79,7 +79,7 @@ impl Storage { } impl BarWidget for Storage { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let font_id = ctx .style() @@ -107,27 +107,27 @@ impl BarWidget for Storage { TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()), ); - if ui - .add( - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - if let Err(error) = Command::new("cmd.exe") - .args([ - "/C", - "explorer.exe", - output.split(' ').collect::>()[0], - ]) - .spawn() + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() { - eprintln!("{}", error) + if let Err(error) = Command::new("cmd.exe") + .args([ + "/C", + "explorer.exe", + output.split(' ').collect::>()[0], + ]) + .spawn() + { + eprintln!("{}", error) + } } - } - - ui.add_space(WIDGET_SPACING); + }); } } } diff --git a/komorebi-bar/src/time.rs b/komorebi-bar/src/time.rs index 37f4160e1..87e031991 100644 --- a/komorebi-bar/src/time.rs +++ b/komorebi-bar/src/time.rs @@ -1,6 +1,6 @@ use crate::config::LabelPrefix; +use crate::render::RenderConfig; use crate::widget::BarWidget; -use crate::WIDGET_SPACING; use eframe::egui::text::LayoutJob; use eframe::egui::Context; use eframe::egui::FontId; @@ -77,7 +77,7 @@ impl Time { } impl BarWidget for Time { - fn render(&mut self, ctx: &Context, ui: &mut Ui) { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let mut output = self.output(); if !output.is_empty() { @@ -110,19 +110,19 @@ impl BarWidget for Time { TextFormat::simple(font_id, ctx.style().visuals.text_color()), ); - if ui - .add( - Label::new(layout_job) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - self.format.toggle() - } + config.apply_on_widget(true, ui, |ui| { + if ui + .add( + Label::new(layout_job) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() + { + self.format.toggle() + } + }); } - - ui.add_space(WIDGET_SPACING); } } } diff --git a/komorebi-bar/src/widget.rs b/komorebi-bar/src/widget.rs index 491443abc..aecf6a080 100644 --- a/komorebi-bar/src/widget.rs +++ b/komorebi-bar/src/widget.rs @@ -12,6 +12,7 @@ use crate::memory::Memory; use crate::memory::MemoryConfig; use crate::network::Network; use crate::network::NetworkConfig; +use crate::render::RenderConfig; use crate::storage::Storage; use crate::storage::StorageConfig; use crate::time::Time; @@ -23,7 +24,7 @@ use serde::Deserialize; use serde::Serialize; pub trait BarWidget { - fn render(&mut self, ctx: &Context, ui: &mut Ui); + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig); } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] diff --git a/komorebi/src/colour.rs b/komorebi/src/colour.rs index 7e69a9207..7577ccd0b 100644 --- a/komorebi/src/colour.rs +++ b/komorebi/src/colour.rs @@ -39,6 +39,18 @@ impl From for Colour { } } +impl From for Color32 { + fn from(value: Colour) -> Self { + match value { + Colour::Rgb(rgb) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8), + Colour::Hex(hex) => { + let rgb = Rgb::from(hex); + Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8) + } + } + } +} + #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct Hex(HexColor);