penrose_ui/bar/widgets/
workspaces.rs

1//! Widgets for the penrose status bar
2use crate::{
3    Result,
4    bar::widgets::Widget,
5    core::{Context, TextStyle},
6};
7use penrose::{
8    Color,
9    core::{ClientSpace, State},
10    pure::geometry::Rect,
11    x::XConn,
12};
13
14const PADDING: u32 = 3;
15
16/// The focus state of a given workspace being rendered within a [WorkspacesWidget].
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FocusState {
19    /// The workspace is not currently focused on any screen.
20    Unfocused,
21    /// The workspace is focused on the screen that the widget is rendered on.
22    FocusedOnThisScreen,
23    /// The workspace is focused on a screen that the widget is not rendered on.
24    FocusedOnOtherScreen,
25}
26
27impl FocusState {
28    /// Whether or not this workspace is focused on any active screen.
29    pub fn focused(&self) -> bool {
30        matches!(self, Self::FocusedOnOtherScreen | Self::FocusedOnThisScreen)
31    }
32}
33
34/// A UI implementation for the [WorkspacesWidget] widget.
35pub trait WorkspacesUi {
36    /// Update the UI properties of the parent [WorkspacesWidget] as part of startup and refresh
37    /// hooks.
38    ///
39    /// The boolean return of this method is used to indicate to the parent widget that a redraw
40    /// is now required. If state has not changed since the last time this method was called then
41    /// you should return `false` to reduce unnecessary rendering.
42    #[allow(unused_variables)]
43    fn update_from_state<X>(
44        &mut self,
45        workspace_meta: &[WsMeta],
46        focused_tags: &[String],
47        state: &State<X>,
48        x: &X,
49    ) -> bool
50    where
51        X: XConn,
52    {
53        false
54    }
55
56    /// The current UI tag string to be shown for a given workspace.
57    fn ui_tag(&self, workspace_meta: &WsMeta) -> String {
58        workspace_meta.tag.clone()
59    }
60
61    /// The background color to be used for the parent [WorkspacesWidget].
62    fn background_color(&self) -> Color;
63
64    /// The foreground and background color to be used for rendering a given workspace.
65    ///
66    /// The [FocusState] provided indicates the current state of the workspace itself, while
67    /// `screen_has_focus` is used to indicate whether or not the screen the parent
68    /// [WorkspacesWidget] is on is currently focused or not.
69    fn colors_for_workspace(
70        &self,
71        workspace_meta: &WsMeta,
72        focus_state: FocusState,
73        screen_has_focus: bool,
74    ) -> (Color, Color);
75}
76
77/// The default UI style of a [WorkspacesWidget].
78#[derive(Debug, Clone, PartialEq)]
79pub struct DefaultUi {
80    fg_1: Color,
81    fg_2: Color,
82    bg_1: Color,
83    bg_2: Color,
84}
85
86impl DefaultUi {
87    fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> Self {
88        Self {
89            fg_1: style.fg,
90            fg_2: empty_fg.into(),
91            bg_1: highlight.into(),
92            bg_2: style.bg.unwrap_or_else(|| 0x000000.into()),
93        }
94    }
95}
96
97impl WorkspacesUi for DefaultUi {
98    fn background_color(&self) -> Color {
99        self.bg_2
100    }
101
102    fn colors_for_workspace(
103        &self,
104        &WsMeta { occupied, .. }: &WsMeta,
105        focus_state: FocusState,
106        screen_has_focus: bool,
107    ) -> (Color, Color) {
108        use FocusState::*;
109
110        match focus_state {
111            FocusedOnThisScreen if screen_has_focus && occupied => (self.fg_1, self.bg_1),
112            FocusedOnThisScreen if screen_has_focus => (self.fg_2, self.bg_1),
113            FocusedOnThisScreen => (self.fg_1, self.fg_2),
114            FocusedOnOtherScreen => (self.bg_1, self.fg_2),
115            Unfocused if occupied => (self.fg_1, self.bg_2),
116            Unfocused => (self.fg_2, self.bg_2),
117        }
118    }
119}
120
121/// Metadata around the content of a particular workspace within the current
122/// window manager state.
123#[derive(Clone, Debug, PartialEq)]
124pub struct WsMeta {
125    tag: String,
126    occupied: bool,
127    extent: (u32, u32),
128}
129
130impl WsMeta {
131    /// The tag used by the window manager for this workspace
132    pub fn tag(&self) -> &str {
133        &self.tag
134    }
135
136    /// Whether or not this workspace currently contains any clients
137    pub fn occupied(&self) -> bool {
138        self.occupied
139    }
140
141    fn from_state<X>(state: &State<X>) -> Vec<Self>
142    where
143        X: XConn,
144    {
145        state
146            .client_set
147            .ordered_workspaces()
148            .map(WsMeta::from)
149            .collect()
150    }
151}
152
153impl From<&ClientSpace> for WsMeta {
154    fn from(w: &ClientSpace) -> Self {
155        Self {
156            tag: w.tag().to_owned(),
157            occupied: !w.is_empty(),
158            extent: (0, 0),
159        }
160    }
161}
162
163fn focused_workspaces<X>(state: &State<X>) -> Vec<String>
164where
165    X: XConn,
166{
167    let mut indexed_screens: Vec<(usize, String)> = state
168        .client_set
169        .screens()
170        .map(|s| (s.index(), s.workspace.tag().to_owned()))
171        .collect();
172
173    indexed_screens.sort_by_key(|(ix, _)| *ix);
174
175    indexed_screens.into_iter().map(|(_, tag)| tag).collect()
176}
177
178/// A simple workspace indicator for a status bar using a default UI and colorscheme
179pub type Workspaces = WorkspacesWidget<DefaultUi>;
180
181impl Workspaces {
182    /// Construct a new [WorkspacesWidget] using the [DefaultUi].
183    pub fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> Self {
184        WorkspacesWidget::new_with_ui(DefaultUi::new(style, highlight, empty_fg))
185    }
186}
187
188/// A simple workspace indicator for a status bar
189#[derive(Debug, Clone, PartialEq)]
190pub struct WorkspacesWidget<U>
191where
192    U: WorkspacesUi,
193{
194    workspaces: Vec<WsMeta>,
195    focused_ws: Vec<String>, // focused ws per screen
196    extent: Option<(u32, u32)>,
197    ui: U,
198    require_draw: bool,
199}
200
201impl<U> WorkspacesWidget<U>
202where
203    U: WorkspacesUi,
204{
205    /// Construct a new [WorkspacesWidget] with the specified [WorkspacesUi] implementation.
206    pub fn new_with_ui(ui: U) -> Self {
207        Self {
208            workspaces: Vec::new(),
209            focused_ws: Vec::new(), // set in startup hook
210            extent: None,
211            ui,
212            require_draw: true,
213        }
214    }
215
216    fn raw_tags(&self) -> Vec<&str> {
217        self.workspaces.iter().map(|w| w.tag.as_ref()).collect()
218    }
219
220    fn update_from_state<X>(&mut self, state: &State<X>, x: &X)
221    where
222        X: XConn,
223    {
224        let focused_ws = focused_workspaces(state);
225        let wss = WsMeta::from_state(state);
226
227        let ui_updated = self.ui.update_from_state(&wss, &focused_ws, state, x);
228        let tags_changed = self.tags_changed(&wss);
229
230        if ui_updated
231            || tags_changed
232            || self.focused_ws != focused_ws
233            || self.occupied_changed(&wss)
234        {
235            self.focused_ws = focused_ws;
236            self.workspaces = wss;
237            self.require_draw = true;
238            self.extent = None;
239        }
240    }
241
242    fn tags_changed(&self, workspaces: &[WsMeta]) -> bool {
243        let new_tags: Vec<&str> = workspaces.iter().map(|w| w.tag.as_ref()).collect();
244
245        self.raw_tags() != new_tags
246    }
247
248    // Called after tags_changed above so we assume that tags are matching
249    fn occupied_changed(&self, workspaces: &[WsMeta]) -> bool {
250        self.workspaces
251            .iter()
252            .zip(workspaces)
253            .any(|(l, r)| l.occupied != r.occupied)
254    }
255
256    fn ws_colors(&self, meta: &WsMeta, screen: usize, screen_has_focus: bool) -> (Color, Color) {
257        let focused = self.focused_ws.iter().any(|t| t == &meta.tag);
258        let focused_on_this_screen = match &self.focused_ws.get(screen) {
259            &Some(focused_tag) => &meta.tag == focused_tag,
260            None => false,
261        };
262
263        let state = match (focused, focused_on_this_screen) {
264            (false, _) => FocusState::Unfocused,
265            (_, true) => FocusState::FocusedOnThisScreen,
266            (true, false) => FocusState::FocusedOnOtherScreen,
267        };
268
269        self.ui.colors_for_workspace(meta, state, screen_has_focus)
270    }
271}
272
273impl<X, U> Widget<X> for WorkspacesWidget<U>
274where
275    X: XConn,
276    U: WorkspacesUi,
277{
278    fn draw(
279        &mut self,
280        ctx: &mut Context<'_>,
281        screen: usize,
282        screen_has_focus: bool,
283        w: u32,
284        h: u32,
285    ) -> Result<()> {
286        ctx.fill_rect(Rect::new(0, 0, w, h), self.ui.background_color())?;
287        ctx.translate(PADDING as i32, 0);
288        let (_, eh) = <Self as Widget<X>>::current_extent(self, ctx, h)?;
289
290        for ws in self.workspaces.iter() {
291            let (fg, bg) = self.ws_colors(ws, screen, screen_has_focus);
292            ctx.fill_rect(Rect::new(0, 0, ws.extent.0, h), bg)?;
293            ctx.draw_text(&self.ui.ui_tag(ws), h - eh, (PADDING, PADDING), fg)?;
294            ctx.translate(ws.extent.0 as i32, 0);
295        }
296
297        self.require_draw = false;
298
299        Ok(())
300    }
301
302    fn current_extent(&mut self, ctx: &mut Context<'_>, _h: u32) -> Result<(u32, u32)> {
303        match self.extent {
304            Some(extent) => Ok(extent),
305            None => {
306                let mut total = 0;
307                let mut h_max = 0;
308                for ws in self.workspaces.iter_mut() {
309                    let (w, h) = ctx.text_extent(&self.ui.ui_tag(ws))?;
310                    total += w + 2 * PADDING;
311                    h_max = if h > h_max { h } else { h_max };
312                    ws.extent = (w + 2 * PADDING, h);
313                }
314
315                let ext = (total + PADDING, h_max);
316                self.extent = Some(ext);
317
318                Ok(ext)
319            }
320        }
321    }
322
323    fn is_greedy(&self) -> bool {
324        false
325    }
326
327    fn require_draw(&self) -> bool {
328        self.require_draw
329    }
330
331    fn on_startup(&mut self, state: &mut State<X>, x: &mut X) -> Result<()> {
332        self.update_from_state(state, x);
333
334        Ok(())
335    }
336
337    fn on_refresh(&mut self, state: &mut State<X>, x: &mut X) -> Result<()> {
338        self.update_from_state(state, x);
339
340        Ok(())
341    }
342}