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}