lios 0.1.74

A gorgeous GTK4/VTE Linux terminal with live themes, glass backgrounds, session prompt profiles, Sixel images, and safe GPU controls.
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: &gtk::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());
    }
}