From bb31e7155d9eed68b03b54c83e6c4e8fb1e6d84c Mon Sep 17 00:00:00 2001 From: CtByte <165908630+CtByte@users.noreply.github.com> Date: Tue, 3 Dec 2024 01:12:22 +0100 Subject: [PATCH] feat(bar): komorebi widget visual changes The visual changes include: * the focused_window section is now indicating the active window in a stack and has hover effect. * custom icons for all the layouts, including `paused`, `floating`, `monocle` states. * custom layout/state picker with configurable options. * display format configuration for the layouts (Icon/Text/IconAndText) * display format configuration for the focused_window section (Icon/Text/IconAndText) * display format configuration for the workspaces section (Icon/Text/IconAndText) --- komorebi-bar/src/config.rs | 10 + komorebi-bar/src/komorebi.rs | 617 +++++++++++++++------------- komorebi-bar/src/komorebi_layout.rs | 311 ++++++++++++++ komorebi-bar/src/main.rs | 2 + komorebi-bar/src/render.rs | 16 + komorebi-bar/src/selected_frame.rs | 55 +++ 6 files changed, 720 insertions(+), 291 deletions(-) create mode 100644 komorebi-bar/src/komorebi_layout.rs create mode 100644 komorebi-bar/src/selected_frame.rs diff --git a/komorebi-bar/src/config.rs b/komorebi-bar/src/config.rs index 1a6970d57..74838fb45 100644 --- a/komorebi-bar/src/config.rs +++ b/komorebi-bar/src/config.rs @@ -187,3 +187,13 @@ pub enum LabelPrefix { /// Show an icon and text IconAndText, } + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub enum DisplayFormat { + /// Show only icon + Icon, + /// Show only text + Text, + /// Show both icon and text + IconAndText, +} diff --git a/komorebi-bar/src/komorebi.rs b/komorebi-bar/src/komorebi.rs index 22d53e8f9..7c596c8e8 100644 --- a/komorebi-bar/src/komorebi.rs +++ b/komorebi-bar/src/komorebi.rs @@ -1,38 +1,44 @@ use crate::bar::apply_theme; +use crate::config::DisplayFormat; use crate::config::KomobarTheme; +use crate::komorebi_layout::KomorebiLayout; use crate::render::RenderConfig; +use crate::selected_frame::SelectableFrame; use crate::ui::CustomUi; use crate::widget::BarWidget; use crate::MAX_LABEL_WIDTH; use crate::MONITOR_INDEX; use crossbeam_channel::Receiver; use crossbeam_channel::TryRecvError; -use eframe::egui::text::LayoutJob; +use eframe::egui::vec2; use eframe::egui::Color32; use eframe::egui::ColorImage; use eframe::egui::Context; use eframe::egui::FontId; +use eframe::egui::Frame; use eframe::egui::Image; use eframe::egui::Label; -use eframe::egui::SelectableLabel; +use eframe::egui::Margin; +use eframe::egui::Rounding; use eframe::egui::Sense; +use eframe::egui::Stroke; use eframe::egui::TextStyle; use eframe::egui::TextureHandle; use eframe::egui::TextureOptions; use eframe::egui::Ui; use eframe::egui::Vec2; use image::RgbaImage; -use komorebi_client::CycleDirection; +use komorebi_client::Container; use komorebi_client::NotificationEvent; use komorebi_client::Rect; use komorebi_client::SocketMessage; +use komorebi_client::Window; +use komorebi_client::Workspace; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::cell::RefCell; use std::collections::BTreeMap; -use std::fmt::Display; -use std::fmt::Formatter; use std::path::PathBuf; use std::rc::Rc; use std::sync::atomic::Ordering; @@ -55,20 +61,28 @@ pub struct KomorebiWorkspacesConfig { pub enable: bool, /// Hide workspaces without any windows pub hide_empty_workspaces: bool, + /// Display format of the workspace + pub display: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiLayoutConfig { /// Enable the Komorebi Layout widget pub enable: bool, + /// List of layout options + pub options: Option>, + /// Display format of the current layout + pub display: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiFocusedWindowConfig { /// Enable the Komorebi Focused Window widget pub enable: bool, - /// Show the icon of the currently focused window - pub show_icon: bool, + /// DEPRECATED: use 'display' instead (Show the icon of the currently focused window) + pub show_icon: Option, + /// Display format of the currently focused window + pub display: Option, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -102,12 +116,12 @@ impl From<&KomorebiConfig> for Komorebi { hide_empty_workspaces: value.workspaces.hide_empty_workspaces, mouse_follows_focus: true, work_area_offset: None, - focused_container_information: (vec![], vec![], 0), + focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY, stack_accent: None, monitor_index: MONITOR_INDEX.load(Ordering::SeqCst), })), workspaces: value.workspaces, - layout: value.layout, + layout: value.layout.clone(), focused_window: value.focused_window, configuration_switcher, } @@ -130,121 +144,154 @@ impl BarWidget for Komorebi { if self.workspaces.enable { let mut update = None; - // 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() - { - if *should_show - && ui - .add(SelectableLabel::new( - komorebi_notification_state.selected_workspace.eq(ws), - ws.to_string(), - )) - .clicked() + if !komorebi_notification_state.workspaces.is_empty() { + let format = self.workspaces.display.unwrap_or(DisplayFormat::Text); + + config.apply_on_widget(false, ui, |ui| { + for (i, (ws, container_information)) in + komorebi_notification_state.workspaces.iter().enumerate() { - update = Some(ws.to_string()); - let mut proceed = true; + if SelectableFrame::new( + komorebi_notification_state.selected_workspace.eq(ws), + ) + .show(ui, |ui| { + let mut has_icon = false; + + if let DisplayFormat::Icon | DisplayFormat::IconAndText = format { + let icons: Vec<_> = + container_information.icons.iter().flatten().collect(); + + if !icons.is_empty() { + Frame::none() + .inner_margin(Margin::same( + ui.style().spacing.button_padding.y, + )) + .show(ui, |ui| { + for icon in icons { + ui.add( + Image::from(&img_to_texture(ctx, icon)) + .maintain_aspect_ratio(true) + .shrink_to_fit(), + ); - if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false)) - .is_err() - { - tracing::error!( - "could not send message to komorebi: MouseFollowsFocus" - ); - proceed = false; - } + if !has_icon { + has_icon = true; + } + } + }); + } + } - if proceed - && komorebi_client::send_message( - &SocketMessage::FocusMonitorWorkspaceNumber( - komorebi_notification_state.monitor_index, - i, - ), - ) - .is_err() - { - tracing::error!( - "could not send message to komorebi: FocusWorkspaceNumber" - ); - proceed = false; - } + // draw a custom icon when there is no app icon + if match format { + DisplayFormat::Icon => !has_icon, + _ => false, + } { + let font_id = ctx + .style() + .text_styles + .get(&TextStyle::Body) + .cloned() + .unwrap_or_else(FontId::default); - if proceed - && komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( - komorebi_notification_state.mouse_follows_focus, - )) - .is_err() + let (response, painter) = + ui.allocate_painter(Vec2::splat(font_id.size), Sense::hover()); + let stroke = + Stroke::new(1.0, ctx.style().visuals.selection.stroke.color); + let mut rect = response.rect; + let rounding = Rounding::same(rect.width() * 0.1); + rect = rect.shrink(stroke.width); + let c = rect.center(); + let r = rect.width() / 2.0; + painter.rect_stroke(rect, rounding, stroke); + painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke); + + response.on_hover_text(ws.to_string()) + } else if match format { + DisplayFormat::Icon => has_icon, + _ => false, + } { + ui.response().on_hover_text(ws.to_string()) + } else { + ui.add(Label::new(ws.to_string()).selectable(false)) + } + }) + .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::RetileWithResizeDimensions, - ) + if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( + false, + )) .is_err() - { - tracing::error!("could not send message to komorebi: Retile"); - } - } - } - }); + { + tracing::error!( + "could not send message to komorebi: MouseFollowsFocus" + ); + proceed = false; + } - if let Some(update) = update { - komorebi_notification_state.selected_workspace = update; - } - } + if proceed + && komorebi_client::send_message( + &SocketMessage::FocusMonitorWorkspaceNumber( + komorebi_notification_state.monitor_index, + i, + ), + ) + .is_err() + { + tracing::error!( + "could not send message to komorebi: FocusWorkspaceNumber" + ); + proceed = false; + } - if let Some(layout) = self.layout { - if layout.enable { - 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, + if proceed + && komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( + komorebi_notification_state.mouse_follows_focus, )) .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" - ); - } + { + tracing::error!( + "could not send message to komorebi: MouseFollowsFocus" + ); + proceed = false; } - KomorebiLayout::Paused => { - if komorebi_client::send_message(&SocketMessage::TogglePause) - .is_err() - { - tracing::error!( - "could not send message to komorebi: TogglePause" - ); - } + + if proceed + && komorebi_client::send_message( + &SocketMessage::RetileWithResizeDimensions, + ) + .is_err() + { + tracing::error!("could not send message to komorebi: Retile"); } - KomorebiLayout::Custom => {} } } }); } + + if let Some(update) = update { + komorebi_notification_state.selected_workspace = update; + } + } + + if let Some(layout_config) = &self.layout { + if layout_config.enable { + let workspace_idx: Option = komorebi_notification_state + .workspaces + .iter() + .position(|o| komorebi_notification_state.selected_workspace.eq(&o.0)); + + komorebi_notification_state.layout.show( + ctx, + ui, + config, + layout_config, + workspace_idx, + ); + } } if let Some(configuration_switcher) = &self.configuration_switcher { @@ -252,9 +299,10 @@ impl BarWidget for Komorebi { for (name, location) in configuration_switcher.configurations.iter() { let path = PathBuf::from(location); if path.is_file() { - config.apply_on_widget(true, ui,|ui|{ - if ui - .add(Label::new(name).selectable(false).sense(Sense::click())) + config.apply_on_widget(false, ui,|ui|{ + if SelectableFrame::new(false).show(ui, |ui|{ + ui.add(Label::new(name).selectable(false)) + }) .clicked() { let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path); @@ -307,112 +355,103 @@ impl BarWidget for Komorebi { if let Some(focused_window) = self.focused_window { if focused_window.enable { - let titles = &komorebi_notification_state.focused_container_information.0; + let titles = &komorebi_notification_state + .focused_container_information + .titles; 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; + config.apply_on_widget(false, ui, |ui| { + let icons = &komorebi_notification_state + .focused_container_information + .icons; + let focused_window_idx = komorebi_notification_state + .focused_container_information + .focused_window_idx; 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), + let selected = i == focused_window_idx; + + if SelectableFrame::new(selected) + .show(ui, |ui| { + // handle legacy setting + let format = focused_window.display.unwrap_or( + if focused_window.show_icon.unwrap_or(false) { + DisplayFormat::IconAndText + } else { + DisplayFormat::Text + }, ); - } - } - 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() + if let DisplayFormat::Icon | DisplayFormat::IconAndText = format { - tracing::error!( - "could not send message to komorebi: MouseFollowsFocus" - ); + if let Some(img) = icon { + Frame::none() + .inner_margin(Margin::same( + ui.style().spacing.button_padding.y, + )) + .show(ui, |ui| { + let response = ui.add( + Image::from(&img_to_texture(ctx, img)) + .maintain_aspect_ratio(true) + .shrink_to_fit(), + ); + + if let DisplayFormat::Icon = format { + response.on_hover_text(title); + } + }); + } } - if komorebi_client::send_message( - &SocketMessage::FocusStackWindow(i), - ) - .is_err() + if let DisplayFormat::Text | DisplayFormat::IconAndText = format { - tracing::error!( - "could not send message to komorebi: FocusStackWindow" + 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(), ); } + }) + .clicked() + { + if selected { + return; + } - 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 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" + ); } } } @@ -432,9 +471,9 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle { #[derive(Clone, Debug)] pub struct KomorebiNotificationState { - pub workspaces: Vec<(String, bool)>, + pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>, pub selected_workspace: String, - pub focused_container_information: (Vec, Vec>, usize), + pub focused_container_information: KomorebiNotificationStateContainerInformation, pub layout: KomorebiLayout, pub hide_empty_workspaces: bool, pub mouse_follows_focus: bool, @@ -443,25 +482,6 @@ pub struct KomorebiNotificationState { pub monitor_index: usize, } -#[derive(Copy, Clone, Debug)] -pub enum KomorebiLayout { - Default(komorebi_client::DefaultLayout), - Floating, - Paused, - Custom, -} - -impl Display for KomorebiLayout { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - KomorebiLayout::Default(layout) => write!(f, "{layout}"), - KomorebiLayout::Floating => write!(f, "Floating"), - KomorebiLayout::Paused => write!(f, "Paused"), - KomorebiLayout::Custom => write!(f, "Custom"), - } - } -} - impl KomorebiNotificationState { pub fn update_from_config(&mut self, config: &Self) { self.hide_empty_workspaces = config.hide_empty_workspaces; @@ -526,85 +546,100 @@ impl KomorebiNotificationState { true }; - workspaces.push(( - ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)), - should_show, - )); + if should_show { + workspaces.push(( + ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)), + ws.into(), + )); + } } self.workspaces = workspaces; - self.layout = match monitor.workspaces()[focused_workspace_idx].layout() { - komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout), - komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom, - }; - if !*monitor.workspaces()[focused_workspace_idx].tile() { + if monitor.workspaces()[focused_workspace_idx] + .monocle_container() + .is_some() + { + self.layout = KomorebiLayout::Monocle; + } else if !*monitor.workspaces()[focused_workspace_idx].tile() { self.layout = KomorebiLayout::Floating; - } - - if notification.state.is_paused { + } else if notification.state.is_paused { self.layout = KomorebiLayout::Paused; + } else { + self.layout = match monitor.workspaces()[focused_workspace_idx].layout() { + komorebi_client::Layout::Default(layout) => { + KomorebiLayout::Default(*layout) + } + komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom, + }; } - let mut has_window_container_information = false; + self.focused_container_information = + (&monitor.workspaces()[focused_workspace_idx]).into(); + } + } + } +} - if let Some(container) = - monitor.workspaces()[focused_workspace_idx].monocle_container() - { - has_window_container_information = true; - self.focused_container_information = ( - container - .windows() - .iter() - .map(|w| w.title().unwrap_or_default()) - .collect::>(), - container - .windows() - .iter() - .map(|w| windows_icons::get_icon_by_process_id(w.process_id())) - .collect::>(), - container.focused_window_idx(), - ); - } else if let Some(container) = - monitor.workspaces()[focused_workspace_idx].focused_container() - { - has_window_container_information = true; - self.focused_container_information = ( - container - .windows() - .iter() - .map(|w| w.title().unwrap_or_default()) - .collect::>(), - container - .windows() - .iter() - .map(|w| windows_icons::get_icon_by_process_id(w.process_id())) - .collect::>(), - container.focused_window_idx(), - ); - } +#[derive(Clone, Debug)] +pub struct KomorebiNotificationStateContainerInformation { + pub titles: Vec, + pub icons: Vec>, + pub focused_window_idx: usize, +} - for floating_window in - monitor.workspaces()[focused_workspace_idx].floating_windows() - { - if floating_window.is_focused() { - has_window_container_information = true; - self.focused_container_information = ( - vec![floating_window.title().unwrap_or_default()], - vec![windows_icons::get_icon_by_process_id( - floating_window.process_id(), - )], - 0, - ); - } - } +impl From<&Workspace> for KomorebiNotificationStateContainerInformation { + fn from(value: &Workspace) -> Self { + let mut container_info = Self::EMPTY; - if !has_window_container_information { - self.focused_container_information.0.clear(); - self.focused_container_information.1.clear(); - self.focused_container_information.2 = 0; - } + if let Some(container) = value.monocle_container() { + container_info = container.into(); + } else if let Some(container) = value.focused_container() { + container_info = container.into(); + } + + for floating_window in value.floating_windows() { + if floating_window.is_focused() { + container_info = floating_window.into(); } } + + container_info + } +} + +impl From<&Container> for KomorebiNotificationStateContainerInformation { + fn from(value: &Container) -> Self { + Self { + titles: value + .windows() + .iter() + .map(|w| w.title().unwrap_or_default()) + .collect::>(), + icons: value + .windows() + .iter() + .map(|w| windows_icons::get_icon_by_process_id(w.process_id())) + .collect::>(), + focused_window_idx: value.focused_window_idx(), + } + } +} + +impl From<&Window> for KomorebiNotificationStateContainerInformation { + fn from(value: &Window) -> Self { + Self { + titles: vec![value.title().unwrap_or_default()], + icons: vec![windows_icons::get_icon_by_process_id(value.process_id())], + focused_window_idx: 0, + } } } + +impl KomorebiNotificationStateContainerInformation { + pub const EMPTY: Self = Self { + titles: vec![], + icons: vec![], + focused_window_idx: 0, + }; +} diff --git a/komorebi-bar/src/komorebi_layout.rs b/komorebi-bar/src/komorebi_layout.rs new file mode 100644 index 000000000..7c99a2888 --- /dev/null +++ b/komorebi-bar/src/komorebi_layout.rs @@ -0,0 +1,311 @@ +use crate::config::DisplayFormat; +use crate::komorebi::KomorebiLayoutConfig; +use crate::render::RenderConfig; +use crate::selected_frame::SelectableFrame; +use eframe::egui::vec2; +use eframe::egui::Context; +use eframe::egui::FontId; +use eframe::egui::Frame; +use eframe::egui::Label; +use eframe::egui::Rounding; +use eframe::egui::Sense; +use eframe::egui::Stroke; +use eframe::egui::TextStyle; +use eframe::egui::Ui; +use eframe::egui::Vec2; +use komorebi_client::SocketMessage; +use schemars::JsonSchema; +use serde::de::Error; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde_json::from_str; +use std::fmt::Display; +use std::fmt::Formatter; + +#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)] +#[serde(untagged)] +pub enum KomorebiLayout { + Default(komorebi_client::DefaultLayout), + Monocle, + Floating, + Paused, + Custom, +} + +impl<'de> Deserialize<'de> for KomorebiLayout { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = String::deserialize(deserializer)?; + + // Attempt to deserialize the string as a DefaultLayout + if let Ok(default_layout) = + from_str::(&format!("\"{}\"", s)) + { + return Ok(KomorebiLayout::Default(default_layout)); + } + + // Handle other cases + match s.as_str() { + "Monocle" => Ok(KomorebiLayout::Monocle), + "Floating" => Ok(KomorebiLayout::Floating), + "Paused" => Ok(KomorebiLayout::Paused), + "Custom" => Ok(KomorebiLayout::Custom), + _ => Err(Error::custom(format!("Invalid layout: {}", s))), + } + } +} + +impl Display for KomorebiLayout { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + KomorebiLayout::Default(layout) => write!(f, "{layout}"), + KomorebiLayout::Monocle => write!(f, "Monocle"), + KomorebiLayout::Floating => write!(f, "Floating"), + KomorebiLayout::Paused => write!(f, "Paused"), + KomorebiLayout::Custom => write!(f, "Custom"), + } + } +} + +impl KomorebiLayout { + fn is_default(&mut self) -> bool { + matches!(self, KomorebiLayout::Default(_)) + } + + fn on_click( + &mut self, + show_options: &bool, + monitor_idx: usize, + workspace_idx: Option, + ) -> bool { + if self.is_default() { + !show_options + } else { + self.on_click_option(monitor_idx, workspace_idx); + false + } + } + + fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option) { + match self { + KomorebiLayout::Default(option) => { + if let Some(ws_idx) = workspace_idx { + if komorebi_client::send_message(&SocketMessage::WorkspaceLayout( + monitor_idx, + ws_idx, + *option, + )) + .is_err() + { + tracing::error!("could not send message to komorebi: WorkspaceLayout"); + } + } + } + KomorebiLayout::Monocle => { + if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() { + tracing::error!("could not send message to komorebi: ToggleMonocle"); + } + } + 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::Custom => {} + } + } + + fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) { + // paint custom icons for the layout + let size = Vec2::splat(font_id.size); + let (response, painter) = ui.allocate_painter(size, Sense::hover()); + let color = ctx.style().visuals.selection.stroke.color; + let stroke = Stroke::new(1.0, color); + let mut rect = response.rect; + let rounding = Rounding::same(rect.width() * 0.1); + rect = rect.shrink(stroke.width); + let c = rect.center(); + let r = rect.width() / 2.0; + painter.rect_stroke(rect, rounding, stroke); + + match self { + KomorebiLayout::Default(layout) => match layout { + komorebi_client::DefaultLayout::BSP => { + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c, c + vec2(r, 0.0)], stroke); + painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke); + } + komorebi_client::DefaultLayout::Columns => { + painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke); + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke); + } + komorebi_client::DefaultLayout::Rows => { + painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke); + painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke); + painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke); + } + komorebi_client::DefaultLayout::VerticalStack => { + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c, c + vec2(r, 0.0)], stroke); + } + komorebi_client::DefaultLayout::RightMainVerticalStack => { + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c - vec2(r, 0.0), c], stroke); + } + komorebi_client::DefaultLayout::HorizontalStack => { + painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke); + painter.line_segment([c, c + vec2(0.0, r)], stroke); + } + komorebi_client::DefaultLayout::UltrawideVerticalStack => { + painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke); + painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke); + painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke); + } + komorebi_client::DefaultLayout::Grid => { + painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke); + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + } + }, + KomorebiLayout::Monocle => {} + KomorebiLayout::Floating => { + let mut rect_left = response.rect; + rect_left.set_width(rect.width() * 0.5); + rect_left.set_height(rect.height() * 0.5); + let mut rect_right = rect_left; + rect_left = rect_left.translate(Vec2::new( + rect.width() * 0.1 + stroke.width, + rect.width() * 0.1 + stroke.width, + )); + rect_right = rect_right.translate(Vec2::new( + rect.width() * 0.35 + stroke.width, + rect.width() * 0.35 + stroke.width, + )); + painter.rect_filled(rect_left, rounding, color); + painter.rect_stroke(rect_right, rounding, stroke); + } + KomorebiLayout::Paused => { + let mut rect_left = response.rect; + rect_left.set_width(rect.width() * 0.25); + rect_left.set_height(rect.height() * 0.8); + let mut rect_right = rect_left; + rect_left = rect_left.translate(Vec2::new( + rect.width() * 0.2 + stroke.width, + rect.width() * 0.1 + stroke.width, + )); + rect_right = rect_right.translate(Vec2::new( + rect.width() * 0.55 + stroke.width, + rect.width() * 0.1 + stroke.width, + )); + painter.rect_filled(rect_left, rounding, color); + painter.rect_filled(rect_right, rounding, color); + } + KomorebiLayout::Custom => { + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke); + painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke); + } + } + } + + pub fn show( + &mut self, + ctx: &Context, + ui: &mut Ui, + render_config: &mut RenderConfig, + layout_config: &KomorebiLayoutConfig, + workspace_idx: Option, + ) { + let monitor_idx = render_config.monitor_idx; + let font_id = ctx + .style() + .text_styles + .get(&TextStyle::Body) + .cloned() + .unwrap_or_else(FontId::default); + + let mut show_options = RenderConfig::load_show_komorebi_layout_options(); + let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText); + + if !self.is_default() { + show_options = false; + } + + render_config.apply_on_widget(false, ui, |ui| { + let layout_frame = SelectableFrame::new(false) + .show(ui, |ui| { + if let DisplayFormat::Icon | DisplayFormat::IconAndText = format { + self.show_icon(font_id.clone(), ctx, ui); + } + + if let DisplayFormat::Text | DisplayFormat::IconAndText = format { + ui.add(Label::new(self.to_string()).selectable(false)); + } + }) + .on_hover_text(self.to_string()); + + if layout_frame.clicked() { + show_options = self.on_click(&show_options, monitor_idx, workspace_idx); + } + + if show_options { + if let Some(workspace_idx) = workspace_idx { + Frame::none().show(ui, |ui| { + ui.add( + Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string()) + .selectable(false), + ); + + let mut layout_options = layout_config.options.clone().unwrap_or(vec![ + KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP), + KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns), + KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows), + KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack), + KomorebiLayout::Default( + komorebi_client::DefaultLayout::RightMainVerticalStack, + ), + KomorebiLayout::Default( + komorebi_client::DefaultLayout::HorizontalStack, + ), + KomorebiLayout::Default( + komorebi_client::DefaultLayout::UltrawideVerticalStack, + ), + KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid), + //KomorebiLayout::Custom, + KomorebiLayout::Monocle, + KomorebiLayout::Floating, + KomorebiLayout::Paused, + ]); + + for layout_option in &mut layout_options { + if SelectableFrame::new(self == layout_option) + .show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui)) + .on_hover_text(match layout_option { + KomorebiLayout::Default(layout) => layout.to_string(), + KomorebiLayout::Monocle => "Toggle monocle".to_string(), + KomorebiLayout::Floating => "Toggle tiling".to_string(), + KomorebiLayout::Paused => "Toggle pause".to_string(), + KomorebiLayout::Custom => "Custom".to_string(), + }) + .clicked() + { + layout_option.on_click_option(monitor_idx, Some(workspace_idx)); + show_options = false; + }; + } + }); + } + } + }); + + RenderConfig::store_show_komorebi_layout_options(show_options); + } +} diff --git a/komorebi-bar/src/main.rs b/komorebi-bar/src/main.rs index 242af9bea..5c4afd85b 100644 --- a/komorebi-bar/src/main.rs +++ b/komorebi-bar/src/main.rs @@ -4,10 +4,12 @@ mod config; mod cpu; mod date; mod komorebi; +mod komorebi_layout; mod media; mod memory; mod network; mod render; +mod selected_frame; mod storage; mod time; mod ui; diff --git a/komorebi-bar/src/render.rs b/komorebi-bar/src/render.rs index c8d8c4076..b3708502b 100644 --- a/komorebi-bar/src/render.rs +++ b/komorebi-bar/src/render.rs @@ -11,6 +11,10 @@ use eframe::egui::Vec2; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0); #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind")] @@ -27,6 +31,8 @@ pub enum Grouping { #[derive(Copy, Clone)] pub struct RenderConfig { + /// Komorebi monitor index of the monitor on which to render the bar + pub monitor_idx: usize, /// Spacing between widgets pub spacing: f32, /// Sets how widgets are grouped @@ -48,6 +54,7 @@ pub trait RenderExt { impl RenderExt for &KomobarConfig { fn new_renderconfig(&self, background_color: Color32) -> RenderConfig { RenderConfig { + monitor_idx: self.monitor.index, spacing: self.widget_spacing.unwrap_or(10.0), grouping: self.grouping.unwrap_or(Grouping::None), background_color, @@ -59,8 +66,17 @@ impl RenderExt for &KomobarConfig { } impl RenderConfig { + pub fn load_show_komorebi_layout_options() -> bool { + SHOW_KOMOREBI_LAYOUT_OPTIONS.load(Ordering::SeqCst) != 0 + } + + pub fn store_show_komorebi_layout_options(show: bool) { + SHOW_KOMOREBI_LAYOUT_OPTIONS.store(show as usize, Ordering::SeqCst); + } + pub fn new() -> Self { Self { + monitor_idx: 0, spacing: 0.0, grouping: Grouping::None, background_color: Color32::BLACK, diff --git a/komorebi-bar/src/selected_frame.rs b/komorebi-bar/src/selected_frame.rs new file mode 100644 index 000000000..5e893d71d --- /dev/null +++ b/komorebi-bar/src/selected_frame.rs @@ -0,0 +1,55 @@ +use eframe::egui::Frame; +use eframe::egui::Margin; +use eframe::egui::Response; +use eframe::egui::Sense; +use eframe::egui::Ui; + +/// Same as SelectableLabel, but supports all content +pub struct SelectableFrame { + selected: bool, +} + +impl SelectableFrame { + pub fn new(selected: bool) -> Self { + Self { selected } + } + + pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response { + let Self { selected } = self; + + Frame::none() + .show(ui, |ui| { + let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click()); + + if ui.is_rect_visible(response.rect) { + let inner_margin = Margin::symmetric( + ui.style().spacing.button_padding.x, + ui.style().spacing.button_padding.y, + ); + + if selected + || response.hovered() + || response.highlighted() + || response.has_focus() + { + let visuals = ui.style().interact_selectable(&response, selected); + + Frame::none() + .stroke(visuals.bg_stroke) + .rounding(visuals.rounding) + .fill(visuals.bg_fill) + .inner_margin(inner_margin) + .show(ui, add_contents); + } else { + Frame::none() + .inner_margin(inner_margin) + .show(ui, add_contents); + } + } + + response + }) + .inner + .on_hover_cursor(eframe::egui::CursorIcon::PointingHand) + } +}