nrelm 0.1.0

An idiomatic GUI library inspired by Elm and based on gtk3-rs
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;

use gtk::cairo::Context;
use gtk::cairo::{Format, ImageSurface};
use gtk::prelude::WidgetExt;

/// A drawing surface that can be shared between drawing handlers.
///
/// The underlying [`ImageSurface`] is reference counted, so that every
/// `DrawHandler` that observes the same drawing area sees the same content.
#[derive(Clone, Debug)]
struct Surface {
    surface: Rc<RefCell<ImageSurface>>,
}

impl Surface {
    fn new(surface: ImageSurface) -> Self {
        Self {
            surface: Rc::new(RefCell::new(surface)),
        }
    }

    fn get(&self) -> ImageSurface {
        self.surface.borrow().clone()
    }

    fn set(&self, surface: &ImageSurface) {
        *self.surface.borrow_mut() = surface.clone();
    }
}

/// A Cairo drawing context that draws onto the edit surface of a
/// [`DrawHandler`].
///
/// The drawn content is copied to the draw surface and the drawing area is
/// redrawn when the context is dropped.
#[derive(Debug)]
pub struct DrawContext {
    context: Context,
    draw_surface: Surface,
    edit_surface: ImageSurface,
    drawing_area: gtk::DrawingArea,
}

impl DrawContext {
    fn new(
        draw_surface: &Surface,
        edit_surface: &ImageSurface,
        drawing_area: &gtk::DrawingArea,
    ) -> Self {
        Self {
            context: Context::new(edit_surface).unwrap(),
            draw_surface: draw_surface.clone(),
            edit_surface: edit_surface.clone(),
            drawing_area: drawing_area.clone(),
        }
    }
}

impl Deref for DrawContext {
    type Target = Context;

    fn deref(&self) -> &Self::Target {
        &self.context
    }
}

impl Drop for DrawContext {
    fn drop(&mut self) {
        self.draw_surface.set(&self.edit_surface);
        self.drawing_area.queue_draw();
    }
}

/// A double-buffered drawing area abstraction.
///
/// Drawing operations are performed on an off-screen surface via
/// [`get_context`](DrawHandler::get_context) and are applied to the drawing
/// area when the context is dropped. This avoids flickering and enables
/// partial redraws.
#[derive(Debug)]
#[must_use]
pub struct DrawHandler {
    draw_surface: Surface,
    edit_surface: ImageSurface,
    drawing_area: gtk::DrawingArea,
}

impl Default for DrawHandler {
    fn default() -> Self {
        Self::new()
    }
}

impl DrawHandler {
    /// Creates a new [`DrawHandler`] with a default drawing area.
    pub fn new() -> Self {
        Self::new_with_drawing_area(gtk::DrawingArea::default())
    }

    /// Creates a new [`DrawHandler`] that renders to the given drawing area.
    pub fn new_with_drawing_area(drawing_area: gtk::DrawingArea) -> Self {
        let draw_surface = Surface::new(ImageSurface::create(Format::ARgb32, 100, 100).unwrap());
        let edit_surface = ImageSurface::create(Format::ARgb32, 100, 100).unwrap();

        let cloned_surface = draw_surface.clone();
        drawing_area.connect_draw(move |_widget, context| {
            if let Err(error) = context.set_source_surface(cloned_surface.get(), 0.0, 0.0) {
                tracing::error!("Cannot set source surface: {:?}", error);
            }

            if let Err(error) = context.paint() {
                tracing::error!("Cannot paint: {:?}", error);
            }

            gtk::glib::Propagation::Proceed
        });

        Self {
            draw_surface,
            edit_surface,
            drawing_area,
        }
    }

    /// Returns a [`DrawContext`] for drawing onto the edit surface, resizing
    /// it to the current allocation of the drawing area if necessary.
    #[allow(deprecated)]
    pub fn get_context(&mut self) -> DrawContext {
        let allocation = self.drawing_area.allocation();
        let scale = self.drawing_area.scale_factor();
        let width = allocation.width() * scale;
        let height = allocation.height() * scale;

        if (width, height) != (self.edit_surface.width(), self.edit_surface.height()) {
            match ImageSurface::create(Format::ARgb32, width, height) {
                Ok(surface) => {
                    surface.set_device_scale(f64::from(scale), f64::from(scale));
                    self.edit_surface = surface;
                }
                Err(error) => tracing::error!("Cannot resize image surface: {:?}", error),
            }
        }
        DrawContext::new(&self.draw_surface, &self.edit_surface, &self.drawing_area)
    }

    /// Returns the size of the drawing area in logical (unscaled) pixels.
    #[must_use]
    pub fn size(&self) -> (i32, i32) {
        let scale = self.drawing_area.scale_factor();
        (
            self.edit_surface.width() / scale,
            self.edit_surface.height() / scale,
        )
    }

    /// Returns the height of the drawing area in logical pixels.
    #[must_use]
    pub fn height(&self) -> i32 {
        let scale = self.drawing_area.scale_factor();
        self.edit_surface.height() / scale
    }

    /// Returns the width of the drawing area in logical pixels.
    #[must_use]
    pub fn width(&self) -> i32 {
        let scale = self.drawing_area.scale_factor();
        self.edit_surface.width() / scale
    }

    /// Returns the height of the edit surface in device (scaled) pixels.
    #[must_use]
    pub fn surface_height(&self) -> i32 {
        self.edit_surface.height()
    }

    /// Returns the width of the edit surface in device (scaled) pixels.
    #[must_use]
    pub fn surface_width(&self) -> i32 {
        self.edit_surface.width()
    }

    /// Returns the drawing area the handler renders to.
    #[must_use]
    pub fn drawing_area(&self) -> &gtk::DrawingArea {
        &self.drawing_area
    }
}