From ab9eeb6e8256a45e1a5f4bbe3af2eecc4993d518 Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Fri, 23 Aug 2024 13:46:29 +0100 Subject: [PATCH] feat: support customising the behaviour of the workspaces status bar widget (#309) * feat: allowing users to customise the UI of a workspaces status bar widget * fix: adding missing export for WorkspacesWidget * feat(penrose-ui): exposing a new_with_ui method for WorkspacesWidget * adding access to XConn inside of update_from_state --- crates/penrose_ui/src/bar/widgets/mod.rs | 2 +- .../penrose_ui/src/bar/widgets/workspaces.rs | 245 +++++++++++++----- 2 files changed, 186 insertions(+), 61 deletions(-) diff --git a/crates/penrose_ui/src/bar/widgets/mod.rs b/crates/penrose_ui/src/bar/widgets/mod.rs index 4ad6a27c..f0e16683 100644 --- a/crates/penrose_ui/src/bar/widgets/mod.rs +++ b/crates/penrose_ui/src/bar/widgets/mod.rs @@ -19,7 +19,7 @@ mod simple; mod workspaces; pub use simple::{ActiveWindowName, CurrentLayout, RootWindowName}; -pub use workspaces::Workspaces; +pub use workspaces::{DefaultUi, FocusState, Workspaces, WorkspacesUi, WorkspacesWidget, WsMeta}; /// A status bar widget that can be rendered using a [Context] pub trait Widget diff --git a/crates/penrose_ui/src/bar/widgets/workspaces.rs b/crates/penrose_ui/src/bar/widgets/workspaces.rs index d378eac6..31903331 100644 --- a/crates/penrose_ui/src/bar/widgets/workspaces.rs +++ b/crates/penrose_ui/src/bar/widgets/workspaces.rs @@ -13,15 +13,135 @@ use penrose::{ const PADDING: u32 = 3; +/// The focus state of a given workspace being rendered within a [WorkspacesWidget]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FocusState { + /// The workspace is not currently focused on any screen. + Unfocused, + /// The workspace is focused on the screen that the widget is rendered on. + FocusedOnThisScreen, + /// The workspace is focused on a screen that the widget is not rendered on. + FocusedOnOtherScreen, +} + +impl FocusState { + /// Whether or not this workspace is focused on any active screen. + pub fn focused(&self) -> bool { + matches!(self, Self::FocusedOnOtherScreen | Self::FocusedOnThisScreen) + } +} + +/// A UI implementation for the [WorkspacesWidget] widget. +pub trait WorkspacesUi { + /// Update the UI properties of the parent [WorkspacesWidget] as part of startup and refresh + /// hooks. + /// + /// The boolean return of this method is used to indicate to the parent widget that a redraw + /// is now required. If state has not changed since the last time this method was called then + /// you should return `false` to reduce unnecessary rendering. + #[allow(unused_variables)] + fn update_from_state( + &mut self, + workspace_meta: &[WsMeta], + focused_tags: &[String], + state: &State, + x: &X, + ) -> bool + where + X: XConn, + { + false + } + + /// The current UI tag string to be shown for a given workspace. + fn ui_tag(&self, workspace_meta: &WsMeta) -> String { + workspace_meta.tag.clone() + } + + /// The background color to be used for the parent [WorkspacesWidget]. + fn background_color(&self) -> Color; + + /// The foreground and background color to be used for rendering a given workspace. + /// + /// The [FocusState] provided indicates the current state of the workspace itself, while + /// `screen_has_focus` is used to indicate whether or not the screen the parent + /// [WorkspacesWidget] is on is currently focused or not. + fn colors_for_workspace( + &self, + workspace_meta: &WsMeta, + focus_state: FocusState, + screen_has_focus: bool, + ) -> (Color, Color); +} + +/// The default UI style of a [WorkspacesWidget]. +#[derive(Debug, Clone, PartialEq)] +pub struct DefaultUi { + fg_1: Color, + fg_2: Color, + bg_1: Color, + bg_2: Color, +} + +impl DefaultUi { + fn new(style: TextStyle, highlight: impl Into, empty_fg: impl Into) -> Self { + Self { + fg_1: style.fg, + fg_2: empty_fg.into(), + bg_1: highlight.into(), + bg_2: style.bg.unwrap_or_else(|| 0x000000.into()), + } + } +} + +impl WorkspacesUi for DefaultUi { + fn background_color(&self) -> Color { + self.bg_2 + } + + fn colors_for_workspace( + &self, + &WsMeta { occupied, .. }: &WsMeta, + focus_state: FocusState, + screen_has_focus: bool, + ) -> (Color, Color) { + use FocusState::*; + + match focus_state { + FocusedOnThisScreen if screen_has_focus && occupied => (self.fg_1, self.bg_1), + FocusedOnThisScreen if screen_has_focus => (self.fg_2, self.bg_1), + FocusedOnThisScreen => (self.fg_1, self.fg_2), + FocusedOnOtherScreen => (self.bg_1, self.fg_2), + Unfocused if occupied => (self.fg_1, self.bg_2), + Unfocused => (self.fg_2, self.bg_2), + } + } +} + +/// Metadata around the content of a particular workspace within the current +/// window manager state. #[derive(Clone, Debug, PartialEq)] -struct WsMeta { +pub struct WsMeta { tag: String, occupied: bool, extent: (u32, u32), } impl WsMeta { - fn from_state(state: &State) -> Vec { + /// The tag used by the window manager for this workspace + pub fn tag(&self) -> &str { + &self.tag + } + + /// Whether or not this workspace currently contains any clients + pub fn occupied(&self) -> bool { + self.occupied + } + + fn from_state(state: &State) -> Vec + where + X: XConn, + { state .client_set .ordered_workspaces() @@ -40,7 +160,10 @@ impl From<&ClientSpace> for WsMeta { } } -fn focused_workspaces(state: &State) -> Vec { +fn focused_workspaces(state: &State) -> Vec +where + X: XConn, +{ let mut indexed_screens: Vec<(usize, String)> = state .client_set .screens() @@ -52,61 +175,73 @@ fn focused_workspaces(state: &State) -> Vec { indexed_screens.into_iter().map(|(_, tag)| tag).collect() } +/// A simple workspace indicator for a status bar using a default UI and colorscheme +pub type Workspaces = WorkspacesWidget; + +impl Workspaces { + /// Construct a new [WorkspacesWidget] using the [DefaultUi]. + pub fn new(style: TextStyle, highlight: impl Into, empty_fg: impl Into) -> Self { + WorkspacesWidget::new_with_ui(DefaultUi::new(style, highlight, empty_fg)) + } +} + /// A simple workspace indicator for a status bar -#[derive(Clone, Debug, PartialEq)] -pub struct Workspaces { +#[derive(Debug, Clone, PartialEq)] +pub struct WorkspacesWidget +where + U: WorkspacesUi, +{ workspaces: Vec, focused_ws: Vec, // focused ws per screen extent: Option<(u32, u32)>, - fg_1: Color, - fg_2: Color, - bg_1: Color, - bg_2: Color, + ui: U, require_draw: bool, } -impl Workspaces { - /// Construct a new WorkspaceWidget - pub fn new(style: TextStyle, highlight: impl Into, empty_fg: impl Into) -> Self { +impl WorkspacesWidget +where + U: WorkspacesUi, +{ + /// Construct a new [WorkspacesWidget] with the specified [WorkspacesUi] implementation. + pub fn new_with_ui(ui: U) -> Self { Self { - workspaces: vec![], - focused_ws: vec![], // set in startup hook + workspaces: Vec::new(), + focused_ws: Vec::new(), // set in startup hook extent: None, - fg_1: style.fg, - fg_2: empty_fg.into(), - bg_1: highlight.into(), - bg_2: style.bg.unwrap_or_else(|| 0x000000.into()), + ui, require_draw: true, } } - fn tags(&self) -> Vec<&str> { + fn raw_tags(&self) -> Vec<&str> { self.workspaces.iter().map(|w| w.tag.as_ref()).collect() } - fn update_from_state(&mut self, state: &State) { - let wss = WsMeta::from_state(state); + fn update_from_state(&mut self, state: &State, x: &X) + where + X: XConn, + { let focused_ws = focused_workspaces(state); + let wss = WsMeta::from_state(state); + let ui_updated = self.ui.update_from_state(&wss, &focused_ws, state, x); let tags_changed = self.tags_changed(&wss); - if tags_changed { - self.extent = None; + if ui_updated || tags_changed { self.require_draw = true; - } - - if self.occupied_changed(&wss) || self.focused_ws != focused_ws { + self.extent = None; + } else if self.focused_ws != focused_ws || self.occupied_changed(&wss) { self.require_draw = true; } - self.workspaces = wss; self.focused_ws = focused_ws; + self.workspaces = wss; } fn tags_changed(&self, workspaces: &[WsMeta]) -> bool { let new_tags: Vec<&str> = workspaces.iter().map(|w| w.tag.as_ref()).collect(); - self.tags() == new_tags + self.raw_tags() == new_tags } // Called after tags_changed above so we assume that tags are matching @@ -117,38 +252,28 @@ impl Workspaces { .any(|(l, r)| l.occupied != r.occupied) } - fn ws_colors( - &self, - tag: &str, - screen: usize, - screen_has_focus: bool, - occupied: bool, - ) -> (Color, Color) { + fn ws_colors(&self, meta: &WsMeta, screen: usize, screen_has_focus: bool) -> (Color, Color) { + let focused = self.focused_ws.iter().any(|t| t == &meta.tag); let focused_on_this_screen = match &self.focused_ws.get(screen) { - &Some(focused_tag) => tag == focused_tag, + &Some(focused_tag) => &meta.tag == focused_tag, None => false, }; - let focused = self.focused_ws.iter().any(|t| t == tag); - let focused_other = focused && !focused_on_this_screen; - - if focused_on_this_screen && screen_has_focus { - let fg = if occupied { self.fg_1 } else { self.fg_2 }; - - (fg, self.bg_1) - } else if focused { - let fg = if focused_other { self.bg_1 } else { self.fg_1 }; - - (fg, self.fg_2) - } else { - let fg = if occupied { self.fg_1 } else { self.fg_2 }; + let state = match (focused, focused_on_this_screen) { + (false, _) => FocusState::Unfocused, + (_, true) => FocusState::FocusedOnThisScreen, + (true, false) => FocusState::FocusedOnOtherScreen, + }; - (fg, self.bg_2) - } + self.ui.colors_for_workspace(meta, state, screen_has_focus) } } -impl Widget for Workspaces { +impl Widget for WorkspacesWidget +where + X: XConn, + U: WorkspacesUi, +{ fn draw( &mut self, ctx: &mut Context<'_>, @@ -157,14 +282,14 @@ impl Widget for Workspaces { w: u32, h: u32, ) -> Result<()> { - ctx.fill_rect(Rect::new(0, 0, w, h), self.bg_2)?; + ctx.fill_rect(Rect::new(0, 0, w, h), self.ui.background_color())?; ctx.translate(PADDING as i32, 0); let (_, eh) = >::current_extent(self, ctx, h)?; for ws in self.workspaces.iter() { - let (fg, bg) = self.ws_colors(&ws.tag, screen, screen_has_focus, ws.occupied); + let (fg, bg) = self.ws_colors(ws, screen, screen_has_focus); ctx.fill_rect(Rect::new(0, 0, ws.extent.0, h), bg)?; - ctx.draw_text(&ws.tag, h - eh, (PADDING, PADDING), fg)?; + ctx.draw_text(&self.ui.ui_tag(ws), h - eh, (PADDING, PADDING), fg)?; ctx.translate(ws.extent.0 as i32, 0); } @@ -180,7 +305,7 @@ impl Widget for Workspaces { let mut total = 0; let mut h_max = 0; for ws in self.workspaces.iter_mut() { - let (w, h) = ctx.text_extent(&ws.tag)?; + let (w, h) = ctx.text_extent(&self.ui.ui_tag(ws))?; total += w + 2 * PADDING; h_max = if h > h_max { h } else { h_max }; ws.extent = (w + 2 * PADDING, h); @@ -202,14 +327,14 @@ impl Widget for Workspaces { self.require_draw } - fn on_startup(&mut self, state: &mut State, _: &X) -> Result<()> { - self.update_from_state(state); + fn on_startup(&mut self, state: &mut State, x: &X) -> Result<()> { + self.update_from_state(state, x); Ok(()) } - fn on_refresh(&mut self, state: &mut State, _: &X) -> Result<()> { - self.update_from_state(state); + fn on_refresh(&mut self, state: &mut State, x: &X) -> Result<()> { + self.update_from_state(state, x); Ok(()) }