From 82ffe3187a32bad4ecca0736882a23793a800822 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Fri, 3 Feb 2023 19:59:43 +0100 Subject: add support for images, and some chores - SVG files not yet supported - Remove dead commented code - Add inherent constraints for text, path, and images - Formatting --- Cargo.lock | 60 ++++++++++++++++ cairo-renderer/Cargo.toml | 1 + cairo-renderer/src/lib.rs | 43 +++++++++-- core/src/core_shapes.rs | 174 +++++++++++++++++++++++++++------------------ core/src/rendering.rs | 29 ++++++-- core/src/runtime.rs | 107 +++++++--------------------- core/src/solving.rs | 2 + flake.nix | 2 + lua-bindings/diaphragm.lua | 11 +++ lua-bindings/src/lib.rs | 84 +++++++++++++++------- 10 files changed, 328 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fd149f..444470e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,6 +177,7 @@ version = "0.1.0" dependencies = [ "cairo-rs", "diaphragm-core", + "gdk", "pango", "pangocairo", ] @@ -291,6 +292,65 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "gio" version = "0.16.7" diff --git a/cairo-renderer/Cargo.toml b/cairo-renderer/Cargo.toml index ebef7d5..53f95be 100644 --- a/cairo-renderer/Cargo.toml +++ b/cairo-renderer/Cargo.toml @@ -10,5 +10,6 @@ edition = "2021" diaphragm-core = { path = "../core" } cairo-rs = { version = "0.16", features = ["svg", "png"] } +gdk = "0.16" pango = "0.16" pangocairo = "0.16" diff --git a/cairo-renderer/src/lib.rs b/cairo-renderer/src/lib.rs index 40d24d1..5a730f4 100644 --- a/cairo-renderer/src/lib.rs +++ b/cairo-renderer/src/lib.rs @@ -1,3 +1,10 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use gdk::{gdk_pixbuf::Pixbuf, prelude::GdkContextExt}; + use diaphragm_core::{ styles::{DefinedDashStyle, Pattern}, text::{DefinedFontDescription, FontDescription}, @@ -6,6 +13,7 @@ use diaphragm_core::{ pub struct CairoRenderer { ctx: cairo::Context, + loaded_images: HashMap, } impl CairoRenderer { @@ -15,7 +23,17 @@ impl CairoRenderer { let surface = cairo::SvgSurface::for_stream(1920., 1080., std::io::sink()).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); - Self { ctx } + Self { + ctx, + loaded_images: HashMap::new(), + } + } + + fn get_image(&mut self, path: &Path) -> &Pixbuf { + let path = path.to_owned(); + self.loaded_images + .entry(path.clone()) + .or_insert_with(|| Pixbuf::from_file(path).unwrap()) } } @@ -102,9 +120,6 @@ impl Renderer for CairoRenderer { extents.width() as f64 / TEST_ABSOLUTE_SIZE, extents.height() as f64 / TEST_ABSOLUTE_SIZE, ) - - //let (width, height) = layout.get_pixel_size(); - //(width as f64, height as f64) } fn show_text(&mut self, text: &str, font: &DefinedFontDescription) { @@ -115,7 +130,25 @@ impl Renderer for CairoRenderer { layout.set_font_description(Some(&font_desc)); layout.set_markup(text); - //self.ctx.set_font_size(dbg!(font.size)); pangocairo::show_layout(&self.ctx, &layout); } + + fn show_image(&mut self, path: &Path, x: f64, y: f64, width: f64, height: f64) { + let image = self.get_image(path).clone(); + + self.ctx.save().unwrap(); + + self.ctx + .scale(width / image.width() as f64, height / image.height() as f64); + + self.ctx.set_source_pixbuf(&image, x, y); + self.ctx.paint().unwrap(); + + self.ctx.restore().unwrap(); + } + + fn geometry_for_image(&mut self, path: &Path) -> (f64, f64) { + let image = self.get_image(path); + (image.width() as f64, image.height() as f64) + } } diff --git a/core/src/core_shapes.rs b/core/src/core_shapes.rs index e6d79c9..c380870 100644 --- a/core/src/core_shapes.rs +++ b/core/src/core_shapes.rs @@ -1,7 +1,12 @@ -use crate::types::{CoreShapeContext, DefinedCoreShapeContext, DefinedPoint2D, Point2D}; +use std::path::PathBuf; -pub use super::text::{DefinedText, Text}; +use crate::{ + text::{DefinedText, Text}, + types::{CoreShapeContext, DefinedCoreShapeContext, DefinedPoint2D, Float, Point2D}, + Renderer, SolverContext, +}; +#[derive(Debug, Clone)] pub struct CoreDrawable { pub(crate) shape: CoreShape, pub(crate) context: CoreShapeContext, @@ -11,12 +16,91 @@ impl CoreDrawable { pub fn new(shape: CoreShape, context: CoreShapeContext) -> Self { Self { shape, context } } + + // TODO: I don't like this design + // TODO: plus, having an enum for CoreShape could be bad since types have + // various sizes + pub(crate) fn inherent_constraints( + &self, + solver: &mut dyn SolverContext, + renderer: &mut dyn Renderer, + ) { + match &self.shape { + CoreShape::Text(text) => { + let (text_width, text_height) = renderer.text_extents(&text.content, &text.font); + + let calculated_width = + solver.float_mul(&[Float::Fixed(text_width), text.font.size]); + let bounds_width = self.context.bounds().width(solver); + let width_constraint = solver.float_eq(bounds_width, calculated_width); + solver.constrain(width_constraint); + + let calculated_height = + solver.float_mul(&[Float::Fixed(text_height), text.font.size]); + let bounds_height = self.context.bounds().height(solver); + let height_constraint = solver.float_eq(bounds_height, calculated_height); + solver.constrain(height_constraint); + } + CoreShape::StraightPath(path) => { + let (all_x, all_y): (Vec<_>, Vec<_>) = + path.points.iter().map(|p| (p.x, p.y)).unzip(); + + let min_x = solver.float_min(&all_x); + let max_x = solver.float_max(&all_x); + let min_y = solver.float_min(&all_y); + let max_y = solver.float_max(&all_y); + + let bounds_top = self.context.bounds().top(solver); + let bounds_bottom = self.context.bounds().bottom(solver); + let bounds_left = self.context.bounds().left(solver); + let bounds_right = self.context.bounds().right(solver); + + let top_constraint = solver.float_eq(bounds_top, min_x); + solver.constrain(top_constraint); + + let bottom_constraint = solver.float_eq(bounds_bottom, max_x); + solver.constrain(bottom_constraint); + + let left_constraint = solver.float_eq(bounds_left, min_y); + solver.constrain(left_constraint); + + let right_constraint = solver.float_eq(bounds_right, max_y); + solver.constrain(right_constraint); + } + CoreShape::Image(image) => { + if !image.keep_aspect_ratio { + return; + } + + let scale_x = solver.new_free_float(); + let scale_y = solver.new_free_float(); + + let constraint = solver.float_eq(scale_x, scale_y); + solver.constrain(constraint); + + let (orig_width, orig_height) = renderer.geometry_for_image(&image.path); + + let orig_width_scaled = solver.float_mul(&[scale_x, Float::Fixed(orig_width)]); + let width_constraint = + solver.float_eq(self.context.bounds().width, orig_width_scaled); + solver.constrain(width_constraint); + + let orig_height_scaled = solver.float_mul(&[scale_y, Float::Fixed(orig_height)]); + let height_constraint = + solver.float_eq(self.context.bounds().height, orig_height_scaled); + solver.constrain(height_constraint); + } + _ => (), + } + } } +#[derive(Debug, Clone)] pub enum CoreShape { Rectangle(Rectangle), Text(Text), StraightPath(StraightPath), + Image(Image), } impl From for CoreShape { @@ -37,6 +121,12 @@ impl From for CoreShape { } } +impl From for CoreShape { + fn from(image: Image) -> Self { + CoreShape::Image(image) + } +} + pub struct DefinedCoreDrawable { pub(crate) shape: DefinedCoreShape, pub(crate) context: DefinedCoreShapeContext, @@ -46,72 +136,12 @@ pub enum DefinedCoreShape { Rectangle(Rectangle), Text(DefinedText), StraightPath(DefinedStraightPath), + Image(Image), } -/* -pub trait CoreShape { - fn constrain( - &self, - _context: &ShapeContext, - _solver: &mut dyn SolverContext, - _renderer: &dyn Renderer, - ) { - } - fn to_render(&self, model: &dyn SolverModel) -> Option>; -} - -impl ComplexShape for T { - fn as_core_shape(&self) -> Option<&dyn CoreShape> { - Some(self) - } - - fn draw(&self, _context: &ShapeContext, _solver: &mut dyn SolverContext) -> DrawResult { - panic!("Tried to decompose core shape") - } -} -*/ - #[derive(Copy, Clone, Debug, Default)] pub struct Rectangle {} -/* -impl CoreShape for Rectangle { - fn to_render(&self, _model: &dyn SolverModel) -> Option> { - Some(Box::new(*self)) - } -} -*/ - -// TODO: re-enable this in some way -/* -impl CoreShape for Text { - fn constrain( - &self, - context: &ShapeContext, - solver: &mut dyn SolverContext, - renderer: &dyn Renderer, - ) { - let height_constraint = solver.float_eq(context.bounds.height, self.font.size); - solver.constrain(height_constraint); - - // TODO: handle multiline - let (width, height) = renderer.text_extents(&self.content, &self.font); - dbg!(height); - - let scale = solver.float_div(self.font.size, Float::Fixed(height)); - - let calculated_width = solver.float_mul(&[Float::Fixed(width), scale]); - let width_constraint = solver.float_eq(context.bounds.width, calculated_width); - solver.constrain(width_constraint); - } - - fn to_render(&self, model: &dyn SolverModel) -> Option> { - self.fixate(model) - .map(|path| -> Box { Box::new(path) }) - } -} -*/ - #[derive(Clone, Debug, Default)] pub struct StraightPath { pub(crate) points: Vec, @@ -127,11 +157,17 @@ pub struct DefinedStraightPath { pub(crate) points: Vec, } -/* -impl CoreShape for StraightPath { - fn to_render(&self, model: &dyn SolverModel) -> Option> { - self.fixate(model) - .map(|path| -> Box { Box::new(path) }) +#[derive(Clone, Debug, Default)] +pub struct Image { + pub(crate) path: PathBuf, + pub(crate) keep_aspect_ratio: bool, +} + +impl Image { + pub fn new(path: PathBuf, keep_aspect_ratio: bool) -> Self { + Self { + path, + keep_aspect_ratio, + } } } -*/ diff --git a/core/src/rendering.rs b/core/src/rendering.rs index 73daa2a..9c719c4 100644 --- a/core/src/rendering.rs +++ b/core/src/rendering.rs @@ -1,7 +1,11 @@ -use super::core_shapes::*; -use super::styles::{DefinedDashStyle, DefinedStrokeStyle, FillStyle, Pattern}; -use super::text::{DefinedFontDescription, FontDescription}; -use super::types::DefinedCoreShapeContext; +use std::path::Path; + +use crate::{ + core_shapes::*, + styles::{DefinedDashStyle, DefinedStrokeStyle, FillStyle, Pattern}, + text::{DefinedFontDescription, DefinedText, FontDescription}, + types::DefinedCoreShapeContext, +}; pub trait Renderer { // Must be called once before any drawing happens @@ -19,6 +23,9 @@ pub trait Renderer { // For a font of size 1. fn text_extents(&self, text: &str, font: &FontDescription) -> (f64, f64); fn show_text(&mut self, text: &str, font: &DefinedFontDescription); + + fn show_image(&mut self, path: &Path, x: f64, y: f64, width: f64, height: f64); + fn geometry_for_image(&mut self, path: &Path) -> (f64, f64); } pub trait Render { @@ -31,6 +38,7 @@ impl Render for DefinedCoreShape { Self::Rectangle(r) => r.render(context, renderer), Self::Text(t) => t.render(context, renderer), Self::StraightPath(p) => p.render(context, renderer), + Self::Image(i) => i.render(context, renderer), } } } @@ -96,3 +104,16 @@ impl Render for DefinedStraightPath { draw(&context.fill, &context.stroke, renderer); } } + +impl Render for Image { + fn render(&self, context: DefinedCoreShapeContext, renderer: &mut dyn Renderer) { + // TODO: what about pattern, and fill color? Do we do something with that? + renderer.show_image( + &self.path, + context.bounds.left, + context.bounds.top, + context.bounds.width, + context.bounds.height, + ); + } +} diff --git a/core/src/runtime.rs b/core/src/runtime.rs index 6b1b5d1..5fd17e9 100644 --- a/core/src/runtime.rs +++ b/core/src/runtime.rs @@ -1,11 +1,11 @@ -use crate::core_shapes::CoreDrawable; -use crate::rendering::Render; -use crate::types::Bounds; - -// use super::complex_shapes::{ComplexShape, Drawable}; -use super::rendering::Renderer; -use super::solving::{Constrainable, SolverContext}; +use crate::{ + core_shapes::CoreDrawable, + rendering::{Render, Renderer}, + solving::{Constrainable, SolverContext}, + types::Bounds, +}; +// TODO: // const RECURSION_LIMIT: u64 = 10_000; pub struct Runtime<'a> { @@ -20,15 +20,10 @@ impl<'a> Runtime<'a> { Self { solver_ctx, renderer, - // drawables: Vec::new(), drawables: Vec::new(), } } - // pub fn add_drawable(&mut self, drawable: Drawable) { - // self.drawables.push(drawable.into()) - // } - pub fn add_drawable(&mut self, drawable: CoreDrawable) { self.drawables.push(drawable) } @@ -41,88 +36,34 @@ impl<'a> Runtime<'a> { &mut *self.renderer } - pub fn render(mut self, bounds: Bounds) { - let model = self.solver_ctx.solve(); + pub fn render(self, bounds: Bounds) { + // Separate self into several variables, so we can get mutable references to each at the + // same time + let Runtime { + mut solver_ctx, + mut renderer, + drawables, + } = self; + + for drawable in &drawables { + drawable.inherent_constraints(&mut *solver_ctx, &mut *renderer); + } + + let model = solver_ctx.solve(); let bounds = bounds .fixate(&*model) .expect("Could not fixate figure bounds"); - self.renderer.set_size(bounds.width, bounds.height); + renderer.set_size(bounds.width, bounds.height); - for drawable in &self.drawables { + for drawable in &drawables { let defined_drawable = drawable .fixate(&*model) .expect("Could not fixate core shape"); defined_drawable .shape - .render(defined_drawable.context, &mut *self.renderer); + .render(defined_drawable.context, &mut *renderer); } } - - // TODO: preserve ordering of shapes - // pub fn render(mut self) { - // let mut drawables = self.drawables; - // let mut waited_on_variables = Vec::new(); - // let mut core_shapes = Vec::new(); - // - // /* - // for drawable in &self.shapes { - // let bounds = &drawable.bounds; - // let shape = &drawable.shape; - // - // if let Some(core_shape) = shape.to_render() { - // drawables.push((*drawable).clone()); - // continue; - // } - // - // let mut result = shape.draw(bounds); - // drawables.append(&mut result.subshapes); - // waited_on_variables.append(&mut result.waiting_on); - // } - // */ - // - // let mut recursion_count = 0; - // - // while !drawables.is_empty() { - // recursion_count += 1; - // - // if recursion_count > RECURSION_LIMIT { - // panic!("Recursion limit reached"); - // } - // - // let mut tmp_drawables = Vec::new(); - // - // for drawable in drawables.drain(..) { - // let shape_ctx = &drawable.context; - // let shape = &drawable.shape; - // - // if let Some(core_shape) = shape.as_core_shape() { - // core_shape.constrain(shape_ctx, &mut *self.solver_ctx, &*self.renderer); - // core_shapes.push((shape.dyn_clone(), shape_ctx.clone())); // Better to Arc? Cow? - // continue; - // } - // - // let mut result = shape.draw(shape_ctx, &mut *self.solver_ctx); - // tmp_drawables.append(&mut result.subshapes); - // waited_on_variables.append(&mut result.waiting_on); - // } - // - // drawables = tmp_drawables; - // } - // - // let model = self.solver_ctx.solve(); - // - // // Delay rendering core shapes until later to have all the constraints - // for (core_shape, shape_ctx) in core_shapes { - // let core_shape = core_shape.as_core_shape().unwrap(); - // - // match (core_shape.to_render(&*model), shape_ctx.fixate(&*model)) { - // (Some(defined_shape), Some(shape_ctx)) => { - // defined_shape.render(shape_ctx, &mut *self.renderer) - // } - // _ => panic!("Failed to fixate core shape"), - // } - // } - // } } diff --git a/core/src/solving.rs b/core/src/solving.rs index bfe3ff3..63e4562 100644 --- a/core/src/solving.rs +++ b/core/src/solving.rs @@ -176,11 +176,13 @@ pub trait Constrainable { impl Constrainable for CoreShape { type Fixated = DefinedCoreShape; + // TODO: why &self, why not self? fn fixate(&self, model: &dyn SolverModel) -> Option { match self { CoreShape::Rectangle(r) => Some(DefinedCoreShape::Rectangle(*r)), CoreShape::Text(t) => t.fixate(model).map(DefinedCoreShape::Text), CoreShape::StraightPath(p) => p.fixate(model).map(DefinedCoreShape::StraightPath), + CoreShape::Image(i) => Some(DefinedCoreShape::Image(i.clone())), } } } diff --git a/flake.nix b/flake.nix index 36a85c7..b247db4 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,8 @@ buildInputs = with pkgs; [ pkgconfig cairo + gdk-pixbuf + gtk3 pango z3 diff --git a/lua-bindings/diaphragm.lua b/lua-bindings/diaphragm.lua index a308c28..d4e3dc9 100644 --- a/lua-bindings/diaphragm.lua +++ b/lua-bindings/diaphragm.lua @@ -1,3 +1,5 @@ +-- TODO: split file + local M = {} M.core = require("libdiaphragm") @@ -184,6 +186,15 @@ function M.straight_path.new(params) return result end +M.image = {} + +function M.image.new(params) + params = params or {} + local result = M.core.image(params) + M.util.tbl_assign_reserved(params, result, bounds_reserved) + return result +end + function M.shape(params) params = params or {} diff --git a/lua-bindings/src/lib.rs b/lua-bindings/src/lib.rs index 86a759d..26081c8 100644 --- a/lua-bindings/src/lib.rs +++ b/lua-bindings/src/lib.rs @@ -11,7 +11,8 @@ use diaphragm_cairo_renderer::CairoRenderer; use diaphragm_core::{ colors::Color, core_shapes::{ - CoreDrawable, CoreShape, Rectangle as CoreRectangle, StraightPath as CoreStraightPath, + CoreDrawable, CoreShape, Image as CoreImage, Rectangle as CoreRectangle, + StraightPath as CoreStraightPath, }, solving::VariableHandle, styles::Pattern, @@ -517,28 +518,6 @@ fn text(_: &Lua, params: LuaTable) -> LuaResult { let context = new_shape_context(); - let content_2 = content.clone(); - let font_2 = font.0.clone(); - let context_2 = context.clone(); - - // TODO: this shouldn't be here, this should be an innate property of Text. Move the - // Drawable struct in core? - runtime_thread_do(Box::new(move |r| { - let (text_width, text_height) = r.renderer().text_extents(&content_2, &font_2); - let solver = r.solver_ctx(); - // let scale = solver.float_div(font.0.size, CoreFloat::Fixed(height)); - - let calculated_width = solver.float_mul(&[CoreFloat::Fixed(text_width), font.0.size]); - let bounds_width = context_2.bounds().width(solver); - let width_constraint = solver.float_eq(bounds_width, calculated_width); - solver.constrain(width_constraint); - - let calculated_height = solver.float_mul(&[CoreFloat::Fixed(text_height), font.0.size]); - let bounds_height = context_2.bounds().height(solver); - let height_constraint = solver.float_eq(bounds_height, calculated_height); - solver.constrain(height_constraint); - })); - Ok(Text(Drawable { shape: CoreText { content, @@ -646,6 +625,63 @@ fn straight_path(_: &Lua, params: LuaTable) -> LuaResult { points.try_into() } +#[derive(Debug, Clone)] +struct Image(Drawable); +impl LuaUserData for Image { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("draw", |_, this, _params: ()| { + let drawable = this.0.clone().into(); + runtime_thread_do(Box::new(|r| { + r.add_drawable(drawable); + })); + Ok(()) + }) + } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + add_bound_fields(fields); + } +} + +impl HasContext for Image { + fn get_context(&self) -> &CoreShapeContext { + &self.0.context + } + + fn get_context_mut(&mut self) -> &mut CoreShapeContext { + &mut self.0.context + } +} + +impl TryFrom> for Image { + type Error = LuaError; + + fn try_from(value: LuaValue) -> Result { + match value { + LuaValue::Table(t) => { + let path: String = t.get("path")?; + let keep_aspect_ratio = t.get::<_, Option<_>>("keep_aspect_ratio")?.unwrap_or(true); + + Ok(Image(Drawable { + shape: CoreImage::new(path.into(), keep_aspect_ratio), + context: new_shape_context(), + })) + } + + LuaValue::UserData(u) => Ok(Image(u.borrow::()?.0.clone())), + _ => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "StraightPath", + message: Some("Only a list of points are allowed".to_string()), + }), + } + } +} + +fn image(_: &Lua, params: LuaTable) -> LuaResult { + LuaValue::Table(params).try_into() +} + // It just has a context for bounds, the rest is handled manually in Lua #[derive(Debug, Clone)] struct ComplexShape(CoreShapeContext); @@ -800,8 +836,8 @@ fn libdiaphragm(lua: &Lua) -> LuaResult { exports.set("font", lua.create_function(font)?)?; exports.set("rectangle", lua.create_function(rectangle)?)?; - exports.set("straight_path", lua.create_function(straight_path)?)?; + exports.set("image", lua.create_function(image)?)?; exports.set("complex_shape", lua.create_function(complex_shape)?)?; -- cgit v1.2.3