ezel 0.0.1

A good plotting library
Documentation
#![feature(min_specialization)]

pub mod cartesian;

pub mod grid;
pub mod plain_box;
pub mod primitive;
pub mod theme;
mod widget_size;

pub mod data;
pub mod legend;
pub mod prelude;

#[cfg(feature = "gg")]
pub mod gg;
#[cfg(feature = "graph")]
pub mod graph;

use std::path::Path;

use cartesian::cartesian2::Cartesian2;
use cassowary::strength::REQUIRED;
use cassowary::Solver;
use cassowary::WeightedRelation::EQ;
use grid::Grid;
use piet::kurbo::Affine;
use piet::RenderContext;
use plain_box::PlainBox;
use widget_size::WidgetSizeVars;

use crate::theme::MAKIE;

pub trait Renderable {
    // SECTION: layout methods

    /// Adds constraints to the solver.
    /// Assume the widget's width(left+main+right) and height are constraint by the parent.
    /// It must provide the constraints on the top/main/bottom and left/main/right splits.
    ///
    /// ctx is provided for text size measuring
    fn layout<C: RenderContext>(&self, ctx: &mut C, solver: &mut Solver);
    fn size(&self) -> &WidgetSizeVars;
    fn add_zero_protrusion_constraints(&self, solver: &mut Solver) {
        let size = self.size();
        solver
            .add_constraints(&[
                size.top | EQ(REQUIRED) | 0.0,
                size.bottom | EQ(REQUIRED) | 0.0,
                size.left | EQ(REQUIRED) | 0.0,
                size.right | EQ(REQUIRED) | 0.0,
            ])
            .unwrap();
    }

    // SECTION: render methods

    /// This method assumes all variables are already computed and available in the solver.
    /// The caller is responsible for setting up the coordinate like the below:
    ///     
    ///   -------------- <- clipping area
    ///      top (y<0) |
    ///                |
    ///  (0,0)         |
    ///     ---------  |
    ///     |       | <- main area
    ///     |       |  |
    ///     ---------
    ///         (main_width, main_height)
    ///                |
    ///       bottom   |
    ///   --------------
    ///     
    /// The protrusion area is drawn by the widget or its parent grid.
    fn render<C: RenderContext>(&self, ctx: &mut C, layout: &Solver);

    /// Draws to a context given a size (width, height).
    fn draw_to_ctx<C: RenderContext>(&self, ctx: &mut C, (width, height): (usize, usize)) {
        let mut solver = Solver::new();
        let size = self.size();

        // the root element's (top) + (main height) + (bottom) + 2 * root_padding == final height
        solver
            .add_constraints(&[
                size.width() + 2.0 * MAKIE.root_padding | EQ(REQUIRED) | width as f64,
                size.height() + 2.0 * MAKIE.root_padding | EQ(REQUIRED) | height as f64,
            ])
            .unwrap();

        self.layout(ctx, &mut solver);

        let size = self.size().read(&solver);

        ctx.clear(None, piet::Color::WHITE);
        ctx.with_save(|ctx: &mut C| {
            ctx.transform(Affine::translate((
                size.left + MAKIE.root_padding,
                size.top + MAKIE.root_padding,
            )));
            // ctx.clip(Rect {
            //     x0: -size.left,
            //     y0: -size.top,
            //     x1: size.main_width + size.right,
            //     y1: size.main_height + size.bottom,
            // });
            self.render(ctx, &solver);
            Ok(())
        })
        .unwrap();
    }

    #[cfg(feature = "png")]
    fn draw_to_png<P: AsRef<Path>>(
        &self,
        path: P,
        size: (usize, usize), // final image size
    ) -> Result<(), piet_common::Error> {
        self.draw_to_png_scaled(path, size, 1.0)
    }

    #[cfg(feature = "png")]
    fn draw_to_png_scaled<P: AsRef<Path>>(
        &self,
        path: P,
        size: (usize, usize), // final image size
        scale: f64,           // 0.5 makes the text smaller
    ) -> Result<(), piet_common::Error> {
        let mut device = piet_common::Device::new()?;
        let mut bitmap: piet_common::BitmapTarget = device.bitmap_target(size.0, size.1, scale)?;
        let mut rc = bitmap.render_context();

        self.draw_to_ctx(
            &mut rc,
            (
                (size.0 as f64 / scale) as usize,
                (size.1 as f64 / scale) as usize,
            ),
        );
        rc.finish()?;
        drop(rc);
        bitmap.save_to_file(path)?;
        Ok(())
    }

    // fn draw_to_jpg<P: AsRef<Path>>(&self, path: P, size: (usize, usize)) {}

    #[cfg(feature = "svg")]
    fn draw_to_svg<P: AsRef<Path>>(
        &self,
        path: P,
        (width, height): (usize, usize), // final image size
    ) -> std::io::Result<()> {
        let file = std::fs::File::create(path)?;
        self.draw_to_svg_buffer(file, (width, height))
    }

    #[cfg(feature = "svg")]
    fn draw_to_svg_buffer(
        &self,
        writer: impl std::io::Write,
        (width, height): (usize, usize), // final image size
    ) -> std::io::Result<()> {
        // piet > 0.5.0
        // let mut rc = piet_svg::RenderContext::new(piet::kurbo::Size {
        //     width: width as f64,
        //     height: height as f64,
        // });

        // piet 0.5.0
        let mut rc = piet_svg::RenderContext::new();

        self.draw_to_ctx(&mut rc, (width, height));
        rc.write(writer)?;
        Ok(())
    }

    fn draw_to_file<P: AsRef<Path>>(
        &self,
        path: P,
        (width, height): (usize, usize),
    ) -> anyhow::Result<()> {
        let ext = path
            .as_ref()
            .extension()
            .and_then(std::ffi::OsStr::to_str)
            .map(str::to_lowercase);

        match ext.as_deref() {
            Some("svg") => {
                #[cfg(feature = "svg")]
                {
                    Ok(self.draw_to_svg(path, (width, height))?)
                }

                #[cfg(not(feature = "svg"))]
                Err(std::io::Error::new(
                    std::io::ErrorKind::Unsupported,
                    "Please enable ezel's svg feature.",
                ))
            }
            Some("png") => {
                #[cfg(feature = "png")]
                {
                    Ok(self
                        .draw_to_png(path, (width, height))
                        .map_err(|e| anyhow::anyhow!(e.to_string()))?)
                }

                #[cfg(not(feature = "png"))]
                Err(std::io::Error::new(
                    std::io::ErrorKind::Unsupported,
                    "Please enable ezel's svg feature.",
                ))
            }
            Some("jpg" | "jpeg") => {
                todo!()
            }
            _ => Err(std::io::Error::new(
                std::io::ErrorKind::Unsupported,
                "Saving to this file extension is not supported.",
            )
            .into()),
        }
    }
}

/// A wrapper type to avoid boxing and dynamic dispatch
pub enum Widget {
    Cartesian2(Cartesian2),
    Grid(Grid),
    PlainBox(PlainBox),
}

impl From<Cartesian2> for Widget {
    fn from(x: Cartesian2) -> Self {
        Self::Cartesian2(x)
    }
}
impl From<Grid> for Widget {
    fn from(x: Grid) -> Self {
        Self::Grid(x)
    }
}
impl From<PlainBox> for Widget {
    fn from(x: PlainBox) -> Self {
        Self::PlainBox(x)
    }
}

impl Renderable for Widget {
    fn layout<C: RenderContext>(&self, ctx: &mut C, solver: &mut Solver) {
        match self {
            Self::Cartesian2(x) => x.layout(ctx, solver),
            Self::Grid(x) => x.layout(ctx, solver),
            Self::PlainBox(x) => x.layout(ctx, solver),
        }
    }

    fn render<C: RenderContext>(&self, ctx: &mut C, layout: &Solver) {
        match self {
            Self::Cartesian2(x) => x.render(ctx, layout),
            Self::Grid(x) => x.render(ctx, layout),
            Self::PlainBox(x) => x.render(ctx, layout),
        }
    }

    fn size(&self) -> &WidgetSizeVars {
        match self {
            Self::Cartesian2(x) => x.size(),
            Self::Grid(x) => x.size(),
            Self::PlainBox(x) => x.size(),
        }
    }
}