diff --git a/Cargo.toml b/Cargo.toml index 2d3f1c2..6dfe6e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ dirs = "6.0.0" gdk-pixbuf = { version = "0.21.5", optional = true } nix = { version = "0.31.3", features = ["event", "fs", "mman", "process", "time"] } pam-rs = "0.9.5" -pango = "0.22.0" -pangocairo = "0.22.0" +pango = { version = "0.22.0", optional = true } +pangocairo = { version = "0.22.0", optional = true } serde = { version = "1.0.228", features = [ "derive" ] } tracing = "0.1.41" tracing-subscriber = "0.3.20" @@ -27,8 +27,9 @@ xkbcommon = "0.9.0" zeroize = "1.8.2" [features] -default = ["gdk-pixbuf"] +default = ["gdk-pixbuf", "pango"] gdk-pixbuf = ["dep:gdk-pixbuf"] +pango = ["dep:pango", "dep:pangocairo"] [build-dependencies] time = { version = "0.3.47", features = ["formatting"] } diff --git a/src/args.rs b/src/args.rs index 61fa723..5e5f6f0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -12,8 +12,9 @@ use clap::{ }; use clap_complete::{Shell, aot::generate as generate_completions}; -use crate::util::{ - BackgroundImageScale, BackgroundType, FontSlant, FontWeight, InputVisibility, LogLevel, Rgba, +use crate::{ + font::{FontSlant, FontWeight}, + util::{BackgroundImageScale, BackgroundType, InputVisibility, LogLevel, Rgba}, }; /// Customisable, minimalist screen locker for Wayland diff --git a/src/config.rs b/src/config.rs index bc16a0b..c38795e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,8 @@ use tracing::debug; use crate::{ args::NLockArgs, - util::{BackgroundImageScale, BackgroundType, FontSlant, FontWeight, InputVisibility, Rgba}, + font::{FontSlant, FontWeight}, + util::{BackgroundImageScale, BackgroundType, InputVisibility, Rgba}, }; const CONFIG_FILE_NAME: &str = "nlock.toml"; diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 0000000..1349356 --- /dev/null +++ b/src/font.rs @@ -0,0 +1,307 @@ +pub trait FontInfo { + fn font_height(&self) -> f64; +} + +pub trait TextInfo { + fn text_height(&self) -> f64; + fn text_width(&self) -> f64; + fn text_x(&self) -> f64; + fn text_y(&self) -> f64; +} + +pub trait ShowText { + fn show_text(&self, context: &cairo::Context) -> anyhow::Result<()>; +} + +#[cfg(not(feature = "pango"))] +mod cairo_backend { + use anyhow::Result; + use clap::ValueEnum; + use serde::Deserialize; + + use crate::{ + config::NLockConfig, + font::{FontInfo, ShowText, TextInfo}, + render::{DEFAULT_DPI, DEFAULT_SCALE}, + }; + + pub struct NLockFont { + font_height: f64, + text: String, + extents: cairo::TextExtents, + } + + impl NLockFont { + pub fn new( + config: &NLockConfig, + context: &cairo::Context, + dpi: Option, + scale: Option, + subpixel: Option, + ) -> Result { + let dpi = dpi.unwrap_or(DEFAULT_DPI); + let scale = scale.unwrap_or(DEFAULT_SCALE); + let subpixel = subpixel.unwrap_or(cairo::SubpixelOrder::Default); + + let mut fo = cairo::FontOptions::new()?; + fo.set_hint_style(cairo::HintStyle::Full); + fo.set_antialias(cairo::Antialias::Subpixel); + fo.set_subpixel_order(subpixel); + + context.set_font_options(&fo); + context.select_font_face( + &config.font.family, + config.font.slant.into(), + config.font.weight.into(), + ); + context.set_font_size((config.font.size / 72.0) * dpi * scale); + + let fe = context.font_extents()?; + let extents = context.text_extents("")?; + + Ok(Self { + font_height: fe.height(), + text: "".to_string(), + extents, + }) + } + + pub fn set_text(&mut self, context: &cairo::Context, text: T) -> Result<()> + where + T: AsRef, + { + self.text = text.as_ref().to_string(); + self.extents = context.text_extents(text.as_ref())?; + Ok(()) + } + } + + impl FontInfo for NLockFont { + fn font_height(&self) -> f64 { + self.font_height + } + } + + impl TextInfo for NLockFont { + fn text_height(&self) -> f64 { + self.extents.height() + } + + fn text_width(&self) -> f64 { + self.extents.width() + } + + fn text_x(&self) -> f64 { + self.extents.x_bearing() + } + + fn text_y(&self) -> f64 { + self.extents.y_bearing() + } + } + + impl ShowText for NLockFont { + fn show_text(&self, context: &cairo::Context) -> Result<()> { + context.show_text(&self.text)?; + Ok(()) + } + } + + #[derive(Debug, Deserialize, Copy, Clone, ValueEnum)] + #[serde(rename_all = "lowercase")] + pub enum FontSlant { + Normal, + Italic, + Oblique, + } + + impl From for cairo::FontSlant { + fn from(value: FontSlant) -> Self { + match value { + FontSlant::Normal => Self::Normal, + FontSlant::Italic => Self::Italic, + FontSlant::Oblique => Self::Oblique, + } + } + } + + #[derive(Debug, Deserialize, Copy, Clone, ValueEnum)] + #[serde(rename_all = "lowercase")] + pub enum FontWeight { + Normal, + Bold, + } + + impl From for cairo::FontWeight { + fn from(value: FontWeight) -> Self { + match value { + FontWeight::Normal => Self::Normal, + FontWeight::Bold => Self::Bold, + } + } + } +} + +#[cfg(feature = "pango")] +mod pango_backend { + use anyhow::Result; + use clap::ValueEnum; + use pango::Rectangle; + use pangocairo::functions::{create_layout, show_layout}; + use serde::Deserialize; + + use crate::{ + config::NLockConfig, + font::{FontInfo, ShowText, TextInfo}, + render::{DEFAULT_DPI, DEFAULT_SCALE}, + }; + + pub struct NLockFont { + layout: pango::Layout, + metrics: pango::FontMetrics, + text: String, + extents: Rectangle, + } + + impl NLockFont { + pub fn new( + config: &NLockConfig, + context: &cairo::Context, + dpi: Option, + scale: Option, + ) -> Self { + let dpi = dpi.unwrap_or(DEFAULT_DPI); + let scale = scale.unwrap_or(DEFAULT_SCALE); + + let mut fd = pango::FontDescription::new(); + fd.set_family(&config.font.family); + fd.set_style(config.font.slant.into()); + fd.set_weight(config.font.weight.into()); + fd.set_absolute_size(((config.font.size / 72.0) * dpi * scale) * PANGO_SCALE as f64); + + let layout = create_layout(context); + layout.set_font_description(Some(&fd)); + + let p_ctx = layout.context(); + let metrics = p_ctx.metrics(Some(&fd), None); + + layout.set_text(""); + let extents = layout.pixel_extents().0; + + Self { + layout, + metrics, + text: "".to_string(), + extents, + } + } + + pub fn set_text(&mut self, text: T) + where + T: AsRef, + { + self.text = text.as_ref().to_string(); + self.layout.set_text(text.as_ref()); + self.extents = self.layout.pixel_extents().0; + } + } + + impl FontInfo for NLockFont { + fn font_height(&self) -> f64 { + (pango_pixels(self.metrics.ascent()) + pango_pixels(self.metrics.descent())) as f64 + } + } + + impl TextInfo for NLockFont { + fn text_height(&self) -> f64 { + self.extents.height() as f64 + } + + fn text_width(&self) -> f64 { + self.extents.width() as f64 + } + + fn text_x(&self) -> f64 { + self.extents.x() as f64 + } + + fn text_y(&self) -> f64 { + self.extents.y() as f64 + } + } + + impl ShowText for NLockFont { + fn show_text(&self, context: &cairo::Context) -> Result<()> { + show_layout(context, &self.layout); + Ok(()) + } + } + + #[derive(Debug, Deserialize, Copy, Clone, ValueEnum)] + #[serde(rename_all = "lowercase")] + pub enum FontSlant { + Normal, + Italic, + Oblique, + } + + impl From for pango::Style { + fn from(value: FontSlant) -> Self { + match value { + FontSlant::Normal => Self::Normal, + FontSlant::Italic => Self::Italic, + FontSlant::Oblique => Self::Oblique, + } + } + } + + #[derive(Debug, Deserialize, Copy, Clone, ValueEnum)] + #[serde(rename_all = "lowercase")] + pub enum FontWeight { + Thin, + Ultralight, + Light, + Semilight, + Book, + Normal, + Medium, + Semibold, + Bold, + Ultrabold, + Heavy, + Ultraheavy, + } + + impl From for pango::Weight { + fn from(value: FontWeight) -> Self { + match value { + FontWeight::Thin => Self::Thin, + FontWeight::Ultralight => Self::Ultralight, + FontWeight::Light => Self::Light, + FontWeight::Semilight => Self::Semilight, + FontWeight::Book => Self::Book, + FontWeight::Normal => Self::Normal, + FontWeight::Medium => Self::Medium, + FontWeight::Semibold => Self::Semibold, + FontWeight::Bold => Self::Bold, + FontWeight::Ultrabold => Self::Ultrabold, + FontWeight::Heavy => Self::Heavy, + FontWeight::Ultraheavy => Self::Ultraheavy, + } + } + } + + // Pango scale factor + const PANGO_SCALE: i32 = 1024; + + #[inline] + /// Convert Pango units to pixels + fn pango_pixels(d: i32) -> i32 { + (d + 512) >> 10 + } +} + +#[cfg(not(feature = "pango"))] +pub use cairo_backend::*; +#[cfg(feature = "pango")] +pub use pango_backend::*; diff --git a/src/main.rs b/src/main.rs index c0c0625..16a4244 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ pub mod comm; pub mod config; pub mod event; pub mod event_loop; +pub mod font; pub mod render; pub mod seat; pub mod state; diff --git a/src/render.rs b/src/render.rs index 3a337b1..a54e0c2 100644 --- a/src/render.rs +++ b/src/render.rs @@ -3,14 +3,14 @@ use anyhow::{Result, anyhow, bail}; use cairo::SurfacePattern; -use pangocairo::functions::{create_layout, show_layout}; use tracing::warn; use crate::{ auth::AuthState, cairo_ext::CairoExt, config::NLockConfig, - util::{BackgroundImageScale, BackgroundType, InputVisibility, PANGO_SCALE, pango_pixels}, + font::{FontInfo, NLockFont, ShowText, TextInfo}, + util::{BackgroundImageScale, BackgroundType, InputVisibility}, }; pub const DEFAULT_DPI: f64 = 96.0; @@ -109,29 +109,6 @@ impl NLockRenderer { Ok(()) } - fn create_font( - &self, - config: &NLockConfig, - context: &cairo::Context, - ) -> Result<(pango::Layout, pango::FontMetrics)> { - let dpi = self.dpi.unwrap_or(DEFAULT_DPI); - let scale = self.scale.unwrap_or(DEFAULT_SCALE); - - let mut fd = pango::FontDescription::new(); - fd.set_family(&config.font.family); - fd.set_style(config.font.slant.into()); - fd.set_weight(config.font.weight.into()); - fd.set_absolute_size(((config.font.size / 72.0) * dpi * scale) * PANGO_SCALE as f64); - - let layout = create_layout(context); - layout.set_font_description(Some(&fd)); - - let p_ctx = layout.context(); - let metrics = p_ctx.metrics(Some(&fd), None); - - Ok((layout, metrics)) - } - fn draw_rounded_rect(context: &cairo::Context, x: f64, y: f64, w: f64, h: f64, r: f64) { context.new_sub_path(); context.arc(x + w - r, y + r, r, -90f64.to_radians(), 0f64.to_radians()); @@ -278,27 +255,34 @@ impl NLockRenderer { return Ok(()); } - let (layout, metrics) = self.create_font(config, context)?; + let text = config.input.mask_char.repeat(pwd_len); - let f_ascent = pango_pixels(metrics.ascent()) as f64; - let f_descent = pango_pixels(metrics.descent()) as f64; + // TODO: keep font across render cycles? + #[cfg(not(feature = "pango"))] + let font = { + let mut font = + NLockFont::new(config, context, self.dpi, self.scale, self.subpixel_order)?; + font.set_text(context, text)?; + font + }; + #[cfg(feature = "pango")] + let font = { + let mut font = NLockFont::new(config, context, self.dpi, self.scale); + font.set_text(text); + font + }; let padding_x = config.input.padding_x * buf_width; let padding_y = config.input.padding_y * buf_height; - // Calculate text extents here, so input box width can be determined - let text = config.input.mask_char.repeat(pwd_len); - layout.set_text(&text); - let text_ext = layout.pixel_extents().0; // use ink extents for drawing - let mut inner_w = buf_width * config.input.width; if config.input.fit_to_content { // Cap computed width to specified width - inner_w = (text_ext.width() as f64).min(inner_w); + inner_w = font.text_width().min(inner_w); } - let inner_h = f_ascent + f_descent; + let inner_h = font.font_height(); let inner_x = (buf_width - inner_w) / 2.0; let inner_y = (buf_height - inner_h) / 2.0; @@ -330,13 +314,13 @@ impl NLockRenderer { context.rectangle(inner_x, inner_y, inner_w, inner_h); context.clip(); - let text_x = inner_x + (inner_w - (text_ext.width() as f64)) / 2.0 - (text_ext.x() as f64); - let text_y = inner_y + (inner_h - text_ext.height() as f64) / 2.0 - (text_ext.y() as f64); + let text_x = inner_x + (inner_w - font.text_width()) / 2.0 - font.text_x(); + let text_y = inner_y + (inner_h - font.text_height()) / 2.0 - font.text_y(); // Actually draw the text context.ext_set_source_rgba(config.colors.text); context.move_to(text_x, text_y); - show_layout(context, &layout); + font.show_text(context)?; context.restore()?; diff --git a/src/util.rs b/src/util.rs index a8d60e7..0ac9574 100644 --- a/src/util.rs +++ b/src/util.rs @@ -107,60 +107,6 @@ impl<'de> Deserialize<'de> for Rgba { } } -#[derive(Debug, Deserialize, Copy, Clone, ValueEnum)] -#[serde(rename_all = "lowercase")] -pub enum FontSlant { - Normal, - Italic, - Oblique, -} - -impl From for pango::Style { - fn from(value: FontSlant) -> Self { - match value { - FontSlant::Normal => Self::Normal, - FontSlant::Italic => Self::Italic, - FontSlant::Oblique => Self::Oblique, - } - } -} - -#[derive(Debug, Deserialize, Copy, Clone, ValueEnum)] -#[serde(rename_all = "lowercase")] -pub enum FontWeight { - Thin, - Ultralight, - Light, - Semilight, - Book, - Normal, - Medium, - Semibold, - Bold, - Ultrabold, - Heavy, - Ultraheavy, -} - -impl From for pango::Weight { - fn from(value: FontWeight) -> Self { - match value { - FontWeight::Thin => Self::Thin, - FontWeight::Ultralight => Self::Ultralight, - FontWeight::Light => Self::Light, - FontWeight::Semilight => Self::Semilight, - FontWeight::Book => Self::Book, - FontWeight::Normal => Self::Normal, - FontWeight::Medium => Self::Medium, - FontWeight::Semibold => Self::Semibold, - FontWeight::Bold => Self::Bold, - FontWeight::Ultrabold => Self::Ultrabold, - FontWeight::Heavy => Self::Heavy, - FontWeight::Ultraheavy => Self::Ultraheavy, - } - } -} - #[derive(Debug, Copy, Clone, ValueEnum)] pub enum LogLevel { Trace, @@ -213,15 +159,6 @@ pub fn open_shm() -> Option { None } -#[inline] -/// Convert Pango units to pixels -pub fn pango_pixels(d: i32) -> i32 { - (d + 512) >> 10 -} - -// Pango scale factor -pub const PANGO_SCALE: i32 = 1024; - const PNG_SIG: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; // Detect if a source stream starts with a PNG signature.