From bb921be814d3d869b72016c58642d660dbe38c40 Mon Sep 17 00:00:00 2001 From: Jeff Muizelaar Date: Fri, 4 Jan 2019 17:13:24 -0500 Subject: [PATCH 01/30] WIP CoreGraphics backend --- Cargo.toml | 5 +- piet-coregraphics/Cargo.toml | 14 +++ piet-coregraphics/src/lib.rs | 197 +++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 piet-coregraphics/Cargo.toml create mode 100644 piet-coregraphics/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 3e864c719..becc446c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,10 @@ members = [ "piet", - "piet-cairo", + #"piet-cairo", "piet-common", - "piet-direct2d", + "piet-coregraphics", + #"piet-direct2d", "piet-web", "piet-web/examples/basic", "piet-svg" diff --git a/piet-coregraphics/Cargo.toml b/piet-coregraphics/Cargo.toml new file mode 100644 index 000000000..01cc8c103 --- /dev/null +++ b/piet-coregraphics/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "piet-coregraphics" +version = "0.1.0" +authors = ["Jeff Muizelaar "] +description = "CoreGraphics backend for piet 2D graphics abstraction." +license = "MIT/Apache-2.0" +edition = "2018" +keywords = ["graphics", "2d"] +categories = ["rendering::graphics-api"] + +[dependencies] +kurbo = "0.1.0" +piet = { path = "../piet" } +core-graphics = { git = "https://github.com/servo/core-foundation-rs" } diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs new file mode 100644 index 000000000..aaa4d1601 --- /dev/null +++ b/piet-coregraphics/src/lib.rs @@ -0,0 +1,197 @@ +//! The CoreGraphics backend for the Piet 2D graphics abstraction. + +use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; + +use kurbo::{PathEl, QuadBez, Vec2, Shape}; + +use piet::{RenderContext, RoundInto, FillRule}; + +pub struct CoreGraphicsContext<'a> { + // Cairo has this as Clone and with &self methods, but we do this to avoid + // concurrency problems. + ctx: &'a mut CGContext, +} + +impl<'a> CoreGraphicsContext<'a> { + pub fn new(ctx: &mut CGContext) -> CoreGraphicsContext { + CoreGraphicsContext { ctx } + } +} + +pub enum Brush { + Solid(u32), +} + +// TODO: This cannot be used yet because the `piet::RenderContext` trait +// needs to expose a way to create stroke styles. +pub struct StrokeStyle { + line_join: Option, + line_cap: Option, + dash: Option<(Vec, f64)>, + miter_limit: Option, +} + +impl StrokeStyle { + pub fn new() -> StrokeStyle { + StrokeStyle { + line_join: None, + line_cap: None, + dash: None, + miter_limit: None, + } + } + + pub fn line_join(mut self, line_join: CGLineJoin) -> Self { + self.line_join = Some(line_join); + self + } + + pub fn line_cap(mut self, line_cap: CGLineCap) -> Self { + self.line_cap = Some(line_cap); + self + } + + pub fn dash(mut self, dashes: Vec, offset: f64) -> Self { + self.dash = Some((dashes, offset)); + self + } + + pub fn miter_limit(mut self, miter_limit: f64) -> Self { + self.miter_limit = Some(miter_limit); + self + } +} + +impl<'a> RenderContext for CoreGraphicsContext<'a> { + type Point = Vec2; + type Coord = f64; + type Brush = Brush; + type StrokeStyle = StrokeStyle; + + fn clear(&mut self, rgb: u32) { + self.ctx.set_rgb_fill_color( + byte_to_frac(rgb >> 16), + byte_to_frac(rgb >> 8), + byte_to_frac(rgb), + 1.0, + ); + self.ctx.fill_rect(self.ctx.clip_bounding_box()); + } + + fn solid_brush(&mut self, rgba: u32) -> Brush { + Brush::Solid(rgba) + } + + /// Fill a shape. + fn fill(&mut self, + shape: &impl Shape, + brush: &Self::Brush, + fill_rule: FillRule, + ) { + self.set_path(shape); + self.set_fill_brush(brush); + match fill_rule { + FillRule::NonZero => self.ctx.fill_path(), + FillRule::EvenOdd => self.ctx.eo_fill_path(), + } + } + + fn stroke( + &mut self, + shape: &impl Shape, + brush: &Self::Brush, + width: impl RoundInto, + style: Option<&Self::StrokeStyle>, + ) { + self.set_path(shape); + self.set_stroke(width.round_into(), style); + self.set_stroke_brush(brush); + self.ctx.stroke_path(); + } +} + +impl<'a> CoreGraphicsContext<'a> { + /// Set the source pattern to the brush. + /// + /// Cairo is super stateful, and we're trying to have more retained stuff. + /// This is part of the impedance matching. + fn set_fill_brush(&mut self, brush: &Brush) { + match *brush { + Brush::Solid(rgba) => self.ctx.set_rgb_fill_color( + byte_to_frac(rgba >> 24), + byte_to_frac(rgba >> 16), + byte_to_frac(rgba >> 8), + byte_to_frac(rgba), + ), + } + } + + fn set_stroke_brush(&mut self, brush: &Brush) { + match *brush { + Brush::Solid(rgba) => self.ctx.set_rgb_stroke_color( + byte_to_frac(rgba >> 24), + byte_to_frac(rgba >> 16), + byte_to_frac(rgba >> 8), + byte_to_frac(rgba), + ), + } + } + + /// Set the stroke parameters. + fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) { + self.ctx.set_line_width(width); + + let line_join = style + .and_then(|style| style.line_join) + .unwrap_or(CGLineJoin::CGLineJoinMiter); + self.ctx.set_line_join(line_join); + + let line_cap = style + .and_then(|style| style.line_cap) + .unwrap_or(CGLineCap::CGLineCapButt); + self.ctx.set_line_cap(line_cap); + + let miter_limit = style.and_then(|style| style.miter_limit).unwrap_or(10.0); + self.ctx.set_miter_limit(miter_limit); + + match style.and_then(|style| style.dash.as_ref()) { + None => self.ctx.set_line_dash(0.0, &[]), + Some((dashes, offset)) => self.ctx.set_line_dash(*offset, dashes), + } + } + + fn set_path(&mut self, shape: &impl Shape) { + // This shouldn't be necessary, we always leave the context in no-path + // state. But just in case, and it should be harmless. + self.ctx.begin_path(); + let mut last = Vec2::default(); + for el in shape.to_bez_path(1e-3) { + match el { + PathEl::Moveto(p) => { + self.ctx.move_to_point(p.x, p.y); + last = p; + } + PathEl::Lineto(p) => { + self.ctx.add_line_to_point(p.x, p.y); + last = p; + } + PathEl::Quadto(p1, p2) => { + let q = QuadBez::new(last, p1, p2); + let c = q.raise(); + self.ctx + .add_curve_to_point(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y); + last = p2; + } + PathEl::Curveto(p1, p2, p3) => { + self.ctx.add_curve_to_point(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + last = p3; + } + PathEl::Closepath => self.ctx.close_path(), + } + } + } +} + +fn byte_to_frac(byte: u32) -> f64 { + ((byte & 255) as f64) * (1.0 / 255.0) +} From 14c509e3accba49e12b717cfd8b3580503d38fc0 Mon Sep 17 00:00:00 2001 From: Jeff Muizelaar Date: Wed, 10 Apr 2019 22:52:37 -0400 Subject: [PATCH 02/30] Get building after rebase --- piet-coregraphics/Cargo.toml | 2 +- piet-coregraphics/src/lib.rs | 181 +++++++++++++++++++++++++++++++---- 2 files changed, 165 insertions(+), 18 deletions(-) diff --git a/piet-coregraphics/Cargo.toml b/piet-coregraphics/Cargo.toml index 01cc8c103..7ccba7c39 100644 --- a/piet-coregraphics/Cargo.toml +++ b/piet-coregraphics/Cargo.toml @@ -9,6 +9,6 @@ keywords = ["graphics", "2d"] categories = ["rendering::graphics-api"] [dependencies] -kurbo = "0.1.0" +kurbo = "0.2.0" piet = { path = "../piet" } core-graphics = { git = "https://github.com/servo/core-foundation-rs" } diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index aaa4d1601..41e6c6595 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,20 +1,26 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; +use core_graphics::image::CGImage; +use core_graphics::base::{CGFloat}; -use kurbo::{PathEl, QuadBez, Vec2, Shape}; +use kurbo::{PathEl, Rect, Affine, QuadBez, Vec2, Shape}; -use piet::{RenderContext, RoundInto, FillRule}; +use piet::{ + new_error, Error, ErrorKind, LineCap, LineJoin, FillRule, Font, FontBuilder, Gradient, ImageFormat, + InterpolationMode, RenderContext, RoundInto, StrokeStyle, Text, TextLayout, TextLayoutBuilder, +}; pub struct CoreGraphicsContext<'a> { // Cairo has this as Clone and with &self methods, but we do this to avoid // concurrency problems. ctx: &'a mut CGContext, + text: CoreGraphicsText, } impl<'a> CoreGraphicsContext<'a> { pub fn new(ctx: &mut CGContext) -> CoreGraphicsContext { - CoreGraphicsContext { ctx } + CoreGraphicsContext { ctx, text: CoreGraphicsText } } } @@ -22,9 +28,64 @@ pub enum Brush { Solid(u32), } +pub struct CoreGraphicsFont; +impl Font for CoreGraphicsFont { +} + +pub struct CoreGraphicsFontBuilder; + +impl FontBuilder for CoreGraphicsFontBuilder { + type Out = CoreGraphicsFont; + + fn build(self) -> Result { +panic!() + } +} +pub struct CoreGraphicsLayout; + +impl TextLayout for CoreGraphicsLayout { + type Coord = CGFloat; + fn width(&self) -> Self::Coord { + panic!(); + } +} + +pub struct CoreGraphicsTextLayoutBuilder { +} + +impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder { + type Out = CoreGraphicsLayout; + + fn build(self) -> Result { + panic!() + } +} + +pub struct CoreGraphicsText; + +impl Text for CoreGraphicsText { + type Coord = CGFloat; + type FontBuilder = CoreGraphicsFontBuilder; + type Font = CoreGraphicsFont; + type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder; + type TextLayout = CoreGraphicsLayout; + + fn new_font_by_name(&mut self, name: &str, size: impl RoundInto,) -> Result { + panic!() + } + + fn new_text_layout( + &mut self, + font: &Self::Font, + text: &str, + ) -> Result { + panic!() + } +} + // TODO: This cannot be used yet because the `piet::RenderContext` trait // needs to expose a way to create stroke styles. -pub struct StrokeStyle { +/*pub struct StrokeStyle { line_join: Option, line_cap: Option, dash: Option<(Vec, f64)>, @@ -60,13 +121,16 @@ impl StrokeStyle { self.miter_limit = Some(miter_limit); self } -} +}*/ impl<'a> RenderContext for CoreGraphicsContext<'a> { type Point = Vec2; - type Coord = f64; + type Coord = f64;//XXX: this needs to be fixed for 32bit type Brush = Brush; - type StrokeStyle = StrokeStyle; + type Text = CoreGraphicsText; + type TextLayout = CoreGraphicsLayout; + type Image = CGImage; + //type StrokeStyle = StrokeStyle; fn clear(&mut self, rgb: u32) { self.ctx.set_rgb_fill_color( @@ -78,13 +142,17 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { self.ctx.fill_rect(self.ctx.clip_bounding_box()); } - fn solid_brush(&mut self, rgba: u32) -> Brush { - Brush::Solid(rgba) + fn solid_brush(&mut self, rgba: u32) -> Result { + Ok(Brush::Solid(rgba)) + } + + fn gradient(&mut self, gradient: Gradient) -> Result { + unimplemented!() } /// Fill a shape. fn fill(&mut self, - shape: &impl Shape, + shape: impl Shape, brush: &Self::Brush, fill_rule: FillRule, ) { @@ -96,18 +164,96 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } } + fn clip(&mut self, shape: impl Shape, fill_rule: FillRule) { + self.set_path(shape); + match fill_rule { + FillRule::NonZero => self.ctx.clip(), + FillRule::EvenOdd => self.ctx.eo_clip(), + } + } + fn stroke( &mut self, - shape: &impl Shape, + shape: impl Shape, brush: &Self::Brush, width: impl RoundInto, - style: Option<&Self::StrokeStyle>, + style: Option<&StrokeStyle>, ) { self.set_path(shape); self.set_stroke(width.round_into(), style); self.set_stroke_brush(brush); self.ctx.stroke_path(); } + + fn text(&mut self) -> &mut Self::Text { + &mut self.text + } + + fn draw_text( + &mut self, + layout: &Self::TextLayout, + pos: impl RoundInto, + brush: &Self::Brush, + ) { + unimplemented!() + } + + fn save(&mut self) -> Result<(), Error> { + self.ctx.save(); + Ok(()) + } + + fn restore(&mut self) -> Result<(), Error> { + self.ctx.restore(); + Ok(()) + } + + fn finish(&mut self) -> Result<(), Error> { + unimplemented!() + } + + fn transform(&mut self, transform: Affine) { + unimplemented!() + } + + fn make_image( + &mut self, + width: usize, + height: usize, + buf: &[u8], + format: ImageFormat, + ) -> Result { + unimplemented!() + } + + fn draw_image( + &mut self, + image: &Self::Image, + rect: impl Into, + interp: InterpolationMode, + ) { + unimplemented!() + } + + fn status(&mut self) -> Result<(), Error> { + unimplemented!() + } +} + +fn convert_line_join(line_join: LineJoin) -> CGLineJoin { + match line_join { + LineJoin::Miter => CGLineJoin::CGLineJoinMiter, + LineJoin::Round => CGLineJoin::CGLineJoinRound, + LineJoin::Bevel => CGLineJoin::CGLineJoinBevel, + } +} + +fn convert_line_cap(line_cap: LineCap) -> CGLineCap { + match line_cap { + LineCap::Butt => CGLineCap::CGLineCapButt, + LineCap::Round => CGLineCap::CGLineCapRound, + LineCap::Square => CGLineCap::CGLineCapSquare, + } } impl<'a> CoreGraphicsContext<'a> { @@ -143,13 +289,13 @@ impl<'a> CoreGraphicsContext<'a> { let line_join = style .and_then(|style| style.line_join) - .unwrap_or(CGLineJoin::CGLineJoinMiter); - self.ctx.set_line_join(line_join); + .unwrap_or(LineJoin::Miter); + self.ctx.set_line_join(convert_line_join(line_join)); let line_cap = style .and_then(|style| style.line_cap) - .unwrap_or(CGLineCap::CGLineCapButt); - self.ctx.set_line_cap(line_cap); + .unwrap_or(LineCap::Butt); + self.ctx.set_line_cap(convert_line_cap(line_cap)); let miter_limit = style.and_then(|style| style.miter_limit).unwrap_or(10.0); self.ctx.set_miter_limit(miter_limit); @@ -160,7 +306,7 @@ impl<'a> CoreGraphicsContext<'a> { } } - fn set_path(&mut self, shape: &impl Shape) { + fn set_path(&mut self, shape: impl Shape) { // This shouldn't be necessary, we always leave the context in no-path // state. But just in case, and it should be harmless. self.ctx.begin_path(); @@ -190,6 +336,7 @@ impl<'a> CoreGraphicsContext<'a> { } } } + } fn byte_to_frac(byte: u32) -> f64 { From 948f9e42bfaf1895c2f41233782f0e7d2641196f Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 22 Apr 2020 13:01:58 -0700 Subject: [PATCH 03/30] Update Core Graphics back-end to piet 0.12 Still work in progress, but it draws and saves a PNG. --- piet-coregraphics/Cargo.toml | 8 +- piet-coregraphics/examples/basic-cg.rs | 53 +++++ piet-coregraphics/src/lib.rs | 272 +++++++++++++++---------- 3 files changed, 226 insertions(+), 107 deletions(-) create mode 100644 piet-coregraphics/examples/basic-cg.rs diff --git a/piet-coregraphics/Cargo.toml b/piet-coregraphics/Cargo.toml index 7ccba7c39..ac4592949 100644 --- a/piet-coregraphics/Cargo.toml +++ b/piet-coregraphics/Cargo.toml @@ -9,6 +9,8 @@ keywords = ["graphics", "2d"] categories = ["rendering::graphics-api"] [dependencies] -kurbo = "0.2.0" -piet = { path = "../piet" } -core-graphics = { git = "https://github.com/servo/core-foundation-rs" } +piet = { version = "0.0.12", path = "../piet" } +core-graphics = "0.19" + +[dev-dependencies] +png = "0.16.2" diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs new file mode 100644 index 000000000..88e72e333 --- /dev/null +++ b/piet-coregraphics/examples/basic-cg.rs @@ -0,0 +1,53 @@ +use std::fs::File; +use std::io::BufWriter; +use std::path::Path; + +use core_graphics::color_space::CGColorSpace; +use core_graphics::context::CGContext; + +use piet::kurbo::Circle; +use piet::{Color, RenderContext}; + +const WIDTH: usize = 800; +const HEIGHT: usize = 600; + +fn main() { + let mut cg_ctx = CGContext::create_bitmap_context( + None, + WIDTH, + HEIGHT, + 8, + 0, + &CGColorSpace::create_device_rgb(), + core_graphics::base::kCGImageAlphaPremultipliedLast + ); + let mut piet = piet_coregraphics::CoreGraphicsContext::new(&mut cg_ctx); + piet.fill(Circle::new((100.0, 100.0), 50.0), &Color::rgb8(255, 0, 0).with_alpha(0.5)); + piet.finish().unwrap(); + + unpremultiply(cg_ctx.data()); + + // Write image as PNG file. + let path = Path::new("image.png"); + let file = File::create(path).unwrap(); + let ref mut w = BufWriter::new(file); + + let mut encoder = png::Encoder::new(w, WIDTH as u32, HEIGHT as u32); + encoder.set_color(png::ColorType::RGBA); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().unwrap(); + + writer.write_image_data(cg_ctx.data()).unwrap(); +} + +fn unpremultiply(data: &mut [u8]) { + for i in (0..data.len()).step_by(4) { + let a = data[i + 3]; + if a != 0 { + let scale = 255.0 / (a as f64); + data[i] = (scale * (data[i] as f64)).round() as u8; + data[i + 1] = (scale * (data[i + 1] as f64)).round() as u8; + data[i + 2] = (scale * (data[i + 2] as f64)).round() as u8; + } + } +} diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 41e6c6595..e7876604d 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,14 +1,18 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. +use std::borrow::Cow; + +use core_graphics::base::CGFloat; use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; use core_graphics::image::CGImage; -use core_graphics::base::{CGFloat}; -use kurbo::{PathEl, Rect, Affine, QuadBez, Vec2, Shape}; +use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Vec2}; use piet::{ - new_error, Error, ErrorKind, LineCap, LineJoin, FillRule, Font, FontBuilder, Gradient, ImageFormat, - InterpolationMode, RenderContext, RoundInto, StrokeStyle, Text, TextLayout, TextLayoutBuilder, + new_error, Color, Error, ErrorKind, FixedGradient, Font, FontBuilder, HitTestMetrics, + HitTestPoint, HitTestTextPosition, ImageFormat, InterpolationMode, IntoBrush, LineCap, + LineJoin, LineMetric, RenderContext, RoundInto, StrokeStyle, Text, TextLayout, + TextLayoutBuilder, }; pub struct CoreGraphicsContext<'a> { @@ -20,69 +24,30 @@ pub struct CoreGraphicsContext<'a> { impl<'a> CoreGraphicsContext<'a> { pub fn new(ctx: &mut CGContext) -> CoreGraphicsContext { - CoreGraphicsContext { ctx, text: CoreGraphicsText } + CoreGraphicsContext { + ctx, + text: CoreGraphicsText, + } } } +#[derive(Clone)] pub enum Brush { Solid(u32), + Gradient, } pub struct CoreGraphicsFont; -impl Font for CoreGraphicsFont { -} pub struct CoreGraphicsFontBuilder; -impl FontBuilder for CoreGraphicsFontBuilder { - type Out = CoreGraphicsFont; - - fn build(self) -> Result { -panic!() - } -} -pub struct CoreGraphicsLayout; - -impl TextLayout for CoreGraphicsLayout { - type Coord = CGFloat; - fn width(&self) -> Self::Coord { - panic!(); - } -} +#[derive(Clone)] +pub struct CoreGraphicsTextLayout; -pub struct CoreGraphicsTextLayoutBuilder { -} - -impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder { - type Out = CoreGraphicsLayout; - - fn build(self) -> Result { - panic!() - } -} +pub struct CoreGraphicsTextLayoutBuilder {} pub struct CoreGraphicsText; -impl Text for CoreGraphicsText { - type Coord = CGFloat; - type FontBuilder = CoreGraphicsFontBuilder; - type Font = CoreGraphicsFont; - type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder; - type TextLayout = CoreGraphicsLayout; - - fn new_font_by_name(&mut self, name: &str, size: impl RoundInto,) -> Result { - panic!() - } - - fn new_text_layout( - &mut self, - font: &Self::Font, - text: &str, - ) -> Result { - panic!() - } -} - // TODO: This cannot be used yet because the `piet::RenderContext` trait // needs to expose a way to create stroke styles. /*pub struct StrokeStyle { @@ -124,64 +89,70 @@ impl StrokeStyle { }*/ impl<'a> RenderContext for CoreGraphicsContext<'a> { - type Point = Vec2; - type Coord = f64;//XXX: this needs to be fixed for 32bit type Brush = Brush; type Text = CoreGraphicsText; - type TextLayout = CoreGraphicsLayout; + type TextLayout = CoreGraphicsTextLayout; type Image = CGImage; //type StrokeStyle = StrokeStyle; - fn clear(&mut self, rgb: u32) { + fn clear(&mut self, color: Color) { + let rgba = color.as_rgba_u32(); self.ctx.set_rgb_fill_color( - byte_to_frac(rgb >> 16), - byte_to_frac(rgb >> 8), - byte_to_frac(rgb), - 1.0, + byte_to_frac(rgba >> 24), + byte_to_frac(rgba >> 16), + byte_to_frac(rgba >> 8), + byte_to_frac(rgba), ); self.ctx.fill_rect(self.ctx.clip_bounding_box()); } - fn solid_brush(&mut self, rgba: u32) -> Result { - Ok(Brush::Solid(rgba)) + fn solid_brush(&mut self, color: Color) -> Brush { + Brush::Solid(color.as_rgba_u32()) } - fn gradient(&mut self, gradient: Gradient) -> Result { + fn gradient(&mut self, gradient: impl Into) -> Result { unimplemented!() } /// Fill a shape. - fn fill(&mut self, - shape: impl Shape, - brush: &Self::Brush, - fill_rule: FillRule, - ) { + fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush) { + let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); - self.set_fill_brush(brush); - match fill_rule { - FillRule::NonZero => self.ctx.fill_path(), - FillRule::EvenOdd => self.ctx.eo_fill_path(), - } + self.set_fill_brush(&brush); + self.ctx.fill_path(); } - fn clip(&mut self, shape: impl Shape, fill_rule: FillRule) { + fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush) { + let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); - match fill_rule { - FillRule::NonZero => self.ctx.clip(), - FillRule::EvenOdd => self.ctx.eo_clip(), - } + self.set_fill_brush(&brush); + self.ctx.eo_fill_path(); + } + + fn clip(&mut self, shape: impl Shape) { + self.set_path(shape); + self.ctx.clip(); } - fn stroke( + fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush, width: f64) { + let brush = brush.make_brush(self, || shape.bounding_box()); + self.set_path(shape); + self.set_stroke(width.round_into(), None); + self.set_stroke_brush(&brush); + self.ctx.stroke_path(); + } + + fn stroke_styled( &mut self, shape: impl Shape, - brush: &Self::Brush, - width: impl RoundInto, - style: Option<&StrokeStyle>, + brush: &impl IntoBrush, + width: f64, + style: &StrokeStyle, ) { + let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); - self.set_stroke(width.round_into(), style); - self.set_stroke_brush(brush); + self.set_stroke(width.round_into(), Some(style)); + self.set_stroke_brush(&brush); self.ctx.stroke_path(); } @@ -189,12 +160,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { &mut self.text } - fn draw_text( - &mut self, - layout: &Self::TextLayout, - pos: impl RoundInto, - brush: &Self::Brush, - ) { + fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into, brush: &impl IntoBrush) { unimplemented!() } @@ -209,7 +175,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn finish(&mut self) -> Result<(), Error> { - unimplemented!() + Ok(()) } fn transform(&mut self, transform: Affine) { @@ -227,19 +193,115 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn draw_image( - &mut self, - image: &Self::Image, - rect: impl Into, - interp: InterpolationMode, - ) { + &mut self, + image: &Self::Image, + rect: impl Into, + interp: InterpolationMode, + ) { + unimplemented!() + } + + fn draw_image_area( + &mut self, + _image: &Self::Image, + _src_rect: impl Into, + _dst_rect: impl Into, + _interp: InterpolationMode, + ) { + unimplemented!() + } + + fn blurred_rect(&mut self, _rect: Rect, _blur_radius: f64, _brush: &impl IntoBrush) { unimplemented!() } + fn current_transform(&self) -> Affine { + Default::default() + } + fn status(&mut self) -> Result<(), Error> { unimplemented!() } } +impl Text for CoreGraphicsText { + type Font = CoreGraphicsFont; + type FontBuilder = CoreGraphicsFontBuilder; + type TextLayout = CoreGraphicsTextLayout; + type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder; + + fn new_font_by_name(&mut self, _name: &str, _size: f64) -> Self::FontBuilder { + unimplemented!(); + } + + fn new_text_layout( + &mut self, + _font: &Self::Font, + _text: &str, + _width: impl Into>, + ) -> Self::TextLayoutBuilder { + unimplemented!(); + } +} + +impl Font for CoreGraphicsFont {} + +impl FontBuilder for CoreGraphicsFontBuilder { + type Out = CoreGraphicsFont; + + fn build(self) -> Result { + unimplemented!(); + } +} + +impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder { + type Out = CoreGraphicsTextLayout; + + fn build(self) -> Result { + unimplemented!() + } +} + +impl TextLayout for CoreGraphicsTextLayout { + fn width(&self) -> f64 { + 0.0 + } + + fn update_width(&mut self, _new_width: impl Into>) -> Result<(), Error> { + unimplemented!() + } + + fn line_text(&self, _line_number: usize) -> Option<&str> { + unimplemented!() + } + + fn line_metric(&self, _line_number: usize) -> Option { + unimplemented!() + } + + fn line_count(&self) -> usize { + unimplemented!() + } + + fn hit_test_point(&self, _point: Point) -> HitTestPoint { + unimplemented!() + } + + fn hit_test_text_position(&self, _text_position: usize) -> Option { + unimplemented!() + } +} + +impl<'a> IntoBrush> for Brush { + fn make_brush<'b>( + &'b self, + _piet: &mut CoreGraphicsContext, + _bbox: impl FnOnce() -> Rect, + ) -> std::borrow::Cow<'b, Brush> { + Cow::Borrowed(self) + } +} + fn convert_line_join(line_join: LineJoin) -> CGLineJoin { match line_join { LineJoin::Miter => CGLineJoin::CGLineJoinMiter, @@ -269,6 +331,7 @@ impl<'a> CoreGraphicsContext<'a> { byte_to_frac(rgba >> 8), byte_to_frac(rgba), ), + Brush::Gradient => unimplemented!(), } } @@ -280,6 +343,7 @@ impl<'a> CoreGraphicsContext<'a> { byte_to_frac(rgba >> 8), byte_to_frac(rgba), ), + Brush::Gradient => unimplemented!(), } } @@ -310,33 +374,33 @@ impl<'a> CoreGraphicsContext<'a> { // This shouldn't be necessary, we always leave the context in no-path // state. But just in case, and it should be harmless. self.ctx.begin_path(); - let mut last = Vec2::default(); + let mut last = Point::default(); for el in shape.to_bez_path(1e-3) { match el { - PathEl::Moveto(p) => { + PathEl::MoveTo(p) => { self.ctx.move_to_point(p.x, p.y); last = p; } - PathEl::Lineto(p) => { + PathEl::LineTo(p) => { self.ctx.add_line_to_point(p.x, p.y); last = p; } - PathEl::Quadto(p1, p2) => { + PathEl::QuadTo(p1, p2) => { let q = QuadBez::new(last, p1, p2); let c = q.raise(); self.ctx .add_curve_to_point(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y); last = p2; } - PathEl::Curveto(p1, p2, p3) => { - self.ctx.add_curve_to_point(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + PathEl::CurveTo(p1, p2, p3) => { + self.ctx + .add_curve_to_point(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); last = p3; } - PathEl::Closepath => self.ctx.close_path(), + PathEl::ClosePath => self.ctx.close_path(), } } } - } fn byte_to_frac(byte: u32) -> f64 { From f4b4c71906047ba2e22458ff303b1410dc6582ea Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 28 Apr 2020 09:20:11 -0700 Subject: [PATCH 04/30] Test paths, start images Still work in progress as images are not tested, but should be a start. --- piet-coregraphics/examples/basic-cg.rs | 7 +- piet-coregraphics/src/lib.rs | 116 ++++++++++++++----------- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index 88e72e333..777b63516 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -19,10 +19,13 @@ fn main() { 8, 0, &CGColorSpace::create_device_rgb(), - core_graphics::base::kCGImageAlphaPremultipliedLast + core_graphics::base::kCGImageAlphaPremultipliedLast, ); let mut piet = piet_coregraphics::CoreGraphicsContext::new(&mut cg_ctx); - piet.fill(Circle::new((100.0, 100.0), 50.0), &Color::rgb8(255, 0, 0).with_alpha(0.5)); + piet.fill( + Circle::new((100.0, 100.0), 50.0), + &Color::rgb8(255, 0, 0).with_alpha(0.5), + ); piet.finish().unwrap(); unpremultiply(cg_ctx.data()); diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index e7876604d..1235ccde7 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,12 +1,18 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. use std::borrow::Cow; +use std::sync::Arc; -use core_graphics::base::CGFloat; +use core_graphics::base::{ + kCGImageAlphaLast, kCGImageAlphaPremultipliedLast, kCGRenderingIntentDefault, CGFloat, +}; +use core_graphics::color_space::CGColorSpace; use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; +use core_graphics::data_provider::CGDataProvider; use core_graphics::image::CGImage; +use core_graphics::geometry::{CGPoint, CGRect, CGSize}; -use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Vec2}; +use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size, Vec2}; use piet::{ new_error, Color, Error, ErrorKind, FixedGradient, Font, FontBuilder, HitTestMetrics, @@ -48,46 +54,6 @@ pub struct CoreGraphicsTextLayoutBuilder {} pub struct CoreGraphicsText; -// TODO: This cannot be used yet because the `piet::RenderContext` trait -// needs to expose a way to create stroke styles. -/*pub struct StrokeStyle { - line_join: Option, - line_cap: Option, - dash: Option<(Vec, f64)>, - miter_limit: Option, -} - -impl StrokeStyle { - pub fn new() -> StrokeStyle { - StrokeStyle { - line_join: None, - line_cap: None, - dash: None, - miter_limit: None, - } - } - - pub fn line_join(mut self, line_join: CGLineJoin) -> Self { - self.line_join = Some(line_join); - self - } - - pub fn line_cap(mut self, line_cap: CGLineCap) -> Self { - self.line_cap = Some(line_cap); - self - } - - pub fn dash(mut self, dashes: Vec, offset: f64) -> Self { - self.dash = Some((dashes, offset)); - self - } - - pub fn miter_limit(mut self, miter_limit: f64) -> Self { - self.miter_limit = Some(miter_limit); - self - } -}*/ - impl<'a> RenderContext for CoreGraphicsContext<'a> { type Brush = Brush; type Text = CoreGraphicsText; @@ -160,7 +126,12 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { &mut self.text } - fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into, brush: &impl IntoBrush) { + fn draw_text( + &mut self, + layout: &Self::TextLayout, + pos: impl Into, + brush: &impl IntoBrush, + ) { unimplemented!() } @@ -189,26 +160,58 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { buf: &[u8], format: ImageFormat, ) -> Result { - unimplemented!() + let data = Arc::new(buf.to_owned()); + let data_provider = CGDataProvider::from_buffer(data); + let (colorspace, bitmap_info, bytes) = match format { + ImageFormat::Rgb => (CGColorSpace::create_device_rgb(), 0, 3), + ImageFormat::RgbaPremul => ( + CGColorSpace::create_device_rgb(), + kCGImageAlphaPremultipliedLast, + 4, + ), + ImageFormat::RgbaSeparate => (CGColorSpace::create_device_rgb(), kCGImageAlphaLast, 4), + _ => unimplemented!(), + }; + let bits_per_component = 8; + // TODO: we don't know this until drawing time, so defer actual image creation til then. + let should_interpolate = true; + let rendering_intent = kCGRenderingIntentDefault; + let image = CGImage::new( + width, + height, + bits_per_component, + bytes * bits_per_component, + width * bytes, + &colorspace, + bitmap_info, + &data_provider, + should_interpolate, + rendering_intent, + ); + Ok(image) } fn draw_image( &mut self, image: &Self::Image, rect: impl Into, - interp: InterpolationMode, + _interp: InterpolationMode, ) { - unimplemented!() + // TODO: apply interpolation mode + self.ctx.draw_image(to_cgrect(rect), image); } fn draw_image_area( &mut self, - _image: &Self::Image, - _src_rect: impl Into, - _dst_rect: impl Into, + image: &Self::Image, + src_rect: impl Into, + dst_rect: impl Into, _interp: InterpolationMode, ) { - unimplemented!() + if let Some(cropped) = image.cropped(to_cgrect(src_rect)) { + // TODO: apply interpolation mode + self.ctx.draw_image(to_cgrect(dst_rect), &cropped); + } } fn blurred_rect(&mut self, _rect: Rect, _blur_radius: f64, _brush: &impl IntoBrush) { @@ -406,3 +409,16 @@ impl<'a> CoreGraphicsContext<'a> { fn byte_to_frac(byte: u32) -> f64 { ((byte & 255) as f64) * (1.0 / 255.0) } + +fn to_cgpoint(point: Point) -> CGPoint { + CGPoint::new(point.x as CGFloat, point.y as CGFloat) +} + +fn to_cgsize(size: Size) -> CGSize { + CGSize::new(size.width, size.height) +} + +fn to_cgrect(rect: impl Into) -> CGRect { + let rect = rect.into(); + CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size())) +} From 7c78164fd77d8488baee1a5ce2a97cb7c5698b9f Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 28 Apr 2020 09:39:46 -0700 Subject: [PATCH 05/30] Clean up fmt and clippy warnings I actually consider the "unused" warnings to be useful, as they signal where implementation is needed, but grepping for "unimplemented" also works :) --- piet-coregraphics/src/lib.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 1235ccde7..1a483dd1e 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -9,16 +9,15 @@ use core_graphics::base::{ use core_graphics::color_space::CGColorSpace; use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; use core_graphics::data_provider::CGDataProvider; -use core_graphics::image::CGImage; use core_graphics::geometry::{CGPoint, CGRect, CGSize}; +use core_graphics::image::CGImage; -use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size, Vec2}; +use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size}; use piet::{ - new_error, Color, Error, ErrorKind, FixedGradient, Font, FontBuilder, HitTestMetrics, - HitTestPoint, HitTestTextPosition, ImageFormat, InterpolationMode, IntoBrush, LineCap, - LineJoin, LineMetric, RenderContext, RoundInto, StrokeStyle, Text, TextLayout, - TextLayoutBuilder, + Color, Error, FixedGradient, Font, FontBuilder, HitTestPoint, HitTestTextPosition, ImageFormat, + InterpolationMode, IntoBrush, LineCap, LineJoin, LineMetric, RenderContext, RoundInto, + StrokeStyle, Text, TextLayout, TextLayoutBuilder, }; pub struct CoreGraphicsContext<'a> { @@ -76,7 +75,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { Brush::Solid(color.as_rgba_u32()) } - fn gradient(&mut self, gradient: impl Into) -> Result { + fn gradient(&mut self, _gradient: impl Into) -> Result { unimplemented!() } @@ -128,9 +127,9 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { fn draw_text( &mut self, - layout: &Self::TextLayout, - pos: impl Into, - brush: &impl IntoBrush, + _layout: &Self::TextLayout, + _pos: impl Into, + _brush: &impl IntoBrush, ) { unimplemented!() } @@ -149,7 +148,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { Ok(()) } - fn transform(&mut self, transform: Affine) { + fn transform(&mut self, _transform: Affine) { unimplemented!() } @@ -418,7 +417,7 @@ fn to_cgsize(size: Size) -> CGSize { CGSize::new(size.width, size.height) } -fn to_cgrect(rect: impl Into) -> CGRect { +fn to_cgrect(rect: impl Into) -> CGRect { let rect = rect.into(); CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size())) } From 642e19f9ddfbe7e483006893d1db5f201c57c6d4 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 28 Apr 2020 09:50:42 -0700 Subject: [PATCH 06/30] Fix clippy warning in cg example --- piet-coregraphics/examples/basic-cg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index 777b63516..51bf31629 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -33,7 +33,7 @@ fn main() { // Write image as PNG file. let path = Path::new("image.png"); let file = File::create(path).unwrap(); - let ref mut w = BufWriter::new(file); + let w = BufWriter::new(file); let mut encoder = png::Encoder::new(w, WIDTH as u32, HEIGHT as u32); encoder.set_color(png::ColorType::RGBA); From b6b47b60907b78bc0fe16217f687c66de4a17fd8 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 28 Apr 2020 10:02:15 -0700 Subject: [PATCH 07/30] CI changes to add coregraphics --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2102687d3..b56aa8d8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,21 +59,28 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace --all-targets --exclude piet-cairo -- -D warnings + args: --workspace --all-targets --exclude piet-cairo --exclude piet-coregraphics -- -D warnings if: contains(matrix.os, 'windows') - - name: cargo clippy (not windows) + - name: cargo clippy (mac) uses: actions-rs/cargo@v1 with: command: clippy args: --workspace --all-targets --exclude piet-direct2d -- -D warnings - if: contains(matrix.os, 'windows') != true + if: contains(matrix.os, 'macOS') + + - name: cargo clippy (linux) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace --all-targets --exclude piet-direct2d --exclude piet-coregraphics -- -D warnings + if: contains(matrix.os, 'ubuntu') - name: cargo test (windows) uses: actions-rs/cargo@v1 with: command: test - args: --workspace --exclude piet-cairo + args: --workspace --exclude piet-cairo --exclude piet-coregraphics if: contains(matrix.os, 'windows') - name: cargo test (not windows) @@ -81,7 +88,14 @@ jobs: with: command: test args: --workspace --exclude piet-direct2d - if: contains(matrix.os, 'windows') != true + if: contains(matrix.os, 'macOS') + + - name: cargo test (not windows) + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --exclude piet-direct2d --exclude piet-coregraphics + if: contains(matrix.os, 'ubuntu') test-stable-wasm: runs-on: ${{ matrix.os }} @@ -105,13 +119,13 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace --all-targets --exclude piet-cairo --exclude piet-direct2d --target wasm32-unknown-unknown -- -D warnings + args: --workspace --all-targets --exclude piet-cairo --exclude piet-direct2d --exclude piet-coregraphics --target wasm32-unknown-unknown -- -D warnings - name: cargo test compile uses: actions-rs/cargo@v1 with: command: test - args: --workspace --exclude piet-cairo --exclude piet-direct2d --no-run --target wasm32-unknown-unknown + args: --workspace --exclude piet-cairo --exclude piet-direct2d --exclude piet-coregraphics --no-run --target wasm32-unknown-unknown test-nightly: runs-on: ${{ matrix.os }} @@ -143,15 +157,22 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --workspace --exclude piet-cairo + args: --workspace --exclude piet-cairo --exclude piet-coregraphics if: contains(matrix.os, 'windows') - - name: cargo test (not windows) + - name: cargo test (mac) uses: actions-rs/cargo@v1 with: command: test args: --workspace --exclude piet-direct2d - if: contains(matrix.os, 'windows') != true + if: contains(matrix.os, 'macOS') + + - name: cargo test (linux) + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --exclude piet-direct2d --exclude piet-coregraphics + if: contains(matrix.os, 'ubuntu') check-docs: name: Docs From eb3a3e45df9e7a5823586da4f9daa5041aeae52d Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 28 Apr 2020 12:40:29 -0700 Subject: [PATCH 08/30] CI name improvements --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b56aa8d8a..2fcede958 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,14 +83,14 @@ jobs: args: --workspace --exclude piet-cairo --exclude piet-coregraphics if: contains(matrix.os, 'windows') - - name: cargo test (not windows) + - name: cargo test (mac) uses: actions-rs/cargo@v1 with: command: test args: --workspace --exclude piet-direct2d if: contains(matrix.os, 'macOS') - - name: cargo test (not windows) + - name: cargo test (linux) uses: actions-rs/cargo@v1 with: command: test From fa5975c82ee2d9adb887d897b5485a04b96e1429 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Thu, 30 Apr 2020 11:39:09 -0400 Subject: [PATCH 09/30] [cg] Add text module, move text stubs there --- piet-coregraphics/src/lib.rs | 91 ++++------------------------------- piet-coregraphics/src/text.rs | 86 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 82 deletions(-) create mode 100644 piet-coregraphics/src/text.rs diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 1a483dd1e..dcab03c9f 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,5 +1,7 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. +mod text; + use std::borrow::Cow; use std::sync::Arc; @@ -15,9 +17,13 @@ use core_graphics::image::CGImage; use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size}; use piet::{ - Color, Error, FixedGradient, Font, FontBuilder, HitTestPoint, HitTestTextPosition, ImageFormat, - InterpolationMode, IntoBrush, LineCap, LineJoin, LineMetric, RenderContext, RoundInto, - StrokeStyle, Text, TextLayout, TextLayoutBuilder, + Color, Error, FixedGradient, ImageFormat, InterpolationMode, IntoBrush, LineCap, LineJoin, + RenderContext, RoundInto, StrokeStyle, +}; + +pub use crate::text::{ + CoreGraphicsFont, CoreGraphicsFontBuilder, CoreGraphicsText, CoreGraphicsTextLayout, + CoreGraphicsTextLayoutBuilder, }; pub struct CoreGraphicsContext<'a> { @@ -42,17 +48,6 @@ pub enum Brush { Gradient, } -pub struct CoreGraphicsFont; - -pub struct CoreGraphicsFontBuilder; - -#[derive(Clone)] -pub struct CoreGraphicsTextLayout; - -pub struct CoreGraphicsTextLayoutBuilder {} - -pub struct CoreGraphicsText; - impl<'a> RenderContext for CoreGraphicsContext<'a> { type Brush = Brush; type Text = CoreGraphicsText; @@ -226,74 +221,6 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } } -impl Text for CoreGraphicsText { - type Font = CoreGraphicsFont; - type FontBuilder = CoreGraphicsFontBuilder; - type TextLayout = CoreGraphicsTextLayout; - type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder; - - fn new_font_by_name(&mut self, _name: &str, _size: f64) -> Self::FontBuilder { - unimplemented!(); - } - - fn new_text_layout( - &mut self, - _font: &Self::Font, - _text: &str, - _width: impl Into>, - ) -> Self::TextLayoutBuilder { - unimplemented!(); - } -} - -impl Font for CoreGraphicsFont {} - -impl FontBuilder for CoreGraphicsFontBuilder { - type Out = CoreGraphicsFont; - - fn build(self) -> Result { - unimplemented!(); - } -} - -impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder { - type Out = CoreGraphicsTextLayout; - - fn build(self) -> Result { - unimplemented!() - } -} - -impl TextLayout for CoreGraphicsTextLayout { - fn width(&self) -> f64 { - 0.0 - } - - fn update_width(&mut self, _new_width: impl Into>) -> Result<(), Error> { - unimplemented!() - } - - fn line_text(&self, _line_number: usize) -> Option<&str> { - unimplemented!() - } - - fn line_metric(&self, _line_number: usize) -> Option { - unimplemented!() - } - - fn line_count(&self) -> usize { - unimplemented!() - } - - fn hit_test_point(&self, _point: Point) -> HitTestPoint { - unimplemented!() - } - - fn hit_test_text_position(&self, _text_position: usize) -> Option { - unimplemented!() - } -} - impl<'a> IntoBrush> for Brush { fn make_brush<'b>( &'b self, diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs new file mode 100644 index 000000000..be8ae8bb8 --- /dev/null +++ b/piet-coregraphics/src/text.rs @@ -0,0 +1,86 @@ +//! Text related stuff for the coregraphics backend + +use piet::kurbo::Point; +use piet::{ + Error, Font, FontBuilder, HitTestPoint, HitTestTextPosition, LineMetric, Text, TextLayout, + TextLayoutBuilder, +}; + +pub struct CoreGraphicsFont; + +pub struct CoreGraphicsFontBuilder; + +#[derive(Clone)] +pub struct CoreGraphicsTextLayout; + +pub struct CoreGraphicsTextLayoutBuilder {} + +pub struct CoreGraphicsText; + +impl Text for CoreGraphicsText { + type Font = CoreGraphicsFont; + type FontBuilder = CoreGraphicsFontBuilder; + type TextLayout = CoreGraphicsTextLayout; + type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder; + + fn new_font_by_name(&mut self, _name: &str, _size: f64) -> Self::FontBuilder { + unimplemented!(); + } + + fn new_text_layout( + &mut self, + _font: &Self::Font, + _text: &str, + _width: impl Into>, + ) -> Self::TextLayoutBuilder { + unimplemented!(); + } +} + +impl Font for CoreGraphicsFont {} + +impl FontBuilder for CoreGraphicsFontBuilder { + type Out = CoreGraphicsFont; + + fn build(self) -> Result { + unimplemented!(); + } +} + +impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder { + type Out = CoreGraphicsTextLayout; + + fn build(self) -> Result { + unimplemented!() + } +} + +impl TextLayout for CoreGraphicsTextLayout { + fn width(&self) -> f64 { + 0.0 + } + + fn update_width(&mut self, _new_width: impl Into>) -> Result<(), Error> { + unimplemented!() + } + + fn line_text(&self, _line_number: usize) -> Option<&str> { + unimplemented!() + } + + fn line_metric(&self, _line_number: usize) -> Option { + unimplemented!() + } + + fn line_count(&self) -> usize { + unimplemented!() + } + + fn hit_test_point(&self, _point: Point) -> HitTestPoint { + unimplemented!() + } + + fn hit_test_text_position(&self, _text_position: usize) -> Option { + unimplemented!() + } +} From 39d955feeaf6e930760cbbd439e6dc81fc3a9e90 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Fri, 1 May 2020 15:04:11 -0400 Subject: [PATCH 10/30] [cg] Implement transform and current_transform methods --- piet-coregraphics/src/lib.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index dcab03c9f..b10037f89 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -11,7 +11,7 @@ use core_graphics::base::{ use core_graphics::color_space::CGColorSpace; use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; use core_graphics::data_provider::CGDataProvider; -use core_graphics::geometry::{CGPoint, CGRect, CGSize}; +use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize}; use core_graphics::image::CGImage; use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size}; @@ -143,8 +143,9 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { Ok(()) } - fn transform(&mut self, _transform: Affine) { - unimplemented!() + fn transform(&mut self, transform: Affine) { + let transform = to_cgaffine(transform); + self.ctx.concat_ctm(transform); } fn make_image( @@ -213,7 +214,8 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn current_transform(&self) -> Affine { - Default::default() + let ctm = self.ctx.get_ctm(); + from_cgaffine(ctm) } fn status(&mut self) -> Result<(), Error> { @@ -348,3 +350,13 @@ fn to_cgrect(rect: impl Into) -> CGRect { let rect = rect.into(); CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size())) } + +fn from_cgaffine(affine: CGAffineTransform) -> Affine { + let CGAffineTransform { a, b, c, d, tx, ty } = affine; + Affine::new([a, b, c, d, tx, ty]) +} + +fn to_cgaffine(affine: Affine) -> CGAffineTransform { + let [a, b, c, d, tx, ty] = affine.as_coeffs(); + CGAffineTransform::new(a, b, c, d, tx, ty) +} From 3ed7d1dfc6117cf3e8c201adcee2908e3e2cecfb Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Thu, 30 Apr 2020 14:29:07 -0400 Subject: [PATCH 11/30] [cg] Add initial textlayout impl This doesn't do hit testing, but it does paint multiline text. --- piet-coregraphics/Cargo.toml | 3 + piet-coregraphics/examples/basic-cg.rs | 28 +++++- piet-coregraphics/src/lib.rs | 17 +++- piet-coregraphics/src/text.rs | 116 +++++++++++++++++++++---- 4 files changed, 143 insertions(+), 21 deletions(-) diff --git a/piet-coregraphics/Cargo.toml b/piet-coregraphics/Cargo.toml index ac4592949..b1c5b4cee 100644 --- a/piet-coregraphics/Cargo.toml +++ b/piet-coregraphics/Cargo.toml @@ -11,6 +11,9 @@ categories = ["rendering::graphics-api"] [dependencies] piet = { version = "0.0.12", path = "../piet" } core-graphics = "0.19" +core-text = "15.0" +core-foundation = "0.7" +core-foundation-sys = "0.7" [dev-dependencies] png = "0.16.2" diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index 51bf31629..383c5c3e9 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -5,8 +5,8 @@ use std::path::Path; use core_graphics::color_space::CGColorSpace; use core_graphics::context::CGContext; -use piet::kurbo::Circle; -use piet::{Color, RenderContext}; +use piet::kurbo::{Circle, Rect, Size}; +use piet::{Color, FontBuilder, RenderContext, Text, TextLayoutBuilder}; const WIDTH: usize = 800; const HEIGHT: usize = 600; @@ -22,10 +22,34 @@ fn main() { core_graphics::base::kCGImageAlphaPremultipliedLast, ); let mut piet = piet_coregraphics::CoreGraphicsContext::new(&mut cg_ctx); + let bounds = Size::new(WIDTH as f64, HEIGHT as f64).to_rect(); + piet.stroke(bounds, &Color::rgba8(0, 255, 0, 128), 20.0); + piet.fill( + bounds.inset((0., 0., -bounds.width() * 0.5, 0.)), + &Color::rgba8(0, 0, 255, 128), + ); piet.fill( Circle::new((100.0, 100.0), 50.0), &Color::rgb8(255, 0, 0).with_alpha(0.5), ); + piet.fill(Rect::new(0., 0., 200., 200.), &Color::rgba8(0, 0, 255, 128)); + + let font = piet + .text() + .new_font_by_name("Georgia", 24.0) + .build() + .unwrap(); + + let layout = piet + .text() + .new_text_layout(&font, "this is my cool\nmultiline string, I like it very much, do you also like it? why or why not? Show your work.", 400.0) + .build() + .unwrap(); + + piet.draw_text(&layout, (200.0, 200.0), &Color::BLACK); + piet.draw_text(&layout, (0., 00.0), &Color::WHITE); + piet.draw_text(&layout, (400.0, 400.0), &Color::rgba8(255, 0, 0, 150)); + piet.finish().unwrap(); unpremultiply(cg_ctx.data()); diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index b10037f89..7f0360986 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -122,11 +122,20 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { fn draw_text( &mut self, - _layout: &Self::TextLayout, - _pos: impl Into, - _brush: &impl IntoBrush, + layout: &Self::TextLayout, + pos: impl Into, + brush: &impl IntoBrush, ) { - unimplemented!() + let brush = brush.make_brush(self, || layout.frame_size.to_rect()); + let pos = pos.into(); + self.ctx.save(); + // inverted coordinate system; text is drawn from bottom left corner, + // and (0, 0) in context is also bottom left. + let y_off = self.ctx.height() as f64 - layout.frame_size.height; + self.ctx.translate(pos.x, y_off - pos.y); + self.set_fill_brush(&brush); + layout.frame.draw(self.ctx); + self.ctx.restore(); } fn save(&mut self) -> Result<(), Error> { diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index be8ae8bb8..9a80ac8ab 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -1,19 +1,43 @@ //! Text related stuff for the coregraphics backend -use piet::kurbo::Point; +use core_foundation::array::{CFArray, CFArrayRef}; +use core_foundation::attributed_string::CFMutableAttributedString; +use core_foundation::base::TCFType; +use core_foundation::dictionary::CFDictionaryRef; +use core_foundation::number::CFNumber; +use core_foundation::string::CFString; + +use core_foundation_sys::base::CFRange; +use core_graphics::base::CGFloat; +use core_graphics::geometry::{CGPoint, CGRect, CGSize}; +use core_graphics::path::CGPath; +use core_text::font::{self, CTFont}; +use core_text::frame::{CTFrame, CTFrameRef}; +use core_text::framesetter::{CTFramesetter, CTFramesetterRef}; +use core_text::line::CTLine; +use core_text::string_attributes; + +use piet::kurbo::{Point, Size}; use piet::{ Error, Font, FontBuilder, HitTestPoint, HitTestTextPosition, LineMetric, Text, TextLayout, TextLayoutBuilder, }; -pub struct CoreGraphicsFont; +// inner is an nsfont. +#[derive(Debug, Clone)] +pub struct CoreGraphicsFont(CTFont); -pub struct CoreGraphicsFontBuilder; +pub struct CoreGraphicsFontBuilder(Option); #[derive(Clone)] -pub struct CoreGraphicsTextLayout; +pub struct CoreGraphicsTextLayout { + framesetter: CTFramesetter, + pub(crate) frame: CTFrame, + pub(crate) frame_size: Size, + line_count: usize, +} -pub struct CoreGraphicsTextLayoutBuilder {} +pub struct CoreGraphicsTextLayoutBuilder(CoreGraphicsTextLayout); pub struct CoreGraphicsText; @@ -23,17 +47,66 @@ impl Text for CoreGraphicsText { type TextLayout = CoreGraphicsTextLayout; type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder; - fn new_font_by_name(&mut self, _name: &str, _size: f64) -> Self::FontBuilder { - unimplemented!(); + fn new_font_by_name(&mut self, name: &str, size: f64) -> Self::FontBuilder { + CoreGraphicsFontBuilder(font::new_from_name(name, size).ok()) } fn new_text_layout( &mut self, - _font: &Self::Font, - _text: &str, - _width: impl Into>, + font: &Self::Font, + text: &str, + width: impl Into>, ) -> Self::TextLayoutBuilder { - unimplemented!(); + let width = width.into().unwrap_or(f64::INFINITY); + let constraints = CGSize::new(width as CGFloat, CGFloat::INFINITY); + let mut string = CFMutableAttributedString::new(); + let range = CFRange::init(0, 0); + string.replace_str(&CFString::new(text), range); + + let str_len = string.char_len(); + let char_range = CFRange::init(0, str_len); + unsafe { + string.set_attribute( + char_range, + string_attributes::kCTFontAttributeName, + font.0.clone(), + ); + string.set_attribute::( + char_range, + string_attributes::kCTForegroundColorFromContextAttributeName, + 1i32.into(), + ); + } + + let framesetter = CTFramesetter::new_with_attributed_string(string.as_concrete_TypeRef()); + + let mut fit_range = CFRange::init(0, 0); + let frame_size = unsafe { + CTFramesetterSuggestFrameSizeWithConstraints( + framesetter.as_concrete_TypeRef(), + char_range, + std::ptr::null(), + constraints, + &mut fit_range, + ) + }; + + let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); + let path = CGPath::from_rect(rect, None); + let frame = framesetter.create_frame(char_range, &path); + + let lines: CFArray = + unsafe { TCFType::wrap_under_get_rule(CTFrameGetLines(frame.as_concrete_TypeRef())) }; + let line_count = lines.len() as usize; + + let frame_size = Size::new(frame_size.width, frame_size.height); + let layout = CoreGraphicsTextLayout { + framesetter, + frame, + frame_size, + line_count, + }; + CoreGraphicsTextLayoutBuilder(layout) } } @@ -43,7 +116,7 @@ impl FontBuilder for CoreGraphicsFontBuilder { type Out = CoreGraphicsFont; fn build(self) -> Result { - unimplemented!(); + self.0.map(CoreGraphicsFont).ok_or(Error::MissingFont) } } @@ -51,13 +124,13 @@ impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder { type Out = CoreGraphicsTextLayout; fn build(self) -> Result { - unimplemented!() + Ok(self.0) } } impl TextLayout for CoreGraphicsTextLayout { fn width(&self) -> f64 { - 0.0 + self.frame_size.width } fn update_width(&mut self, _new_width: impl Into>) -> Result<(), Error> { @@ -73,7 +146,7 @@ impl TextLayout for CoreGraphicsTextLayout { } fn line_count(&self) -> usize { - unimplemented!() + self.line_count } fn hit_test_point(&self, _point: Point) -> HitTestPoint { @@ -84,3 +157,16 @@ impl TextLayout for CoreGraphicsTextLayout { unimplemented!() } } + +#[link(name = "CoreText", kind = "framework")] +extern "C" { + fn CTFramesetterSuggestFrameSizeWithConstraints( + framesetter: CTFramesetterRef, + string_range: CFRange, + frame_attributes: CFDictionaryRef, + constraints: CGSize, + fitRange: *mut CFRange, + ) -> CGSize; + + fn CTFrameGetLines(frame: CTFrameRef) -> CFArrayRef; +} From 3b70be39e2f9c616fc9f57399d53dda93279b4da Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Fri, 1 May 2020 17:31:56 -0400 Subject: [PATCH 12/30] [cg] Move to using wrappers for CT types This just makes our API cleaner? Ideally? --- piet-coregraphics/src/ct_helpers.rs | 104 ++++++++++++++++++++++++++++ piet-coregraphics/src/lib.rs | 3 +- piet-coregraphics/src/text.rs | 71 +++---------------- 3 files changed, 116 insertions(+), 62 deletions(-) create mode 100644 piet-coregraphics/src/ct_helpers.rs diff --git a/piet-coregraphics/src/ct_helpers.rs b/piet-coregraphics/src/ct_helpers.rs new file mode 100644 index 000000000..84979d0d1 --- /dev/null +++ b/piet-coregraphics/src/ct_helpers.rs @@ -0,0 +1,104 @@ +//! Wrappers around CF/CT types, with nice interfaces. + +use core_foundation::{ + array::{CFArray, CFArrayRef}, + attributed_string::CFMutableAttributedString, + base::TCFType, + dictionary::CFDictionaryRef, + number::CFNumber, + string::CFString, +}; +use core_foundation_sys::base::CFRange; +use core_graphics::{geometry::CGSize, path::CGPathRef}; +use core_text::{ + font::CTFont, + frame::{CTFrame, CTFrameRef}, + framesetter::{CTFramesetter, CTFramesetterRef}, + line::CTLine, + string_attributes, +}; + +pub(crate) struct AttributedString(pub(crate) CFMutableAttributedString); +#[derive(Debug, Clone)] +pub(crate) struct Framesetter(CTFramesetter); +#[derive(Debug, Clone)] +pub(crate) struct Frame(pub(crate) CTFrame); + +impl AttributedString { + pub(crate) fn new(text: &str, font: &CTFont) -> Self { + let mut string = CFMutableAttributedString::new(); + let range = CFRange::init(0, 0); + string.replace_str(&CFString::new(text), range); + + let str_len = string.char_len(); + let char_range = CFRange::init(0, str_len); + + unsafe { + string.set_attribute( + char_range, + string_attributes::kCTFontAttributeName, + font.clone(), + ); + string.set_attribute::( + char_range, + string_attributes::kCTForegroundColorFromContextAttributeName, + 1i32.into(), + ); + } + AttributedString(string) + } + + pub(crate) fn range(&self) -> CFRange { + CFRange::init(0, self.0.char_len()) + } +} + +impl Framesetter { + pub(crate) fn new(attributed_string: &AttributedString) -> Self { + Framesetter(CTFramesetter::new_with_attributed_string( + attributed_string.0.as_concrete_TypeRef(), + )) + } + + /// returns the suggested size and the range of the string that fits. + pub(crate) fn suggest_frame_size( + &self, + range: CFRange, + constraints: CGSize, + ) -> (CGSize, CFRange) { + unsafe { + let mut fit_range = CFRange::init(0, 0); + let size = CTFramesetterSuggestFrameSizeWithConstraints( + self.0.as_concrete_TypeRef(), + range, + std::ptr::null(), + constraints, + &mut fit_range, + ); + (size, fit_range) + } + } + + pub(crate) fn create_frame(&self, range: CFRange, path: &CGPathRef) -> Frame { + Frame(self.0.create_frame(range, path)) + } +} + +impl Frame { + pub(crate) fn get_lines(&self) -> CFArray { + unsafe { TCFType::wrap_under_get_rule(CTFrameGetLines(self.0.as_concrete_TypeRef())) } + } +} + +#[link(name = "CoreText", kind = "framework")] +extern "C" { + fn CTFramesetterSuggestFrameSizeWithConstraints( + framesetter: CTFramesetterRef, + string_range: CFRange, + frame_attributes: CFDictionaryRef, + constraints: CGSize, + fitRange: *mut CFRange, + ) -> CGSize; + + fn CTFrameGetLines(frame: CTFrameRef) -> CFArrayRef; +} diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 7f0360986..8acda5ca7 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,5 +1,6 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. +mod ct_helpers; mod text; use std::borrow::Cow; @@ -134,7 +135,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { let y_off = self.ctx.height() as f64 - layout.frame_size.height; self.ctx.translate(pos.x, y_off - pos.y); self.set_fill_brush(&brush); - layout.frame.draw(self.ctx); + layout.frame.0.draw(self.ctx); self.ctx.restore(); } diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 9a80ac8ab..ea53e84da 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -1,21 +1,9 @@ //! Text related stuff for the coregraphics backend -use core_foundation::array::{CFArray, CFArrayRef}; -use core_foundation::attributed_string::CFMutableAttributedString; -use core_foundation::base::TCFType; -use core_foundation::dictionary::CFDictionaryRef; -use core_foundation::number::CFNumber; -use core_foundation::string::CFString; - -use core_foundation_sys::base::CFRange; use core_graphics::base::CGFloat; use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use core_graphics::path::CGPath; use core_text::font::{self, CTFont}; -use core_text::frame::{CTFrame, CTFrameRef}; -use core_text::framesetter::{CTFramesetter, CTFramesetterRef}; -use core_text::line::CTLine; -use core_text::string_attributes; use piet::kurbo::{Point, Size}; use piet::{ @@ -23,6 +11,8 @@ use piet::{ TextLayoutBuilder, }; +use crate::ct_helpers::{AttributedString, Frame, Framesetter}; + // inner is an nsfont. #[derive(Debug, Clone)] pub struct CoreGraphicsFont(CTFont); @@ -31,8 +21,8 @@ pub struct CoreGraphicsFontBuilder(Option); #[derive(Clone)] pub struct CoreGraphicsTextLayout { - framesetter: CTFramesetter, - pub(crate) frame: CTFrame, + framesetter: Framesetter, + pub(crate) frame: Frame, pub(crate) frame_size: Size, line_count: usize, } @@ -59,44 +49,16 @@ impl Text for CoreGraphicsText { ) -> Self::TextLayoutBuilder { let width = width.into().unwrap_or(f64::INFINITY); let constraints = CGSize::new(width as CGFloat, CGFloat::INFINITY); - let mut string = CFMutableAttributedString::new(); - let range = CFRange::init(0, 0); - string.replace_str(&CFString::new(text), range); - - let str_len = string.char_len(); - let char_range = CFRange::init(0, str_len); - unsafe { - string.set_attribute( - char_range, - string_attributes::kCTFontAttributeName, - font.0.clone(), - ); - string.set_attribute::( - char_range, - string_attributes::kCTForegroundColorFromContextAttributeName, - 1i32.into(), - ); - } - - let framesetter = CTFramesetter::new_with_attributed_string(string.as_concrete_TypeRef()); - - let mut fit_range = CFRange::init(0, 0); - let frame_size = unsafe { - CTFramesetterSuggestFrameSizeWithConstraints( - framesetter.as_concrete_TypeRef(), - char_range, - std::ptr::null(), - constraints, - &mut fit_range, - ) - }; + let string = AttributedString::new(text, &font.0); + + let framesetter = Framesetter::new(&string); + let char_range = string.range(); + let (frame_size, _) = framesetter.suggest_frame_size(char_range, constraints); let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); let path = CGPath::from_rect(rect, None); let frame = framesetter.create_frame(char_range, &path); - - let lines: CFArray = - unsafe { TCFType::wrap_under_get_rule(CTFrameGetLines(frame.as_concrete_TypeRef())) }; + let lines = frame.get_lines(); let line_count = lines.len() as usize; let frame_size = Size::new(frame_size.width, frame_size.height); @@ -157,16 +119,3 @@ impl TextLayout for CoreGraphicsTextLayout { unimplemented!() } } - -#[link(name = "CoreText", kind = "framework")] -extern "C" { - fn CTFramesetterSuggestFrameSizeWithConstraints( - framesetter: CTFramesetterRef, - string_range: CFRange, - frame_attributes: CFDictionaryRef, - constraints: CGSize, - fitRange: *mut CFRange, - ) -> CGSize; - - fn CTFrameGetLines(frame: CTFrameRef) -> CFArrayRef; -} From f58814d1ec3b714921c658623b69311005d6a2ce Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Fri, 1 May 2020 17:47:53 -0400 Subject: [PATCH 13/30] [cg] Implement TextLayout::update_width --- piet-coregraphics/examples/basic-cg.rs | 13 +++++++------ piet-coregraphics/src/ct_helpers.rs | 1 + piet-coregraphics/src/text.rs | 24 ++++++++++++++++++++---- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index 383c5c3e9..45098f4df 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -5,8 +5,8 @@ use std::path::Path; use core_graphics::color_space::CGColorSpace; use core_graphics::context::CGContext; -use piet::kurbo::{Circle, Rect, Size}; -use piet::{Color, FontBuilder, RenderContext, Text, TextLayoutBuilder}; +use piet::kurbo::{Circle, Size}; +use piet::{Color, FontBuilder, RenderContext, Text, TextLayout, TextLayoutBuilder}; const WIDTH: usize = 800; const HEIGHT: usize = 600; @@ -32,7 +32,6 @@ fn main() { Circle::new((100.0, 100.0), 50.0), &Color::rgb8(255, 0, 0).with_alpha(0.5), ); - piet.fill(Rect::new(0., 0., 200., 200.), &Color::rgba8(0, 0, 255, 128)); let font = piet .text() @@ -40,14 +39,16 @@ fn main() { .build() .unwrap(); - let layout = piet + let mut layout = piet .text() - .new_text_layout(&font, "this is my cool\nmultiline string, I like it very much, do you also like it? why or why not? Show your work.", 400.0) + .new_text_layout(&font, "this is my cool\nmultiline string, I like it very much, do you also like it? why or why not? Show your work.", None) .build() .unwrap(); - piet.draw_text(&layout, (200.0, 200.0), &Color::BLACK); piet.draw_text(&layout, (0., 00.0), &Color::WHITE); + layout.update_width(400.).unwrap(); + piet.draw_text(&layout, (200.0, 200.0), &Color::BLACK); + layout.update_width(200.).unwrap(); piet.draw_text(&layout, (400.0, 400.0), &Color::rgba8(255, 0, 0, 150)); piet.finish().unwrap(); diff --git a/piet-coregraphics/src/ct_helpers.rs b/piet-coregraphics/src/ct_helpers.rs index 84979d0d1..1e6e8869d 100644 --- a/piet-coregraphics/src/ct_helpers.rs +++ b/piet-coregraphics/src/ct_helpers.rs @@ -18,6 +18,7 @@ use core_text::{ string_attributes, }; +#[derive(Clone)] pub(crate) struct AttributedString(pub(crate) CFMutableAttributedString); #[derive(Debug, Clone)] pub(crate) struct Framesetter(CTFramesetter); diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index ea53e84da..8fff95e75 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -21,10 +21,12 @@ pub struct CoreGraphicsFontBuilder(Option); #[derive(Clone)] pub struct CoreGraphicsTextLayout { + string: AttributedString, framesetter: Framesetter, pub(crate) frame: Frame, pub(crate) frame_size: Size, line_count: usize, + width_constraint: f64, } pub struct CoreGraphicsTextLayoutBuilder(CoreGraphicsTextLayout); @@ -47,8 +49,8 @@ impl Text for CoreGraphicsText { text: &str, width: impl Into>, ) -> Self::TextLayoutBuilder { - let width = width.into().unwrap_or(f64::INFINITY); - let constraints = CGSize::new(width as CGFloat, CGFloat::INFINITY); + let width_constraint = width.into().unwrap_or(f64::INFINITY); + let constraints = CGSize::new(width_constraint as CGFloat, CGFloat::INFINITY); let string = AttributedString::new(text, &font.0); let framesetter = Framesetter::new(&string); @@ -63,10 +65,12 @@ impl Text for CoreGraphicsText { let frame_size = Size::new(frame_size.width, frame_size.height); let layout = CoreGraphicsTextLayout { + string, framesetter, frame, frame_size, line_count, + width_constraint, }; CoreGraphicsTextLayoutBuilder(layout) } @@ -95,8 +99,20 @@ impl TextLayout for CoreGraphicsTextLayout { self.frame_size.width } - fn update_width(&mut self, _new_width: impl Into>) -> Result<(), Error> { - unimplemented!() + fn update_width(&mut self, new_width: impl Into>) -> Result<(), Error> { + let width = new_width.into().unwrap_or(f64::INFINITY); + if width != self.width_constraint { + let constraints = CGSize::new(width as CGFloat, CGFloat::INFINITY); + let char_range = self.string.range(); + let (frame_size, _) = self.framesetter.suggest_frame_size(char_range, constraints); + let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); + let path = CGPath::from_rect(rect, None); + self.width_constraint = width; + self.frame = self.framesetter.create_frame(char_range, &path); + self.line_count = self.frame.get_lines().len() as usize; + self.frame_size = Size::new(frame_size.width, frame_size.height); + } + Ok(()) } fn line_text(&self, _line_number: usize) -> Option<&str> { From 1ab5b8ec5a7dcb72ee2ae15b097ea59c4bda9458 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Sat, 2 May 2020 12:03:21 -0400 Subject: [PATCH 14/30] [cg] Implmenent line_text method This involves stashing our source string, and generating a map to get utf8 offsets from utf16 offsets. This also adds stashing of line origins, which we will need for hit testing. --- piet-coregraphics/src/ct_helpers.rs | 31 ++++++- piet-coregraphics/src/text.rs | 138 ++++++++++++++++++++++------ 2 files changed, 137 insertions(+), 32 deletions(-) diff --git a/piet-coregraphics/src/ct_helpers.rs b/piet-coregraphics/src/ct_helpers.rs index 1e6e8869d..8d2b3c577 100644 --- a/piet-coregraphics/src/ct_helpers.rs +++ b/piet-coregraphics/src/ct_helpers.rs @@ -1,5 +1,7 @@ //! Wrappers around CF/CT types, with nice interfaces. +use std::ops::Deref; + use core_foundation::{ array::{CFArray, CFArrayRef}, attributed_string::CFMutableAttributedString, @@ -9,12 +11,15 @@ use core_foundation::{ string::CFString, }; use core_foundation_sys::base::CFRange; -use core_graphics::{geometry::CGSize, path::CGPathRef}; +use core_graphics::{ + geometry::{CGPoint, CGSize}, + path::CGPathRef, +}; use core_text::{ font::CTFont, frame::{CTFrame, CTFrameRef}, framesetter::{CTFramesetter, CTFramesetterRef}, - line::CTLine, + line::{CTLine, CTLineRef}, string_attributes, }; @@ -24,6 +29,8 @@ pub(crate) struct AttributedString(pub(crate) CFMutableAttributedString); pub(crate) struct Framesetter(CTFramesetter); #[derive(Debug, Clone)] pub(crate) struct Frame(pub(crate) CTFrame); +#[derive(Debug, Clone)] +pub(crate) struct Line<'a>(pub(crate) &'a CTLine); impl AttributedString { pub(crate) fn new(text: &str, font: &CTFont) -> Self { @@ -89,6 +96,24 @@ impl Frame { pub(crate) fn get_lines(&self) -> CFArray { unsafe { TCFType::wrap_under_get_rule(CTFrameGetLines(self.0.as_concrete_TypeRef())) } } + + pub(crate) fn get_line_origins(&self, range: CFRange) -> Vec { + let mut origins = vec![CGPoint::new(0.0, 0.0); range.length as usize]; + unsafe { + CTFrameGetLineOrigins(self.0.as_concrete_TypeRef(), range, origins.as_mut_ptr()); + } + origins + } +} + +impl<'a> Line<'a> { + pub(crate) fn new(inner: &'a impl Deref) -> Line<'a> { + Line(inner.deref()) + } + + pub(crate) fn get_string_range(&self) -> CFRange { + unsafe { CTLineGetStringRange(self.0.as_concrete_TypeRef()) } + } } #[link(name = "CoreText", kind = "framework")] @@ -102,4 +127,6 @@ extern "C" { ) -> CGSize; fn CTFrameGetLines(frame: CTFrameRef) -> CFArrayRef; + fn CTFrameGetLineOrigins(frame: CTFrameRef, range: CFRange, origins: *mut CGPoint); + fn CTLineGetStringRange(line: CTLineRef) -> CFRange; } diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 8fff95e75..1b7097814 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -1,5 +1,6 @@ //! Text related stuff for the coregraphics backend +use core_foundation_sys::base::CFRange; use core_graphics::base::CGFloat; use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use core_graphics::path::CGPath; @@ -11,7 +12,7 @@ use piet::{ TextLayoutBuilder, }; -use crate::ct_helpers::{AttributedString, Frame, Framesetter}; +use crate::ct_helpers::{AttributedString, Frame, Framesetter, Line}; // inner is an nsfont. #[derive(Debug, Clone)] @@ -21,11 +22,14 @@ pub struct CoreGraphicsFontBuilder(Option); #[derive(Clone)] pub struct CoreGraphicsTextLayout { - string: AttributedString, + string: String, + attr_string: AttributedString, framesetter: Framesetter, pub(crate) frame: Frame, + line_origins: Vec, + /// offsets in utf8 of lines + line_offsets: Vec, pub(crate) frame_size: Size, - line_count: usize, width_constraint: f64, } @@ -50,28 +54,7 @@ impl Text for CoreGraphicsText { width: impl Into>, ) -> Self::TextLayoutBuilder { let width_constraint = width.into().unwrap_or(f64::INFINITY); - let constraints = CGSize::new(width_constraint as CGFloat, CGFloat::INFINITY); - let string = AttributedString::new(text, &font.0); - - let framesetter = Framesetter::new(&string); - let char_range = string.range(); - - let (frame_size, _) = framesetter.suggest_frame_size(char_range, constraints); - let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); - let path = CGPath::from_rect(rect, None); - let frame = framesetter.create_frame(char_range, &path); - let lines = frame.get_lines(); - let line_count = lines.len() as usize; - - let frame_size = Size::new(frame_size.width, frame_size.height); - let layout = CoreGraphicsTextLayout { - string, - framesetter, - frame, - frame_size, - line_count, - width_constraint, - }; + let layout = CoreGraphicsTextLayout::new(font, text, width_constraint); CoreGraphicsTextLayoutBuilder(layout) } } @@ -103,20 +86,23 @@ impl TextLayout for CoreGraphicsTextLayout { let width = new_width.into().unwrap_or(f64::INFINITY); if width != self.width_constraint { let constraints = CGSize::new(width as CGFloat, CGFloat::INFINITY); - let char_range = self.string.range(); + let char_range = self.attr_string.range(); let (frame_size, _) = self.framesetter.suggest_frame_size(char_range, constraints); let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); let path = CGPath::from_rect(rect, None); self.width_constraint = width; self.frame = self.framesetter.create_frame(char_range, &path); - self.line_count = self.frame.get_lines().len() as usize; + let line_count = self.frame.get_lines().len(); + self.line_origins = self.frame.get_line_origins(CFRange::init(0, line_count)); self.frame_size = Size::new(frame_size.width, frame_size.height); + self.rebuild_line_offsets(); } Ok(()) } - fn line_text(&self, _line_number: usize) -> Option<&str> { - unimplemented!() + fn line_text(&self, line_number: usize) -> Option<&str> { + self.line_range(line_number) + .map(|(start, end)| unsafe { self.string.get_unchecked(start..end) }) } fn line_metric(&self, _line_number: usize) -> Option { @@ -124,7 +110,7 @@ impl TextLayout for CoreGraphicsTextLayout { } fn line_count(&self) -> usize { - self.line_count + self.line_origins.len() } fn hit_test_point(&self, _point: Point) -> HitTestPoint { @@ -135,3 +121,95 @@ impl TextLayout for CoreGraphicsTextLayout { unimplemented!() } } + +impl CoreGraphicsTextLayout { + fn new(font: &CoreGraphicsFont, text: &str, width_constraint: f64) -> Self { + let constraints = CGSize::new(width_constraint as CGFloat, CGFloat::INFINITY); + let string = AttributedString::new(text, &font.0); + + let framesetter = Framesetter::new(&string); + let char_range = string.range(); + + let (frame_size, _) = framesetter.suggest_frame_size(char_range, constraints); + let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); + let path = CGPath::from_rect(rect, None); + let frame = framesetter.create_frame(char_range, &path); + let lines = frame.get_lines(); + let line_origins = frame.get_line_origins(CFRange::init(0, lines.len())); + + let frame_size = Size::new(frame_size.width, frame_size.height); + + let mut layout = CoreGraphicsTextLayout { + string: text.into(), + attr_string: string, + framesetter, + frame, + frame_size, + line_origins, + width_constraint, + line_offsets: Vec::new(), + }; + layout.rebuild_line_offsets(); + layout + } + + /// for each line in a layout, determine its offset in utf8. + fn rebuild_line_offsets(&mut self) { + let lines = self.frame.get_lines(); + + let utf16_line_offsets = lines.iter().map(|l| { + let line = Line::new(&l); + let range = line.get_string_range(); + range.location as usize + }); + + let mut chars = self.string.chars(); + let mut cur_16 = 0; + let mut cur_8 = 0; + + self.line_offsets = utf16_line_offsets + .map(|off_16| { + if off_16 == 0 { + return 0; + } + while let Some(c) = chars.next() { + cur_16 += c.len_utf16(); + cur_8 += c.len_utf8(); + if cur_16 == off_16 { + return cur_8; + } + } + panic!("error calculating utf8 offsets"); + }) + .collect::>(); + } + + fn line_range(&self, line: usize) -> Option<(usize, usize)> { + if line <= self.line_count() { + let start = self.line_offsets[line]; + let end = if line == self.line_count() - 1 { + self.string.len() + } else { + self.line_offsets[line + 1] + }; + Some((start, end)) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn line_offsets() { + let text = "hi\ni'm\nπŸ˜€ four\nlines"; + let a_font = font::new_from_name("Helvetica", 16.0).unwrap(); + let layout = CoreGraphicsTextLayout::new(&CoreGraphicsFont(a_font), text, f64::INFINITY); + assert_eq!(layout.line_text(0), Some("hi\n")); + assert_eq!(layout.line_text(1), Some("i'm\n")); + assert_eq!(layout.line_text(2), Some("πŸ˜€ four\n")); + assert_eq!(layout.line_text(3), Some("lines")); + } +} From ac76e5f13ca8782d0df3992fee2588268187706e Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Sat, 2 May 2020 16:40:01 -0400 Subject: [PATCH 15/30] [cg] Implement line_metric method --- piet-coregraphics/src/ct_helpers.rs | 30 +++++++ piet-coregraphics/src/lib.rs | 2 +- piet-coregraphics/src/text.rs | 119 ++++++++++++++++++++++------ 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/piet-coregraphics/src/ct_helpers.rs b/piet-coregraphics/src/ct_helpers.rs index 8d2b3c577..523c7fadc 100644 --- a/piet-coregraphics/src/ct_helpers.rs +++ b/piet-coregraphics/src/ct_helpers.rs @@ -12,6 +12,7 @@ use core_foundation::{ }; use core_foundation_sys::base::CFRange; use core_graphics::{ + base::CGFloat, geometry::{CGPoint, CGSize}, path::CGPathRef, }; @@ -32,6 +33,14 @@ pub(crate) struct Frame(pub(crate) CTFrame); #[derive(Debug, Clone)] pub(crate) struct Line<'a>(pub(crate) &'a CTLine); +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct TypographicBounds { + pub(crate) width: CGFloat, + pub(crate) ascent: CGFloat, + pub(crate) descent: CGFloat, + pub(crate) leading: CGFloat, +} + impl AttributedString { pub(crate) fn new(text: &str, font: &CTFont) -> Self { let mut string = CFMutableAttributedString::new(); @@ -114,6 +123,20 @@ impl<'a> Line<'a> { pub(crate) fn get_string_range(&self) -> CFRange { unsafe { CTLineGetStringRange(self.0.as_concrete_TypeRef()) } } + + pub(crate) fn get_typographic_bounds(&self) -> TypographicBounds { + let mut out = TypographicBounds::default(); + let width = unsafe { + CTLineGetTypographicBounds( + self.0.as_concrete_TypeRef(), + &mut out.ascent, + &mut out.descent, + &mut out.leading, + ) + }; + out.width = width; + out + } } #[link(name = "CoreText", kind = "framework")] @@ -128,5 +151,12 @@ extern "C" { fn CTFrameGetLines(frame: CTFrameRef) -> CFArrayRef; fn CTFrameGetLineOrigins(frame: CTFrameRef, range: CFRange, origins: *mut CGPoint); + fn CTLineGetStringRange(line: CTLineRef) -> CFRange; + fn CTLineGetTypographicBounds( + line: CTLineRef, + ascent: *mut CGFloat, + descent: *mut CGFloat, + leading: *mut CGFloat, + ) -> CGFloat; } diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 8acda5ca7..3d44a036b 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -135,7 +135,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { let y_off = self.ctx.height() as f64 - layout.frame_size.height; self.ctx.translate(pos.x, y_off - pos.y); self.set_fill_brush(&brush); - layout.frame.0.draw(self.ctx); + layout.draw(self.ctx); self.ctx.restore(); } diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 1b7097814..2fc2a9c71 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -2,6 +2,7 @@ use core_foundation_sys::base::CFRange; use core_graphics::base::CGFloat; +use core_graphics::context::CGContext; use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use core_graphics::path::CGPath; use core_text::font::{self, CTFont}; @@ -25,8 +26,9 @@ pub struct CoreGraphicsTextLayout { string: String, attr_string: AttributedString, framesetter: Framesetter, - pub(crate) frame: Frame, - line_origins: Vec, + pub(crate) frame: Option, + // distance from the top of the frame to the baseline of each line + line_y_positions: Vec, /// offsets in utf8 of lines line_offsets: Vec, pub(crate) frame_size: Size, @@ -91,9 +93,14 @@ impl TextLayout for CoreGraphicsTextLayout { let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); let path = CGPath::from_rect(rect, None); self.width_constraint = width; - self.frame = self.framesetter.create_frame(char_range, &path); - let line_count = self.frame.get_lines().len(); - self.line_origins = self.frame.get_line_origins(CFRange::init(0, line_count)); + let frame = self.framesetter.create_frame(char_range, &path); + let line_count = frame.get_lines().len(); + let line_origins = frame.get_line_origins(CFRange::init(0, line_count)); + self.line_y_positions = line_origins + .iter() + .map(|l| frame_size.height - l.y) + .collect(); + self.frame = Some(frame); self.frame_size = Size::new(frame_size.width, frame_size.height); self.rebuild_line_offsets(); } @@ -105,12 +112,44 @@ impl TextLayout for CoreGraphicsTextLayout { .map(|(start, end)| unsafe { self.string.get_unchecked(start..end) }) } - fn line_metric(&self, _line_number: usize) -> Option { - unimplemented!() + fn line_metric(&self, line_number: usize) -> Option { + let lines = self + .frame + .as_ref() + .expect("always inited in ::new") + .get_lines(); + let line = lines.get(line_number.min(isize::max_value() as usize) as isize)?; + let line = Line::new(&line); + let typo_bounds = line.get_typographic_bounds(); + let (start_offset, end_offset) = self.line_range(line_number)?; + let text = self.line_text(line_number)?; + //FIXME: this is just ascii whitespace + let trailing_whitespace = text + .as_bytes() + .iter() + .rev() + .take_while(|b| match b { + b' ' | b'\t' | b'\n' | b'\r' => true, + _ => false, + }) + .count(); + let height = typo_bounds.ascent + typo_bounds.descent + typo_bounds.leading; + // this may not be exactly right, but i'm also not sure we ever use this? + // see https://stackoverflow.com/questions/5511830/how-does-line-spacing-work-in-core-text-and-why-is-it-different-from-nslayoutm + let cumulative_height = + (self.line_y_positions[line_number] + typo_bounds.descent + typo_bounds.leading).ceil(); + Some(LineMetric { + start_offset, + end_offset, + trailing_whitespace, + baseline: typo_bounds.ascent, + height, + cumulative_height, + }) } fn line_count(&self) -> usize { - self.line_origins.len() + self.line_y_positions.len() } fn hit_test_point(&self, _point: Point) -> HitTestPoint { @@ -124,38 +163,40 @@ impl TextLayout for CoreGraphicsTextLayout { impl CoreGraphicsTextLayout { fn new(font: &CoreGraphicsFont, text: &str, width_constraint: f64) -> Self { - let constraints = CGSize::new(width_constraint as CGFloat, CGFloat::INFINITY); let string = AttributedString::new(text, &font.0); - let framesetter = Framesetter::new(&string); - let char_range = string.range(); - - let (frame_size, _) = framesetter.suggest_frame_size(char_range, constraints); - let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &frame_size); - let path = CGPath::from_rect(rect, None); - let frame = framesetter.create_frame(char_range, &path); - let lines = frame.get_lines(); - let line_origins = frame.get_line_origins(CFRange::init(0, lines.len())); - - let frame_size = Size::new(frame_size.width, frame_size.height); let mut layout = CoreGraphicsTextLayout { string: text.into(), attr_string: string, framesetter, - frame, - frame_size, - line_origins, - width_constraint, + // all of this is correctly set in `update_width` below + frame: None, + frame_size: Size::ZERO, + line_y_positions: Vec::new(), + // NaN to ensure we always execute code in update_width + width_constraint: f64::NAN, line_offsets: Vec::new(), }; - layout.rebuild_line_offsets(); + layout.update_width(width_constraint).unwrap(); layout } + pub(crate) fn draw(&self, ctx: &mut CGContext) { + self.frame + .as_ref() + .expect("always inited in ::new") + .0 + .draw(ctx) + } + /// for each line in a layout, determine its offset in utf8. fn rebuild_line_offsets(&mut self) { - let lines = self.frame.get_lines(); + let lines = self + .frame + .as_ref() + .expect("always inited in ::new") + .get_lines(); let utf16_line_offsets = lines.iter().map(|l| { let line = Line::new(&l); @@ -212,4 +253,30 @@ mod tests { assert_eq!(layout.line_text(2), Some("πŸ˜€ four\n")); assert_eq!(layout.line_text(3), Some("lines")); } + + #[test] + fn metrics() { + let text = "🀑:\na string\nwith a number \n of lines"; + let a_font = font::new_from_name("Helvetica", 16.0).unwrap(); + let layout = CoreGraphicsTextLayout::new(&CoreGraphicsFont(a_font), text, f64::INFINITY); + let line1 = layout.line_metric(0).unwrap(); + assert_eq!(line1.start_offset, 0); + assert_eq!(line1.end_offset, 6); + assert_eq!(line1.trailing_whitespace, 1); + layout.line_metric(1); + + let line3 = layout.line_metric(2).unwrap(); + assert_eq!(line3.start_offset, 15); + assert_eq!(line3.end_offset, 30); + assert_eq!(line3.trailing_whitespace, 2); + + let line4 = layout.line_metric(3).unwrap(); + assert_eq!(layout.line_text(3), Some(" of lines")); + assert_eq!(line4.trailing_whitespace, 0); + + let total_height = layout.frame_size.height; + assert_eq!(line4.cumulative_height, total_height); + + assert!(layout.line_metric(4).is_none()); + } } From 48d5e2c522292a01ecde5981ad777f968c7f7102 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Mon, 4 May 2020 14:38:14 -0400 Subject: [PATCH 16/30] [cg] Implement hit_test_point This is not currently extensively tested, and there may be some funny little things, but it gets us started. --- piet-coregraphics/src/ct_helpers.rs | 28 ++++++- piet-coregraphics/src/text.rs | 121 +++++++++++++++++++++++----- 2 files changed, 127 insertions(+), 22 deletions(-) diff --git a/piet-coregraphics/src/ct_helpers.rs b/piet-coregraphics/src/ct_helpers.rs index 523c7fadc..bfe3cac19 100644 --- a/piet-coregraphics/src/ct_helpers.rs +++ b/piet-coregraphics/src/ct_helpers.rs @@ -1,9 +1,11 @@ //! Wrappers around CF/CT types, with nice interfaces. +use std::borrow::Cow; +use std::convert::TryInto; use std::ops::Deref; use core_foundation::{ - array::{CFArray, CFArrayRef}, + array::{CFArray, CFArrayRef, CFIndex}, attributed_string::CFMutableAttributedString, base::TCFType, dictionary::CFDictionaryRef, @@ -31,7 +33,7 @@ pub(crate) struct Framesetter(CTFramesetter); #[derive(Debug, Clone)] pub(crate) struct Frame(pub(crate) CTFrame); #[derive(Debug, Clone)] -pub(crate) struct Line<'a>(pub(crate) &'a CTLine); +pub(crate) struct Line<'a>(pub(crate) Cow<'a, CTLine>); #[derive(Default, Debug, Copy, Clone)] pub(crate) struct TypographicBounds { @@ -106,6 +108,14 @@ impl Frame { unsafe { TCFType::wrap_under_get_rule(CTFrameGetLines(self.0.as_concrete_TypeRef())) } } + pub(crate) fn get_line(&self, line_number: usize) -> Option { + let idx: CFIndex = line_number.try_into().ok()?; + let lines = self.get_lines(); + lines + .get(idx) + .map(|l| unsafe { TCFType::wrap_under_get_rule(l.as_concrete_TypeRef()) }) + } + pub(crate) fn get_line_origins(&self, range: CFRange) -> Vec { let mut origins = vec![CGPoint::new(0.0, 0.0); range.length as usize]; unsafe { @@ -117,7 +127,7 @@ impl Frame { impl<'a> Line<'a> { pub(crate) fn new(inner: &'a impl Deref) -> Line<'a> { - Line(inner.deref()) + Line(Cow::Borrowed(inner.deref())) } pub(crate) fn get_string_range(&self) -> CFRange { @@ -137,6 +147,16 @@ impl<'a> Line<'a> { out.width = width; out } + + pub(crate) fn get_string_index_for_position(&self, position: CGPoint) -> CFIndex { + unsafe { CTLineGetStringIndexForPosition(self.0.as_concrete_TypeRef(), position) } + } +} + +impl<'a> From for Line<'a> { + fn from(src: CTLine) -> Line<'a> { + Line(Cow::Owned(src)) + } } #[link(name = "CoreText", kind = "framework")] @@ -159,4 +179,6 @@ extern "C" { descent: *mut CGFloat, leading: *mut CGFloat, ) -> CGFloat; + + fn CTLineGetStringIndexForPosition(line: CTLineRef, position: CGPoint) -> CFIndex; } diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 2fc2a9c71..1b17d3de8 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -9,8 +9,8 @@ use core_text::font::{self, CTFont}; use piet::kurbo::{Point, Size}; use piet::{ - Error, Font, FontBuilder, HitTestPoint, HitTestTextPosition, LineMetric, Text, TextLayout, - TextLayoutBuilder, + Error, Font, FontBuilder, HitTestMetrics, HitTestPoint, HitTestTextPosition, LineMetric, Text, + TextLayout, TextLayoutBuilder, }; use crate::ct_helpers::{AttributedString, Frame, Framesetter, Line}; @@ -113,11 +113,7 @@ impl TextLayout for CoreGraphicsTextLayout { } fn line_metric(&self, line_number: usize) -> Option { - let lines = self - .frame - .as_ref() - .expect("always inited in ::new") - .get_lines(); + let lines = self.unwrap_frame().get_lines(); let line = lines.get(line_number.min(isize::max_value() as usize) as isize)?; let line = Line::new(&line); let typo_bounds = line.get_typographic_bounds(); @@ -152,8 +148,68 @@ impl TextLayout for CoreGraphicsTextLayout { self.line_y_positions.len() } - fn hit_test_point(&self, _point: Point) -> HitTestPoint { - unimplemented!() + // given a point on the screen, return an offset in the text, basically + fn hit_test_point(&self, point: Point) -> HitTestPoint { + let mut line_num = self + .line_y_positions + .iter() + .position(|y| y >= &point.y) + // if we're past the last line, use the last line + .unwrap_or_else(|| self.line_y_positions.len().saturating_sub(1)); + // because y_positions is the position of the baseline, check that we don't + // fall between the preceding baseline and that line's descent + if line_num > 0 { + let prev_line = self.unwrap_frame().get_line(line_num - 1).unwrap(); + let typo_bounds = Line::new(&&prev_line).get_typographic_bounds(); + if self.line_y_positions[line_num - 1] + typo_bounds.descent >= point.y { + line_num -= 1; + } + } + let line: Line = self + .unwrap_frame() + .get_line(line_num) + .map(Into::into) + .unwrap(); + let fake_y = self.line_y_positions[line_num]; + // map that back into our inverted coordinate space + let fake_y = -(self.frame_size.height - fake_y); + let point_in_string_space = CGPoint::new(point.x, fake_y); + let offset_utf16 = line.get_string_index_for_position(point_in_string_space); + let offset = match offset_utf16 { + // this is 'kCFNotFound'. + // if nothing is found just go end of string? should this be len - 1? do we have an + // implicit newline at end of file? so many mysteries + -1 => self.string.len(), + n if n >= 0 => { + let utf16_range = line.get_string_range(); + let utf8_range = self.line_range(line_num).unwrap(); + let line_txt = self.line_text(line_num).unwrap(); + let rel_offset = (n - utf16_range.location) as usize; + let mut off16 = 0; + let mut off8 = 0; + for c in line_txt.chars() { + if rel_offset == off16 { + break; + } + off16 = c.len_utf16(); + off8 = c.len_utf8(); + } + utf8_range.0 + off8 + } + // some other value; should never happen + _ => panic!("gross violation of api contract"), + }; + + let typo_bounds = line.get_typographic_bounds(); + let is_inside_y = point.y >= 0. && point.y <= self.frame_size.height; + let is_inside_x = point.x >= 0. && point.x <= typo_bounds.width; + + HitTestPoint { + metrics: HitTestMetrics { + text_position: offset, + }, + is_inside: is_inside_x && is_inside_y, + } } fn hit_test_text_position(&self, _text_position: usize) -> Option { @@ -183,20 +239,17 @@ impl CoreGraphicsTextLayout { } pub(crate) fn draw(&self, ctx: &mut CGContext) { - self.frame - .as_ref() - .expect("always inited in ::new") - .0 - .draw(ctx) + self.unwrap_frame().0.draw(ctx) + } + + #[inline] + fn unwrap_frame(&self) -> &Frame { + self.frame.as_ref().expect("always inited in ::new") } /// for each line in a layout, determine its offset in utf8. fn rebuild_line_offsets(&mut self) { - let lines = self - .frame - .as_ref() - .expect("always inited in ::new") - .get_lines(); + let lines = self.unwrap_frame().get_lines(); let utf16_line_offsets = lines.iter().map(|l| { let line = Line::new(&l); @@ -279,4 +332,34 @@ mod tests { assert!(layout.line_metric(4).is_none()); } + + // test that at least we're landing on the correct line + #[test] + fn basic_hit_testing() { + let text = "1\nπŸ˜€\n8\nA"; + let a_font = font::new_from_name("Helvetica", 16.0).unwrap(); + let layout = CoreGraphicsTextLayout::new(&CoreGraphicsFont(a_font), text, f64::INFINITY); + let p1 = layout.hit_test_point(Point::ZERO); + assert_eq!(p1.metrics.text_position, 0); + assert!(p1.is_inside); + let p2 = layout.hit_test_point(Point::new(2.0, 19.0)); + assert_eq!(p2.metrics.text_position, 0); + assert!(p2.is_inside); + + let p3 = layout.hit_test_point(Point::new(50.0, 10.0)); + assert_eq!(p3.metrics.text_position, 1); + assert!(!p3.is_inside); + + let p4 = layout.hit_test_point(Point::new(4.0, 25.0)); + assert_eq!(p4.metrics.text_position, 2); + assert!(p4.is_inside); + + let p5 = layout.hit_test_point(Point::new(2.0, 83.0)); + assert_eq!(p5.metrics.text_position, 9); + assert!(p5.is_inside); + + let p6 = layout.hit_test_point(Point::new(10.0, 83.0)); + assert_eq!(p6.metrics.text_position, 10); + assert!(p6.is_inside); + } } From a011ea02ee3232d61370b467a861ef074a804da6 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Mon, 4 May 2020 17:39:27 -0400 Subject: [PATCH 17/30] [cg] Implement hit_test_text_position --- piet-coregraphics/src/ct_helpers.rs | 19 +++++++++++++++ piet-coregraphics/src/text.rs | 36 +++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/piet-coregraphics/src/ct_helpers.rs b/piet-coregraphics/src/ct_helpers.rs index bfe3cac19..1ad9a421f 100644 --- a/piet-coregraphics/src/ct_helpers.rs +++ b/piet-coregraphics/src/ct_helpers.rs @@ -151,6 +151,19 @@ impl<'a> Line<'a> { pub(crate) fn get_string_index_for_position(&self, position: CGPoint) -> CFIndex { unsafe { CTLineGetStringIndexForPosition(self.0.as_concrete_TypeRef(), position) } } + + /// return the 'primary' and 'secondary' offsets on the given line that the boundary of the + /// character at the provided index. + /// + /// I don't know what the secondary offset is for. There are docs at: + /// https://developer.apple.com/documentation/coretext/1509629-ctlinegetoffsetforstringindex + pub(crate) fn get_offset_for_string_index(&self, index: CFIndex) -> (CGFloat, CGFloat) { + let mut secondary: f64 = 0.0; + let primary = unsafe { + CTLineGetOffsetForStringIndex(self.0.as_concrete_TypeRef(), index, &mut secondary) + }; + (primary, secondary) + } } impl<'a> From for Line<'a> { @@ -181,4 +194,10 @@ extern "C" { ) -> CGFloat; fn CTLineGetStringIndexForPosition(line: CTLineRef, position: CGPoint) -> CFIndex; + + fn CTLineGetOffsetForStringIndex( + line: CTLineRef, + charIndex: CFIndex, + secondaryOffset: *mut CGFloat, + ) -> CGFloat; } diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 1b17d3de8..1a752e520 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -212,8 +212,26 @@ impl TextLayout for CoreGraphicsTextLayout { } } - fn hit_test_text_position(&self, _text_position: usize) -> Option { - unimplemented!() + fn hit_test_text_position(&self, offset: usize) -> Option { + let line_num = match self.line_offsets.binary_search(&offset) { + Ok(line) => line.saturating_sub(1), + Err(line) => line.saturating_sub(1), + }; + let line: Line = self.unwrap_frame().get_line(line_num)?.into(); + let text = self.line_text(line_num)?; + + let offset_remainder = offset - self.line_offsets.get(line_num)?; + let off16: usize = text[..offset_remainder].chars().map(char::len_utf16).sum(); + let line_range = line.get_string_range(); + let char_idx = line_range.location + off16 as isize; + let (x_pos, _) = line.get_offset_for_string_index(char_idx); + let y_pos = self.line_y_positions[line_num]; + Some(HitTestTextPosition { + point: Point::new(x_pos, y_pos), + metrics: HitTestMetrics { + text_position: offset, + }, + }) } } @@ -362,4 +380,18 @@ mod tests { assert_eq!(p6.metrics.text_position, 10); assert!(p6.is_inside); } + + #[test] + fn hit_test_text_position() { + let text = "aaaaa\nbbbbb"; + let a_font = font::new_from_name("Helvetica", 16.0).unwrap(); + let layout = CoreGraphicsTextLayout::new(&CoreGraphicsFont(a_font), text, f64::INFINITY); + let p1 = layout.hit_test_text_position(0).unwrap(); + assert_eq!(p1.point, Point::new(0.0, 16.0)); + + let p1 = layout.hit_test_text_position(7).unwrap(); + assert_eq!(p1.point.y, 36.0); + // just the general idea that this is the second character + assert!(p1.point.x > 5.0 && p1.point.x < 15.0); + } } From 4b9272141c0a5e61a5aaaefa839b475e7124db84 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Tue, 5 May 2020 11:43:23 -0400 Subject: [PATCH 18/30] [cg] Implement gradient support This isn't well supported in the core-graphics crate, so we have to do a bit of our own FFI; there's a weird combination of traits and types used for FFI between the different core-foundation and core-graphics crates, and it was hard to get them to play nicely together, so this ends up throwing various aliases out the window for FFI. --- piet-coregraphics/examples/basic-cg.rs | 27 +++-- piet-coregraphics/src/gradient.rs | 151 +++++++++++++++++++++++++ piet-coregraphics/src/lib.rs | 118 ++++++++++++------- 3 files changed, 248 insertions(+), 48 deletions(-) create mode 100644 piet-coregraphics/src/gradient.rs diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index 45098f4df..e09cd52c7 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -6,7 +6,10 @@ use core_graphics::color_space::CGColorSpace; use core_graphics::context::CGContext; use piet::kurbo::{Circle, Size}; -use piet::{Color, FontBuilder, RenderContext, Text, TextLayout, TextLayoutBuilder}; +use piet::{ + Color, FontBuilder, LinearGradient, RadialGradient, RenderContext, Text, TextLayout, + TextLayoutBuilder, UnitPoint, +}; const WIDTH: usize = 800; const HEIGHT: usize = 600; @@ -23,15 +26,21 @@ fn main() { ); let mut piet = piet_coregraphics::CoreGraphicsContext::new(&mut cg_ctx); let bounds = Size::new(WIDTH as f64, HEIGHT as f64).to_rect(); - piet.stroke(bounds, &Color::rgba8(0, 255, 0, 128), 20.0); - piet.fill( - bounds.inset((0., 0., -bounds.width() * 0.5, 0.)), - &Color::rgba8(0, 0, 255, 128), - ); - piet.fill( - Circle::new((100.0, 100.0), 50.0), - &Color::rgb8(255, 0, 0).with_alpha(0.5), + + let linear = LinearGradient::new( + UnitPoint::TOP_LEFT, + UnitPoint::BOTTOM_RIGHT, + ( + Color::rgba(1.0, 0.2, 0.5, 0.4), + Color::rgba(0.9, 0.0, 0.9, 0.8), + ), ); + let radial = RadialGradient::new(0.8, (Color::WHITE, Color::BLACK)) + .with_origin(UnitPoint::new(0.2, 0.7)); + + piet.fill(bounds.inset((0., 0., -bounds.width() * 0.5, 0.)), &radial); + piet.fill(Circle::new((100.0, 100.0), 50.0), &linear); + piet.stroke(bounds, &linear, 20.0); let font = piet .text() diff --git a/piet-coregraphics/src/gradient.rs b/piet-coregraphics/src/gradient.rs new file mode 100644 index 000000000..fd674b73a --- /dev/null +++ b/piet-coregraphics/src/gradient.rs @@ -0,0 +1,151 @@ +//! core graphics gradient support + +use core_foundation::{ + array::{CFArray, CFArrayRef}, + base::{CFTypeID, TCFType}, + declare_TCFType, impl_TCFType, +}; +use core_graphics::{ + base::CGFloat, + color::{CGColor, SysCGColorRef}, + color_space::{kCGColorSpaceSRGB, CGColorSpace, CGColorSpaceRef}, + context::{CGContext, CGContextRef}, + geometry::CGPoint, +}; + +use piet::kurbo::Point; +use piet::{Color, FixedGradient, FixedLinearGradient, FixedRadialGradient, GradientStop}; + +// core-graphics does not provide a CGGradient type +pub enum CGGradientT {} +pub type CGGradientRef = *mut CGGradientT; + +declare_TCFType! { + CGGradient, CGGradientRef +} + +impl_TCFType!(CGGradient, CGGradientRef, CGGradientGetTypeID); + +/// A wrapper around CGGradient +#[derive(Clone)] +pub struct Gradient { + cg_grad: CGGradient, + piet_grad: FixedGradient, +} + +impl Gradient { + pub(crate) fn from_piet_gradient(gradient: FixedGradient) -> Gradient { + let cg_grad = match &gradient { + FixedGradient::Linear(grad) => new_cg_gradient(&grad.stops), + FixedGradient::Radial(grad) => new_cg_gradient(&grad.stops), + }; + Gradient { + cg_grad, + piet_grad: gradient, + } + } + + pub(crate) fn first_color(&self) -> Color { + match &self.piet_grad { + FixedGradient::Linear(grad) => grad.stops.first().map(|g| g.color.clone()), + FixedGradient::Radial(grad) => grad.stops.first().map(|g| g.color.clone()), + } + .unwrap_or(Color::BLACK) + } + + pub(crate) fn fill(&self, ctx: &mut CGContext) { + let context_ref: *mut u8 = &mut **ctx as *mut CGContextRef as *mut u8; + match self.piet_grad { + FixedGradient::Radial(FixedRadialGradient { + center, + origin_offset, + radius, + .. + }) => { + let start_center = to_cgpoint(center + origin_offset); + let center = to_cgpoint(center); + unsafe { + CGContextDrawRadialGradient( + context_ref, + self.cg_grad.as_concrete_TypeRef(), + start_center, + 0.0, + center, + radius as CGFloat, + 0, + ) + } + } + FixedGradient::Linear(FixedLinearGradient { start, end, .. }) => { + let start = to_cgpoint(start); + let end = to_cgpoint(end); + unsafe { + CGContextDrawLinearGradient( + context_ref, + self.cg_grad.as_concrete_TypeRef(), + start, + end, + 0, + ) + } + } + } + } +} + +fn new_cg_gradient(stops: &[GradientStop]) -> CGGradient { + unsafe { + //FIXME: is this expensive enough we should be reusing it? + let space = CGColorSpace::create_with_name(kCGColorSpaceSRGB).unwrap(); + let space_ref: *const u8 = &*space as *const CGColorSpaceRef as *const u8; + let mut colors = Vec::::new(); + let mut locations = Vec::::new(); + for GradientStop { pos, color } in stops { + let (r, g, b, a) = Color::as_rgba(&color); + let color = CGColorCreate(space_ref as *const u8, [r, g, b, a].as_ptr()); + let color = CGColor::wrap_under_create_rule(color); + colors.push(color); + locations.push(*pos as CGFloat); + } + + let colors = CFArray::from_CFTypes(&colors); + let gradient = CGGradientCreateWithColors( + space_ref as *const u8, + colors.as_concrete_TypeRef(), + locations.as_ptr(), + ); + + CGGradient::wrap_under_create_rule(gradient) + } +} + +fn to_cgpoint(point: Point) -> CGPoint { + CGPoint::new(point.x as CGFloat, point.y as CGFloat) +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGGradientGetTypeID() -> CFTypeID; + fn CGGradientCreateWithColors( + space: *const u8, + colors: CFArrayRef, + locations: *const CGFloat, + ) -> CGGradientRef; + fn CGColorCreate(space: *const u8, components: *const CGFloat) -> SysCGColorRef; + fn CGContextDrawLinearGradient( + ctx: *mut u8, + gradient: CGGradientRef, + startPoint: CGPoint, + endPoint: CGPoint, + options: u32, + ); + fn CGContextDrawRadialGradient( + ctx: *mut u8, + gradient: CGGradientRef, + startCenter: CGPoint, + startRadius: CGFloat, + endCenter: CGPoint, + endRadius: CGFloat, + options: u32, + ); +} diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 3d44a036b..fbc949fb5 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,6 +1,7 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. mod ct_helpers; +mod gradient; mod text; use std::borrow::Cow; @@ -27,6 +28,8 @@ pub use crate::text::{ CoreGraphicsTextLayoutBuilder, }; +use gradient::Gradient; + pub struct CoreGraphicsContext<'a> { // Cairo has this as Clone and with &self methods, but we do this to avoid // concurrency problems. @@ -45,8 +48,8 @@ impl<'a> CoreGraphicsContext<'a> { #[derive(Clone)] pub enum Brush { - Solid(u32), - Gradient, + Solid(Color), + Gradient(Gradient), } impl<'a> RenderContext for CoreGraphicsContext<'a> { @@ -68,26 +71,47 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn solid_brush(&mut self, color: Color) -> Brush { - Brush::Solid(color.as_rgba_u32()) + Brush::Solid(color) } - fn gradient(&mut self, _gradient: impl Into) -> Result { - unimplemented!() + fn gradient(&mut self, gradient: impl Into) -> Result { + let gradient = Gradient::from_piet_gradient(gradient.into()); + Ok(Brush::Gradient(gradient)) } /// Fill a shape. fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush) { let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); - self.set_fill_brush(&brush); - self.ctx.fill_path(); + match brush.as_ref() { + Brush::Solid(color) => { + self.set_fill_color(color); + self.ctx.fill_path(); + } + Brush::Gradient(grad) => { + self.ctx.save(); + self.ctx.clip(); + grad.fill(self.ctx); + self.ctx.restore(); + } + } } fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush) { let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); - self.set_fill_brush(&brush); - self.ctx.eo_fill_path(); + match brush.as_ref() { + Brush::Solid(color) => { + self.set_fill_color(color); + self.ctx.fill_path(); + } + Brush::Gradient(grad) => { + self.ctx.save(); + self.ctx.eo_clip(); + grad.fill(self.ctx); + self.ctx.restore(); + } + } } fn clip(&mut self, shape: impl Shape) { @@ -99,8 +123,19 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); self.set_stroke(width.round_into(), None); - self.set_stroke_brush(&brush); - self.ctx.stroke_path(); + match brush.as_ref() { + Brush::Solid(color) => { + self.set_stroke_color(color); + self.ctx.stroke_path(); + } + Brush::Gradient(grad) => { + self.ctx.save(); + self.ctx.replace_path_with_stroked_path(); + self.ctx.clip(); + grad.fill(self.ctx); + self.ctx.restore(); + } + } } fn stroke_styled( @@ -113,8 +148,19 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { let brush = brush.make_brush(self, || shape.bounding_box()); self.set_path(shape); self.set_stroke(width.round_into(), Some(style)); - self.set_stroke_brush(&brush); - self.ctx.stroke_path(); + match brush.as_ref() { + Brush::Solid(color) => { + self.set_stroke_color(color); + self.ctx.stroke_path(); + } + Brush::Gradient(grad) => { + self.ctx.save(); + self.ctx.replace_path_with_stroked_path(); + self.ctx.clip(); + grad.fill(self.ctx); + self.ctx.restore(); + } + } } fn text(&mut self) -> &mut Self::Text { @@ -134,8 +180,20 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { // and (0, 0) in context is also bottom left. let y_off = self.ctx.height() as f64 - layout.frame_size.height; self.ctx.translate(pos.x, y_off - pos.y); - self.set_fill_brush(&brush); - layout.draw(self.ctx); + match brush.as_ref() { + Brush::Solid(color) => { + self.set_fill_color(color); + layout.draw(self.ctx); + } + Brush::Gradient(grad) => { + //FIXME: there's no simple way to fill text with a gradient here. + //To do this correctly we need to work glyph by glyph, using + //CTFontCreatePathForGlyph, and I'm not doing that today. + + self.set_fill_color(&grad.first_color()); + layout.draw(self.ctx); + } + } self.ctx.restore(); } @@ -260,32 +318,14 @@ fn convert_line_cap(line_cap: LineCap) -> CGLineCap { } impl<'a> CoreGraphicsContext<'a> { - /// Set the source pattern to the brush. - /// - /// Cairo is super stateful, and we're trying to have more retained stuff. - /// This is part of the impedance matching. - fn set_fill_brush(&mut self, brush: &Brush) { - match *brush { - Brush::Solid(rgba) => self.ctx.set_rgb_fill_color( - byte_to_frac(rgba >> 24), - byte_to_frac(rgba >> 16), - byte_to_frac(rgba >> 8), - byte_to_frac(rgba), - ), - Brush::Gradient => unimplemented!(), - } + fn set_fill_color(&mut self, color: &Color) { + let (r, g, b, a) = Color::as_rgba(&color); + self.ctx.set_rgb_fill_color(r, g, b, a); } - fn set_stroke_brush(&mut self, brush: &Brush) { - match *brush { - Brush::Solid(rgba) => self.ctx.set_rgb_stroke_color( - byte_to_frac(rgba >> 24), - byte_to_frac(rgba >> 16), - byte_to_frac(rgba >> 8), - byte_to_frac(rgba), - ), - Brush::Gradient => unimplemented!(), - } + fn set_stroke_color(&mut self, color: &Color) { + let (r, g, b, a) = Color::as_rgba(&color); + self.ctx.set_rgb_stroke_color(r, g, b, a); } /// Set the stroke parameters. From 95e573c18d542b0cf00cbb7284af3536e8b7113e Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Tue, 5 May 2020 13:42:42 -0400 Subject: [PATCH 19/30] [cg] Implement blurred rects And 'implement' status, which is apparently always peachy. This is the last bit of feature work; after this we'll just have fixups and getting geometry right etc. --- piet-coregraphics/examples/basic-cg.rs | 7 ++- piet-coregraphics/src/blurred_rect.rs | 63 ++++++++++++++++++++++++++ piet-coregraphics/src/lib.rs | 25 ++++++++-- 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 piet-coregraphics/src/blurred_rect.rs diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index e09cd52c7..c8a1bb543 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -5,7 +5,7 @@ use std::path::Path; use core_graphics::color_space::CGColorSpace; use core_graphics::context::CGContext; -use piet::kurbo::{Circle, Size}; +use piet::kurbo::{Circle, Rect, Size}; use piet::{ Color, FontBuilder, LinearGradient, RadialGradient, RenderContext, Text, TextLayout, TextLayoutBuilder, UnitPoint, @@ -54,6 +54,11 @@ fn main() { .build() .unwrap(); + piet.blurred_rect(Rect::new(100.0, 100., 150., 150.), 5.0, &Color::BLACK); + piet.fill( + Rect::new(95.0, 105., 145., 155.), + &Color::rgb(0.0, 0.4, 0.2), + ); piet.draw_text(&layout, (0., 00.0), &Color::WHITE); layout.update_width(400.).unwrap(); piet.draw_text(&layout, (200.0, 200.0), &Color::BLACK); diff --git a/piet-coregraphics/src/blurred_rect.rs b/piet-coregraphics/src/blurred_rect.rs new file mode 100644 index 000000000..bc3673ca6 --- /dev/null +++ b/piet-coregraphics/src/blurred_rect.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use core_graphics::{color_space::CGColorSpace, data_provider::CGDataProvider, image::CGImage}; +use piet::kurbo::Rect; + +/// Extent to which to expand the blur. +const BLUR_EXTENT: f64 = 2.5; + +//TODO: reuse between this and cairo + +pub(crate) fn compute_blurred_rect(rect: Rect, radius: f64) -> (CGImage, Rect) { + let radius_recip = radius.recip(); + let xmax = rect.width() * radius_recip; + let ymax = rect.height() * radius_recip; + let padding = BLUR_EXTENT * radius; + let rect_padded = rect.inflate(padding, padding); + let rect_exp = rect_padded.expand(); + let xfrac = rect_padded.x0 - rect_exp.x0; + let yfrac = rect_padded.y0 - rect_exp.y0; + let width = rect_exp.width() as usize; + let height = rect_exp.height() as usize; + let strip = (0..width) + .map(|i| { + let x = ((i as f64) - (xfrac + padding)) * radius_recip; + (255.0 * 0.25) * (compute_erf7(x) + compute_erf7(xmax - x)) + }) + .collect::>(); + + let mut data = vec![0u8; width * height]; + + for j in 0..height { + let y = ((j as f64) - (yfrac + padding)) * radius_recip; + let z = compute_erf7(y) + compute_erf7(ymax - y); + for i in 0..width { + data[j * width + i] = (z * strip[i]).round() as u8; + } + } + + let data_provider = CGDataProvider::from_buffer(Arc::new(data)); + let color_space = CGColorSpace::create_device_gray(); + let image = CGImage::new( + width, + height, + 8, + 8, + width, + &color_space, + 0, + &data_provider, + false, + 0, + ); + (image, rect_exp) +} + +// See https://raphlinus.github.io/audio/2018/09/05/sigmoid.html for a little +// explanation of this approximation to the erf function. +fn compute_erf7(x: f64) -> f64 { + let x = x * std::f64::consts::FRAC_2_SQRT_PI; + let xx = x * x; + let x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx); + x / (1.0 + x * x).sqrt() +} diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index fbc949fb5..2df54f076 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -1,5 +1,6 @@ //! The CoreGraphics backend for the Piet 2D graphics abstraction. +mod blurred_rect; mod ct_helpers; mod gradient; mod text; @@ -11,10 +12,10 @@ use core_graphics::base::{ kCGImageAlphaLast, kCGImageAlphaPremultipliedLast, kCGRenderingIntentDefault, CGFloat, }; use core_graphics::color_space::CGColorSpace; -use core_graphics::context::{CGContext, CGLineCap, CGLineJoin}; +use core_graphics::context::{CGContext, CGContextRef, CGLineCap, CGLineJoin}; use core_graphics::data_provider::CGDataProvider; use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize}; -use core_graphics::image::CGImage; +use core_graphics::image::{CGImage, CGImageRef}; use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size}; @@ -277,8 +278,17 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } } - fn blurred_rect(&mut self, _rect: Rect, _blur_radius: f64, _brush: &impl IntoBrush) { - unimplemented!() + fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush) { + let (image, rect) = blurred_rect::compute_blurred_rect(rect, blur_radius); + let cg_rect = to_cgrect(rect); + self.ctx.save(); + let context_ref: *mut u8 = &mut **self.ctx as *mut CGContextRef as *mut u8; + let image_ref: *const u8 = &*image as *const CGImageRef as *const u8; + unsafe { + CGContextClipToMask(context_ref, cg_rect, image_ref); + } + self.fill(rect, brush); + self.ctx.restore() } fn current_transform(&self) -> Affine { @@ -287,7 +297,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn status(&mut self) -> Result<(), Error> { - unimplemented!() + Ok(()) } } @@ -410,3 +420,8 @@ fn to_cgaffine(affine: Affine) -> CGAffineTransform { let [a, b, c, d, tx, ty] = affine.as_coeffs(); CGAffineTransform::new(a, b, c, d, tx, ty) } + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGContextClipToMask(ctx: *mut u8, rect: CGRect, mask: *const u8); +} From 581ac9abcceb3ef6a116af26bed30bcd0d6839cb Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 6 May 2020 13:13:34 -0400 Subject: [PATCH 20/30] [cg] Add examples/test-picture.rs This can execute the examples defined in piet-test. --- piet-coregraphics/Cargo.toml | 1 + piet-coregraphics/examples/test-picture.rs | 44 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 piet-coregraphics/examples/test-picture.rs diff --git a/piet-coregraphics/Cargo.toml b/piet-coregraphics/Cargo.toml index b1c5b4cee..d3ece303d 100644 --- a/piet-coregraphics/Cargo.toml +++ b/piet-coregraphics/Cargo.toml @@ -17,3 +17,4 @@ core-foundation-sys = "0.7" [dev-dependencies] png = "0.16.2" +piet = { version = "0.0.12", path = "../piet", features = ["samples"] } diff --git a/piet-coregraphics/examples/test-picture.rs b/piet-coregraphics/examples/test-picture.rs new file mode 100644 index 000000000..18e9cee40 --- /dev/null +++ b/piet-coregraphics/examples/test-picture.rs @@ -0,0 +1,44 @@ +//! Run the piet-test examples with the coregraphics backend. + +use std::fs::File; +use std::io::BufWriter; + +use core_graphics::color_space::CGColorSpace; +use core_graphics::context::CGContext; + +use piet::RenderContext; +use piet_coregraphics::CoreGraphicsContext; + +const WIDTH: i32 = 400; +const HEIGHT: i32 = 200; + +fn main() { + let test_picture_number = std::env::args() + .nth(1) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + let mut cg_ctx = CGContext::create_bitmap_context( + None, + WIDTH as usize, + HEIGHT as usize, + 8, + 0, + &CGColorSpace::create_device_rgb(), + core_graphics::base::kCGImageAlphaPremultipliedLast, + ); + cg_ctx.scale(2.0, 2.0); + let mut piet_context = CoreGraphicsContext::new(&mut cg_ctx); + piet::draw_test_picture(&mut piet_context, test_picture_number).unwrap(); + piet_context.finish().unwrap(); + let file = File::create(format!("coregraphics-test-{}.png", test_picture_number)).unwrap(); + + let w = BufWriter::new(file); + + let mut encoder = png::Encoder::new(w, WIDTH as u32, HEIGHT as u32); + encoder.set_color(png::ColorType::RGBA); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().unwrap(); + + writer.write_image_data(cg_ctx.data()).unwrap(); +} From cdecfadf66e3a6380923e3fd65448550107de0e5 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 6 May 2020 16:06:04 -0400 Subject: [PATCH 21/30] [cg] API tweaks for integration with druid These are small changes required to work with piet-common and druid. --- piet-coregraphics/src/gradient.rs | 6 +++--- piet-coregraphics/src/lib.rs | 14 +++++++------- piet-coregraphics/src/text.rs | 18 ++++++++++++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/piet-coregraphics/src/gradient.rs b/piet-coregraphics/src/gradient.rs index fd674b73a..36e2845ed 100644 --- a/piet-coregraphics/src/gradient.rs +++ b/piet-coregraphics/src/gradient.rs @@ -9,7 +9,7 @@ use core_graphics::{ base::CGFloat, color::{CGColor, SysCGColorRef}, color_space::{kCGColorSpaceSRGB, CGColorSpace, CGColorSpaceRef}, - context::{CGContext, CGContextRef}, + context::CGContextRef, geometry::CGPoint, }; @@ -53,8 +53,8 @@ impl Gradient { .unwrap_or(Color::BLACK) } - pub(crate) fn fill(&self, ctx: &mut CGContext) { - let context_ref: *mut u8 = &mut **ctx as *mut CGContextRef as *mut u8; + pub(crate) fn fill(&self, ctx: &mut CGContextRef) { + let context_ref: *mut u8 = ctx as *mut CGContextRef as *mut u8; match self.piet_grad { FixedGradient::Radial(FixedRadialGradient { center, diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 2df54f076..7c41878d3 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -12,7 +12,7 @@ use core_graphics::base::{ kCGImageAlphaLast, kCGImageAlphaPremultipliedLast, kCGRenderingIntentDefault, CGFloat, }; use core_graphics::color_space::CGColorSpace; -use core_graphics::context::{CGContext, CGContextRef, CGLineCap, CGLineJoin}; +use core_graphics::context::{CGContextRef, CGLineCap, CGLineJoin}; use core_graphics::data_provider::CGDataProvider; use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize}; use core_graphics::image::{CGImage, CGImageRef}; @@ -34,15 +34,15 @@ use gradient::Gradient; pub struct CoreGraphicsContext<'a> { // Cairo has this as Clone and with &self methods, but we do this to avoid // concurrency problems. - ctx: &'a mut CGContext, - text: CoreGraphicsText, + ctx: &'a mut CGContextRef, + text: CoreGraphicsText<'a>, } impl<'a> CoreGraphicsContext<'a> { - pub fn new(ctx: &mut CGContext) -> CoreGraphicsContext { + pub fn new(ctx: &mut CGContextRef) -> CoreGraphicsContext { CoreGraphicsContext { ctx, - text: CoreGraphicsText, + text: CoreGraphicsText::new(), } } } @@ -55,7 +55,7 @@ pub enum Brush { impl<'a> RenderContext for CoreGraphicsContext<'a> { type Brush = Brush; - type Text = CoreGraphicsText; + type Text = CoreGraphicsText<'a>; type TextLayout = CoreGraphicsTextLayout; type Image = CGImage; //type StrokeStyle = StrokeStyle; @@ -282,7 +282,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { let (image, rect) = blurred_rect::compute_blurred_rect(rect, blur_radius); let cg_rect = to_cgrect(rect); self.ctx.save(); - let context_ref: *mut u8 = &mut **self.ctx as *mut CGContextRef as *mut u8; + let context_ref: *mut u8 = self.ctx as *mut CGContextRef as *mut u8; let image_ref: *const u8 = &*image as *const CGImageRef as *const u8; unsafe { CGContextClipToMask(context_ref, cg_rect, image_ref); diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 1a752e520..c36f7c59e 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -1,8 +1,10 @@ //! Text related stuff for the coregraphics backend +use std::marker::PhantomData; + use core_foundation_sys::base::CFRange; use core_graphics::base::CGFloat; -use core_graphics::context::CGContext; +use core_graphics::context::CGContextRef; use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use core_graphics::path::CGPath; use core_text::font::{self, CTFont}; @@ -37,9 +39,17 @@ pub struct CoreGraphicsTextLayout { pub struct CoreGraphicsTextLayoutBuilder(CoreGraphicsTextLayout); -pub struct CoreGraphicsText; +pub struct CoreGraphicsText<'a>(PhantomData<&'a ()>); + +impl<'a> CoreGraphicsText<'a> { + /// Create a new factory that satisfies the piet `Text` trait. + #[allow(clippy::new_without_default)] + pub fn new() -> CoreGraphicsText<'a> { + CoreGraphicsText(PhantomData) + } +} -impl Text for CoreGraphicsText { +impl<'a> Text for CoreGraphicsText<'a> { type Font = CoreGraphicsFont; type FontBuilder = CoreGraphicsFontBuilder; type TextLayout = CoreGraphicsTextLayout; @@ -256,7 +266,7 @@ impl CoreGraphicsTextLayout { layout } - pub(crate) fn draw(&self, ctx: &mut CGContext) { + pub(crate) fn draw(&self, ctx: &mut CGContextRef) { self.unwrap_frame().0.draw(ctx) } From 7040943a9bf679382ca4e8476417a43e3c2f73f4 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 6 May 2020 16:14:00 -0400 Subject: [PATCH 22/30] Add piet-coregraphics backend to piet-common Also includes other small manifest changes to get that all working. --- Cargo.toml | 4 +- piet-common/Cargo.toml | 8 +- piet-common/src/cg_back.rs | 168 +++++++++++++++++++++++++++++++++++ piet-common/src/lib.rs | 5 +- piet-coregraphics/Cargo.toml | 4 +- 5 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 piet-common/src/cg_back.rs diff --git a/Cargo.toml b/Cargo.toml index becc446c1..3d17ec591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ members = [ "piet", - #"piet-cairo", + "piet-cairo", "piet-common", "piet-coregraphics", - #"piet-direct2d", + "piet-direct2d", "piet-web", "piet-web/examples/basic", "piet-svg" diff --git a/piet-common/Cargo.toml b/piet-common/Cargo.toml index 3b0fc646e..51dba8041 100644 --- a/piet-common/Cargo.toml +++ b/piet-common/Cargo.toml @@ -17,15 +17,21 @@ web = ["piet-web"] cfg-if = "0.1.10" piet = { version = "0.0.12", path = "../piet" } piet-cairo = { version = "0.0.12", path = "../piet-cairo", optional = true } +piet-coregraphics = { version = "0.0.12", path = "../piet-coregraphics", optional = true } piet-direct2d = { version = "0.0.12", path = "../piet-direct2d", optional = true } piet-web = { version = "0.0.12", path = "../piet-web", optional = true } cairo-rs = { version = "0.8.1", default_features = false, optional = true} -[target.'cfg(not(any(target_arch="wasm32", target_os="windows")))'.dependencies] +[target.'cfg(not(any(target_arch="wasm32", target_os="windows", target_os="macos")))'.dependencies] piet-cairo = { version = "0.0.12", path = "../piet-cairo" } cairo-rs = { version = "0.8.1", default_features = false} png = { version = "0.16.1", optional = true } +[target.'cfg(target_os="macos")'.dependencies] +piet-coregraphics = { version = "0.0.12", path = "../piet-coregraphics" } +core-graphics = { version = "0.19" } +png = { version = "0.16.1", optional = true } + [target.'cfg(target_os="windows")'.dependencies] piet-direct2d = { version = "0.0.12", path = "../piet-direct2d" } direct2d = "0.2.0" diff --git a/piet-common/src/cg_back.rs b/piet-common/src/cg_back.rs new file mode 100644 index 000000000..ced047842 --- /dev/null +++ b/piet-common/src/cg_back.rs @@ -0,0 +1,168 @@ +//! Support for piet CoreGraphics back-end. + +use std::marker::PhantomData; +use std::path::Path; +#[cfg(feature = "png")] +use std::{fs::File, io::BufWriter}; + +use core_graphics::{color_space::CGColorSpace, context::CGContext, image::CGImage}; +#[cfg(feature = "png")] +use png::{ColorType, Encoder}; + +use piet::{Error, ImageFormat}; +#[doc(hidden)] +pub use piet_coregraphics::*; + +/// The `RenderContext` for the CoreGraphics backend, which is selected. +pub type Piet<'a> = CoreGraphicsContext<'a>; + +/// The associated brush type for this backend. +/// +/// This type matches `RenderContext::Brush` +pub type Brush = piet_coregraphics::Brush; + +/// The associated text factory for this backend. +/// +/// This type matches `RenderContext::Text` +pub type PietText<'a> = CoreGraphicsText<'a>; + +/// The associated font type for this backend. +/// +/// This type matches `RenderContext::Text::Font` +pub type PietFont = CoreGraphicsFont; + +/// The associated font builder for this backend. +/// +/// This type matches `RenderContext::Text::FontBuilder` +pub type PietFontBuilder<'a> = CoreGraphicsFontBuilder; + +/// The associated text layout type for this backend. +/// +/// This type matches `RenderContext::Text::TextLayout` +pub type PietTextLayout = CoreGraphicsTextLayout; + +/// The associated text layout builder for this backend. +/// +/// This type matches `RenderContext::Text::TextLayoutBuilder` +pub type PietTextLayoutBuilder<'a> = CoreGraphicsTextLayout; + +/// The associated image type for this backend. +/// +/// This type matches `RenderContext::Image` +pub type Image = CGImage; + +/// A struct that can be used to create bitmap render contexts. +pub struct Device; + +/// A struct provides a `RenderContext` and then can have its bitmap extracted. +pub struct BitmapTarget<'a> { + ctx: CGContext, + phantom: PhantomData<&'a ()>, +} + +impl Device { + /// Create a new device. + pub fn new() -> Result { + Ok(Device) + } + + /// Create a new bitmap target. + pub fn bitmap_target( + &mut self, + width: usize, + height: usize, + pix_scale: f64, + ) -> Result { + let ctx = CGContext::create_bitmap_context( + None, + width, + height, + 8, + 0, + &CGColorSpace::create_device_rgb(), + core_graphics::base::kCGImageAlphaPremultipliedLast, + ); + ctx.scale(pix_scale, pix_scale); + Ok(BitmapTarget { + ctx, + phantom: PhantomData, + }) + } +} + +impl<'a> BitmapTarget<'a> { + /// Get a piet `RenderContext` for the bitmap. + /// + /// Note: caller is responsible for calling `finish` on the render + /// context at the end of rendering. + pub fn render_context(&mut self) -> CoreGraphicsContext { + CoreGraphicsContext::new(&mut self.ctx) + } + + /// Get raw RGBA pixels from the bitmap. + pub fn into_raw_pixels(mut self, fmt: ImageFormat) -> Result, piet::Error> { + // TODO: convert other formats. + if fmt != ImageFormat::RgbaPremul { + return Err(Error::NotSupported); + } + + let width = self.ctx.width() as usize; + let height = self.ctx.height() as usize; + let stride = self.ctx.bytes_per_row() as usize; + + let data = self.ctx.data(); + if stride != width { + let mut raw_data = vec![0; width * height * 4]; + for y in 0..height { + let src_off = y * stride; + let dst_off = y * width * 4; + for x in 0..width { + raw_data[dst_off + x * 4 + 0] = data[src_off + x * 4 + 2]; + raw_data[dst_off + x * 4 + 1] = data[src_off + x * 4 + 1]; + raw_data[dst_off + x * 4 + 2] = data[src_off + x * 4 + 0]; + raw_data[dst_off + x * 4 + 3] = data[src_off + x * 4 + 3]; + } + } + Ok(raw_data) + } else { + Ok(data.to_owned()) + } + } + + /// Save bitmap to RGBA PNG file + #[cfg(feature = "png")] + pub fn save_to_file>(self, path: P) -> Result<(), piet::Error> { + let width = self.ctx.width() as usize; + let height = self.ctx.height() as usize; + let mut data = self.into_raw_pixels(ImageFormat::RgbaPremul)?; + unpremultiply(&mut data); + let file = BufWriter::new(File::create(path).map_err(|e| Into::>::into(e))?); + let mut encoder = Encoder::new(file, width as u32, height as u32); + encoder.set_color(ColorType::RGBA); + encoder.set_depth(png::BitDepth::Eight); + encoder + .write_header() + .map_err(|e| Into::>::into(e))? + .write_image_data(&data) + .map_err(|e| Into::>::into(e))?; + Ok(()) + } + + /// Stub for feature is missing + #[cfg(not(feature = "png"))] + pub fn save_to_file>(self, _path: P) -> Result<(), piet::Error> { + Err(Error::MissingFeature) + } +} +#[cfg(feature = "png")] +fn unpremultiply(data: &mut [u8]) { + for i in (0..data.len()).step_by(4) { + let a = data[i + 3]; + if a != 0 { + let scale = 255.0 / (a as f64); + data[i] = (scale * (data[i] as f64)).round() as u8; + data[i + 1] = (scale * (data[i + 1] as f64)).round() as u8; + data[i + 2] = (scale * (data[i + 2] as f64)).round() as u8; + } + } +} diff --git a/piet-common/src/lib.rs b/piet-common/src/lib.rs index 1b2f4c49f..ea3f19a01 100644 --- a/piet-common/src/lib.rs +++ b/piet-common/src/lib.rs @@ -31,9 +31,12 @@ pub use piet::kurbo; cfg_if::cfg_if! { // if we have explicitly asked for cairo *or* we are not wasm, web, or windows: - if #[cfg(any(feature = "cairo", not(any(target_arch = "wasm32", feature="web", target_os = "windows"))))] { + if #[cfg(any(feature = "cairo", not(any(target_arch = "wasm32", feature="web", target_os="macos", target_os = "windows"))))] { #[path = "cairo_back.rs"] mod backend; + } else if #[cfg(target_os = "macos")] { + #[path = "cg_back.rs"] + mod backend; } else if #[cfg(target_os = "windows")] { #[path = "direct2d_back.rs"] mod backend; diff --git a/piet-coregraphics/Cargo.toml b/piet-coregraphics/Cargo.toml index d3ece303d..c86c61f94 100644 --- a/piet-coregraphics/Cargo.toml +++ b/piet-coregraphics/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "piet-coregraphics" -version = "0.1.0" -authors = ["Jeff Muizelaar "] +version = "0.0.12" +authors = ["Jeff Muizelaar , Raph Levien , Colin Rofls "] description = "CoreGraphics backend for piet 2D graphics abstraction." license = "MIT/Apache-2.0" edition = "2018" From 6df132ebfc13672c400b8b25aff0b20117909bc0 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 6 May 2020 20:14:50 -0400 Subject: [PATCH 23/30] Final tweaks to integrate piet-coregraphics This involves explicitly handling the different coordinate spaces between piet and coregraphics. I've chosen to be explicit; when creating a CoreGraphicsContext, you must either ask for y-up and pass in a height (so that we can apply a transform under the hood) or you can ask for y-down and handle conversion yourself (which plays nicely with Cocoa, and NSView.isFlipped. --- piet-common/src/cg_back.rs | 5 ++- piet-coregraphics/examples/basic-cg.rs | 3 +- piet-coregraphics/examples/test-picture.rs | 8 +++-- piet-coregraphics/src/lib.rs | 37 ++++++++++++++++++++-- piet-coregraphics/src/text.rs | 2 +- piet/src/samples/picture_5.rs | 5 ++- 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/piet-common/src/cg_back.rs b/piet-common/src/cg_back.rs index ced047842..c47bb9a40 100644 --- a/piet-common/src/cg_back.rs +++ b/piet-common/src/cg_back.rs @@ -57,6 +57,7 @@ pub struct Device; /// A struct provides a `RenderContext` and then can have its bitmap extracted. pub struct BitmapTarget<'a> { ctx: CGContext, + height: f64, phantom: PhantomData<&'a ()>, } @@ -83,8 +84,10 @@ impl Device { core_graphics::base::kCGImageAlphaPremultipliedLast, ); ctx.scale(pix_scale, pix_scale); + let height = height as f64 * pix_scale; Ok(BitmapTarget { ctx, + height, phantom: PhantomData, }) } @@ -96,7 +99,7 @@ impl<'a> BitmapTarget<'a> { /// Note: caller is responsible for calling `finish` on the render /// context at the end of rendering. pub fn render_context(&mut self) -> CoreGraphicsContext { - CoreGraphicsContext::new(&mut self.ctx) + CoreGraphicsContext::new_y_up(&mut self.ctx, self.height) } /// Get raw RGBA pixels from the bitmap. diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index c8a1bb543..6ea5366f5 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -24,7 +24,7 @@ fn main() { &CGColorSpace::create_device_rgb(), core_graphics::base::kCGImageAlphaPremultipliedLast, ); - let mut piet = piet_coregraphics::CoreGraphicsContext::new(&mut cg_ctx); + let mut piet = piet_coregraphics::CoreGraphicsContext::new_y_up(&mut cg_ctx, HEIGHT as f64); let bounds = Size::new(WIDTH as f64, HEIGHT as f64).to_rect(); let linear = LinearGradient::new( @@ -66,6 +66,7 @@ fn main() { piet.draw_text(&layout, (400.0, 400.0), &Color::rgba8(255, 0, 0, 150)); piet.finish().unwrap(); + std::mem::drop(piet); unpremultiply(cg_ctx.data()); diff --git a/piet-coregraphics/examples/test-picture.rs b/piet-coregraphics/examples/test-picture.rs index 18e9cee40..a922ba76c 100644 --- a/piet-coregraphics/examples/test-picture.rs +++ b/piet-coregraphics/examples/test-picture.rs @@ -11,6 +11,7 @@ use piet_coregraphics::CoreGraphicsContext; const WIDTH: i32 = 400; const HEIGHT: i32 = 200; +const SCALE: f64 = 2.0; fn main() { let test_picture_number = std::env::args() @@ -27,12 +28,13 @@ fn main() { &CGColorSpace::create_device_rgb(), core_graphics::base::kCGImageAlphaPremultipliedLast, ); - cg_ctx.scale(2.0, 2.0); - let mut piet_context = CoreGraphicsContext::new(&mut cg_ctx); + cg_ctx.scale(SCALE, SCALE); + let mut piet_context = + CoreGraphicsContext::new_y_up(&mut cg_ctx, HEIGHT as f64 * SCALE.recip()); piet::draw_test_picture(&mut piet_context, test_picture_number).unwrap(); piet_context.finish().unwrap(); + std::mem::drop(piet_context); let file = File::create(format!("coregraphics-test-{}.png", test_picture_number)).unwrap(); - let w = BufWriter::new(file); let mut encoder = png::Encoder::new(w, WIDTH as u32, HEIGHT as u32); diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 7c41878d3..14950c7da 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -35,11 +35,34 @@ pub struct CoreGraphicsContext<'a> { // Cairo has this as Clone and with &self methods, but we do this to avoid // concurrency problems. ctx: &'a mut CGContextRef, + // the height of the context; we need this in order to correctly flip the coordinate space text: CoreGraphicsText<'a>, } impl<'a> CoreGraphicsContext<'a> { - pub fn new(ctx: &mut CGContextRef) -> CoreGraphicsContext { + /// Create a new context with the y-origin at the top-left corner. + /// + /// This is not the default for CoreGraphics; but it is the defualt for piet. + /// To map between the two coordinate spaces you must also pass an explicit + /// height argument. + pub fn new_y_up(ctx: &mut CGContextRef, height: f64) -> CoreGraphicsContext { + Self::new_impl(ctx, Some(height)) + } + + /// Create a new context with the y-origin at the bottom right corner. + /// + /// This is the default for core graphics, but not for piet. + pub fn new_y_down(ctx: &mut CGContextRef) -> CoreGraphicsContext { + Self::new_impl(ctx, None) + } + + fn new_impl(ctx: &mut CGContextRef, height: Option) -> CoreGraphicsContext { + ctx.save(); + if let Some(height) = height { + let xform = Affine::FLIP_Y * Affine::translate((0.0, -height)); + ctx.concat_ctm(to_cgaffine(xform.into())); + } + CoreGraphicsContext { ctx, text: CoreGraphicsText::new(), @@ -47,6 +70,12 @@ impl<'a> CoreGraphicsContext<'a> { } } +impl<'a> Drop for CoreGraphicsContext<'a> { + fn drop(&mut self) { + self.ctx.restore(); + } +} + #[derive(Clone)] pub enum Brush { Solid(Color), @@ -177,10 +206,12 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { let brush = brush.make_brush(self, || layout.frame_size.to_rect()); let pos = pos.into(); self.ctx.save(); + // drawing is from the baseline of the first line, which is normally flipped + let y_off = layout.frame_size.height - layout.line_y_positions.first().unwrap_or(&0.); // inverted coordinate system; text is drawn from bottom left corner, // and (0, 0) in context is also bottom left. - let y_off = self.ctx.height() as f64 - layout.frame_size.height; - self.ctx.translate(pos.x, y_off - pos.y); + self.ctx.translate(pos.x, y_off + pos.y); + self.ctx.scale(1.0, -1.0); match brush.as_ref() { Brush::Solid(color) => { self.set_fill_color(color); diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index c36f7c59e..6825bbf14 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -30,7 +30,7 @@ pub struct CoreGraphicsTextLayout { framesetter: Framesetter, pub(crate) frame: Option, // distance from the top of the frame to the baseline of each line - line_y_positions: Vec, + pub(crate) line_y_positions: Vec, /// offsets in utf8 of lines line_offsets: Vec, pub(crate) frame_size: Size, diff --git a/piet/src/samples/picture_5.rs b/piet/src/samples/picture_5.rs index 1cb7a617a..15a49d494 100644 --- a/piet/src/samples/picture_5.rs +++ b/piet/src/samples/picture_5.rs @@ -1,6 +1,6 @@ //! Basic example of just text -use crate::kurbo::Line; +use crate::kurbo::{Line, Rect}; use crate::{Color, Error, FontBuilder, RenderContext, Text, TextLayout, TextLayoutBuilder}; pub fn draw(rc: &mut impl RenderContext) -> Result<(), Error> { @@ -24,6 +24,9 @@ pub fn draw(rc: &mut impl RenderContext) -> Result<(), Error> { let brush = rc.solid_brush(Color::rgba8(0x00, 0x80, 0x80, 0xF0)); + let multiline_bg = Rect::from_origin_size((20.0, 50.0), (50.0, 100.0)); + rc.fill(multiline_bg, &Color::rgb(0.3, 0.0, 0.4)); + rc.draw_text(&layout, (100.0, 50.0), &brush); rc.draw_text(&layout_multiline, (20.0, 50.0), &brush); From f6a91df34d6d9c6701c3785cbcb664e0bb11a723 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 6 May 2020 20:47:46 -0400 Subject: [PATCH 24/30] Clippy --- piet-common/src/cg_back.rs | 3 +++ piet-coregraphics/src/lib.rs | 2 +- piet-coregraphics/src/text.rs | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/piet-common/src/cg_back.rs b/piet-common/src/cg_back.rs index c47bb9a40..a419fb5a1 100644 --- a/piet-common/src/cg_back.rs +++ b/piet-common/src/cg_back.rs @@ -1,3 +1,6 @@ +// allows e.g. raw_data[dst_off + x * 4 + 2] = buf[src_off + x * 4 + 0]; +#![allow(clippy::identity_op)] + //! Support for piet CoreGraphics back-end. use std::marker::PhantomData; diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 14950c7da..0b5a7378c 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -60,7 +60,7 @@ impl<'a> CoreGraphicsContext<'a> { ctx.save(); if let Some(height) = height { let xform = Affine::FLIP_Y * Affine::translate((0.0, -height)); - ctx.concat_ctm(to_cgaffine(xform.into())); + ctx.concat_ctm(to_cgaffine(xform)); } CoreGraphicsContext { diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 6825bbf14..fddf9076e 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -94,9 +94,10 @@ impl TextLayout for CoreGraphicsTextLayout { self.frame_size.width } + #[allow(clippy::float_cmp)] fn update_width(&mut self, new_width: impl Into>) -> Result<(), Error> { let width = new_width.into().unwrap_or(f64::INFINITY); - if width != self.width_constraint { + if width.ceil() != self.width_constraint.ceil() { let constraints = CGSize::new(width as CGFloat, CGFloat::INFINITY); let char_range = self.attr_string.range(); let (frame_size, _) = self.framesetter.suggest_frame_size(char_range, constraints); @@ -276,6 +277,7 @@ impl CoreGraphicsTextLayout { } /// for each line in a layout, determine its offset in utf8. + #[allow(clippy::while_let_on_iterator)] fn rebuild_line_offsets(&mut self) { let lines = self.unwrap_frame().get_lines(); @@ -322,6 +324,7 @@ impl CoreGraphicsTextLayout { } #[cfg(test)] +#[allow(clippy::float_cmp)] mod tests { use super::*; #[test] From e2a611dd22d73a78f1d8065dc4c1eb215f2eeb3e Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Fri, 8 May 2020 12:49:15 -0400 Subject: [PATCH 25/30] [cg] Fix hit_test_point This was just broken and not well enough tested. --- piet-coregraphics/src/text.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index fddf9076e..490ac444f 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -202,8 +202,8 @@ impl TextLayout for CoreGraphicsTextLayout { if rel_offset == off16 { break; } - off16 = c.len_utf16(); - off8 = c.len_utf8(); + off16 += c.len_utf16(); + off8 += c.len_utf8(); } utf8_range.0 + off8 } @@ -377,9 +377,12 @@ mod tests { assert_eq!(p2.metrics.text_position, 0); assert!(p2.is_inside); - let p3 = layout.hit_test_point(Point::new(50.0, 10.0)); - assert_eq!(p3.metrics.text_position, 1); - assert!(!p3.is_inside); + //FIXME: figure out correct multiline behaviour; this should be + //before the newline, but other backends aren't doing this right now either? + + //let p3 = layout.hit_test_point(Point::new(50.0, 10.0)); + //assert_eq!(p3.metrics.text_position, 1); + //assert!(!p3.is_inside); let p4 = layout.hit_test_point(Point::new(4.0, 25.0)); assert_eq!(p4.metrics.text_position, 2); @@ -394,6 +397,23 @@ mod tests { assert!(p6.is_inside); } + #[test] + fn hit_test_end_of_single_line() { + let text = "hello"; + let a_font = font::new_from_name("Helvetica", 16.0).unwrap(); + let layout = CoreGraphicsTextLayout::new(&CoreGraphicsFont(a_font), text, f64::INFINITY); + let pt = layout.hit_test_point(Point::new(0.0, 5.0)); + assert_eq!(pt.metrics.text_position, 0); + assert_eq!(pt.is_inside, true); + let next_to_last = layout.frame_size.width - 10.0; + let pt = layout.hit_test_point(Point::new(next_to_last, 0.0)); + assert_eq!(pt.metrics.text_position, 4); + assert_eq!(pt.is_inside, true); + let pt = layout.hit_test_point(Point::new(100.0, 5.0)); + assert_eq!(pt.metrics.text_position, 5); + assert_eq!(pt.is_inside, false); + } + #[test] fn hit_test_text_position() { let text = "aaaaa\nbbbbb"; From b5b5f69d180657803b4202c602209f11b70abc9e Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Fri, 8 May 2020 14:43:19 -0400 Subject: [PATCH 26/30] [cg] Manually track our current transform Relying on CoreGraphics here causes problems from druid; because druid-shell for mac flips the coordinate system, there is a transform applied to the context before we see it that we want to ignore. --- piet-coregraphics/src/lib.rs | 102 +++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 0b5a7378c..43681dcf8 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -37,6 +37,11 @@ pub struct CoreGraphicsContext<'a> { ctx: &'a mut CGContextRef, // the height of the context; we need this in order to correctly flip the coordinate space text: CoreGraphicsText<'a>, + // because of the relationship between cocoa and coregraphics (where cocoa + // may be asked to flip the y-axis) we cannot trust the transform returned + // by CTContextGetCTM. Instead we maintain our own stack, which will contain + // only those transforms applied by us. + transform_stack: Vec, } impl<'a> CoreGraphicsContext<'a> { @@ -66,6 +71,7 @@ impl<'a> CoreGraphicsContext<'a> { CoreGraphicsContext { ctx, text: CoreGraphicsText::new(), + transform_stack: vec![Affine::default()], } } } @@ -231,11 +237,16 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { fn save(&mut self) -> Result<(), Error> { self.ctx.save(); + let state = self.transform_stack.last().copied().unwrap_or_default(); + self.transform_stack.push(state); Ok(()) } + //TODO: this panics in CoreGraphics if unbalanced. We could try and track stack depth + //and return an error, maybe? fn restore(&mut self) -> Result<(), Error> { self.ctx.restore(); + self.transform_stack.pop(); Ok(()) } @@ -244,8 +255,12 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn transform(&mut self, transform: Affine) { - let transform = to_cgaffine(transform); - self.ctx.concat_ctm(transform); + if let Some(last) = self.transform_stack.last_mut() { + *last = *last * transform; + } else { + self.transform_stack.push(transform); + } + self.ctx.concat_ctm(to_cgaffine(transform)); } fn make_image( @@ -323,8 +338,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { } fn current_transform(&self) -> Affine { - let ctm = self.ctx.get_ctm(); - from_cgaffine(ctm) + self.transform_stack.last().copied().unwrap_or_default() } fn status(&mut self) -> Result<(), Error> { @@ -442,11 +456,6 @@ fn to_cgrect(rect: impl Into) -> CGRect { CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size())) } -fn from_cgaffine(affine: CGAffineTransform) -> Affine { - let CGAffineTransform { a, b, c, d, tx, ty } = affine; - Affine::new([a, b, c, d, tx, ty]) -} - fn to_cgaffine(affine: Affine) -> CGAffineTransform { let [a, b, c, d, tx, ty] = affine.as_coeffs(); CGAffineTransform::new(a, b, c, d, tx, ty) @@ -456,3 +465,78 @@ fn to_cgaffine(affine: Affine) -> CGAffineTransform { extern "C" { fn CGContextClipToMask(ctx: *mut u8, rect: CGRect, mask: *const u8); } + +#[cfg(test)] +mod tests { + use super::*; + use core_graphics::color_space::CGColorSpace; + use core_graphics::context::CGContext; + + fn make_context(size: impl Into) -> CGContext { + let size = size.into(); + CGContext::create_bitmap_context( + None, + size.width as usize, + size.height as usize, + 8, + 0, + &CGColorSpace::create_device_rgb(), + core_graphics::base::kCGImageAlphaPremultipliedLast, + ) + } + + fn equalish_affine(one: Affine, two: Affine) -> bool { + one.as_coeffs() + .iter() + .zip(two.as_coeffs().iter()) + .all(|(a, b)| (a - b).abs() < f64::EPSILON) + } + + macro_rules! assert_affine_eq { + ($left:expr, $right:expr) => {{ + if !equalish_affine($left, $right) { + panic!( + "assertion failed: `(one == two)`\n\ + one: {:?}\n\ + two: {:?}", + $left.as_coeffs(), + $right.as_coeffs() + ) + } + }}; + } + + #[test] + fn get_affine_y_up() { + let mut ctx = make_context((400.0, 400.0)); + let mut piet = CoreGraphicsContext::new_y_up(&mut ctx, 400.0); + let affine = piet.current_transform(); + assert_affine_eq!(affine, Affine::default()); + + let one = Affine::translate((50.0, 20.0)); + let two = Affine::rotate(2.2); + let three = Affine::FLIP_Y; + let four = Affine::scale_non_uniform(2.0, -1.5); + + piet.save().unwrap(); + piet.transform(one); + piet.transform(one); + piet.save().unwrap(); + piet.transform(two); + piet.save().unwrap(); + piet.transform(three); + assert_affine_eq!(piet.current_transform(), one * one * two * three); + piet.transform(four); + piet.save().unwrap(); + + assert_affine_eq!(piet.current_transform(), one * one * two * three * four); + piet.restore().unwrap(); + assert_affine_eq!(piet.current_transform(), one * one * two * three * four); + piet.restore().unwrap(); + assert_affine_eq!(piet.current_transform(), one * one * two); + piet.restore().unwrap(); + assert_affine_eq!(piet.current_transform(), one * one); + piet.restore().unwrap(); + assert_affine_eq!(piet.current_transform(), Affine::default()); + } +} From 340ff85230275a9a78db328c555e906246038e0b Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Sat, 9 May 2020 18:00:09 -0400 Subject: [PATCH 27/30] [cg] Address code review A bunch of small fixes. Larger things include: - reusing unpremultiply_rgba function - allowing explicit CGGradientDrawingOptions - better signatures in FFI --- piet-common/src/cg_back.rs | 37 ++-------------- piet-coregraphics/examples/basic-cg.rs | 14 +------ piet-coregraphics/examples/test-picture.rs | 1 + piet-coregraphics/src/gradient.rs | 49 ++++++++++++---------- piet-coregraphics/src/lib.rs | 45 ++++++++++++-------- piet-coregraphics/src/text.rs | 27 ++++++------ 6 files changed, 72 insertions(+), 101 deletions(-) diff --git a/piet-common/src/cg_back.rs b/piet-common/src/cg_back.rs index a419fb5a1..c31c2569f 100644 --- a/piet-common/src/cg_back.rs +++ b/piet-common/src/cg_back.rs @@ -87,7 +87,7 @@ impl Device { core_graphics::base::kCGImageAlphaPremultipliedLast, ); ctx.scale(pix_scale, pix_scale); - let height = height as f64 * pix_scale; + let height = height as f64 * pix_scale.recip(); Ok(BitmapTarget { ctx, height, @@ -112,27 +112,8 @@ impl<'a> BitmapTarget<'a> { return Err(Error::NotSupported); } - let width = self.ctx.width() as usize; - let height = self.ctx.height() as usize; - let stride = self.ctx.bytes_per_row() as usize; - let data = self.ctx.data(); - if stride != width { - let mut raw_data = vec![0; width * height * 4]; - for y in 0..height { - let src_off = y * stride; - let dst_off = y * width * 4; - for x in 0..width { - raw_data[dst_off + x * 4 + 0] = data[src_off + x * 4 + 2]; - raw_data[dst_off + x * 4 + 1] = data[src_off + x * 4 + 1]; - raw_data[dst_off + x * 4 + 2] = data[src_off + x * 4 + 0]; - raw_data[dst_off + x * 4 + 3] = data[src_off + x * 4 + 3]; - } - } - Ok(raw_data) - } else { - Ok(data.to_owned()) - } + Ok(data.to_owned()) } /// Save bitmap to RGBA PNG file @@ -141,7 +122,7 @@ impl<'a> BitmapTarget<'a> { let width = self.ctx.width() as usize; let height = self.ctx.height() as usize; let mut data = self.into_raw_pixels(ImageFormat::RgbaPremul)?; - unpremultiply(&mut data); + piet_coregraphics::unpremultiply(&mut data); let file = BufWriter::new(File::create(path).map_err(|e| Into::>::into(e))?); let mut encoder = Encoder::new(file, width as u32, height as u32); encoder.set_color(ColorType::RGBA); @@ -160,15 +141,3 @@ impl<'a> BitmapTarget<'a> { Err(Error::MissingFeature) } } -#[cfg(feature = "png")] -fn unpremultiply(data: &mut [u8]) { - for i in (0..data.len()).step_by(4) { - let a = data[i + 3]; - if a != 0 { - let scale = 255.0 / (a as f64); - data[i] = (scale * (data[i] as f64)).round() as u8; - data[i + 1] = (scale * (data[i + 1] as f64)).round() as u8; - data[i + 2] = (scale * (data[i + 2] as f64)).round() as u8; - } - } -} diff --git a/piet-coregraphics/examples/basic-cg.rs b/piet-coregraphics/examples/basic-cg.rs index 6ea5366f5..056f9686c 100644 --- a/piet-coregraphics/examples/basic-cg.rs +++ b/piet-coregraphics/examples/basic-cg.rs @@ -68,7 +68,7 @@ fn main() { piet.finish().unwrap(); std::mem::drop(piet); - unpremultiply(cg_ctx.data()); + piet_coregraphics::unpremultiply_rgba(cg_ctx.data()); // Write image as PNG file. let path = Path::new("image.png"); @@ -82,15 +82,3 @@ fn main() { writer.write_image_data(cg_ctx.data()).unwrap(); } - -fn unpremultiply(data: &mut [u8]) { - for i in (0..data.len()).step_by(4) { - let a = data[i + 3]; - if a != 0 { - let scale = 255.0 / (a as f64); - data[i] = (scale * (data[i] as f64)).round() as u8; - data[i + 1] = (scale * (data[i + 1] as f64)).round() as u8; - data[i + 2] = (scale * (data[i + 2] as f64)).round() as u8; - } - } -} diff --git a/piet-coregraphics/examples/test-picture.rs b/piet-coregraphics/examples/test-picture.rs index a922ba76c..a2d40cd7c 100644 --- a/piet-coregraphics/examples/test-picture.rs +++ b/piet-coregraphics/examples/test-picture.rs @@ -42,5 +42,6 @@ fn main() { encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder.write_header().unwrap(); + piet_coregraphics::unpremultiply_rgba(cg_ctx.data()); writer.write_image_data(cg_ctx.data()).unwrap(); } diff --git a/piet-coregraphics/src/gradient.rs b/piet-coregraphics/src/gradient.rs index 36e2845ed..0841928ce 100644 --- a/piet-coregraphics/src/gradient.rs +++ b/piet-coregraphics/src/gradient.rs @@ -1,3 +1,5 @@ +#![allow(non_upper_case_globals)] + //! core graphics gradient support use core_foundation::{ @@ -16,14 +18,15 @@ use core_graphics::{ use piet::kurbo::Point; use piet::{Color, FixedGradient, FixedLinearGradient, FixedRadialGradient, GradientStop}; +//FIXME: remove all this when core-graphics 0.20.0 is released // core-graphics does not provide a CGGradient type pub enum CGGradientT {} pub type CGGradientRef = *mut CGGradientT; +pub type CGGradientDrawingOptions = u32; +pub const CGGradientDrawsBeforeStartLocation: CGGradientDrawingOptions = 1; +pub const CGGradientDrawsAfterEndLocation: CGGradientDrawingOptions = 1 << 1; -declare_TCFType! { - CGGradient, CGGradientRef -} - +declare_TCFType!(CGGradient, CGGradientRef); impl_TCFType!(CGGradient, CGGradientRef, CGGradientGetTypeID); /// A wrapper around CGGradient @@ -53,8 +56,7 @@ impl Gradient { .unwrap_or(Color::BLACK) } - pub(crate) fn fill(&self, ctx: &mut CGContextRef) { - let context_ref: *mut u8 = ctx as *mut CGContextRef as *mut u8; + pub(crate) fn fill(&self, ctx: &mut CGContextRef, options: CGGradientDrawingOptions) { match self.piet_grad { FixedGradient::Radial(FixedRadialGradient { center, @@ -63,16 +65,16 @@ impl Gradient { .. }) => { let start_center = to_cgpoint(center + origin_offset); - let center = to_cgpoint(center); + let end_center = to_cgpoint(center); unsafe { CGContextDrawRadialGradient( - context_ref, + ctx, self.cg_grad.as_concrete_TypeRef(), start_center, - 0.0, - center, + 0.0, // start_radius + end_center, radius as CGFloat, - 0, + options, ) } } @@ -81,7 +83,7 @@ impl Gradient { let end = to_cgpoint(end); unsafe { CGContextDrawLinearGradient( - context_ref, + ctx, self.cg_grad.as_concrete_TypeRef(), start, end, @@ -97,23 +99,19 @@ fn new_cg_gradient(stops: &[GradientStop]) -> CGGradient { unsafe { //FIXME: is this expensive enough we should be reusing it? let space = CGColorSpace::create_with_name(kCGColorSpaceSRGB).unwrap(); - let space_ref: *const u8 = &*space as *const CGColorSpaceRef as *const u8; let mut colors = Vec::::new(); let mut locations = Vec::::new(); for GradientStop { pos, color } in stops { let (r, g, b, a) = Color::as_rgba(&color); - let color = CGColorCreate(space_ref as *const u8, [r, g, b, a].as_ptr()); + let color = CGColorCreate(&*space, [r, g, b, a].as_ptr()); let color = CGColor::wrap_under_create_rule(color); colors.push(color); locations.push(*pos as CGFloat); } let colors = CFArray::from_CFTypes(&colors); - let gradient = CGGradientCreateWithColors( - space_ref as *const u8, - colors.as_concrete_TypeRef(), - locations.as_ptr(), - ); + let gradient = + CGGradientCreateWithColors(&*space, colors.as_concrete_TypeRef(), locations.as_ptr()); CGGradient::wrap_under_create_rule(gradient) } @@ -126,21 +124,26 @@ fn to_cgpoint(point: Point) -> CGPoint { #[link(name = "CoreGraphics", kind = "framework")] extern "C" { fn CGGradientGetTypeID() -> CFTypeID; + //CGColorSpaceRef is missing repr(c). + #[allow(improper_ctypes)] fn CGGradientCreateWithColors( - space: *const u8, + space: *const CGColorSpaceRef, colors: CFArrayRef, locations: *const CGFloat, ) -> CGGradientRef; - fn CGColorCreate(space: *const u8, components: *const CGFloat) -> SysCGColorRef; + #[allow(improper_ctypes)] + fn CGColorCreate(space: *const CGColorSpaceRef, components: *const CGFloat) -> SysCGColorRef; + #[allow(improper_ctypes)] fn CGContextDrawLinearGradient( - ctx: *mut u8, + ctx: *mut CGContextRef, gradient: CGGradientRef, startPoint: CGPoint, endPoint: CGPoint, options: u32, ); + #[allow(improper_ctypes)] fn CGContextDrawRadialGradient( - ctx: *mut u8, + ctx: *mut CGContextRef, gradient: CGGradientRef, startCenter: CGPoint, startRadius: CGFloat, diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 43681dcf8..57da816aa 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -29,13 +29,18 @@ pub use crate::text::{ CoreGraphicsTextLayoutBuilder, }; -use gradient::Gradient; +use gradient::{ + CGGradientDrawingOptions, CGGradientDrawsAfterEndLocation, CGGradientDrawsBeforeStartLocation, + Gradient, +}; + +const GRADIENT_DRAW_BEFORE_AND_AFTER: CGGradientDrawingOptions = + CGGradientDrawsAfterEndLocation | CGGradientDrawsBeforeStartLocation; pub struct CoreGraphicsContext<'a> { // Cairo has this as Clone and with &self methods, but we do this to avoid // concurrency problems. ctx: &'a mut CGContextRef, - // the height of the context; we need this in order to correctly flip the coordinate space text: CoreGraphicsText<'a>, // because of the relationship between cocoa and coregraphics (where cocoa // may be asked to flip the y-axis) we cannot trust the transform returned @@ -96,13 +101,8 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { //type StrokeStyle = StrokeStyle; fn clear(&mut self, color: Color) { - let rgba = color.as_rgba_u32(); - self.ctx.set_rgb_fill_color( - byte_to_frac(rgba >> 24), - byte_to_frac(rgba >> 16), - byte_to_frac(rgba >> 8), - byte_to_frac(rgba), - ); + let (r, g, b, a) = color.as_rgba(); + self.ctx.set_rgb_fill_color(r, g, b, a); self.ctx.fill_rect(self.ctx.clip_bounding_box()); } @@ -127,7 +127,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { Brush::Gradient(grad) => { self.ctx.save(); self.ctx.clip(); - grad.fill(self.ctx); + grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER); self.ctx.restore(); } } @@ -144,7 +144,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { Brush::Gradient(grad) => { self.ctx.save(); self.ctx.eo_clip(); - grad.fill(self.ctx); + grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER); self.ctx.restore(); } } @@ -168,7 +168,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { self.ctx.save(); self.ctx.replace_path_with_stroked_path(); self.ctx.clip(); - grad.fill(self.ctx); + grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER); self.ctx.restore(); } } @@ -193,7 +193,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { self.ctx.save(); self.ctx.replace_path_with_stroked_path(); self.ctx.clip(); - grad.fill(self.ctx); + grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER); self.ctx.restore(); } } @@ -256,7 +256,7 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { fn transform(&mut self, transform: Affine) { if let Some(last) = self.transform_stack.last_mut() { - *last = *last * transform; + *last *= transform; } else { self.transform_stack.push(transform); } @@ -439,10 +439,6 @@ impl<'a> CoreGraphicsContext<'a> { } } -fn byte_to_frac(byte: u32) -> f64 { - ((byte & 255) as f64) * (1.0 / 255.0) -} - fn to_cgpoint(point: Point) -> CGPoint { CGPoint::new(point.x as CGFloat, point.y as CGFloat) } @@ -461,6 +457,19 @@ fn to_cgaffine(affine: Affine) -> CGAffineTransform { CGAffineTransform::new(a, b, c, d, tx, ty) } +#[allow(dead_code)] +pub fn unpremultiply_rgba(data: &mut [u8]) { + for i in (0..data.len()).step_by(4) { + let a = data[i + 3]; + if a != 0 { + let scale = 255.0 / (a as f64); + data[i] = (scale * (data[i] as f64)).round() as u8; + data[i + 1] = (scale * (data[i + 1] as f64)).round() as u8; + data[i + 2] = (scale * (data[i + 2] as f64)).round() as u8; + } + } +} + #[link(name = "CoreGraphics", kind = "framework")] extern "C" { fn CGContextClipToMask(ctx: *mut u8, rect: CGRect, mask: *const u8); diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 490ac444f..70f9dc4fe 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -17,7 +17,6 @@ use piet::{ use crate::ct_helpers::{AttributedString, Frame, Framesetter, Line}; -// inner is an nsfont. #[derive(Debug, Clone)] pub struct CoreGraphicsFont(CTFont); @@ -188,24 +187,13 @@ impl TextLayout for CoreGraphicsTextLayout { let offset_utf16 = line.get_string_index_for_position(point_in_string_space); let offset = match offset_utf16 { // this is 'kCFNotFound'. - // if nothing is found just go end of string? should this be len - 1? do we have an - // implicit newline at end of file? so many mysteries -1 => self.string.len(), n if n >= 0 => { let utf16_range = line.get_string_range(); let utf8_range = self.line_range(line_num).unwrap(); let line_txt = self.line_text(line_num).unwrap(); let rel_offset = (n - utf16_range.location) as usize; - let mut off16 = 0; - let mut off8 = 0; - for c in line_txt.chars() { - if rel_offset == off16 { - break; - } - off16 += c.len_utf16(); - off8 += c.len_utf8(); - } - utf8_range.0 + off8 + utf8_range.0 + utf8_offset_for_utf16_offset(line_txt, rel_offset) } // some other value; should never happen _ => panic!("gross violation of api contract"), @@ -323,6 +311,19 @@ impl CoreGraphicsTextLayout { } } +fn utf8_offset_for_utf16_offset(text: &str, utf16_offset: usize) -> usize { + let mut off16 = 0; + let mut off8 = 0; + for c in text.chars() { + if utf16_offset == off16 { + break; + } + off16 += c.len_utf16(); + off8 += c.len_utf8(); + } + off8 +} + #[cfg(test)] #[allow(clippy::float_cmp)] mod tests { From 041635a1334be8df8742b1872a2542a22cc47b9a Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Mon, 11 May 2020 10:20:59 -0400 Subject: [PATCH 28/30] [cg] Correct image orientation when drawing --- piet-coregraphics/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index 57da816aa..a3c3bc753 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -308,7 +308,13 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { _interp: InterpolationMode, ) { // TODO: apply interpolation mode - self.ctx.draw_image(to_cgrect(rect), image); + self.ctx.save(); + let rect = to_cgrect(rect); + // CGImage is drawn flipped by default + self.ctx.translate(0., rect.size.height); + self.ctx.scale(1.0, -1.0); + self.ctx.draw_image(rect, image); + self.ctx.restore(); } fn draw_image_area( From 3940a1fa403006d0a2ac5cb2b0b21d26df185432 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Mon, 11 May 2020 11:56:10 -0400 Subject: [PATCH 29/30] [cg] Use piet::util for utf8/16 conversion --- piet-coregraphics/src/text.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/piet-coregraphics/src/text.rs b/piet-coregraphics/src/text.rs index 70f9dc4fe..ea136dcb2 100644 --- a/piet-coregraphics/src/text.rs +++ b/piet-coregraphics/src/text.rs @@ -10,6 +10,7 @@ use core_graphics::path::CGPath; use core_text::font::{self, CTFont}; use piet::kurbo::{Point, Size}; +use piet::util; use piet::{ Error, Font, FontBuilder, HitTestMetrics, HitTestPoint, HitTestTextPosition, LineMetric, Text, TextLayout, TextLayoutBuilder, @@ -193,7 +194,9 @@ impl TextLayout for CoreGraphicsTextLayout { let utf8_range = self.line_range(line_num).unwrap(); let line_txt = self.line_text(line_num).unwrap(); let rel_offset = (n - utf16_range.location) as usize; - utf8_range.0 + utf8_offset_for_utf16_offset(line_txt, rel_offset) + utf8_range.0 + + util::count_until_utf16(line_txt, rel_offset) + .unwrap_or_else(|| line_txt.len()) } // some other value; should never happen _ => panic!("gross violation of api contract"), @@ -220,7 +223,7 @@ impl TextLayout for CoreGraphicsTextLayout { let text = self.line_text(line_num)?; let offset_remainder = offset - self.line_offsets.get(line_num)?; - let off16: usize = text[..offset_remainder].chars().map(char::len_utf16).sum(); + let off16: usize = util::count_utf16(&text[..offset_remainder]); let line_range = line.get_string_range(); let char_idx = line_range.location + off16 as isize; let (x_pos, _) = line.get_offset_for_string_index(char_idx); @@ -311,19 +314,6 @@ impl CoreGraphicsTextLayout { } } -fn utf8_offset_for_utf16_offset(text: &str, utf16_offset: usize) -> usize { - let mut off16 = 0; - let mut off8 = 0; - for c in text.chars() { - if utf16_offset == off16 { - break; - } - off16 += c.len_utf16(); - off8 += c.len_utf8(); - } - off8 -} - #[cfg(test)] #[allow(clippy::float_cmp)] mod tests { @@ -428,4 +418,18 @@ mod tests { // just the general idea that this is the second character assert!(p1.point.x > 5.0 && p1.point.x < 15.0); } + + #[test] + fn hit_test_text_position_astral_plane() { + let text = "πŸ‘ΎπŸ€ \nπŸ€–πŸŽƒπŸ‘Ύ"; + let a_font = font::new_from_name("Helvetica", 16.0).unwrap(); + let layout = CoreGraphicsTextLayout::new(&CoreGraphicsFont(a_font), text, f64::INFINITY); + let p0 = layout.hit_test_text_position(4).unwrap(); + let p1 = layout.hit_test_text_position(8).unwrap(); + let p2 = layout.hit_test_text_position(13).unwrap(); + + assert!(p1.point.x > p0.point.x); + assert!(p1.point.y == p0.point.y); + assert!(p2.point.y > p1.point.y); + } } From a36c47643243bcd018d1ce2ab0788e946fe5d4e1 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Mon, 11 May 2020 12:02:01 -0400 Subject: [PATCH 30/30] [cg] Be defensive when restoring saved context state --- piet-coregraphics/src/lib.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/piet-coregraphics/src/lib.rs b/piet-coregraphics/src/lib.rs index a3c3bc753..097376262 100644 --- a/piet-coregraphics/src/lib.rs +++ b/piet-coregraphics/src/lib.rs @@ -242,12 +242,15 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> { Ok(()) } - //TODO: this panics in CoreGraphics if unbalanced. We could try and track stack depth - //and return an error, maybe? fn restore(&mut self) -> Result<(), Error> { - self.ctx.restore(); - self.transform_stack.pop(); - Ok(()) + if self.transform_stack.pop().is_some() { + // we're defensive about calling restore on the inner context, + // because an unbalanced call will trigger an assert in C + self.ctx.restore(); + Ok(()) + } else { + Err(Error::StackUnbalance) + } } fn finish(&mut self) -> Result<(), Error> {