use gtk::prelude::*;
use gtk::subclass::prelude::ObjectSubclassIsExt;
use gtk::{gdk, glib, graphene};
#[derive(Debug, Clone, Copy, PartialEq)]
struct CoverGeometry {
x: f32,
y: f32,
width: f32,
height: f32,
}
fn cover_geometry(
source_width: i32,
source_height: i32,
target_width: i32,
target_height: i32,
) -> Option<CoverGeometry> {
if source_width <= 0 || source_height <= 0 || target_width <= 0 || target_height <= 0 {
return None;
}
let source_width = source_width as f64;
let source_height = source_height as f64;
let target_width = target_width as f64;
let target_height = target_height as f64;
let scale = (target_width / source_width).max(target_height / source_height);
let width = source_width * scale;
let height = source_height * scale;
Some(CoverGeometry {
x: ((target_width - width) / 2.0) as f32,
y: ((target_height - height) / 2.0) as f32,
width: width as f32,
height: height as f32,
})
}
mod imp {
use super::*;
use gtk::subclass::prelude::*;
use std::cell::{Cell, RefCell};
#[derive(Default)]
pub(crate) struct CoverImage {
pub(super) texture: RefCell<Option<gdk::Texture>>,
pub(super) opacity: Cell<f64>,
}
#[glib::object_subclass]
impl ObjectSubclass for CoverImage {
const NAME: &'static str = "LiosCoverImage";
type Type = super::CoverImage;
type ParentType = gtk::Widget;
}
impl ObjectImpl for CoverImage {}
impl WidgetImpl for CoverImage {
fn snapshot(&self, snapshot: >k::Snapshot) {
let texture = self.texture.borrow();
let Some(texture) = texture.as_ref() else {
return;
};
let widget = self.obj();
let Some(geometry) = cover_geometry(
texture.width(),
texture.height(),
widget.width(),
widget.height(),
) else {
return;
};
let clip = graphene::Rect::new(0.0, 0.0, widget.width() as f32, widget.height() as f32);
let image =
graphene::Rect::new(geometry.x, geometry.y, geometry.width, geometry.height);
snapshot.push_clip(&clip);
snapshot.push_opacity(self.opacity.get());
snapshot.append_texture(texture, &image);
snapshot.pop();
snapshot.pop();
}
}
}
glib::wrapper! {
pub(crate) struct CoverImage(ObjectSubclass<imp::CoverImage>)
@extends gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl CoverImage {
pub(crate) fn pending(opacity: f64) -> Self {
let image: Self = glib::Object::new();
image.imp().opacity.set(opacity.clamp(0.0, 1.0));
image.set_hexpand(true);
image.set_vexpand(true);
image.set_can_target(false);
image
}
pub(crate) fn set_texture(&self, texture: &gdk::Texture) {
self.imp().texture.replace(Some(texture.clone()));
self.queue_draw();
}
pub(crate) fn set_opacity(&self, opacity: f64) {
let opacity = opacity.clamp(0.0, 1.0);
if (self.imp().opacity.replace(opacity) - opacity).abs() > f64::EPSILON {
self.queue_draw();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(actual: f32, expected: f32) {
assert!((actual - expected).abs() < 0.001, "{actual} != {expected}");
}
#[test]
fn cover_geometry_center_crops_wide_images() {
let geometry = cover_geometry(1920, 1080, 600, 600).unwrap();
assert_close(geometry.x, -233.333_33);
assert_close(geometry.y, 0.0);
assert_close(geometry.width, 1_066.666_6);
assert_close(geometry.height, 600.0);
}
#[test]
fn cover_geometry_center_crops_tall_images() {
let geometry = cover_geometry(800, 1200, 800, 400).unwrap();
assert_close(geometry.x, 0.0);
assert_close(geometry.y, -400.0);
assert_close(geometry.width, 800.0);
assert_close(geometry.height, 1200.0);
}
#[test]
fn cover_geometry_rejects_empty_dimensions() {
assert!(cover_geometry(0, 100, 100, 100).is_none());
assert!(cover_geometry(100, 0, 100, 100).is_none());
assert!(cover_geometry(100, 100, 0, 100).is_none());
assert!(cover_geometry(100, 100, 100, 0).is_none());
}
}