[ MIRROR ] Customisable, minimalist screen locker for Wayland git.ngill.net/nlock.git
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381// SPDX-License-Identifier: GPL-3.0-or-later// Copyright (C) 2026, Nathan Gill
use anyhow::{Result, anyhow, bail};use cairo::SurfacePattern;use tracing::warn;
use crate::{ auth::AuthState, cairo_ext::CairoExt, config::NLockConfig, font::{FontInfo, NLockFont, ShowText, TextInfo}, util::{BackgroundImageScale, BackgroundType, InputVisibility},};
pub const DEFAULT_DPI: f64 = 96.0;pub const DEFAULT_SCALE: f64 = 1.0;
pub struct NLockRenderBackgroundArgs<'a> { pub buf_height: f64, pub buf_width: f64, pub context: &'a cairo::Context, pub image: Option<&'a cairo::ImageSurface>,}
impl<'a> NLockRenderBackgroundArgs<'a> { fn get_buffer_dimensions(&self) -> Result<(f64, f64)> { if self.buf_width <= 0.0 || self.buf_height <= 0.0 { bail!( "Invalid width or height, {}x{}", self.buf_width, self.buf_height ); }
Ok((self.buf_width, self.buf_height)) }}
pub struct NLockRenderOverlayArgs<'a> { pub auth_state: AuthState, pub buf_height: f64, pub buf_width: f64, pub context: &'a cairo::Context, pub pwd_len: usize,}
impl<'a> NLockRenderOverlayArgs<'a> { fn get_buffer_dimensions(&self) -> Result<(f64, f64)> { if self.buf_width <= 0.0 || self.buf_height <= 0.0 { bail!( "Invalid width or height, {}x{}", self.buf_width, self.buf_height ); }
Ok((self.buf_width, self.buf_height)) }}
#[derive(Default)]pub struct NLockRenderer { dpi: Option<f64>, scale: Option<f64>, subpixel_order: Option<cairo::SubpixelOrder>,}
impl NLockRenderer { pub fn set_dpi<T>(&mut self, dpi: T) where T: Into<f64>, { let dpi: f64 = dpi.into();
if dpi.is_infinite() || dpi.is_nan() || dpi <= 0.0 { warn!("Invalid DPI {dpi}, falling back to {DEFAULT_DPI}"); self.dpi = Some(DEFAULT_DPI); } else { self.dpi = Some(dpi); } }
pub fn set_subpixel_order(&mut self, order: cairo::SubpixelOrder) { self.subpixel_order = Some(order); }
pub fn set_scale(&mut self, scale: f64) { if scale > 0.0 { self.scale = Some(scale); } }
fn clear_background(&self, context: &cairo::Context) -> Result<()> { context.save()?; context.set_operator(cairo::Operator::Source); context.set_source_rgba(0.0, 0.0, 0.0, 0.0); context.paint()?; context.restore()?;
Ok(()) }
fn reset_cairo_context(&self, context: &cairo::Context) -> Result<()> { context.set_antialias(cairo::Antialias::Best); self.clear_background(context)?; context.identity_matrix();
Ok(()) }
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()); context.arc( x + w - r, y + h - r, r, 0f64.to_radians(), 90f64.to_radians(), ); context.arc(x + r, y + h - r, r, 90f64.to_radians(), 180f64.to_radians()); context.arc(x + r, y + r, r, 180f64.to_radians(), 270f64.to_radians()); context.close_path(); }
fn set_frame_border_color( &self, config: &NLockConfig, context: &cairo::Context, auth_state: AuthState, ) { match auth_state { AuthState::Idle => context.ext_set_source_rgba(config.colors.frame_border_idle), AuthState::Success => context.ext_set_source_rgba(config.colors.frame_border_success), AuthState::Fail => context.ext_set_source_rgba(config.colors.frame_border_fail), } }
fn draw_background_image( &self, context: &cairo::Context, image: &cairo::ImageSurface, buf_width: f64, buf_height: f64, mode: BackgroundImageScale, ) -> Result<()> { let width = image.width() as f64; let height = image.height() as f64;
match mode { BackgroundImageScale::Stretch => { context.scale(buf_width / width, buf_height / height); context.set_source_surface(image, 0.0, 0.0)?; } BackgroundImageScale::Center => { context.set_source_surface( image, (buf_width / 2.0 - width / 2.0).floor(), (buf_height / 2.0 - height / 2.0).floor(), )?; } BackgroundImageScale::Tile => { let pattern = SurfacePattern::create(image); pattern.set_extend(cairo::Extend::Repeat); context.set_source(pattern)?; } BackgroundImageScale::Fit => { let buf_ratio = buf_width / buf_height; let bg_ratio = width / height;
if buf_ratio > bg_ratio { let scale = buf_height / height; context.scale(scale, scale); context.set_source_surface( image, buf_width / 2.0 / scale - width / 2.0, 0.0, )?; } else { let scale = buf_width / width; context.scale(scale, scale); context.set_source_surface( image, 0.0, buf_height / 2.0 / scale - height / 2.0, )?; } } BackgroundImageScale::Fill => { let buf_ratio = buf_width / buf_height; let bg_ratio = width / height;
if buf_ratio > bg_ratio { let scale = buf_width / width; context.scale(scale, scale); context.set_source_surface( image, 0.0, buf_height / 2.0 / scale - height / 2.0, )?; } else { let scale = buf_height / height; context.scale(scale, scale); context.set_source_surface( image, buf_width / 2.0 / scale - width / 2.0, 0.0, )?; } } }
Ok(()) }
fn draw_overlay( &self, config: &NLockConfig, context: &cairo::Context, auth_state: AuthState, pwd_len: usize, buf_width: f64, buf_height: f64, ) -> Result<()> { // Reset the context for fresh rendering self.reset_cairo_context(context)?;
let scale = self.scale.unwrap_or(DEFAULT_SCALE);
// Draw border colour context.save()?; self.set_frame_border_color(config, context, auth_state); context.set_line_width(config.frame.border * scale);
let frame_offset = (config.frame.border * scale) / 2.0; let frame_w = buf_width - (frame_offset * 2.0); let frame_h = buf_height - (frame_offset * 2.0);
Self::draw_rounded_rect( context, frame_offset, frame_offset, frame_w, frame_h, config.frame.radius * scale, ); context.stroke()?; context.restore()?;
// Skip drawing input box if the password is empty and config flag set if config.input.visible == InputVisibility::Never || (pwd_len == 0 && config.input.visible == InputVisibility::Content) { return Ok(()); }
let text = config.input.mask_char.repeat(pwd_len);
// 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;
let mut inner_w = buf_width * config.input.width;
if config.input.fit_to_content { // Cap computed width to specified width inner_w = font.text_width().min(inner_w); }
let inner_h = font.font_height(); let inner_x = (buf_width - inner_w) / 2.0; let inner_y = (buf_height - inner_h) / 2.0;
let outer_h = inner_h + (padding_y * 2.0) + (config.input.border * scale); let outer_w = inner_w + (padding_x * 2.0) + (config.input.border * scale); let outer_x = (buf_width - outer_w) / 2.0; let outer_y = (buf_height - outer_h) / 2.0;
context.save()?;
// Draw the outer rectangle, including padding // Outer rectangle should have rounded corners Self::draw_rounded_rect( context, outer_x, outer_y, outer_w, outer_h, config.input.radius * outer_h, // radius is relative, Cairo requires absolute ); context.ext_set_source_rgba(config.colors.input_bg); context.fill_preserve()?; context.ext_set_source_rgba(config.colors.input_border); context.set_line_width(config.input.border * scale); context.stroke_preserve()?; context.clip();
// Clip text to the inner rectangle context.rectangle(inner_x, inner_y, inner_w, inner_h); context.clip();
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); font.show_text(context)?;
context.restore()?;
Ok(()) }
pub fn render_background( &mut self, config: &NLockConfig, args: NLockRenderBackgroundArgs, ) -> Result<()> { let (buf_width, buf_height) = args.get_buffer_dimensions()?;
self.reset_cairo_context(args.context)?;
match config.general.bg_type { BackgroundType::Color => { args.context.ext_set_source_rgba(config.colors.bg); args.context.set_operator(cairo::Operator::Source); } BackgroundType::Image => { let image = args .image .ok_or(anyhow!("Surface in image mode, but no image set!"))?; self.draw_background_image( args.context, image, buf_width, buf_height, config.image.scale, )?; } } args.context.paint()?;
Ok(()) }
pub fn render_overlay( &mut self, config: &NLockConfig, args: NLockRenderOverlayArgs, ) -> Result<()> { let (buf_width, buf_height) = args.get_buffer_dimensions()?;
self.draw_overlay( config, args.context, args.auth_state, args.pwd_len, buf_width, buf_height, )?;
Ok(()) }}