penrose_ui/bar/widgets/
mod.rs

1//! Self rendering building blocks for text based UI elements
2use crate::{Context, Result, TextStyle, bar::schedule::UpdateSchedule};
3use penrose::{
4    Color, WinId,
5    core::State,
6    pure::geometry::Rect,
7    x::{XConn, XEvent},
8};
9use std::{
10    fmt,
11    sync::{Arc, Mutex, MutexGuard},
12    time::Duration,
13};
14
15pub mod debug;
16pub mod sys;
17
18mod simple;
19mod workspaces;
20
21pub use simple::{ActiveWindowName, CurrentLayout, RootWindowName};
22pub use workspaces::{DefaultUi, FocusState, Workspaces, WorkspacesUi, WorkspacesWidget, WsMeta};
23
24/// A status bar widget that can be rendered using a [Context]
25pub trait Widget<X>
26where
27    X: XConn,
28{
29    /// Render the current state of the widget to the status bar window.
30    fn draw(
31        &mut self,
32        ctx: &mut Context<'_>,
33        screen: usize,
34        screen_has_focus: bool,
35        w: u32,
36        h: u32,
37    ) -> Result<()>;
38
39    /// Current required width and height for this widget due to its content
40    fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)>;
41
42    /// Does this widget currently require re-rendering? (should be reset to false when 'draw' is called)
43    fn require_draw(&self) -> bool;
44
45    /// If true, this widget will expand to fill remaining available space after layout has been
46    /// computed. If multiple greedy widgets are present in a given StatusBar then the available
47    /// space will be split evenly between all widgets.
48    fn is_greedy(&self) -> bool;
49
50    /// An [UpdateSchedule] to allow for external updates to this Widget's state independently of
51    /// the window manager event loop.
52    fn update_schedule(&mut self) -> Option<UpdateSchedule> {
53        None
54    }
55
56    #[allow(unused_variables)]
57    /// A startup hook to be run in order to initialise this Widget
58    fn on_startup(&mut self, state: &mut State<X>, x: &mut X) -> Result<()> {
59        Ok(())
60    }
61
62    #[allow(unused_variables)]
63    /// An event hook to be run in order to update this Widget
64    fn on_event(&mut self, event: &XEvent, state: &mut State<X>, x: &mut X) -> Result<()> {
65        Ok(())
66    }
67
68    #[allow(unused_variables)]
69    /// A refresh hook to be run in order to update this Widget
70    fn on_refresh(&mut self, state: &mut State<X>, x: &mut X) -> Result<()> {
71        Ok(())
72    }
73
74    #[allow(unused_variables)]
75    /// A manage hook to be run in order to update this Widget
76    fn on_new_client(&mut self, id: WinId, state: &mut State<X>, x: &mut X) -> Result<()> {
77        Ok(())
78    }
79}
80
81/// A simple piece of static text with an optional background color.
82///
83/// Can be used as a simple static element in a status bar or as an inner element for rendering
84/// more complex text based widgets.
85#[derive(Clone, Debug, PartialEq)]
86pub struct Text {
87    txt: String,
88    fg: Color,
89    bg: Option<Color>,
90    padding: (u32, u32),
91    is_greedy: bool,
92    right_justified: bool,
93    extent: Option<(u32, u32)>,
94    require_draw: bool,
95}
96
97impl Text {
98    /// Construct a new [Text]
99    pub fn new(
100        txt: impl Into<String>,
101        style: TextStyle,
102        is_greedy: bool,
103        right_justified: bool,
104    ) -> Self {
105        Self {
106            txt: txt.into(),
107            fg: style.fg,
108            bg: style.bg,
109            padding: style.padding,
110            is_greedy,
111            right_justified,
112            extent: None,
113            require_draw: true,
114        }
115    }
116
117    /// Borrow the current contents of the widget.
118    pub fn get_text(&self) -> &String {
119        &self.txt
120    }
121
122    /// Mutably borrow the current contents of the widget.
123    pub fn get_text_mut(&mut self) -> &mut String {
124        &mut self.txt
125    }
126
127    /// Set the rendered text and trigger a redraw
128    pub fn set_text(&mut self, txt: impl Into<String>) {
129        let new_text = txt.into();
130        if self.txt != new_text {
131            self.txt = new_text;
132            self.extent = None;
133            self.require_draw = true;
134        }
135    }
136}
137
138impl<X: XConn> Widget<X> for Text {
139    fn draw(&mut self, ctx: &mut Context<'_>, _: usize, _: bool, w: u32, h: u32) -> Result<()> {
140        if let Some(color) = self.bg {
141            ctx.fill_rect(Rect::new(0, 0, w, h), color)?;
142        }
143
144        let (ew, eh) = <Self as Widget<X>>::current_extent(self, ctx, h)?;
145        let offset = w as i32 - ew as i32;
146        let right_justify = self.right_justified && self.is_greedy && offset > 0;
147        if right_justify {
148            ctx.translate(offset, 0);
149            ctx.draw_text(&self.txt, h - eh, self.padding, self.fg)?;
150            ctx.translate(-offset, 0);
151        } else {
152            ctx.draw_text(&self.txt, h - eh, self.padding, self.fg)?;
153        }
154
155        self.require_draw = false;
156
157        Ok(())
158    }
159
160    fn current_extent(&mut self, ctx: &mut Context<'_>, _h: u32) -> Result<(u32, u32)> {
161        match self.extent {
162            Some(extent) => Ok(extent),
163            None => {
164                let (l, r) = self.padding;
165                let (w, h) = ctx.text_extent(&self.txt)?;
166                let extent = (w + l + r, h);
167                self.extent = Some(extent);
168
169                Ok(extent)
170            }
171        }
172    }
173
174    fn require_draw(&self) -> bool {
175        self.require_draw
176    }
177
178    fn is_greedy(&self) -> bool {
179        self.is_greedy
180    }
181}
182
183/// A simple widget that does not care about window manager state.
184///
185/// On each refresh, the provided `get_text` function is called and the output is
186/// stored in a [Text] widget. Whenever the output changes, this widget will trigger
187/// a re-render of the status bar.
188///
189///
190/// ### A note on blocking code
191///
192/// Be aware that the `get_text` function you provide will be run on _every_ refresh
193/// of the internal window manager state, meaning that slow running functions will
194/// very quickly make your window manager sluggish and unresponsive! If you need to
195/// run logic that is slow or may take a variable amount of time (such as pulling
196/// data in over the network) then you will likely want to make use of the
197/// [`IntervalText`] struct instead.
198///
199/// # Example
200/// ```no_run
201/// use penrose::{util::spawn_for_output_with_args, Color};
202/// use penrose_ui::{bar::widgets::RefreshText, core::TextStyle};
203///
204/// // Use the pacman package manager to get a count of how many packages are
205/// // currently installed on the system.
206/// fn my_get_text() -> String {
207///     let n_packages = spawn_for_output_with_args("sh", &["-c", "pacman -Q | wc -l"])
208///         .unwrap_or_default()
209///         .trim()
210///         .to_string();
211///
212///     format!("#pacman packages: {n_packages}")
213/// }
214///
215/// let style = TextStyle {
216///     fg: 0xebdbb2ff.into(),
217///     bg: Some(0x282828ff.into()),
218///     padding: (2, 2),
219/// };
220///
221/// let my_widget = RefreshText::new(style, my_get_text);
222/// ```
223pub struct RefreshText {
224    inner: Text,
225    get_text: Box<dyn Fn() -> String>,
226}
227
228impl fmt::Debug for RefreshText {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        f.debug_struct("RefreshText")
231            .field("inner", &self.inner)
232            .finish()
233    }
234}
235
236impl RefreshText {
237    /// Construct a new [`RefreshText`] using the specified styling and a function for
238    /// generating the widget contents.
239    pub fn new<F>(style: TextStyle, get_text: F) -> Self
240    where
241        F: Fn() -> String + 'static,
242    {
243        Self {
244            inner: Text::new("", style, false, false),
245            get_text: Box::new(get_text),
246        }
247    }
248}
249
250impl<X: XConn> Widget<X> for RefreshText {
251    fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> {
252        Widget::<X>::draw(&mut self.inner, ctx, s, f, w, h)
253    }
254
255    fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> {
256        Widget::<X>::current_extent(&mut self.inner, ctx, h)
257    }
258
259    fn is_greedy(&self) -> bool {
260        Widget::<X>::is_greedy(&self.inner)
261    }
262
263    fn require_draw(&self) -> bool {
264        Widget::<X>::require_draw(&self.inner)
265    }
266
267    fn on_refresh(&mut self, _: &mut State<X>, _: &mut X) -> Result<()> {
268        let txt = (self.get_text)();
269
270        self.inner.set_text(txt);
271
272        Ok(())
273    }
274}
275
276/// A simple widget that does not care about window manager state and refreshes on a
277/// specified interval.
278///
279/// On the requested interval, the provided `get_text` function is called and the output is
280/// stored in a [`Text`] widget. Whenever the output changes, this widget will trigger
281/// a re-render of the status bar.
282///
283/// Unlike [`RefreshText`], your `get_text` function will only be run on the schedule you
284/// specify rather than every time the internal window manager state refreshes. This is
285/// useful for code that is slow to run such as network requests.
286///
287/// # Example
288/// ```no_run
289/// use penrose::{util::spawn_for_output_with_args, Color};
290/// use penrose_ui::{bar::widgets::IntervalText, core::TextStyle};
291/// use std::time::Duration;
292///
293/// // Make a curl request to wttr.in to fetch the current weather information
294/// // for our location.
295/// fn my_get_text() -> Option<String> {
296///     let s = spawn_for_output_with_args("curl", &["-s", "http://wttr.in?format=3"])
297///         .unwrap_or_default()
298///         .trim()
299///         .to_string();
300///
301///     Some(s)
302/// }
303///
304/// let style = TextStyle {
305///     fg: 0xebdbb2ff.into(),
306///     bg: Some(0x282828ff.into()),
307///     padding: (2, 2),
308/// };
309///
310///
311/// let my_widget = IntervalText::new(
312///     style,
313///     my_get_text,
314///     Duration::from_secs(60 * 5),
315///     true,
316///     true
317/// );
318/// ```
319pub struct IntervalText {
320    inner: Arc<Mutex<Text>>,
321    interval: Duration,
322    get_text: Option<Box<dyn Fn() -> Option<String> + Send + 'static>>,
323}
324
325impl fmt::Debug for IntervalText {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        f.debug_struct("RefreshText")
328            .field("inner", &self.inner)
329            .field("interval", &self.interval)
330            .finish()
331    }
332}
333
334impl IntervalText {
335    /// Construct a new [`IntervalText`] using the specified styling and a function for
336    /// generating the widget contents. The function for updating the widget contents
337    /// will be run in its own thread on the interval provided.
338    pub fn new<F>(
339        style: TextStyle,
340        get_text: F,
341        interval: Duration,
342        is_greedy: bool,
343        right_justified: bool,
344    ) -> Self
345    where
346        F: Fn() -> Option<String> + Send + 'static,
347    {
348        let inner = Arc::new(Mutex::new(Text::new("", style, is_greedy, right_justified)));
349
350        Self {
351            inner,
352            interval,
353            get_text: Some(Box::new(get_text)),
354        }
355    }
356
357    fn inner_guard(&self) -> MutexGuard<'_, Text> {
358        match self.inner.lock() {
359            Ok(inner) => inner,
360            Err(poisoned) => poisoned.into_inner(),
361        }
362    }
363}
364
365impl<X: XConn> Widget<X> for IntervalText {
366    fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> {
367        Widget::<X>::draw(&mut *self.inner_guard(), ctx, s, f, w, h)
368    }
369
370    fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> {
371        Widget::<X>::current_extent(&mut *self.inner_guard(), ctx, h)
372    }
373
374    fn is_greedy(&self) -> bool {
375        Widget::<X>::is_greedy(&*self.inner_guard())
376    }
377
378    fn require_draw(&self) -> bool {
379        Widget::<X>::require_draw(&*self.inner_guard())
380    }
381
382    fn update_schedule(&mut self) -> Option<UpdateSchedule> {
383        Some(UpdateSchedule::new(
384            self.interval,
385            self.get_text.take().unwrap(),
386            self.inner.clone(),
387        ))
388    }
389}