penrose_ui/core/
mod.rs

1//! The core [`Draw`] and [`Context`] structs for rendering UI elements.
2//!
3//! If you are only interested in adding functionality to the penrose [StatusBar][0] then you
4//! do not need to worry about the use and implementation of `Draw` and `Context`: the abstractions
5//! provided by the [Widget][1] trait should be sufficient for your needs. If however you wish
6//! to build your own minimal text based UI from scratch then your might find these structs useful.
7//!
8//! > **NOTE**: As mentioned in the crate level docs, this crate is definitely not intended as a
9//! > fully general purpose graphics API. You are unlikely to find full support for operations that
10//! > are not required for implementing a simple text based status bar.
11//!
12//!   [0]: crate::StatusBar
13//!   [1]: crate::bar::widgets::Widget
14use crate::{Error, Result};
15use penrose::{
16    Color, WinId,
17    pure::geometry::{Point, Rect},
18    x::{WinType, XConn},
19    x11rb::RustConn,
20};
21use std::{
22    alloc::{Layout, alloc, dealloc, handle_alloc_error},
23    cmp::max,
24    collections::{HashMap, hash_map::Entry},
25    ffi::CString,
26};
27use tracing::{debug, info};
28use x11::{
29    xft::{XftColor, XftColorAllocName, XftDraw, XftDrawCreate, XftDrawDestroy, XftDrawStringUtf8},
30    xlib::{
31        CapButt, Complex, CoordModeOrigin, Display, Drawable, False, GC, JoinMiter, LineSolid,
32        Window, XCopyArea, XCreateGC, XCreatePixmap, XDefaultColormap, XDefaultDepth,
33        XDefaultVisual, XDrawRectangle, XFillPolygon, XFillRectangle, XFreeGC, XFreePixmap,
34        XOpenDisplay, XPoint, XSetForeground, XSetGraphicsExposures, XSetLineAttributes, XSync,
35    },
36};
37
38mod fontset;
39use fontset::Fontset;
40
41// Xlib manual: https://www.x.org/releases/current/doc/libX11/libX11/libX11.pdf
42
43pub(crate) const SCREEN: i32 = 0;
44
45#[derive(Debug, Copy, Clone, PartialEq, Eq)]
46/// A set of styling options for a text string that is to be rendered using [Draw].
47///
48/// The font itself is specified on the [Draw] instance when it is created or by using the
49/// `set_font` method.
50pub struct TextStyle {
51    /// The foreground color to be used for rendering the text itself.
52    pub fg: Color,
53    /// The background color for the region behind the text (defaults to the [Draw] background if None).
54    pub bg: Option<Color>,
55    /// Padding in pixels around the text to the left and right.
56    pub padding: (u32, u32),
57}
58
59#[derive(Debug)]
60struct Surface {
61    drawable: Drawable,
62    gc: GC,
63    r: Rect,
64    id: u64,
65}
66
67impl Surface {
68    /// SAFETY: dpy must be non-null
69    pub(crate) unsafe fn flush(&self, dpy: *mut Display) {
70        let Rect { w, h, .. } = self.r;
71        // SAFETY: dpy must be non-null
72        unsafe {
73            XCopyArea(dpy, self.drawable, self.id, self.gc, 0, 0, w, h, 0, 0);
74            XSync(dpy, False);
75        }
76    }
77}
78
79/// A minimal back end for rendering simple text based UIs.
80///
81/// > **NOTE**: Your application should create a single [Draw] struct to manage the windows and
82/// > surfaces it needs to render your UI. See the [Context] struct for how to draw to the surfaces
83/// > you have created.
84///
85/// # Fonts
86/// ### Specifying fonts
87/// Font names need to be in a form that can be parsed by `xft`. The simplest way to find the valid
88/// font names on your system is via the `fc-list` program like so:
89/// ```sh
90/// $ fc-list -f '%{family}\n' | sort -u
91/// ```
92/// [Draw] will automtically append `:size={point_size}` to the font name when loading the font via
93/// xft. The [Arch wiki page on fonts][0] is a useful resource on how X11 fonts work if you are
94/// interested in futher reading.
95///
96/// ### Font fallback for missing glyphs
97/// [Draw] makes use of [fontconfig][1] to locate appropriate fallback fonts on your system when a
98/// glyph is encountered that the primary font does not support. If you wish to modify how fallback
99/// fonts are selected you will need to modify your [font-conf][2] (the Arch wiki has a [good page][3]
100/// on how to do this if you are looking for a reference).
101///
102/// # Example usage
103/// > Please see the crate [examples directory][4] for more examples.
104/// ```no_run
105/// use penrose::{
106///     pure::geometry::Rect,
107///     x::{Atom, WinType},
108///     Color,
109/// };
110/// use penrose_ui::Draw;
111/// use std::{thread::sleep, time::Duration};
112///
113/// let fg = Color::try_from("#EBDBB2").unwrap();
114/// let bg = Color::try_from("#282828").unwrap();
115/// let mut drw = Draw::new("mono", 12, bg).unwrap();
116/// let w = drw.new_window(
117///     WinType::InputOutput(Atom::NetWindowTypeDock),
118///     Rect::new(0, 0, 300, 50),
119///     false,
120/// ).unwrap();
121///
122/// let mut ctx = drw.context_for(w).unwrap();
123/// ctx.draw_text("Hello from penrose_ui!", 0, (10, 0), fg).unwrap();
124/// ctx.flush();
125/// drw.flush(w).unwrap();
126///
127/// sleep(Duration::from_secs(2));
128/// ```
129///
130///  [0]: https://wiki.archlinux.org/title/Fonts
131///  [1]: https://www.freedesktop.org/wiki/Software/fontconfig/
132///  [2]: https://man.archlinux.org/man/fonts-conf.5
133///  [3]: https://wiki.archlinux.org/title/Font_configuration#Set_default_or_fallback_fonts
134///  [4]: https://github.com/sminez/penrose/tree/develop/crates/penrose_ui/examples
135#[derive(Debug)]
136pub struct Draw {
137    pub(crate) conn: RustConn,
138    dpy: *mut Display,
139    fss: HashMap<String, Fontset>,
140    bg: Color,
141    surfaces: HashMap<WinId, Surface>,
142    colors: HashMap<Color, XColor>,
143    active_font: String,
144}
145
146impl Drop for Draw {
147    fn drop(&mut self) {
148        // SAFETY: all pointers being freed are known to be non-null
149        unsafe {
150            for (_, s) in self.surfaces.drain() {
151                XFreePixmap(self.dpy, s.drawable);
152                XFreeGC(self.dpy, s.gc);
153            }
154        }
155    }
156}
157
158fn font_key(font: &str, point_size: u8) -> String {
159    format!("{font}:size={point_size}")
160}
161
162impl Draw {
163    /// Construct a new [Draw] instance using the specified font and background color.
164    ///
165    /// ### Font names
166    /// See the top level docs for [Draw] for details on how fonts are specified.
167    ///
168    /// ### Errors
169    /// This method will error if it is unable to establish a connection with the X server.
170    pub fn new(font: &str, point_size: u8, bg: impl Into<Color>) -> Result<Self> {
171        let conn = RustConn::new()?;
172        // SAFETY:
173        //   - passing NULL as the argument here is valid as documented here: https://man.archlinux.org/man/extra/libx11/XOpenDisplay.3.en
174        let dpy = unsafe { XOpenDisplay(std::ptr::null()) };
175        let mut colors = HashMap::new();
176        let bg = bg.into();
177        colors.insert(bg, XColor::try_new(dpy, &bg)?);
178
179        let k = font_key(font, point_size);
180        let fs = Fontset::try_new(dpy, &k)?;
181        let mut fss = HashMap::new();
182        fss.insert(k.clone(), fs);
183
184        Ok(Self {
185            conn,
186            dpy,
187            fss,
188            surfaces: HashMap::new(),
189            bg,
190            colors,
191            active_font: k,
192        })
193    }
194
195    /// Get access to the underlying [XConn] used by this [Draw].
196    pub fn conn(&self) -> &impl XConn {
197        &self.conn
198    }
199
200    /// Create a new X window with an initialised surface for drawing.
201    ///
202    /// Destroying this window should be carried out using the `destroy_window_and_surface` method
203    /// so that the associated graphics state is also cleaned up correctly.
204    pub fn new_window(&mut self, ty: WinType, r: Rect, managed: bool) -> Result<WinId> {
205        info!(?ty, ?r, %managed, "creating new window");
206        let id = self.conn.create_window(ty, r, managed)?;
207
208        debug!("initialising graphics context and pixmap");
209        let root = *self.conn.root() as Window;
210        // SAFETY: self.dpy is non-null and screen index 0 is always valid
211        let (drawable, gc) = unsafe {
212            let depth = XDefaultDepth(self.dpy, SCREEN) as u32;
213            let drawable = XCreatePixmap(self.dpy, root, r.w, r.h, depth);
214            let gc = XCreateGC(self.dpy, root, 0, std::ptr::null_mut());
215            XSetLineAttributes(self.dpy, gc, 1, LineSolid, CapButt, JoinMiter);
216            XSetGraphicsExposures(self.dpy, gc, False);
217
218            (drawable, gc)
219        };
220
221        self.surfaces.insert(
222            id,
223            Surface {
224                id: *id as u64,
225                r,
226                gc,
227                drawable,
228            },
229        );
230
231        Ok(id)
232    }
233
234    /// Destroy the specified window along with any surface and graphics context state held
235    /// within this draw.
236    pub fn destroy_window_and_surface(&mut self, id: WinId) -> Result<()> {
237        if let Some(s) = self.surfaces.remove(&id) {
238            self.conn.destroy_window(id)?;
239            // SAFETY: the pointers being freed are known to be non-null
240            unsafe {
241                XFreePixmap(self.dpy, s.drawable);
242                XFreeGC(self.dpy, s.gc);
243            }
244        }
245
246        Ok(())
247    }
248
249    pub(crate) fn add_font(&mut self, font: &str, point_size: u8) -> Result<()> {
250        let k = font_key(font, point_size);
251        if let Entry::Vacant(e) = self.fss.entry(k) {
252            let fs = Fontset::try_new(self.dpy, e.key())?;
253            e.insert(fs);
254        }
255
256        Ok(())
257    }
258
259    /// Set the font being used for rendering text and clear the existing cache of fallback fonts
260    /// for characters that are not supported by the primary font.
261    pub fn set_font(&mut self, font: &str, point_size: u8) -> Result<()> {
262        self.add_font(font, point_size)?;
263        self.active_font = font_key(font, point_size);
264
265        Ok(())
266    }
267
268    /// Retrieve the drawing [Context] for the given window `WinId`.
269    ///
270    /// This method will error if the requested id does not already have an initialised surface.
271    /// See the `new_window` method for details.
272    pub fn context_for(&mut self, id: WinId) -> Result<Context<'_>> {
273        let s = self
274            .surfaces
275            .get(&id)
276            .ok_or(Error::UnintialisedSurface { id })?;
277
278        Ok(Context {
279            dx: 0,
280            dy: 0,
281            dpy: self.dpy,
282            s,
283            bg: self.bg,
284            fs: self
285                .fss
286                .get_mut(&self.active_font)
287                .expect("active_font to be present"),
288            colors: &mut self.colors,
289        })
290    }
291
292    /// Flush any pending requests to the X server and map the specifed window to the screen.
293    pub fn flush(&mut self, id: WinId) -> Result<()> {
294        if let Some(s) = self.surfaces.get(&id) {
295            // SAFETY: self.dpy is non-null
296            unsafe { s.flush(self.dpy) };
297            self.conn.map(id)?;
298            self.conn.flush();
299        };
300
301        Ok(())
302    }
303}
304
305/// A minimal drawing context for rendering text based UI elements
306///
307/// A [Context] provides you with a backing pixmap for rendering your UI using simple offset and
308/// rendering operations. By default, the context will be positioned in the top left corner of
309/// the parent window created by your [Draw]. You can use the `translate` and `set/reset` offset
310/// methods to modify where the next drawing operation will take place.
311///
312/// > It is worthwhile looking at the implementation of the [StatusBar][0] struct and how it
313/// > handles rendering child widgets for a real example of how to make use of the offseting
314/// > functionality of this struct.
315///
316///   [0]: crate::StatusBar
317#[derive(Debug)]
318pub struct Context<'a> {
319    dx: i32,
320    dy: i32,
321    dpy: *mut Display,
322    s: &'a Surface,
323    bg: Color,
324    fs: &'a mut Fontset,
325    colors: &'a mut HashMap<Color, XColor>,
326}
327
328impl Context<'_> {
329    /// Clear the underlying surface, restoring it to the background color.
330    pub fn clear(&mut self) -> Result<()> {
331        self.fill_rect(Rect::new(0, 0, self.s.r.w, self.s.r.h), self.bg)
332    }
333
334    /// Offset future drawing operations by an additional (dx, dy)
335    pub fn translate(&mut self, dx: i32, dy: i32) {
336        self.dx += dx;
337        self.dy += dy;
338    }
339
340    /// Set future drawing operations to apply from a specified point.
341    pub fn set_offset(&mut self, x: i32, y: i32) {
342        self.dx = x;
343        self.dy = y;
344    }
345
346    /// Set an absolute x offset for future drawing operations.
347    pub fn set_x_offset(&mut self, x: i32) {
348        self.dx = x;
349    }
350
351    /// Set an absolute y offset for future drawing operations.
352    pub fn set_y_offset(&mut self, y: i32) {
353        self.dy = y;
354    }
355
356    /// Set future drawing operations to apply from the origin.
357    pub fn reset_offset(&mut self) {
358        self.dx = 0;
359        self.dy = 0;
360    }
361
362    fn get_or_try_init_xcolor(&mut self, c: Color) -> Result<*mut XftColor> {
363        if let Some(xc) = self.colors.get(&c) {
364            return Ok(xc.0);
365        }
366
367        let xc = XColor::try_new(self.dpy, &c)?;
368        let ptr = xc.0;
369        self.colors.insert(c, xc);
370
371        Ok(ptr)
372    }
373
374    /// Render a rectangular border using the supplied color.
375    pub fn draw_rect(&mut self, Rect { x, y, w, h }: Rect, color: Color) -> Result<()> {
376        let xcol = self.get_or_try_init_xcolor(color)?;
377        let (x, y) = (self.dx + x, self.dy + y);
378
379        // SAFETY:
380        //   - the pointers for self.dpy, s.drawable, s.gc are known to be non-null
381        //   - xcol is known to be non-null so dereferencing is safe
382        unsafe {
383            XSetForeground(self.dpy, self.s.gc, (*xcol).pixel);
384            XDrawRectangle(self.dpy, self.s.drawable, self.s.gc, x, y, w, h);
385        }
386
387        Ok(())
388    }
389
390    /// Render a filled rectangle using the supplied color.
391    pub fn fill_rect(&mut self, Rect { x, y, w, h }: Rect, color: Color) -> Result<()> {
392        let xcol = self.get_or_try_init_xcolor(color)?;
393        let (x, y) = (self.dx + x, self.dy + y);
394
395        // SAFETY:
396        //   - the pointers for self.dpy, s.drawable, s.gc are known to be non-null
397        //   - xcol is known to be non-null so dereferencing is safe
398        unsafe {
399            XSetForeground(self.dpy, self.s.gc, (*xcol).pixel);
400            XFillRectangle(self.dpy, self.s.drawable, self.s.gc, x, y, w, h);
401        }
402
403        Ok(())
404    }
405
406    /// Render a filled rectangle using the supplied color.
407    pub fn fill_polygon(&mut self, points: &[Point], color: Color) -> Result<()> {
408        let xcol = self.get_or_try_init_xcolor(color)?;
409        let mut xpoints: Vec<XPoint> = points
410            .iter()
411            .map(|&Point { x, y }| XPoint {
412                x: self.dx as i16 + x as i16,
413                y: self.dy as i16 + y as i16,
414            })
415            .collect();
416
417        // SAFETY:
418        //   - the pointers for self.dpy, s.drawable, s.gc are known to be non-null
419        //   - xcol is known to be non-null so dereferencing is safe
420        unsafe {
421            XSetForeground(self.dpy, self.s.gc, (*xcol).pixel);
422            XFillPolygon(
423                self.dpy,
424                self.s.drawable,
425                self.s.gc,
426                &mut xpoints[0] as *mut _,
427                points.len() as i32,
428                Complex, // we could be smarter here but for now this works
429                CoordModeOrigin,
430            );
431        }
432
433        Ok(())
434    }
435
436    /// Fill the specified area with this Context's background color
437    pub fn fill_bg(&mut self, r: Rect) -> Result<()> {
438        self.fill_rect(r, self.bg)
439    }
440
441    /// Render the provided text at the current context offset using the supplied color.
442    pub fn draw_text(
443        &mut self,
444        txt: &str,
445        h_offset: u32,
446        padding: (u32, u32),
447        c: Color,
448    ) -> Result<(u32, u32)> {
449        // SAFETY:
450        //   - the pointers for self.dpy and s.drawable are known to be non-null
451        //   - we wrap the returned pointer in DropXftDraw to ensure that we correctly destroy
452        //     the XftDraw we create here (see below)
453        let d = unsafe {
454            XftDrawCreate(
455                self.dpy,
456                self.s.drawable,
457                XDefaultVisual(self.dpy, SCREEN),
458                XDefaultColormap(self.dpy, SCREEN),
459            )
460        };
461
462        let _drop_draw = DropXftDraw { ptr: d };
463
464        let (lpad, rpad) = (padding.0 as i32, padding.1);
465        let (mut x, y) = (lpad + self.dx, self.dy);
466        let (mut total_w, mut total_h) = (x as u32, 0);
467        let xcol = self.get_or_try_init_xcolor(c)?;
468
469        for (chunk, fm) in self.fs.per_font_chunks(txt).into_iter() {
470            let fnt = self.fs.fnt(fm);
471            let (chunk_w, chunk_h) = fnt.get_exts(self.dpy, chunk)?;
472
473            // SAFETY: fnt pointer is non-null
474            let chunk_y = unsafe { y + h_offset as i32 + (*fnt.xfont).ascent };
475            let c_str = CString::new(chunk)?;
476
477            // SAFETY:
478            // - fnt.xfont is known to be non-null
479            // - the string character pointer and length have been obtained from a Rust CString
480            unsafe {
481                XftDrawStringUtf8(
482                    d,
483                    xcol,
484                    fnt.xfont,
485                    x,
486                    chunk_y,
487                    c_str.as_ptr() as *mut _,
488                    c_str.as_bytes().len() as i32,
489                );
490            }
491
492            x += chunk_w as i32;
493            total_w += chunk_w;
494            total_h = max(total_h, chunk_h);
495        }
496
497        return Ok((total_w + rpad, total_h));
498
499        // There are multiple error paths here where we need to make sure that we correctly destroy
500        // the XftDraw we created. Rather than complicate the error handling we use a Drop wrapper
501        // to ensure that we run XftDrawDestroy when the function returns.
502
503        struct DropXftDraw {
504            ptr: *mut XftDraw,
505        }
506
507        impl Drop for DropXftDraw {
508            fn drop(&mut self) {
509                // SAFETY: the pointer we have must be non-null
510                unsafe { XftDrawDestroy(self.ptr) };
511            }
512        }
513    }
514
515    /// Determine the width and height taken up by a given string in pixels.
516    pub fn text_extent(&mut self, txt: &str) -> Result<(u32, u32)> {
517        let (mut w, mut h) = (0, 0);
518        for (chunk, fm) in self.fs.per_font_chunks(txt) {
519            let (cw, ch) = self.fs.fnt(fm).get_exts(self.dpy, chunk)?;
520            w += cw;
521            h = max(h, ch);
522        }
523
524        Ok((w, h))
525    }
526
527    /// Flush pending requests to the X server.
528    ///
529    /// This method does not need to be called explicitly if the flush method for
530    /// the parent [Draw] is being called as well.
531    pub fn flush(&self) {
532        // SAFETY: self.dpy is non-null
533        unsafe { self.s.flush(self.dpy) }
534    }
535}
536
537#[derive(Debug)]
538struct XColor(*mut XftColor);
539
540impl Drop for XColor {
541    fn drop(&mut self) {
542        let layout = Layout::new::<XftColor>();
543        // SAFETY: the memory being deallocated was allocated on creation of the XColor
544        unsafe { dealloc(self.0 as *mut u8, layout) }
545    }
546}
547
548impl XColor {
549    fn try_new(dpy: *mut Display, c: &Color) -> Result<Self> {
550        // SAFETY: this private method is only called with a non-null dpy pointer
551        let inner = unsafe { try_xftcolor_from_name(dpy, &c.as_rgb_hex_string())? };
552
553        Ok(Self(inner))
554    }
555}
556
557unsafe fn try_xftcolor_from_name(dpy: *mut Display, color: &str) -> Result<*mut XftColor> {
558    // https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html#tymethod.alloc
559    let layout = Layout::new::<XftColor>();
560    // SAFETY: layout size is non-zero
561    let ptr = unsafe { alloc(layout) };
562    if ptr.is_null() {
563        handle_alloc_error(layout);
564    }
565
566    let c_name = CString::new(color)?;
567    // SAFETY: pointers are non-null
568    let res = unsafe {
569        XftColorAllocName(
570            dpy,
571            XDefaultVisual(dpy, SCREEN),
572            XDefaultColormap(dpy, SCREEN),
573            c_name.as_ptr(),
574            ptr as *mut XftColor,
575        )
576    };
577
578    if res == 0 {
579        Err(Error::UnableToAllocateColor)
580    } else {
581        Ok(ptr as *mut XftColor)
582    }
583}