ezel/
lib.rs

1#![feature(min_specialization)]
2
3pub mod cartesian;
4
5pub mod grid;
6pub mod plain_box;
7pub mod primitive;
8pub mod theme;
9mod widget_size;
10
11pub mod data;
12pub mod legend;
13pub mod prelude;
14
15#[cfg(feature = "gg")]
16pub mod gg;
17#[cfg(feature = "graph")]
18pub mod graph;
19
20use std::path::Path;
21
22use cartesian::cartesian2::Cartesian2;
23use cassowary::strength::REQUIRED;
24use cassowary::Solver;
25use cassowary::WeightedRelation::EQ;
26use grid::Grid;
27use piet::kurbo::Affine;
28use piet::RenderContext;
29use plain_box::PlainBox;
30use widget_size::WidgetSizeVars;
31
32use crate::theme::MAKIE;
33
34pub trait Renderable {
35    // SECTION: layout methods
36
37    /// Adds constraints to the solver.
38    /// Assume the widget's width(left+main+right) and height are constraint by the parent.
39    /// It must provide the constraints on the top/main/bottom and left/main/right splits.
40    ///
41    /// ctx is provided for text size measuring
42    fn layout<C: RenderContext>(&self, ctx: &mut C, solver: &mut Solver);
43    fn size(&self) -> &WidgetSizeVars;
44    fn add_zero_protrusion_constraints(&self, solver: &mut Solver) {
45        let size = self.size();
46        solver
47            .add_constraints(&[
48                size.top | EQ(REQUIRED) | 0.0,
49                size.bottom | EQ(REQUIRED) | 0.0,
50                size.left | EQ(REQUIRED) | 0.0,
51                size.right | EQ(REQUIRED) | 0.0,
52            ])
53            .unwrap();
54    }
55
56    // SECTION: render methods
57
58    /// This method assumes all variables are already computed and available in the solver.
59    /// The caller is responsible for setting up the coordinate like the below:
60    ///     
61    ///   -------------- <- clipping area
62    ///      top (y<0) |
63    ///                |
64    ///  (0,0)         |
65    ///     ---------  |
66    ///     |       | <- main area
67    ///     |       |  |
68    ///     ---------
69    ///         (main_width, main_height)
70    ///                |
71    ///       bottom   |
72    ///   --------------
73    ///     
74    /// The protrusion area is drawn by the widget or its parent grid.
75    fn render<C: RenderContext>(&self, ctx: &mut C, layout: &Solver);
76
77    /// Draws to a context given a size (width, height).
78    fn draw_to_ctx<C: RenderContext>(&self, ctx: &mut C, (width, height): (usize, usize)) {
79        let mut solver = Solver::new();
80        let size = self.size();
81
82        // the root element's (top) + (main height) + (bottom) + 2 * root_padding == final height
83        solver
84            .add_constraints(&[
85                size.width() + 2.0 * MAKIE.root_padding | EQ(REQUIRED) | width as f64,
86                size.height() + 2.0 * MAKIE.root_padding | EQ(REQUIRED) | height as f64,
87            ])
88            .unwrap();
89
90        self.layout(ctx, &mut solver);
91
92        let size = self.size().read(&solver);
93
94        ctx.clear(None, piet::Color::WHITE);
95        ctx.with_save(|ctx: &mut C| {
96            ctx.transform(Affine::translate((
97                size.left + MAKIE.root_padding,
98                size.top + MAKIE.root_padding,
99            )));
100            // ctx.clip(Rect {
101            //     x0: -size.left,
102            //     y0: -size.top,
103            //     x1: size.main_width + size.right,
104            //     y1: size.main_height + size.bottom,
105            // });
106            self.render(ctx, &solver);
107            Ok(())
108        })
109        .unwrap();
110    }
111
112    #[cfg(feature = "png")]
113    fn draw_to_png<P: AsRef<Path>>(
114        &self,
115        path: P,
116        size: (usize, usize), // final image size
117    ) -> Result<(), piet_common::Error> {
118        self.draw_to_png_scaled(path, size, 1.0)
119    }
120
121    #[cfg(feature = "png")]
122    fn draw_to_png_scaled<P: AsRef<Path>>(
123        &self,
124        path: P,
125        size: (usize, usize), // final image size
126        scale: f64,           // 0.5 makes the text smaller
127    ) -> Result<(), piet_common::Error> {
128        let mut device = piet_common::Device::new()?;
129        let mut bitmap: piet_common::BitmapTarget = device.bitmap_target(size.0, size.1, scale)?;
130        let mut rc = bitmap.render_context();
131
132        self.draw_to_ctx(
133            &mut rc,
134            (
135                (size.0 as f64 / scale) as usize,
136                (size.1 as f64 / scale) as usize,
137            ),
138        );
139        rc.finish()?;
140        drop(rc);
141        bitmap.save_to_file(path)?;
142        Ok(())
143    }
144
145    // fn draw_to_jpg<P: AsRef<Path>>(&self, path: P, size: (usize, usize)) {}
146
147    #[cfg(feature = "svg")]
148    fn draw_to_svg<P: AsRef<Path>>(
149        &self,
150        path: P,
151        (width, height): (usize, usize), // final image size
152    ) -> std::io::Result<()> {
153        let file = std::fs::File::create(path)?;
154        self.draw_to_svg_buffer(file, (width, height))
155    }
156
157    #[cfg(feature = "svg")]
158    fn draw_to_svg_buffer(
159        &self,
160        writer: impl std::io::Write,
161        (width, height): (usize, usize), // final image size
162    ) -> std::io::Result<()> {
163        // piet > 0.5.0
164        // let mut rc = piet_svg::RenderContext::new(piet::kurbo::Size {
165        //     width: width as f64,
166        //     height: height as f64,
167        // });
168
169        // piet 0.5.0
170        let mut rc = piet_svg::RenderContext::new();
171
172        self.draw_to_ctx(&mut rc, (width, height));
173        rc.write(writer)?;
174        Ok(())
175    }
176
177    fn draw_to_file<P: AsRef<Path>>(
178        &self,
179        path: P,
180        (width, height): (usize, usize),
181    ) -> anyhow::Result<()> {
182        let ext = path
183            .as_ref()
184            .extension()
185            .and_then(std::ffi::OsStr::to_str)
186            .map(str::to_lowercase);
187
188        match ext.as_deref() {
189            Some("svg") => {
190                #[cfg(feature = "svg")]
191                {
192                    Ok(self.draw_to_svg(path, (width, height))?)
193                }
194
195                #[cfg(not(feature = "svg"))]
196                Err(std::io::Error::new(
197                    std::io::ErrorKind::Unsupported,
198                    "Please enable ezel's svg feature.",
199                ))
200            }
201            Some("png") => {
202                #[cfg(feature = "png")]
203                {
204                    Ok(self
205                        .draw_to_png(path, (width, height))
206                        .map_err(|e| anyhow::anyhow!(e.to_string()))?)
207                }
208
209                #[cfg(not(feature = "png"))]
210                Err(std::io::Error::new(
211                    std::io::ErrorKind::Unsupported,
212                    "Please enable ezel's svg feature.",
213                ))
214            }
215            Some("jpg" | "jpeg") => {
216                todo!()
217            }
218            _ => Err(std::io::Error::new(
219                std::io::ErrorKind::Unsupported,
220                "Saving to this file extension is not supported.",
221            )
222            .into()),
223        }
224    }
225}
226
227/// A wrapper type to avoid boxing and dynamic dispatch
228pub enum Widget {
229    Cartesian2(Cartesian2),
230    Grid(Grid),
231    PlainBox(PlainBox),
232}
233
234impl From<Cartesian2> for Widget {
235    fn from(x: Cartesian2) -> Self {
236        Self::Cartesian2(x)
237    }
238}
239impl From<Grid> for Widget {
240    fn from(x: Grid) -> Self {
241        Self::Grid(x)
242    }
243}
244impl From<PlainBox> for Widget {
245    fn from(x: PlainBox) -> Self {
246        Self::PlainBox(x)
247    }
248}
249
250impl Renderable for Widget {
251    fn layout<C: RenderContext>(&self, ctx: &mut C, solver: &mut Solver) {
252        match self {
253            Self::Cartesian2(x) => x.layout(ctx, solver),
254            Self::Grid(x) => x.layout(ctx, solver),
255            Self::PlainBox(x) => x.layout(ctx, solver),
256        }
257    }
258
259    fn render<C: RenderContext>(&self, ctx: &mut C, layout: &Solver) {
260        match self {
261            Self::Cartesian2(x) => x.render(ctx, layout),
262            Self::Grid(x) => x.render(ctx, layout),
263            Self::PlainBox(x) => x.render(ctx, layout),
264        }
265    }
266
267    fn size(&self) -> &WidgetSizeVars {
268        match self {
269            Self::Cartesian2(x) => x.size(),
270            Self::Grid(x) => x.size(),
271            Self::PlainBox(x) => x.size(),
272        }
273    }
274}