use std::{ collections::HashMap, path::{Path, PathBuf}, }; use gdk::{gdk_pixbuf::Pixbuf, prelude::GdkContextExt}; use diaphragm_core::{ styles::{DefinedDashStyle, Pattern}, text::{DefinedFontDescription, FontDescription}, Renderer, }; pub struct CairoRenderer { ctx: cairo::Context, loaded_images: HashMap, } impl CairoRenderer { pub fn new() -> Self { // HACK: a bit ugly, but text_extents requires a context, so we have to create a "dummy" // surface let surface = cairo::SvgSurface::for_stream(1920., 1080., std::io::sink()).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); 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()) } } impl Default for CairoRenderer { fn default() -> Self { Self::new() } } impl Renderer for CairoRenderer { fn set_size(&mut self, width: f64, height: f64) { let surface = cairo::SvgSurface::for_stream(width, height, std::io::stdout()).unwrap(); self.ctx = cairo::Context::new(&surface).unwrap(); } fn move_to(&mut self, x: f64, y: f64) { self.ctx.move_to(x, y) } fn stroke(&mut self) { self.ctx.stroke().unwrap(); } fn fill(&mut self) { self.ctx.fill().unwrap(); } fn fill_preserve(&mut self) { self.ctx.fill_preserve().unwrap(); } fn set_pattern(&mut self, pattern: &Pattern) { match pattern { Pattern::Solid(color) => { let (red, green, blue, alpha) = color.rgba(); self.ctx.set_source_rgba(red, green, blue, alpha) } Pattern::None => {} _ => panic!("Unhandled pattern"), } } fn set_dash(&mut self, dash: &DefinedDashStyle) { self.ctx.set_dash(dash.dashes(), dash.offset()); } fn clear_dash(&mut self) { self.ctx.set_dash(&[], 0.); } fn set_line_width(&mut self, width: f64) { self.ctx.set_line_width(width); } fn line_to(&mut self, x: f64, y: f64) { self.ctx.line_to(x, y) } fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) { self.ctx.rectangle(x, y, width, height); } fn text_extents(&self, text: &str, font: &FontDescription) -> (f64, f64) { // Pango gives us integer back, so we lose some precision. We use this for that. const TEST_ABSOLUTE_SIZE: f64 = 10_000.; let layout = pangocairo::create_layout(&self.ctx); let mut font_desc = pango::FontDescription::from_string(&font.family); font_desc.set_absolute_size(TEST_ABSOLUTE_SIZE * pango::SCALE as f64); layout.set_font_description(Some(&font_desc)); layout.set_markup(text); // TODO: get height from the baseline // let mut layout_iter = layout.iter(); // let _height = loop { // if layout_iter.at_last_line() { // break layout_iter.baseline(); // } // layout_iter.next_line(); // }; let (_, extents) = layout.pixel_extents(); ( extents.width() as f64 / TEST_ABSOLUTE_SIZE, extents.height() as f64 / TEST_ABSOLUTE_SIZE, ) } fn show_text(&mut self, text: &str, font: &DefinedFontDescription) { let layout = pangocairo::create_layout(&self.ctx); let mut font_desc = pango::FontDescription::from_string(&font.family); font_desc.set_absolute_size(font.size * pango::SCALE as f64); layout.set_font_description(Some(&font_desc)); layout.set_markup(text); 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(); let scale_x = width / image.width() as f64; let scale_y = height / image.height() as f64; self.ctx.scale(scale_x, scale_y); self.ctx.set_source_pixbuf(&image, x / scale_x, y / scale_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) } }