Skip to content

Commit

Permalink
feat: support customising the behaviour of the workspaces status bar …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
sminez authored Aug 23, 2024
1 parent d5bb36a commit ab9eeb6
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 61 deletions.
2 changes: 1 addition & 1 deletion crates/penrose_ui/src/bar/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<X>
Expand Down
245 changes: 185 additions & 60 deletions crates/penrose_ui/src/bar/widgets/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<X>(
&mut self,
workspace_meta: &[WsMeta],
focused_tags: &[String],
state: &State<X>,
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<Color>, empty_fg: impl Into<Color>) -> 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<X: XConn>(state: &State<X>) -> Vec<Self> {
/// 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<X>(state: &State<X>) -> Vec<Self>
where
X: XConn,
{
state
.client_set
.ordered_workspaces()
Expand All @@ -40,7 +160,10 @@ impl From<&ClientSpace> for WsMeta {
}
}

fn focused_workspaces<X: XConn>(state: &State<X>) -> Vec<String> {
fn focused_workspaces<X>(state: &State<X>) -> Vec<String>
where
X: XConn,
{
let mut indexed_screens: Vec<(usize, String)> = state
.client_set
.screens()
Expand All @@ -52,61 +175,73 @@ fn focused_workspaces<X: XConn>(state: &State<X>) -> Vec<String> {
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<DefaultUi>;

impl Workspaces {
/// Construct a new [WorkspacesWidget] using the [DefaultUi].
pub fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> 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<U>
where
U: WorkspacesUi,
{
workspaces: Vec<WsMeta>,
focused_ws: Vec<String>, // 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<Color>, empty_fg: impl Into<Color>) -> Self {
impl<U> WorkspacesWidget<U>
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<X: XConn>(&mut self, state: &State<X>) {
let wss = WsMeta::from_state(state);
fn update_from_state<X>(&mut self, state: &State<X>, 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
Expand All @@ -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<X: XConn> Widget<X> for Workspaces {
impl<X, U> Widget<X> for WorkspacesWidget<U>
where
X: XConn,
U: WorkspacesUi,
{
fn draw(
&mut self,
ctx: &mut Context<'_>,
Expand All @@ -157,14 +282,14 @@ impl<X: XConn> Widget<X> 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) = <Self as Widget<X>>::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);
}

Expand All @@ -180,7 +305,7 @@ impl<X: XConn> Widget<X> 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);
Expand All @@ -202,14 +327,14 @@ impl<X: XConn> Widget<X> for Workspaces {
self.require_draw
}

fn on_startup(&mut self, state: &mut State<X>, _: &X) -> Result<()> {
self.update_from_state(state);
fn on_startup(&mut self, state: &mut State<X>, x: &X) -> Result<()> {
self.update_from_state(state, x);

Ok(())
}

fn on_refresh(&mut self, state: &mut State<X>, _: &X) -> Result<()> {
self.update_from_state(state);
fn on_refresh(&mut self, state: &mut State<X>, x: &X) -> Result<()> {
self.update_from_state(state, x);

Ok(())
}
Expand Down

0 comments on commit ab9eeb6

Please sign in to comment.