diff --git a/Cargo.toml b/Cargo.toml index 0afda63..e9c76ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ smithay-client-toolkit = "0.16" tiny-skia = { version = "0.7.0", features = ["std", "simd"] } log = "0.4" -crossfont = { version = "0.5.0", features = [ - "force_system_fontconfig", -], optional = true } +# Draw title text using crossfont `--features crossfont` +crossfont = { version = "0.5.0", features = ["force_system_fontconfig"], optional = true } +# Draw title text using ab_glyph `--features ab_glyph` +ab_glyph = { version = "0.2.15", optional = true } [features] -# default = ["title"] -title = ["crossfont"] +default = ["ab_glyph"] diff --git a/README.md b/README.md index d4befc6..ea4f8aa 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,13 @@ ### Dark mode: ![image](https://user-images.githubusercontent.com/20758186/169424673-3b9fa022-f112-4928-8360-305a714ba979.png) + +## Title text: ab_glyph +By default title text is drawn with _ab_glyph_ crate. This can be disabled by disabling default features. + +## Title text: crossfont +Alternatively title text may be drawn with _crossfont_ crate. This adds a requirement on _freetype_. + +```toml +sctk-adwaita = { default-features = false, features = ["crossfont"] } +``` diff --git a/src/title.rs b/src/title.rs index 18dd49b..f4fa122 100644 --- a/src/title.rs +++ b/src/title.rs @@ -3,14 +3,19 @@ use tiny_skia::{Color, Pixmap}; #[cfg(feature = "crossfont")] mod crossfont_renderer; -#[cfg(not(feature = "crossfont"))] +#[cfg(all(not(feature = "crossfont"), feature = "ab_glyph"))] +mod ab_glyph_renderer; + +#[cfg(all(not(feature = "crossfont"), not(feature = "ab_glyph")))] mod dumb; #[derive(Debug)] pub struct TitleText { #[cfg(feature = "crossfont")] imp: crossfont_renderer::CrossfontTitleText, - #[cfg(not(feature = "crossfont"))] + #[cfg(all(not(feature = "crossfont"), feature = "ab_glyph"))] + imp: ab_glyph_renderer::AbGlyphTitleText, + #[cfg(all(not(feature = "crossfont"), not(feature = "ab_glyph")))] imp: dumb::DumbTitleText, } @@ -21,7 +26,12 @@ impl TitleText { .ok() .map(|imp| Self { imp }); - #[cfg(not(feature = "crossfont"))] + #[cfg(all(not(feature = "crossfont"), feature = "ab_glyph"))] + return Some(Self { + imp: ab_glyph_renderer::AbGlyphTitleText::new(color), + }); + + #[cfg(all(not(feature = "crossfont"), not(feature = "ab_glyph")))] { let _ = color; return None; diff --git a/src/title/Cantarell-Regular.ttf b/src/title/Cantarell-Regular.ttf new file mode 100644 index 0000000..0d4fd9a Binary files /dev/null and b/src/title/Cantarell-Regular.ttf differ diff --git a/src/title/ab_glyph_renderer.rs b/src/title/ab_glyph_renderer.rs new file mode 100644 index 0000000..fb7feaf --- /dev/null +++ b/src/title/ab_glyph_renderer.rs @@ -0,0 +1,123 @@ +//! Title renderer using ab_glyph & Cantarell-Regular.ttf (SIL Open Font Licence v1.1). +//! +//! Uses embedded font & requires no dynamically linked dependencies. +use ab_glyph::{point, Font, FontRef, Glyph, PxScale, ScaleFont}; +use tiny_skia::{Color, Pixmap, PremultipliedColorU8}; + +const CANTARELL: &[u8] = include_bytes!("Cantarell-Regular.ttf"); +/// Equivalent to ~10.5pt size, should match the crossfont version fairly well. +const DEFAULT_PX_SIZE: f32 = 19.0; + +#[derive(Debug)] +pub struct AbGlyphTitleText { + title: String, + font: FontRef<'static>, + size: PxScale, + color: Color, + pixmap: Option, +} + +impl AbGlyphTitleText { + pub fn new(color: Color) -> Self { + // TODO Try to pick system default font & size then fallback? + let font = FontRef::try_from_slice(CANTARELL).unwrap(); + let size = PxScale::from(DEFAULT_PX_SIZE); + + Self { + title: <_>::default(), + font, + size, + color, + pixmap: None, + } + } + + pub fn update_scale(&mut self, scale: u32) { + let new_scale = PxScale::from(DEFAULT_PX_SIZE * scale as f32); + if (self.size.x - new_scale.x).abs() > f32::EPSILON { + self.size = new_scale; + self.pixmap = self.render(); + } + } + + pub fn update_title(&mut self, title: impl Into) { + let new_title = title.into(); + if new_title != self.title { + self.title = new_title; + self.pixmap = self.render(); + } + } + + pub fn update_color(&mut self, color: Color) { + if color != self.color { + self.color = color; + self.pixmap = self.render(); + } + } + + pub fn pixmap(&self) -> Option<&Pixmap> { + self.pixmap.as_ref() + } + + /// Render returning the new `Pixmap`. + fn render(&self) -> Option { + let font = self.font.as_scaled(self.size); + + let glyphs = self.layout(); + let last_glyph = glyphs.last()?; + let width = (last_glyph.position.x + font.h_advance(last_glyph.id)).ceil() as u32; + let height = font.height().ceil() as u32; + + let mut pixmap = Pixmap::new(width, height)?; + + let pixels = pixmap.pixels_mut(); + + for glyph in glyphs { + if let Some(outline) = self.font.outline_glyph(glyph) { + let bounds = outline.px_bounds(); + let left = bounds.min.x as u32; + let top = bounds.min.y as u32; + outline.draw(|x, y, c| { + let p_idx = (top + y) * width + (left + x); + let old_alpha_u8 = pixels[p_idx as usize].alpha(); + let new_alpha = c + (old_alpha_u8 as f32 / 255.0); + if let Some(px) = PremultipliedColorU8::from_rgba( + (self.color.red() * new_alpha * 255.0) as _, + (self.color.green() * new_alpha * 255.0) as _, + (self.color.blue() * new_alpha * 255.0) as _, + (new_alpha * 255.0) as _, + ) { + pixels[p_idx as usize] = px; + } + }) + } + } + + Some(pixmap) + } + + /// Simple single-line glyph layout. + fn layout(&self) -> Vec { + let font = self.font.as_scaled(self.size); + + let mut caret = point(0.0, font.ascent()); + let mut last_glyph: Option = None; + let mut target = Vec::new(); + for c in self.title.chars() { + if c.is_control() { + continue; + } + let mut glyph = font.scaled_glyph(c); + if let Some(previous) = last_glyph.take() { + caret.x += font.kern(previous.id, glyph.id); + } + glyph.position = caret; + + last_glyph = Some(glyph.clone()); + caret.x += font.h_advance(glyph.id); + + target.push(glyph); + } + target + } +}