From 020231fdcdf998ffd5e17ea6406ca9a9e90e630d Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Sun, 16 Jul 2023 08:51:47 +0100 Subject: [PATCH] Issue 272 - removing the dependency on pango and cairo (#274) * #272 replacing usage of pango and cairo with xlib and fontconfig * #272 fixing text offset * #272 sorting out dependencies and feature flags * #272 adding missing build dep for CI * #272 simplifying TextStyle and making it Copy * #272 fixing broken doctests * simplifying method implementations for Color * ensuring that graphics state is cleaned up when status bars are recreated * adding safety comments and warn level linting for future missing safety comments * documenting the updated penrose_ui crate --- .github/workflows/rust.yml | 6 +- Cargo.toml | 17 +- crates/penrose_keysyms/Cargo.toml | 2 +- crates/penrose_keysyms/LICENSE | 19 + crates/penrose_ui/Cargo.toml | 13 +- crates/penrose_ui/LICENSE | 19 + crates/penrose_ui/README.md | 5 + crates/penrose_ui/examples/txt-demo.rs | 51 ++ crates/penrose_ui/src/bar/mod.rs | 102 ++- crates/penrose_ui/src/bar/widgets/debug.rs | 16 +- crates/penrose_ui/src/bar/widgets/mod.rs | 67 +- crates/penrose_ui/src/bar/widgets/simple.rs | 27 +- crates/penrose_ui/src/bar/widgets/sys.rs | 8 +- .../penrose_ui/src/bar/widgets/workspaces.rs | 67 +- crates/penrose_ui/src/core/fontset.rs | 267 ++++++++ crates/penrose_ui/src/core/mod.rs | 583 ++++++++++++------ crates/penrose_ui/src/lib.rs | 72 ++- examples/status_bar/main.rs | 6 +- src/core/mod.rs | 2 + src/lib.rs | 67 +- src/x11rb/mod.rs | 13 +- 21 files changed, 981 insertions(+), 448 deletions(-) create mode 100644 crates/penrose_keysyms/LICENSE create mode 100644 crates/penrose_ui/LICENSE create mode 100644 crates/penrose_ui/examples/txt-demo.rs create mode 100644 crates/penrose_ui/src/core/fontset.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9a876f52..cf095422 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: rust-version: ${{ matrix.rust }} - name: Install C deps - run: sudo apt-get update && sudo apt-get install -y libxrandr-dev libx11-xcb-dev libxcb-randr0-dev libpango1.0-dev libcairo2-dev --fix-missing + run: sudo apt-get update && sudo apt-get install -y libxrandr-dev libx11-xcb-dev libxcb-randr0-dev libxft-dev --fix-missing - name: Run tests run: cargo test --workspace --features ${{ matrix.features }} --verbose @@ -55,7 +55,7 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: components: clippy - - run: sudo apt-get update && sudo apt-get install -y libxrandr-dev libx11-xcb-dev libxcb-randr0-dev libpango1.0-dev libcairo2-dev --fix-missing + - run: sudo apt-get update && sudo apt-get install -y libxrandr-dev libx11-xcb-dev libxcb-randr0-dev libxft-dev --fix-missing - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -70,5 +70,5 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: rust-version: nightly - - run: sudo apt-get update && sudo apt-get install -y libxrandr-dev libx11-xcb-dev libxcb-randr0-dev libpango1.0-dev libcairo2-dev --fix-missing + - run: sudo apt-get update && sudo apt-get install -y libxrandr-dev libx11-xcb-dev libxcb-randr0-dev libxft-dev --fix-missing - run: cargo rustdoc --all-features diff --git a/Cargo.toml b/Cargo.toml index c48d10fe..67f85180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "penrose" -version = "0.3.2" +version = "0.3.3" edition = "2021" authors = ["sminez "] license = "MIT" @@ -23,24 +23,21 @@ members = [ ] [features] -default = ["x11rb-xcb", "keysyms"] +default = ["x11rb", "keysyms"] keysyms = ["penrose_keysyms"] x11rb-xcb = ["x11rb", "x11rb/allow-unsafe-code"] [dependencies] -penrose_keysyms = { version = "0.1.1", path = "crates/penrose_keysyms", optional = true } -# penrose_proc = { version = "0.1.3", path = "crates/penrose_proc" } - +anymap = "0.12" bitflags = { version = "2.3", features = ["serde"] } -nix = "0.26" +nix = { version = "0.26", default-features = false, features = ["signal"] } +penrose_keysyms = { version = "0.3", path = "crates/penrose_keysyms", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } strum = { version = "0.25", features = ["derive"] } strum_macros = "0.25" thiserror = "1.0" -tracing = { version = "0.1", features = ["attributes", "log"] } - -serde = { version = "1.0", features = ["derive"], optional = true } +tracing = { version = "0.1", features = ["attributes"] } x11rb = { version = "0.12", features = ["randr"], optional = true } -anymap = "0.12" [dev-dependencies] paste = "1.0.13" diff --git a/crates/penrose_keysyms/Cargo.toml b/crates/penrose_keysyms/Cargo.toml index 512a5fbd..ab518c16 100644 --- a/crates/penrose_keysyms/Cargo.toml +++ b/crates/penrose_keysyms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "penrose_keysyms" -version = "0.1.1" +version = "0.3.3" authors = ["IDAM "] edition = "2018" license = "MIT" diff --git a/crates/penrose_keysyms/LICENSE b/crates/penrose_keysyms/LICENSE new file mode 100644 index 00000000..de5b87e1 --- /dev/null +++ b/crates/penrose_keysyms/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Innes Anderson-Morrison + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/penrose_ui/Cargo.toml b/crates/penrose_ui/Cargo.toml index f753d530..e241dcec 100644 --- a/crates/penrose_ui/Cargo.toml +++ b/crates/penrose_ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "penrose_ui" -version = "0.1.3" +version = "0.3.3" edition = "2021" authors = ["sminez "] license = "MIT" @@ -12,10 +12,11 @@ description = "UI elements for the penrose window manager library" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -cairo-rs = { version = "0.17.10", features = ["xcb"] } -pangocairo = { version = "0.17.10" } -pango = { version = "0.17.10" } penrose = { version = "0.3", path = "../../" } -tracing = { version = "0.1", features = ["attributes", "log"] } +tracing = { version = "0.1", features = ["attributes"] } thiserror = "1.0" -x11rb = { version = "0.12", features = ["randr", "render"] } +yeslogic-fontconfig-sys = "4.0" +x11 = { version = "2.21", features = ["xft", "xlib"] } + +[dev-dependencies] +anyhow = "1.0.71" diff --git a/crates/penrose_ui/LICENSE b/crates/penrose_ui/LICENSE new file mode 100644 index 00000000..de5b87e1 --- /dev/null +++ b/crates/penrose_ui/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Innes Anderson-Morrison + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/penrose_ui/README.md b/crates/penrose_ui/README.md index 99a71367..a447a332 100644 --- a/crates/penrose_ui/README.md +++ b/crates/penrose_ui/README.md @@ -2,5 +2,10 @@ _GUI elements for the penrose window manager library_ +The functionality provided by this crate is a _very_ thin wrapper over xlib and fontconfig +to support minimal text based UIs such as a status bar or simple menu. It may be possible +to use this to write a stand alone UI outside of direct integration with the Penrose window +manager crate but that is not the supported use case this crate has been written for. + ## Bar A lightweight and minimal status bar. diff --git a/crates/penrose_ui/examples/txt-demo.rs b/crates/penrose_ui/examples/txt-demo.rs new file mode 100644 index 00000000..c8fbc727 --- /dev/null +++ b/crates/penrose_ui/examples/txt-demo.rs @@ -0,0 +1,51 @@ +//! Demo of the text rendering API +use penrose::{ + pure::geometry::Rect, + x::{Atom, WinType}, + Color, +}; +use penrose_ui::Draw; +use std::{thread::sleep, time::Duration}; + +const DX: u32 = 100; +const DY: u32 = 100; +const W: u32 = 500; +const H: u32 = 60; +const FONT: &str = "ProFont For Powerline"; +const TXT: &str = "  text is great! ◈ ζ ᛄ ℚ"; + +fn main() -> anyhow::Result<()> { + let fg1 = Color::try_from("#fad07b")?; + let fg2 = Color::try_from("#458588")?; + let fg3 = Color::try_from("#a6cc70")?; + let fg4 = Color::try_from("#b16286")?; + let bg = Color::try_from("#282828")?; + + let mut drw = Draw::new(FONT, 12, bg)?; + let w = drw.new_window( + WinType::InputOutput(Atom::NetWindowTypeDock), + Rect::new(DX, DY, W, H), + false, + )?; + + let mut ctx = drw.context_for(w)?; + ctx.clear()?; + + let (dx, dy) = ctx.draw_text(TXT, 0, (10, 0), fg1)?; + + ctx.set_x_offset(dx as i32 + 10); + ctx.draw_text(TXT, 0, (5, 0), fg2)?; + + ctx.translate(0, dy as i32 + 10); + ctx.draw_text(TXT, 0, (5, 0), fg3)?; + + ctx.translate(-(dx as i32), 0); + ctx.draw_text(TXT, 0, (0, 0), fg4)?; + + ctx.flush(); + drw.flush(w)?; + + sleep(Duration::from_secs(2)); + + Ok(()) +} diff --git a/crates/penrose_ui/src/bar/mod.rs b/crates/penrose_ui/src/bar/mod.rs index 9d8bbaab..e3c69b2a 100644 --- a/crates/penrose_ui/src/bar/mod.rs +++ b/crates/penrose_ui/src/bar/mod.rs @@ -1,8 +1,5 @@ //! A lightweight and configurable status bar for penrose -use crate::{ - core::{Context, Draw}, - Result, -}; +use crate::{core::Draw, Result}; use penrose::{ core::{State, WindowManager}, pure::geometry::Rect, @@ -11,7 +8,6 @@ use penrose::{ }; use std::fmt; use tracing::{debug, error, info}; -use x11rb::protocol::xproto::ConnectionExt as _; pub mod widgets; @@ -31,9 +27,8 @@ pub struct StatusBar { draw: Draw, position: Position, widgets: Vec>>, - screens: Vec<(Xid, f64)>, - hpx: u32, - h: f64, + screens: Vec<(Xid, u32)>, + h: u32, bg: Color, active_screen: usize, } @@ -44,7 +39,7 @@ impl fmt::Debug for StatusBar { .field("position", &self.position) .field("widgets", &stringify!(self.widgets)) .field("screens", &self.screens) - .field("hpx", &self.hpx) + .field("h", &self.h) .field("bg", &self.bg) .field("active_screen", &self.active_screen) .finish() @@ -58,25 +53,22 @@ impl StatusBar { position: Position, h: u32, bg: impl Into, - fonts: &[&str], + font: &str, + point_size: u8, widgets: Vec>>, ) -> Result { - let draw = Draw::new()?; + let bg = bg.into(); + let draw = Draw::new(font, point_size, bg)?; - let mut bar = Self { + Ok(Self { draw, position, widgets, screens: vec![], - hpx: h, - h: h as f64, - bg: bg.into(), + h, + bg, active_screen: 0, - }; - - fonts.iter().for_each(|f| bar.draw.register_font(f)); - - Ok(bar) + }) } /// Add this [`StatusBar`] into the given [`WindowManager`] along with the required @@ -103,13 +95,13 @@ impl StatusBar { .map(|&Rect { x, y, w, h }| { let y = match self.position { Position::Top => y, - Position::Bottom => h - self.hpx, + Position::Bottom => h - self.h, }; debug!("creating new window"); let id = self.draw.new_window( WinType::InputOutput(Atom::NetWindowTypeDock), - Rect::new(x, y, w, self.hpx), + Rect::new(x, y, w, self.h), false, )?; @@ -125,9 +117,9 @@ impl StatusBar { debug!("flushing"); self.draw.flush(id)?; - Ok((id, w as f64)) + Ok((id, w)) }) - .collect::>>()?; + .collect::>>()?; Ok(()) } @@ -138,18 +130,35 @@ impl StatusBar { let screen_has_focus = self.active_screen == i; let mut ctx = self.draw.context_for(id)?; - ctx.clear()?; + ctx.fill_rect(Rect::new(0, 0, w, self.h), self.bg)?; + + let mut extents = Vec::with_capacity(self.widgets.len()); + let mut greedy_indices = vec![]; - ctx.color(&self.bg); - ctx.rectangle(0.0, 0.0, w, self.h)?; + for (i, w) in self.widgets.iter_mut().enumerate() { + extents.push(w.current_extent(&mut ctx, self.h)?); + if w.is_greedy() { + greedy_indices.push(i) + } + } + + let total = extents.iter().map(|(w, _)| w).sum::(); + let n_greedy = greedy_indices.len(); + + if total < w && n_greedy > 0 { + let per_greedy = (w - total) / n_greedy as u32; + for i in greedy_indices.iter() { + let (w, h) = extents[*i]; + extents[*i] = (w + per_greedy, h); + } + } - let extents = self.layout(&mut ctx, w)?; - let mut x = 0.0; + let mut x = 0; for (wd, (w, _)) in self.widgets.iter_mut().zip(extents) { wd.draw(&mut ctx, self.active_screen, screen_has_focus, w, self.h)?; x += w; ctx.flush(); - ctx.set_x_offset(x); + ctx.set_x_offset(x as i32); } self.draw.flush(id)?; @@ -158,32 +167,6 @@ impl StatusBar { Ok(()) } - fn layout(&mut self, ctx: &mut Context, w: f64) -> Result> { - let mut extents = Vec::with_capacity(self.widgets.len()); - let mut greedy_indices = vec![]; - - for (i, w) in self.widgets.iter_mut().enumerate() { - extents.push(w.current_extent(ctx, self.h)?); - if w.is_greedy() { - greedy_indices.push(i) - } - } - - let total = extents.iter().map(|(w, _)| w).sum::(); - let n_greedy = greedy_indices.len(); - - if total < w && n_greedy > 0 { - let per_greedy = (w - total) / n_greedy as f64; - for i in greedy_indices.iter() { - let (w, h) = extents[*i]; - extents[*i] = (w + per_greedy, h); - } - } - - // Allowing overflow to happen - Ok(extents) - } - fn redraw_if_needed(&mut self) -> Result<()> { if self.widgets.iter().any(|w| w.require_draw()) { self.redraw()?; @@ -253,10 +236,13 @@ pub fn event_hook( if matches!(event, RandrNotify) || matches!(event, ConfigureNotify(e) if e.is_root) { info!("screens have changed: recreating status bars"); + let screens: Vec<_> = bar.screens.drain(0..).collect(); - for &(id, _) in bar.screens.iter() { + for (id, _) in screens { info!(%id, "removing previous status bar"); - bar.draw.conn.connection().destroy_window(*id)?; + if let Err(e) = bar.draw.destroy_window_and_surface(id) { + error!(%e, "error when removing previous status bar state"); + } } if let Err(e) = bar.init_for_screens() { diff --git a/crates/penrose_ui/src/bar/widgets/debug.rs b/crates/penrose_ui/src/bar/widgets/debug.rs index cc2064c5..fa5fa75d 100644 --- a/crates/penrose_ui/src/bar/widgets/debug.rs +++ b/crates/penrose_ui/src/bar/widgets/debug.rs @@ -14,7 +14,7 @@ pub struct ActiveWindowId { impl ActiveWindowId { /// Create a new ActiveWindowId widget. - pub fn new(style: &TextStyle, is_greedy: bool, right_justified: bool) -> Self { + pub fn new(style: TextStyle, is_greedy: bool, right_justified: bool) -> Self { Self { inner: Text::new("", style, is_greedy, right_justified), } @@ -22,11 +22,11 @@ impl ActiveWindowId { } impl Widget for ActiveWindowId { - fn draw(&mut self, ctx: &mut Context, s: usize, focused: bool, w: f64, h: f64) -> Result<()> { - Widget::::draw(&mut self.inner, ctx, s, focused, w, h) + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { + Widget::::draw(&mut self.inner, ctx, s, f, w, h) } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { Widget::::current_extent(&mut self.inner, ctx, h) } @@ -60,7 +60,7 @@ pub struct StateSummary { impl StateSummary { /// Create a new StateSummary widget. - pub fn new(style: &TextStyle) -> Self { + pub fn new(style: TextStyle) -> Self { Self { inner: Text::new("", style, false, false), cfg: CurrentStateConfig { @@ -72,11 +72,11 @@ impl StateSummary { } impl Widget for StateSummary { - fn draw(&mut self, ctx: &mut Context, s: usize, focused: bool, w: f64, h: f64) -> Result<()> { - Widget::::draw(&mut self.inner, ctx, s, focused, w, h) + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { + Widget::::draw(&mut self.inner, ctx, s, f, w, h) } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { Widget::::current_extent(&mut self.inner, ctx, h) } diff --git a/crates/penrose_ui/src/bar/widgets/mod.rs b/crates/penrose_ui/src/bar/widgets/mod.rs index 9293d74e..ce22729b 100644 --- a/crates/penrose_ui/src/bar/widgets/mod.rs +++ b/crates/penrose_ui/src/bar/widgets/mod.rs @@ -2,6 +2,7 @@ use crate::{Context, Result, TextStyle}; use penrose::{ core::State, + pure::geometry::Rect, x::{XConn, XEvent}, Color, Xid, }; @@ -30,15 +31,15 @@ where /// Render the current state of the widget to the status bar window. fn draw( &mut self, - ctx: &mut Context, + ctx: &mut Context<'_>, screen: usize, screen_has_focus: bool, - w: f64, - h: f64, + w: u32, + h: u32, ) -> Result<()>; /// Current required width and height for this widget due to its content - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)>; + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)>; /// Does this widget currently require re-rendering? (should be reset to false when 'draw' is called) fn require_draw(&self) -> bool; @@ -80,14 +81,12 @@ where #[derive(Clone, Debug, PartialEq)] pub struct Text { txt: String, - font: String, - point_size: i32, fg: Color, bg: Option, - padding: (f64, f64), + padding: (u32, u32), is_greedy: bool, right_justified: bool, - extent: Option<(f64, f64)>, + extent: Option<(u32, u32)>, require_draw: bool, } @@ -95,14 +94,12 @@ impl Text { /// Construct a new [Text] pub fn new( txt: impl Into, - style: &TextStyle, + style: TextStyle, is_greedy: bool, right_justified: bool, ) -> Self { Self { txt: txt.into(), - font: style.font.clone(), - point_size: style.point_size, fg: style.fg, bg: style.bg, padding: style.padding, @@ -135,24 +132,20 @@ impl Text { } impl Widget for Text { - fn draw(&mut self, ctx: &mut Context, _: usize, _: bool, w: f64, h: f64) -> Result<()> { + fn draw(&mut self, ctx: &mut Context<'_>, _: usize, _: bool, w: u32, h: u32) -> Result<()> { if let Some(color) = self.bg { - ctx.color(&color); - ctx.rectangle(0.0, 0.0, w, h)?; + ctx.fill_rect(Rect::new(0, 0, w, h), color)?; } let (ew, eh) = >::current_extent(self, ctx, h)?; - ctx.font(&self.font, self.point_size)?; - ctx.color(&self.fg); - - let offset = w - ew; - let right_justify = self.right_justified && self.is_greedy && offset > 0.0; + let offset = w as i32 - ew as i32; + let right_justify = self.right_justified && self.is_greedy && offset > 0; if right_justify { - ctx.translate(offset, 0.0); - ctx.text(&self.txt, h - eh, self.padding)?; - ctx.translate(-offset, 0.0); + ctx.translate(offset, 0); + ctx.draw_text(&self.txt, h - eh, self.padding, self.fg)?; + ctx.translate(-offset, 0); } else { - ctx.text(&self.txt, h - eh, self.padding)?; + ctx.draw_text(&self.txt, h - eh, self.padding, self.fg)?; } self.require_draw = false; @@ -160,15 +153,15 @@ impl Widget for Text { Ok(()) } - fn current_extent(&mut self, ctx: &mut Context, _h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, _h: u32) -> Result<(u32, u32)> { match self.extent { Some(extent) => Ok(extent), None => { let (l, r) = self.padding; - ctx.font(&self.font, self.point_size)?; let (w, h) = ctx.text_extent(&self.txt)?; let extent = (w + l + r, h); self.extent = Some(extent); + Ok(extent) } } @@ -216,14 +209,12 @@ impl Widget for Text { /// } /// /// let style = TextStyle { -/// font: "mono".to_string(), -/// point_size: 10, /// fg: 0xebdbb2ff.into(), /// bg: Some(0x282828ff.into()), -/// padding: (2.0, 2.0), +/// padding: (2, 2), /// }; /// -/// let my_widget = RefreshText::new(&style, my_get_text); +/// let my_widget = RefreshText::new(style, my_get_text); /// ``` pub struct RefreshText { inner: Text, @@ -241,7 +232,7 @@ impl fmt::Debug for RefreshText { impl RefreshText { /// Construct a new [`RefreshText`] using the specified styling and a function for /// generating the widget contents. - pub fn new(style: &TextStyle, get_text: F) -> Self + pub fn new(style: TextStyle, get_text: F) -> Self where F: Fn() -> String + 'static, { @@ -253,11 +244,11 @@ impl RefreshText { } impl Widget for RefreshText { - fn draw(&mut self, ctx: &mut Context, s: usize, f: bool, w: f64, h: f64) -> Result<()> { + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { Widget::::draw(&mut self.inner, ctx, s, f, w, h) } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { Widget::::current_extent(&mut self.inner, ctx, h) } @@ -305,16 +296,14 @@ impl Widget for RefreshText { /// } /// /// let style = TextStyle { -/// font: "mono".to_string(), -/// point_size: 10, /// fg: 0xebdbb2ff.into(), /// bg: Some(0x282828ff.into()), -/// padding: (2.0, 2.0), +/// padding: (2, 2), /// }; /// /// /// let my_widget = IntervalText::new( -/// &style, +/// style, /// my_get_text, /// Duration::from_secs(60 * 5) /// ); @@ -328,7 +317,7 @@ impl IntervalText { /// Construct a new [`IntervalText`] using the specified styling and a function for /// generating the widget contents. The function for updating the widget contents /// will be run in its own thread on the interval provided. - pub fn new(style: &TextStyle, get_text: F, interval: Duration) -> Self + pub fn new(style: TextStyle, get_text: F, interval: Duration) -> Self where F: Fn() -> String + 'static + Send, { @@ -355,7 +344,7 @@ impl IntervalText { } impl Widget for IntervalText { - fn draw(&mut self, ctx: &mut Context, s: usize, f: bool, w: f64, h: f64) -> Result<()> { + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { let mut inner = match self.inner.lock() { Ok(inner) => inner, Err(poisoned) => poisoned.into_inner(), @@ -364,7 +353,7 @@ impl Widget for IntervalText { Widget::::draw(&mut *inner, ctx, s, f, w, h) } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { let mut inner = match self.inner.lock() { Ok(inner) => inner, Err(poisoned) => poisoned.into_inner(), diff --git a/crates/penrose_ui/src/bar/widgets/simple.rs b/crates/penrose_ui/src/bar/widgets/simple.rs index c5bc4ccd..79de4c70 100644 --- a/crates/penrose_ui/src/bar/widgets/simple.rs +++ b/crates/penrose_ui/src/bar/widgets/simple.rs @@ -17,7 +17,7 @@ pub struct RootWindowName { impl RootWindowName { /// Create a new RootWindowName widget - pub fn new(style: &TextStyle, is_greedy: bool, right_justified: bool) -> Self { + pub fn new(style: TextStyle, is_greedy: bool, right_justified: bool) -> Self { Self { inner: Text::new("penrose", style, is_greedy, right_justified), } @@ -25,11 +25,11 @@ impl RootWindowName { } impl Widget for RootWindowName { - fn draw(&mut self, ctx: &mut Context, s: usize, f: bool, w: f64, h: f64) -> Result<()> { + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { Widget::::draw(&mut self.inner, ctx, s, f, w, h) } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { Widget::::current_extent(&mut self.inner, ctx, h) } @@ -69,12 +69,7 @@ impl ActiveWindowName { /// Create a new ActiveWindowName widget with a maximum character count. /// /// max_chars can not be lower than 3. - pub fn new( - max_chars: usize, - style: &TextStyle, - is_greedy: bool, - right_justified: bool, - ) -> Self { + pub fn new(max_chars: usize, style: TextStyle, is_greedy: bool, right_justified: bool) -> Self { Self { inner: Text::new("", style, is_greedy, right_justified), max_chars: max_chars.max(3), @@ -92,15 +87,15 @@ impl ActiveWindowName { } impl Widget for ActiveWindowName { - fn draw(&mut self, ctx: &mut Context, s: usize, focused: bool, w: f64, h: f64) -> Result<()> { - if focused { - Widget::::draw(&mut self.inner, ctx, s, focused, w, h) + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { + if f { + Widget::::draw(&mut self.inner, ctx, s, f, w, h) } else { Ok(()) } } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { Widget::::current_extent(&mut self.inner, ctx, h) } @@ -149,7 +144,7 @@ pub struct CurrentLayout { impl CurrentLayout { /// Create a new CurrentLayout widget - pub fn new(style: &TextStyle) -> Self { + pub fn new(style: TextStyle) -> Self { Self { inner: Text::new("", style, false, false), } @@ -157,11 +152,11 @@ impl CurrentLayout { } impl Widget for CurrentLayout { - fn draw(&mut self, ctx: &mut Context, s: usize, f: bool, w: f64, h: f64) -> Result<()> { + fn draw(&mut self, ctx: &mut Context<'_>, s: usize, f: bool, w: u32, h: u32) -> Result<()> { Widget::::draw(&mut self.inner, ctx, s, f, w, h) } - fn current_extent(&mut self, ctx: &mut Context, h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, h: u32) -> Result<(u32, u32)> { Widget::::current_extent(&mut self.inner, ctx, h) } diff --git a/crates/penrose_ui/src/bar/widgets/sys.rs b/crates/penrose_ui/src/bar/widgets/sys.rs index 0ad694cc..890f4ae0 100644 --- a/crates/penrose_ui/src/bar/widgets/sys.rs +++ b/crates/penrose_ui/src/bar/widgets/sys.rs @@ -7,7 +7,7 @@ use std::fs; /// /// If the given battery name is not found on this system, this widget will /// render as an empty string. -pub fn battery_summary(bat: &'static str, style: &TextStyle) -> RefreshText { +pub fn battery_summary(bat: &'static str, style: TextStyle) -> RefreshText { RefreshText::new(style, move || battery_text(bat).unwrap_or_default()) } @@ -44,7 +44,7 @@ fn read_sys_file(bat: &str, fname: &str) -> Option { /// Display the current date and time in YYYY-MM-DD HH:MM format /// /// This widget shells out to the `date` tool to generate its output -pub fn current_date_and_time(style: &TextStyle) -> RefreshText { +pub fn current_date_and_time(style: TextStyle) -> RefreshText { RefreshText::new(style, || { spawn_for_output_with_args("date", &["+%F %R"]) .unwrap_or_default() @@ -55,7 +55,7 @@ pub fn current_date_and_time(style: &TextStyle) -> RefreshText { /// Display the ESSID currently connected to and the signal quality as /// a percentage. -pub fn wifi_network(style: &TextStyle) -> RefreshText { +pub fn wifi_network(style: TextStyle) -> RefreshText { RefreshText::new(style, move || wifi_text().unwrap_or_default()) } @@ -98,7 +98,7 @@ fn signal_quality(interface: &str) -> Option { } /// Display the current volume level as reported by `amixer` -pub fn amixer_volume(channel: &'static str, style: &TextStyle) -> RefreshText { +pub fn amixer_volume(channel: &'static str, style: TextStyle) -> RefreshText { RefreshText::new(style, move || amixer_text(channel).unwrap_or_default()) } diff --git a/crates/penrose_ui/src/bar/widgets/workspaces.rs b/crates/penrose_ui/src/bar/widgets/workspaces.rs index fa2a079f..d378eac6 100644 --- a/crates/penrose_ui/src/bar/widgets/workspaces.rs +++ b/crates/penrose_ui/src/bar/widgets/workspaces.rs @@ -6,17 +6,18 @@ use crate::{ }; use penrose::{ core::{ClientSpace, State}, + pure::geometry::Rect, x::XConn, Color, }; -const PADDING: f64 = 3.0; +const PADDING: u32 = 3; #[derive(Clone, Debug, PartialEq)] struct WsMeta { tag: String, occupied: bool, - extent: (f64, f64), + extent: (u32, u32), } impl WsMeta { @@ -34,7 +35,7 @@ impl From<&ClientSpace> for WsMeta { Self { tag: w.tag().to_owned(), occupied: !w.is_empty(), - extent: (0.0, 0.0), + extent: (0, 0), } } } @@ -56,9 +57,7 @@ fn focused_workspaces(state: &State) -> Vec { pub struct Workspaces { workspaces: Vec, focused_ws: Vec, // focused ws per screen - font: String, - point_size: i32, - extent: Option<(f64, f64)>, + extent: Option<(u32, u32)>, fg_1: Color, fg_2: Color, bg_1: Color, @@ -68,12 +67,10 @@ pub struct Workspaces { impl Workspaces { /// Construct a new WorkspaceWidget - pub fn new(style: &TextStyle, highlight: impl Into, empty_fg: impl Into) -> Self { + pub fn new(style: TextStyle, highlight: impl Into, empty_fg: impl Into) -> Self { Self { workspaces: vec![], focused_ws: vec![], // set in startup hook - font: style.font.clone(), - point_size: style.point_size, extent: None, fg_1: style.fg, fg_2: empty_fg.into(), @@ -126,7 +123,7 @@ impl Workspaces { screen: usize, screen_has_focus: bool, occupied: bool, - ) -> (&Color, Option<&Color>) { + ) -> (Color, Color) { let focused_on_this_screen = match &self.focused_ws.get(screen) { &Some(focused_tag) => tag == focused_tag, None => false, @@ -136,21 +133,17 @@ impl Workspaces { 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 }; + let fg = if occupied { self.fg_1 } else { self.fg_2 }; - (fg, Some(&self.bg_1)) + (fg, self.bg_1) } else if focused { - let fg = if focused_other { - &self.bg_1 - } else { - &self.fg_1 - }; + let fg = if focused_other { self.bg_1 } else { self.fg_1 }; - (fg, Some(&self.fg_2)) + (fg, self.fg_2) } else { - let fg = if occupied { &self.fg_1 } else { &self.fg_2 }; + let fg = if occupied { self.fg_1 } else { self.fg_2 }; - (fg, None) + (fg, self.bg_2) } } } @@ -158,28 +151,21 @@ impl Workspaces { impl Widget for Workspaces { fn draw( &mut self, - ctx: &mut Context, + ctx: &mut Context<'_>, screen: usize, screen_has_focus: bool, - w: f64, - h: f64, + w: u32, + h: u32, ) -> Result<()> { - ctx.color(&self.bg_2); - ctx.rectangle(0.0, 0.0, w, h)?; - ctx.font(&self.font, self.point_size)?; - ctx.translate(PADDING, 0.0); + ctx.fill_rect(Rect::new(0, 0, w, h), self.bg_2)?; + ctx.translate(PADDING as i32, 0); let (_, eh) = >::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); - if let Some(c) = bg { - ctx.color(c); - ctx.rectangle(0.0, 0.0, ws.extent.0, h)?; - } - - ctx.color(fg); - ctx.text(&ws.tag, h - eh, (PADDING, PADDING))?; - ctx.translate(ws.extent.0, 0.0); + ctx.fill_rect(Rect::new(0, 0, ws.extent.0, h), bg)?; + ctx.draw_text(&ws.tag, h - eh, (PADDING, PADDING), fg)?; + ctx.translate(ws.extent.0 as i32, 0); } self.require_draw = false; @@ -187,18 +173,17 @@ impl Widget for Workspaces { Ok(()) } - fn current_extent(&mut self, ctx: &mut Context, _h: f64) -> Result<(f64, f64)> { + fn current_extent(&mut self, ctx: &mut Context<'_>, _h: u32) -> Result<(u32, u32)> { match self.extent { Some(extent) => Ok(extent), None => { - let mut total = 0.0; - let mut h_max = 0.0; + let mut total = 0; + let mut h_max = 0; for ws in self.workspaces.iter_mut() { - ctx.font(&self.font, self.point_size)?; let (w, h) = ctx.text_extent(&ws.tag)?; - total += w + PADDING + PADDING; + total += w + 2 * PADDING; h_max = if h > h_max { h } else { h_max }; - ws.extent = (w + PADDING + PADDING, h); + ws.extent = (w + 2 * PADDING, h); } let ext = (total + PADDING, h_max); diff --git a/crates/penrose_ui/src/core/fontset.rs b/crates/penrose_ui/src/core/fontset.rs new file mode 100644 index 00000000..98ddd5d5 --- /dev/null +++ b/crates/penrose_ui/src/core/fontset.rs @@ -0,0 +1,267 @@ +use crate::{core::SCREEN, Error, Result}; +use fontconfig_sys::{ + constants::{FC_CHARSET, FC_SCALABLE}, + FcCharSetAddChar, FcCharSetCreate, FcCharSetDestroy, FcConfig, FcConfigSubstitute, + FcDefaultSubstitute, FcMatchPattern, FcPatternAddBool, FcPatternAddCharSet, FcPatternDestroy, + FcPatternDuplicate, +}; +use std::{ + alloc::{alloc, handle_alloc_error, Layout}, + collections::HashMap, + ffi::CString, +}; +use x11::{ + xft::{ + FcPattern, FcResult, XftCharExists, XftFont, XftFontClose, XftFontMatch, XftFontOpenName, + XftFontOpenPattern, XftNameParse, XftTextExtentsUtf8, + }, + xlib::Display, + xrender::XGlyphInfo, +}; + +#[derive(Debug)] +pub(crate) struct Fontset { + dpy: *mut Display, + primary: Font, + fallback: Vec, + char_cache: HashMap, +} + +impl Fontset { + pub(crate) fn try_new(dpy: *mut Display, fnt: &str) -> Result { + Ok(Self { + dpy, + primary: Font::try_new_from_name(dpy, fnt)?, + fallback: Default::default(), + char_cache: Default::default(), + }) + } + + // Find boundaries where we need to change the font we are using for rendering utf8 + // characters from the given input. + pub(crate) fn per_font_chunks<'a>(&mut self, txt: &'a str) -> Vec<(&'a str, FontMatch)> { + let mut char_indices = txt.char_indices(); + let mut chunks = Vec::new(); + let mut last_split = 0; + let mut chunk: &str; + let mut rest = txt; + + let mut cur_fm = match char_indices.next() { + Some((_, c)) => self.fnt_for_char(c), + None => return chunks, // empty string: no chunks + }; + + for (i, c) in char_indices { + let fm = self.fnt_for_char(c); + if fm != cur_fm { + (chunk, rest) = rest.split_at(i - last_split); + chunks.push((chunk, cur_fm)); + cur_fm = fm; + last_split = i; + } + } + + if !rest.is_empty() { + chunks.push((rest, cur_fm)); + } + + chunks + } + + pub(crate) fn fnt(&self, fm: FontMatch) -> &Font { + match fm { + FontMatch::Primary => &self.primary, + FontMatch::Fallback(n) => &self.fallback[n], + } + } + + fn fnt_for_char(&mut self, c: char) -> FontMatch { + if let Some(fm) = self.char_cache.get(&c) { + return *fm; + } + + if self.primary.contains_char(self.dpy, c) { + self.char_cache.insert(c, FontMatch::Primary); + return FontMatch::Primary; + } + + for (i, fnt) in self.fallback.iter().enumerate() { + if fnt.contains_char(self.dpy, c) { + self.char_cache.insert(c, FontMatch::Fallback(i)); + return FontMatch::Fallback(i); + } + } + + let fallback = match self.primary.fallback_for_char(self.dpy, c) { + Ok(fnt) => { + self.fallback.push(fnt); + FontMatch::Fallback(self.fallback.len() - 1) + } + + Err(e) => { + // TODO: add tracing to this crate + println!("ERROR: {e}"); + FontMatch::Primary + } + }; + + self.char_cache.insert(c, fallback); + + fallback + } +} + +impl Drop for Fontset { + fn drop(&mut self) { + // SAFETY: the Display we have a pointer to is freed by the parent draw + unsafe { + XftFontClose(self.dpy, self.primary.xfont); + for f in self.fallback.drain(0..) { + XftFontClose(self.dpy, f.xfont); + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum FontMatch { + Primary, + Fallback(usize), +} + +// Fonts contain a resource that requires a Display to free on Drop so they +// are owned by their parent Draw and cleaned up when the Draw is dropped +// +// https://man.archlinux.org/man/extra/libxft/XftFontMatch.3.en +// https://refspecs.linuxfoundation.org/fontconfig-2.6.0/index.html +#[derive(Debug)] +pub(crate) struct Font { + pub(crate) h: u32, + pub(crate) xfont: *mut XftFont, + pattern: *mut FcPattern, +} + +impl Font { + fn try_new_from_name(dpy: *mut Display, name: &str) -> Result { + let c_name = CString::new(name)?; + + // SAFETY: + // - Null pointers are checked and explicitly converted to Rust Errors + // - Raw pointer dereferences are only carried out after checking for null pointers + let (xfont, pattern, h) = unsafe { + let xfont = XftFontOpenName(dpy, SCREEN, c_name.as_ptr()); + if xfont.is_null() { + return Err(Error::UnableToOpenFont(name.to_string())); + } + + let pattern = XftNameParse(c_name.as_ptr()); + if pattern.is_null() { + XftFontClose(dpy, xfont); + return Err(Error::UnableToParseFontPattern(name.to_string())); + } + + let h = (*xfont).ascent + (*xfont).descent; + + (xfont, pattern, h as u32) + }; + + Ok(Font { xfont, pattern, h }) + } + + fn try_new_from_pattern(dpy: *mut Display, pattern: *mut FcPattern) -> Result { + // SAFETY: + // - Null pointers are checked and explicitly converted to Rust Errors + // - Raw pointer dereferences are only carried out after checking for null pointers + let (xfont, h) = unsafe { + let xfont = XftFontOpenPattern(dpy, pattern); + if xfont.is_null() { + return Err(Error::UnableToOpenFontPattern); + } + + let h = (*xfont).ascent + (*xfont).descent; + + (xfont, h as u32) + }; + + Ok(Font { xfont, pattern, h }) + } + + fn contains_char(&self, dpy: *mut Display, c: char) -> bool { + // SAFETY: self.xfont is known to be non-null + unsafe { XftCharExists(dpy, self.xfont, c as u32) == 1 } + } + + pub(crate) fn get_exts(&self, dpy: *mut Display, txt: &str) -> Result<(u32, u32)> { + // SAFETY: + // - allocation failures are explicitly handled + // - invalid C strings are converted to Rust Errors + // - self.xfont is known to be non-null + unsafe { + // https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html#tymethod.alloc + let layout = Layout::new::(); + let ptr = alloc(layout); + if ptr.is_null() { + handle_alloc_error(layout); + } + let ext = ptr as *mut XGlyphInfo; + + let c_str = CString::new(txt)?; + XftTextExtentsUtf8( + dpy, + self.xfont, + c_str.as_ptr() as *mut u8, + c_str.as_bytes().len() as i32, + ext, + ); + + Ok(((*ext).xOff as u32, self.h)) + } + } + + /// Find a font that can handle a given character using fontconfig and this font's pattern + fn fallback_for_char(&self, dpy: *mut Display, c: char) -> Result { + let pat = self.fc_font_match(dpy, c)?; + + Font::try_new_from_pattern(dpy, pat) + } + + fn fc_font_match(&self, dpy: *mut Display, c: char) -> Result<*mut FcPattern> { + // SAFETY: + // - allocation failures are explicitly handled + // - Null pointers are checked and explicitly converted to Rust Errors + // - valid constant values from the fontconfig_sys crate are used for C string parameters + // - null pointer parameter for FcConfigSubstutute config param (first argument) is valid + // as documented here: https://man.archlinux.org/man/extra/fontconfig/FcConfigSubstitute.3.en + unsafe { + let charset = FcCharSetCreate(); + FcCharSetAddChar(charset, c as u32); + + let pat = FcPatternDuplicate(self.pattern as *const _); + FcPatternAddCharSet(pat, FC_CHARSET.as_ptr(), charset); + FcPatternAddBool(pat, FC_SCALABLE.as_ptr(), 1); // FcTrue=1 + + FcConfigSubstitute(std::ptr::null::() as *mut _, pat, FcMatchPattern); + FcDefaultSubstitute(pat); + + // https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html#tymethod.alloc + let layout = Layout::new::(); + let ptr = alloc(layout); + if ptr.is_null() { + handle_alloc_error(layout); + } + let res = ptr as *mut FcResult; + + // Passing the pointer from fontconfig_sys to x11 here + let font_match = XftFontMatch(dpy, SCREEN, pat as *const _, res); + + FcCharSetDestroy(charset); + FcPatternDestroy(pat); + + if font_match.is_null() { + Err(Error::NoFallbackFontForChar(c)) + } else { + Ok(font_match as *mut _) + } + } + } +} diff --git a/crates/penrose_ui/src/core/mod.rs b/crates/penrose_ui/src/core/mod.rs index 6e7b0f36..715d8b45 100644 --- a/crates/penrose_ui/src/core/mod.rs +++ b/crates/penrose_ui/src/core/mod.rs @@ -1,172 +1,252 @@ //! The core [`Draw`] and [`Context`] structs for rendering UI elements. +//! +//! If you are only interested in adding functionality to the penrose [StatusBar][0] then you +//! do not need to worry about the use and implementation of `Draw` and `Context`: the abstractions +//! provided by the [Widget][1] trait should be sufficient for your needs. If however you wish +//! to build your own minimal text based UI from scratch then your might find these structs useful. +//! +//! > **NOTE**: As mentioned in the crate level docs, this crate is definitely not intended as a +//! > fully general purpose graphics API. You are unlikely to find full support for operations that +//! > are not required for implementing a simple text based status bar. +//! +//! [0]: crate::StatusBar +//! [1]: crate::bar::widgets::Widget use crate::{Error, Result}; -use cairo::{Matrix, Operator, XCBConnection, XCBDrawable, XCBSurface, XCBVisualType}; -use pango::{EllipsizeMode, FontDescription, SCALE}; -use pangocairo::functions::{create_layout, show_layout}; use penrose::{ pure::geometry::Rect, x::{WinType, XConn}, - x11rb::XcbConn, + x11rb::RustConn, Color, Xid, }; -use std::collections::HashMap; +use std::{ + alloc::{alloc, dealloc, handle_alloc_error, Layout}, + cmp::max, + collections::HashMap, + ffi::CString, +}; use tracing::{debug, info}; -use x11rb::{connection::Connection, protocol::xproto::Screen}; - -// A rust version of XCB's `xcb_visualtype_t` struct for FFI. -// Taken from https://github.com/psychon/x11rb/blob/c3894c092101a16cedf4c45e487652946a3c4284/cairo-example/src/main.rs -#[derive(Debug, Clone, Copy)] -#[repr(C)] -struct XcbVisualtypeT { - pub visual_id: u32, - pub class: u8, - pub bits_per_rgb_value: u8, - pub colormap_entries: u16, - pub red_mask: u32, - pub green_mask: u32, - pub blue_mask: u32, - pub pad0: [u8; 4], -} +use x11::{ + xft::{XftColor, XftColorAllocName, XftDrawCreate, XftDrawStringUtf8}, + xlib::{ + CapButt, Display, Drawable, False, JoinMiter, LineSolid, Window, XCopyArea, XCreateGC, + XCreatePixmap, XDefaultColormap, XDefaultDepth, XDefaultVisual, XDrawRectangle, + XFillRectangle, XFreeGC, XFreePixmap, XOpenDisplay, XSetForeground, XSetLineAttributes, + XSync, GC, + }, +}; + +mod fontset; +use fontset::Fontset; -#[derive(Clone, Debug, PartialEq)] -/// A set of styling options for a text string +pub(crate) const SCREEN: i32 = 0; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// A set of styling options for a text string that is to be rendered using [Draw]. +/// +/// The font itself is specified on the [Draw] instance when it is created or by using the +/// `set_font` method. pub struct TextStyle { - /// Font name to use for rendering - pub font: String, - /// Point size to render the font at - pub point_size: i32, - /// Foreground color in 0xRRGGBB format + /// The foreground color to be used for rendering the text itself. pub fg: Color, - /// Optional background color in 0xRRGGBB format (default to current background if None) + /// The background color for the region behind the text (defaults to the [Draw] background if None). pub bg: Option, - /// Pixel padding around this piece of text - pub padding: (f64, f64), + /// Padding in pixels around the text to the left and right. + pub padding: (u32, u32), +} + +#[derive(Debug)] +struct Surface { + drawable: Drawable, + gc: GC, + r: Rect, } -/// Your application should create a single [`Draw`] struct to manage the windows and surfaces it -/// needs to render your UI. See the [`Context`] struct for how to draw to the surfaces you have -/// created. +/// A minimal back end for rendering simple text based UIs. +/// +/// > **NOTE**: Your application should create a single [Draw] struct to manage the windows and +/// > surfaces it needs to render your UI. See the [Context] struct for how to draw to the surfaces +/// > you have created. +/// +/// # Fonts +/// ### Specifying fonts +/// Font names need to be in a form that can be parsed by `xft`. The simplest way to find the valid +/// font names on your system is via the `fc-list` program like so: +/// ```sh +/// $ fc-list -f '%{family}\n' | sort -u +/// ``` +/// [Draw] will automtically append `:size={point_size}` to the font name when loading the font via +/// xft. The [Arch wiki page on fonts][0] is a useful resource on how X11 fonts work if you are +/// interested in futher reading. +/// +/// ### Font fallback for missing glyphs +/// [Draw] makes use of [fontconfig][1] to locate appropriate fallback fonts on your system when a +/// glyph is encountered that the primary font does not support. If you wish to modify how fallback +/// fonts are selected you will need to modify your [font-conf][2] (the Arch wiki has a [good page][3] +/// on how to do this if you are looking for a reference). +/// +/// # Example usage +/// > Please see the crate [examples directory][4] for more examples. +/// ```no_run +/// use penrose::{ +/// pure::geometry::Rect, +/// x::{Atom, WinType}, +/// Color, +/// }; +/// use penrose_ui::Draw; +/// use std::{thread::sleep, time::Duration}; +/// +/// let fg = Color::try_from("#EBDBB2").unwrap(); +/// let bg = Color::try_from("#282828").unwrap(); +/// let mut drw = Draw::new("mono", 12, bg).unwrap(); +/// let w = drw.new_window( +/// WinType::InputOutput(Atom::NetWindowTypeDock), +/// Rect::new(0, 0, 300, 50), +/// false, +/// ).unwrap(); +/// +/// let mut ctx = drw.context_for(w).unwrap(); +/// ctx.draw_text("Hello from penrose_ui!", 0, (10, 0), fg).unwrap(); +/// ctx.flush(); +/// drw.flush(w).unwrap(); +/// +/// sleep(Duration::from_secs(2)); +/// ``` +/// +/// [0]: https://wiki.archlinux.org/title/Fonts +/// [1]: https://www.freedesktop.org/wiki/Software/fontconfig/ +/// [2]: https://man.archlinux.org/man/fonts-conf.5 +/// [3]: https://wiki.archlinux.org/title/Font_configuration#Set_default_or_fallback_fonts +/// [4]: https://github.com/sminez/penrose/tree/develop/crates/penrose_ui/examples #[derive(Debug)] pub struct Draw { - /// The underlying [`XConn`] implementation used to communicate with the X server - pub conn: XcbConn, - fonts: HashMap, - surfaces: HashMap, + pub(crate) conn: RustConn, + dpy: *mut Display, + fs: Fontset, + bg: Color, + surfaces: HashMap, + colors: HashMap, +} + +impl Drop for Draw { + fn drop(&mut self) { + // SAFETY: all pointers being freed are known to be non-null + unsafe { + for (_, s) in self.surfaces.drain() { + XFreePixmap(self.dpy, s.drawable); + XFreeGC(self.dpy, s.gc); + } + } + } } impl Draw { - /// Construct a new `Draw` instance backed with an [`XcbConn`]. + /// Construct a new [Draw] instance using the specified font and background color. + /// + /// ### Font names + /// See the top level docs for [Draw] for details on how fonts are specified. /// + /// ### Errors /// This method will error if it is unable to establish a connection with the X server. - pub fn new() -> Result { + pub fn new(font: &str, point_size: u8, bg: impl Into) -> Result { + let conn = RustConn::new()?; + // SAFETY: + // - passing NULL as the argument here is valid as documented here: https://man.archlinux.org/man/extra/libx11/XOpenDisplay.3.en + let dpy = unsafe { XOpenDisplay(std::ptr::null()) }; + let mut colors = HashMap::new(); + let bg = bg.into(); + colors.insert(bg, XColor::try_new(dpy, &bg)?); + Ok(Self { - conn: XcbConn::new()?, - fonts: HashMap::new(), + conn, + dpy, + fs: Fontset::try_new(dpy, &format!("{font}:size={point_size}"))?, surfaces: HashMap::new(), + bg, + colors, }) } - /// Create a new X window and initialise a cairo surface for drawing. + /// Create a new X window with an initialised surface for drawing. + /// + /// Destroying this window should be carried out using the `destroy_window_and_surface` method + /// so that the associated graphics state is also cleaned up correctly. pub fn new_window(&mut self, ty: WinType, r: Rect, managed: bool) -> Result { info!(?ty, ?r, %managed, "creating new window"); let id = self.conn.create_window(ty, r, managed)?; - debug!("getting screen details"); - let screen = &self.conn.connection().setup().roots[0]; + debug!("initialising graphics context and pixmap"); + let root = *self.conn.root() as Window; + // SAFETY: self.dpy is non-null and screen index 0 is always valid + let (drawable, gc) = unsafe { + let depth = XDefaultDepth(self.dpy, SCREEN) as u32; + let drawable = XCreatePixmap(self.dpy, root, r.w, r.h, depth); + let gc = XCreateGC(self.dpy, root, 0, std::ptr::null_mut()); + XSetLineAttributes(self.dpy, gc, 1, LineSolid, CapButt, JoinMiter); - debug!("creating surface"); - let surface = self.surface(*id, screen, r.w as i32, r.h as i32)?; - self.surfaces.insert(id, surface); - - Ok(id) - } - - fn surface(&self, id: u32, screen: &Screen, w: i32, h: i32) -> Result { - let mut visual = self.find_xcb_visualtype(screen.root_visual); - - let surface = unsafe { - debug!(%id, "calling cairo::XCBSurface::create"); - cairo::XCBSurface::create( - &XCBConnection::from_raw_none(self.conn.connection().get_raw_xcb_connection() as _), - &XCBDrawable(id), - &XCBVisualType::from_raw_none(&mut visual as *mut _ as _), - w, - h, - )? + (drawable, gc) }; - debug!(%id, "setting surface size"); - surface.set_size(w, h)?; + self.surfaces.insert(id, Surface { r, gc, drawable }); - Ok(surface) + Ok(id) } - fn find_xcb_visualtype(&self, visual_id: u32) -> XcbVisualtypeT { - for root in &self.conn.connection().setup().roots { - for depth in &root.allowed_depths { - for visual in &depth.visuals { - if visual.visual_id == visual_id { - return XcbVisualtypeT { - visual_id: visual.visual_id, - class: visual.class.into(), - bits_per_rgb_value: visual.bits_per_rgb_value, - colormap_entries: visual.colormap_entries, - red_mask: visual.red_mask, - green_mask: visual.green_mask, - blue_mask: visual.blue_mask, - pad0: [0; 4], - }; - } - } + /// Destroy the specified window along with any surface and graphics context state held + /// within this draw. + pub fn destroy_window_and_surface(&mut self, id: Xid) -> Result<()> { + self.conn.destroy_window(id)?; + if let Some(s) = self.surfaces.remove(&id) { + // SAFETY: the pointerse being freed are known to be non-null + unsafe { + XFreePixmap(self.dpy, s.drawable); + XFreeGC(self.dpy, s.gc); } } - panic!("unable to find XCB visual type") + Ok(()) } - /// Register a new font by name in the font cache so it can be used in a drawing [`Context`]. - pub fn register_font(&mut self, font_name: &str) { - let description = FontDescription::from_string(font_name); - self.fonts.insert(font_name.into(), description); + /// Set the font being used for rendering text and clear the existing cache of fallback fonts + /// for characters that are not supported by the primary font. + pub fn set_font(&mut self, font: &str, point_size: u8) -> Result<()> { + self.fs = Fontset::try_new(self.dpy, &format!("{font}:size={point_size}"))?; + + Ok(()) } - /// Retrieve the drawing [`Context`] for the given window [`Xid`]. + /// Retrieve the drawing [Context] for the given window `Xid`. /// /// This method will error if the requested id does not already have an initialised surface. - /// See the [`new_window`] method for details. - pub fn context_for(&self, id: Xid) -> Result { - let ctx = cairo::Context::new( - self.surfaces - .get(&id) - .ok_or(Error::UnintialisedSurface { id })?, - )?; + /// See the `new_window` method for details. + pub fn context_for(&mut self, id: Xid) -> Result> { + let s = self + .surfaces + .get(&id) + .ok_or(Error::UnintialisedSurface { id })?; Ok(Context { - ctx, - font: None, - fonts: self.fonts.clone(), - }) - } - - /// Construct a disposable context of the specified dimensions without requiring a - /// window [`Xid`]. - pub fn temp_context(&self, w: i32, h: i32) -> Result { - let screen = &self.conn.connection().setup().roots[0]; - let surface = self.surface(*self.conn.root(), screen, w, h)?; - let surface = surface.create_similar(cairo::Content::Color, w, h)?; - let ctx = cairo::Context::new(&surface)?; - - Ok(Context { - ctx, - font: None, - fonts: self.fonts.clone(), + id: *id as u64, + dx: 0, + dy: 0, + dpy: self.dpy, + s, + bg: self.bg, + fs: &mut self.fs, + colors: &mut self.colors, }) } /// Flush any pending requests to the X server and map the specifed window to the screen. pub fn flush(&self, id: Xid) -> Result<()> { if let Some(s) = self.surfaces.get(&id) { - s.flush() + let Rect { x, y, w, h } = s.r; + let (x, y) = (x as i32, y as i32); + + // SAFETY: the pointers for self.dpy, s.drawable, s.gc are known to be non-null + unsafe { + XCopyArea(self.dpy, s.drawable, *id as u64, s.gc, x, y, w, h, x, y); + XSync(self.dpy, False); + } }; self.conn.map(id)?; @@ -176,118 +256,225 @@ impl Draw { } } -/// A minimal drawing context for rendering text based UI elements to a cairo surface. -#[derive(Clone, Debug)] -pub struct Context { - ctx: cairo::Context, - font: Option, - fonts: HashMap, +/// A minimal drawing context for rendering text based UI elements +/// +/// A [Context] provides you with a backing pixmap for rendering your UI using simple offset and +/// rendering operations. By default, the context will be positioned in the top left corner of +/// the parent window created by your [Draw]. You can use the `translate` and `set/reset` offset +/// methods to modify where the next drawing operation will take place. +/// +/// > It is worthwhile looking at the implementation of the [StatusBar][0] struct and how it +/// > handles rendering child widgets for a real example of how to make use of the offseting +/// > functionality of this struct. +/// +/// [0]: crate::StatusBar +#[derive(Debug)] +pub struct Context<'a> { + id: u64, + dx: i32, + dy: i32, + dpy: *mut Display, + s: &'a Surface, + bg: Color, + fs: &'a mut Fontset, + colors: &'a mut HashMap, } -impl Context { - /// Set the current font by name. - /// - /// This method will error if the font has not previously been registered in the parent - /// [`Draw`] struct. - pub fn font(&mut self, font_name: &str, point_size: i32) -> Result<()> { - let mut font = self - .fonts - .get_mut(font_name) - .ok_or_else(|| Error::UnknownFont { - font: font_name.into(), - })? - .clone(); - - font.set_size(point_size * SCALE); - self.font = Some(font); - - Ok(()) +impl<'a> Context<'a> { + /// Clear the underlying surface, restoring it to the background color. + pub fn clear(&mut self) -> Result<()> { + self.fill_rect(Rect::new(0, 0, self.s.r.w, self.s.r.h), self.bg) } - /// Set the active color for following draw operations. - pub fn color(&mut self, color: &Color) { - let (r, g, b, a) = color.rgba(); - self.ctx.set_source_rgba(r, g, b, a); + /// Offset future drawing operations by an additional (dx, dy) + pub fn translate(&mut self, dx: i32, dy: i32) { + self.dx += dx; + self.dy += dy; } - /// Clear the underlying [`cairo::Context`]. - pub fn clear(&mut self) -> Result<()> { - self.ctx.save()?; - self.ctx.set_operator(Operator::Clear); - self.ctx.paint()?; - self.ctx.restore()?; - - Ok(()) + /// Set future drawing operations to apply from the origin. + pub fn reset_offset(&mut self) { + self.dx = 0; + self.dy = 0; } - /// Translate the underlying [`cairo::Context`] by a specified offset - pub fn translate(&self, dx: f64, dy: f64) { - self.ctx.translate(dx, dy) + /// Set an absolute x offset for future drawing operations. + pub fn set_x_offset(&mut self, x: i32) { + self.dx = x; } - /// Set the x offset of the underlying [`cairo::Context`] without modifying the - /// current y position. - pub fn set_x_offset(&self, x: f64) { - let (_, y_offset) = self.ctx.matrix().transform_point(0.0, 0.0); - self.ctx.set_matrix(Matrix::identity()); - self.ctx.translate(x, y_offset); + /// Set an absolute y offset for future drawing operations. + pub fn set_y_offset(&mut self, y: i32) { + self.dy = y; } - /// Set the y offset of the underlying [`cairo::Context`] without modifying the - /// current x position. - pub fn set_y_offset(&self, y: f64) { - let (x_offset, _) = self.ctx.matrix().transform_point(0.0, 0.0); - self.ctx.set_matrix(Matrix::identity()); - self.ctx.translate(x_offset, y); + fn get_or_try_init_xcolor(&mut self, c: Color) -> Result<*mut XftColor> { + Ok(self + .colors + .entry(c) + .or_insert(XColor::try_new(self.dpy, &c)?) + .0) } - /// Fill the specified area with the currently active color. - pub fn rectangle(&self, x: f64, y: f64, w: f64, h: f64) -> Result<()> { - self.ctx.rectangle(x, y, w, h); - self.ctx.fill()?; + /// Render a rectangular border using the supplied color. + pub fn draw_rect(&mut self, Rect { x, y, w, h }: Rect, color: Color) -> Result<()> { + let xcol = self.get_or_try_init_xcolor(color)?; + let (x, y) = (self.dx + x as i32, self.dy + y as i32); + + // SAFETY: + // - the pointers for self.dpy, s.drawable, s.gc are known to be non-null + // - xcol is known to be non-null so dereferencing is safe + unsafe { + XSetForeground(self.dpy, self.s.gc, (*xcol).pixel); + XDrawRectangle(self.dpy, self.s.drawable, self.s.gc, x, y, w, h); + } Ok(()) } - /// Draw the specified text using the currently active font and color. - /// - /// Returns the width and height of the area taken up by the text. - pub fn text(&self, txt: &str, h_offset: f64, padding: (f64, f64)) -> Result<(f64, f64)> { - let layout = create_layout(&self.ctx); - if let Some(ref font) = self.font { - layout.set_font_description(Some(font)); + /// Render a filled rectangle using the supplied color. + pub fn fill_rect(&mut self, Rect { x, y, w, h }: Rect, color: Color) -> Result<()> { + let xcol = self.get_or_try_init_xcolor(color)?; + let (x, y) = (self.dx + x as i32, self.dy + y as i32); + + // SAFETY: + // - the pointers for self.dpy, s.drawable, s.gc are known to be non-null + // - xcol is known to be non-null so dereferencing is safe + unsafe { + XSetForeground(self.dpy, self.s.gc, (*xcol).pixel); + XFillRectangle(self.dpy, self.s.drawable, self.s.gc, x, y, w, h); } - layout.set_text(txt); - layout.set_ellipsize(EllipsizeMode::End); + Ok(()) + } + + /// Render the provided text at the current context offset using the supplied color. + pub fn draw_text( + &mut self, + txt: &str, + h_offset: u32, + padding: (u32, u32), + c: Color, + ) -> Result<(u32, u32)> { + // SAFETY: + // - the pointers for self.dpy and s.drawable are known to be non-null + let d = unsafe { + XftDrawCreate( + self.dpy, + self.s.drawable, + XDefaultVisual(self.dpy, SCREEN), + XDefaultColormap(self.dpy, SCREEN), + ) + }; - let (w, h) = layout.pixel_size(); - let (l, r) = padding; - self.ctx.translate(l, h_offset); - show_layout(&self.ctx, &layout); - self.ctx.translate(-l, -h_offset); + let (lpad, rpad) = (padding.0 as i32, padding.1); + let (mut x, y) = (lpad + self.dx, self.dy); + let (mut total_w, mut total_h) = (x as u32, 0); + let xcol = self.get_or_try_init_xcolor(c)?; + + for (chunk, fm) in self.fs.per_font_chunks(txt).into_iter() { + let fnt = self.fs.fnt(fm); + let (chunk_w, chunk_h) = fnt.get_exts(self.dpy, chunk)?; + + // SAFETY: fnt pointer is non-null + let chunk_y = unsafe { y + h_offset as i32 + (*fnt.xfont).ascent }; + let c_str = CString::new(chunk)?; + + // SAFETY: + // - fnt.xfont is known to be non-null + // - the string character pointer and length have been obtained from a Rust CString + unsafe { + XftDrawStringUtf8( + d, + xcol, + fnt.xfont, + x, + chunk_y, + c_str.as_ptr() as *mut _, + c_str.as_bytes().len() as i32, + ); + } - let width = w as f64 + l + r; - let height = h as f64; + x += chunk_w as i32; + total_w += chunk_w; + total_h = max(total_h, chunk_h); + } - Ok((width, height)) + Ok((total_w + rpad, total_h)) } - /// Determine the width and height required to render a specific piece of text - /// using the current font without rendering it to the underlying [`cairo::Context`]. - pub fn text_extent(&self, s: &str) -> Result<(f64, f64)> { - let layout = create_layout(&self.ctx); - if let Some(ref font) = self.font { - layout.set_font_description(Some(font)); + /// Determine the width and height taken up by a given string in pixels. + pub fn text_extent(&mut self, txt: &str) -> Result<(u32, u32)> { + let (mut w, mut h) = (0, 0); + for (chunk, fm) in self.fs.per_font_chunks(txt) { + let (cw, ch) = self.fs.fnt(fm).get_exts(self.dpy, chunk)?; + w += cw; + h = max(h, ch); } - layout.set_text(s); - let (w, h) = layout.pixel_size(); - Ok((w as f64, h as f64)) + Ok((w, h)) } /// Flush pending requests to the X server. + /// + /// This method does not need to be called explicitly if the flush method for + /// the parent [Draw] is being called as well. pub fn flush(&self) { - self.ctx.target().flush(); + let Surface { + r: Rect { w, h, .. }, + gc, + drawable, + } = *self.s; + + // SAFETY: + // - the pointers for self.dpy, drawable and gc are known to be non-null + unsafe { + XCopyArea(self.dpy, drawable, self.id, gc, 0, 0, w, h, 0, 0); + XSync(self.dpy, False); + } + } +} + +#[derive(Debug)] +struct XColor(*mut XftColor); + +impl Drop for XColor { + fn drop(&mut self) { + let layout = Layout::new::(); + // SAFETY: the memory being deallocated was allocated on creation of the XColor + unsafe { dealloc(self.0 as *mut u8, layout) } + } +} + +impl XColor { + fn try_new(dpy: *mut Display, c: &Color) -> Result { + // SAFETY: this private method is only called with a non-null dpy pointer + let inner = unsafe { try_xftcolor_from_name(dpy, &c.as_rgb_hex_string())? }; + + Ok(Self(inner)) + } +} + +unsafe fn try_xftcolor_from_name(dpy: *mut Display, color: &str) -> Result<*mut XftColor> { + // https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html#tymethod.alloc + let layout = Layout::new::(); + let ptr = alloc(layout); + if ptr.is_null() { + handle_alloc_error(layout); + } + + let c_name = CString::new(color)?; + let res = XftColorAllocName( + dpy, + XDefaultVisual(dpy, SCREEN), + XDefaultColormap(dpy, SCREEN), + c_name.as_ptr(), + ptr as *mut XftColor, + ); + + if res == 0 { + Err(Error::UnableToAllocateColor) + } else { + Ok(ptr as *mut XftColor) } } diff --git a/crates/penrose_ui/src/lib.rs b/crates/penrose_ui/src/lib.rs index ede3f965..94cbd2db 100644 --- a/crates/penrose_ui/src/lib.rs +++ b/crates/penrose_ui/src/lib.rs @@ -9,11 +9,15 @@ //! //! ## Getting started //! The main functionality of this crate is provided through the [`Draw`] nad [`Context`] structs -//! which allow for simple graphics rendering backed by the [pango][1] and [cairo][2] libraries. +//! which allow for simple graphics rendering backed by the xlib and fontconfig libraries. +//! +//! ## A note on the use of unsafe code +//! Given the aims of this crate and the desire to pull in as few dependencies as possible, it +//! makes heavy use of `unsafe` to wrap C FFI calls. Please make sure that you read the available +//! documentation and `SAFETY` comments in the source code to understand what is happening under +//! the hood if you have any concerns about this. //! //! [0]: https://github.com/sminez/penrose -//! [1]: https://pango.gnome.org/ -//! [2]: https://www.cairographics.org/ #![warn( clippy::complexity, clippy::correctness, @@ -22,7 +26,8 @@ missing_debug_implementations, missing_docs, rust_2018_idioms, - rustdoc::all + rustdoc::all, + clippy::undocumented_unsafe_blocks )] #![doc( html_logo_url = "https://raw.githubusercontent.com/sminez/penrose/develop/icon.svg", @@ -30,6 +35,7 @@ )] use penrose::{x::XConn, Color, Xid}; +use std::ffi::NulError; pub mod bar; pub mod core; @@ -42,10 +48,6 @@ use bar::widgets::{ActiveWindowName, CurrentLayout, RootWindowName, Workspaces}; /// Error variants from penrose_ui library. #[derive(thiserror::Error, Debug)] pub enum Error { - /// An error was returned by the cairo crate when attempting to render graphics - #[error(transparent)] - Cairo(#[from] cairo::Error), - /// Creation of a [`Color`] from a string hex code was invalid #[error("Invalid Hex color code: {code}")] InvalidHexColor { @@ -53,6 +55,14 @@ pub enum Error { code: String, }, + /// The specified character can not be rendered by any font on this system + #[error("Unable to find a fallback font for '{0}'")] + NoFallbackFontForChar(char), + + /// A string being passed to underlying C APIs contained an internal null byte + #[error(transparent)] + NulError(#[from] NulError), + /// Unable to parse an integer from a provided string. #[error(transparent)] ParseInt(#[from] std::num::ParseIntError), @@ -61,24 +71,29 @@ pub enum Error { #[error(transparent)] Penrose(#[from] penrose::Error), - /// We were unable to create a text layout using pango - #[error("unable to create pango layout")] - UnableToCreateLayout, + /// Unable to allocate a requested color + #[error("Unable to allocate the requested color using Xft")] + UnableToAllocateColor, + + /// Unable to open a requested font + #[error("Unable to open '{0}' as a font using Xft")] + UnableToOpenFont(String), + + /// Unable to open a font using an Xft font pattern + #[error("Unable to open font from FcPattern using Xft")] + UnableToOpenFontPattern, + + /// Unable to parse an Xft font pattern + #[error("Unable to parse '{0}' as an Xft font patten")] + UnableToParseFontPattern(String), /// An attempt was made to work with a surface for a window that was not initialised /// by the [`Draw`] instance being used. - #[error("no cairo surface for {id}")] + #[error("no surface for {id}")] UnintialisedSurface { /// The window id requested id: Xid, }, - - /// An attempt was made to use a font that has not been registered - #[error("'{font}' is has not been registered as a font")] - UnknownFont { - /// The unknown font name that was requested - font: String, - }, } /// A Result where the error type is a penrose_ui [`Error`] @@ -88,7 +103,9 @@ pub type Result = std::result::Result; /// WM_NAME property of the root window. pub fn status_bar( height: u32, - style: &TextStyle, + font: &str, + point_size: u8, + style: TextStyle, highlight: impl Into, empty_ws: impl Into, position: Position, @@ -100,24 +117,25 @@ pub fn status_bar( position, height, style.bg.unwrap_or_else(|| 0x000000.into()), - &[&style.font], + font, + point_size, vec![ Box::new(Workspaces::new(style, highlight, empty_ws)), Box::new(CurrentLayout::new(style)), Box::new(ActiveWindowName::new( max_active_window_chars, - &TextStyle { + TextStyle { bg: Some(highlight), - padding: (6.0, 4.0), - ..style.clone() + padding: (6, 4), + ..style }, true, false, )), Box::new(RootWindowName::new( - &TextStyle { - padding: (4.0, 2.0), - ..style.clone() + TextStyle { + padding: (4, 2), + ..style }, false, true, diff --git a/examples/status_bar/main.rs b/examples/status_bar/main.rs index 3630e0ff..248d3f7a 100644 --- a/examples/status_bar/main.rs +++ b/examples/status_bar/main.rs @@ -107,14 +107,12 @@ fn main() -> Result<()> { let conn = RustConn::new()?; let key_bindings = parse_keybindings_with_xmodmap(raw_key_bindings())?; let style = TextStyle { - font: FONT.to_string(), - point_size: 8, fg: WHITE.into(), bg: Some(BLACK.into()), - padding: (2.0, 2.0), + padding: (2, 2), }; - let bar = status_bar(BAR_HEIGHT_PX, &style, BLUE, GREY, Position::Top).unwrap(); + let bar = status_bar(BAR_HEIGHT_PX, FONT, 8, style, BLUE, GREY, Position::Top).unwrap(); let wm = bar.add_to(WindowManager::new( config, diff --git a/src/core/mod.rs b/src/core/mod.rs index dcd82fba..16167194 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -441,6 +441,8 @@ where /// explicitly before calling this method or as part of a startup hook. pub fn run(mut self) -> Result<()> { info!("registering SIGCHILD signal handler"); + // SAFETY: there is no previous signal handler so we are safe to set our own without needing + // to worry about UB from the previous handler being invalid. if let Err(e) = unsafe { signal(Signal::SIGCHLD, SigHandler::SigIgn) } { panic!("unable to set signal handler: {}", e); } diff --git a/src/lib.rs b/src/lib.rs index fae55e51..c7e3a980 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,14 +58,15 @@ missing_debug_implementations, missing_docs, rust_2018_idioms, - rustdoc::all + rustdoc::all, + clippy::undocumented_unsafe_blocks )] #![doc( html_logo_url = "https://raw.githubusercontent.com/sminez/penrose/develop/icon.svg", issue_tracker_base_url = "https://github.com/sminez/penrose/issues/" )] -#[cfg(feature = "x11rb-xcb")] +#[cfg(feature = "x11rb")] use ::x11rb::{ errors::{ConnectError, ConnectionError, ReplyError, ReplyOrIdError}, x11_utils::X11Error, @@ -81,7 +82,7 @@ mod macros; pub mod pure; pub mod util; pub mod x; -#[cfg(feature = "x11rb-xcb")] +#[cfg(feature = "x11rb")] pub mod x11rb; #[doc(inline)] @@ -198,27 +199,27 @@ pub enum Error { // set of common error variants that they can be mapped to without // needing to extend the enum conditionally when flags are enabled /// An error that occurred while connecting to an X11 server - #[cfg(feature = "x11rb-xcb")] + #[cfg(feature = "x11rb")] #[error(transparent)] X11rbConnect(#[from] ConnectError), /// An error that occurred on an already established X11 connection - #[cfg(feature = "x11rb-xcb")] + #[cfg(feature = "x11rb")] #[error(transparent)] X11rbConnection(#[from] ConnectionError), /// An error that occurred with some request. - #[cfg(feature = "x11rb-xcb")] + #[cfg(feature = "x11rb")] #[error(transparent)] X11rbReplyError(#[from] ReplyError), /// An error caused by some request or by the exhaustion of IDs. - #[cfg(feature = "x11rb-xcb")] + #[cfg(feature = "x11rb")] #[error(transparent)] X11rbReplyOrIdError(#[from] ReplyOrIdError), /// Representation of an X11 error packet that was sent by the server. - #[cfg(feature = "x11rb-xcb")] + #[cfg(feature = "x11rb")] #[error("X11 error: {0:?}")] X11rbX11Error(X11Error), } @@ -227,41 +228,37 @@ pub enum Error { pub type Result = std::result::Result; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// A simple RGBA based color pub struct Color { - r: f64, - g: f64, - b: f64, - a: f64, + rgba_hex: u32, } -// helper for methods in Color -macro_rules! _f2u { { $f:expr, $s:expr } => { (($f * 255.0) as u32) << $s } } - impl Color { /// Create a new Color from a hex encoded u32: 0xRRGGBB or 0xRRGGBBAA - pub fn new_from_hex(hex: u32) -> Self { - let floats: Vec = hex - .to_be_bytes() - .iter() - .map(|n| *n as f64 / 255.0) - .collect(); - - let (r, g, b, a) = (floats[0], floats[1], floats[2], floats[3]); - Self { r, g, b, a } + pub fn new_from_hex(rgba_hex: u32) -> Self { + Self { rgba_hex } } /// The RGB information of this color as 0.0-1.0 range floats representing /// proportions of 255 for each of R, G, B pub fn rgb(&self) -> (f64, f64, f64) { - (self.r, self.g, self.b) + let (r, g, b, _) = self.rgba(); + + (r, g, b) } /// The RGBA information of this color as 0.0-1.0 range floats representing /// proportions of 255 for each of R, G, B, A pub fn rgba(&self) -> (f64, f64, f64, f64) { - (self.r, self.g, self.b, self.a) + let floats: Vec = self + .rgba_hex + .to_be_bytes() + .iter() + .map(|n| *n as f64 / 255.0) + .collect(); + + (floats[0], floats[1], floats[2], floats[3]) } /// Render this color as a #RRGGBB hew color string @@ -271,17 +268,17 @@ impl Color { /// 0xRRGGBB representation of this Color (no alpha information) pub fn rgb_u32(&self) -> u32 { - _f2u!(self.r, 16) + _f2u!(self.g, 8) + _f2u!(self.b, 0) + self.rgba_hex >> 8 } /// 0xRRGGBBAA representation of this Color pub fn rgba_u32(&self) -> u32 { - _f2u!(self.r, 24) + _f2u!(self.g, 16) + _f2u!(self.b, 8) + _f2u!(self.a, 0) + self.rgba_hex } /// 0xAARRGGBB representation of this Color pub fn argb_u32(&self) -> u32 { - _f2u!(self.a, 24) + _f2u!(self.r, 16) + _f2u!(self.g, 8) + _f2u!(self.b, 0) + ((self.rgba_hex & 0x000000FF) << 24) + (self.rgba_hex >> 8) } } @@ -291,17 +288,23 @@ impl From for Color { } } +macro_rules! _f2u { { $f:expr, $s:expr } => { (($f * 255.0) as u32) << $s } } + impl From<(f64, f64, f64)> for Color { fn from(rgb: (f64, f64, f64)) -> Self { let (r, g, b) = rgb; - Self { r, g, b, a: 1.0 } + let rgba_hex = _f2u!(r, 24) + _f2u!(g, 16) + _f2u!(b, 8) + _f2u!(1.0, 0); + + Self { rgba_hex } } } impl From<(f64, f64, f64, f64)> for Color { fn from(rgba: (f64, f64, f64, f64)) -> Self { let (r, g, b, a) = rgba; - Self { r, g, b, a } + let rgba_hex = _f2u!(r, 24) + _f2u!(g, 16) + _f2u!(b, 8) + _f2u!(a, 0); + + Self { rgba_hex } } } diff --git a/src/x11rb/mod.rs b/src/x11rb/mod.rs index bf240b3e..63f4abf1 100644 --- a/src/x11rb/mod.rs +++ b/src/x11rb/mod.rs @@ -44,10 +44,12 @@ use x11rb::{ }, rust_connection::RustConnection, wrapper::ConnectionExt as _, - xcb_ffi::XCBConnection, CURRENT_TIME, }; +#[cfg(feature = "x11rb-xcb")] +use x11rb::xcb_ffi::XCBConnection; + pub mod conversions; use conversions::convert_event; @@ -109,9 +111,11 @@ impl Conn { } } +#[cfg(feature = "x11rb-xcb")] /// An C based connection to the X server using an [XCBConnection]. pub type XcbConn = Conn; +#[cfg(feature = "x11rb-xcb")] impl Conn { /// Construct an X11rbConnection backed by the [x11rb][crate::x11rb] backend using /// [x11rb::xcb_ffi::XCBConnection]. @@ -223,6 +227,13 @@ where Ok(id) } + + /// Destroy the window identified by the given `Xid`. + pub fn destroy_window(&self, id: Xid) -> Result<()> { + self.conn.destroy_window(*id)?; + + Ok(()) + } } impl XConn for Conn