diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 9bda7bf4..39b921ec 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -430,6 +430,7 @@ struct State { shadow_blur: f32, shadow_offset: Vector2F, text_align: TextAlign, + text_baseline: TextBaseline, image_smoothing_enabled: bool, image_smoothing_quality: ImageSmoothingQuality, global_alpha: f32, @@ -455,6 +456,7 @@ impl State { shadow_blur: 0.0, shadow_offset: Vector2F::default(), text_align: TextAlign::Left, + text_baseline: TextBaseline::Alphabetic, image_smoothing_enabled: true, image_smoothing_quality: ImageSmoothingQuality::Low, global_alpha: 1.0, @@ -639,6 +641,16 @@ pub enum TextAlign { Center, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TextBaseline { + Alphabetic, + Top, + Hanging, + Middle, + Ideographic, + Bottom, +} + // We duplicate `pathfinder_content::stroke::LineJoin` here because the HTML canvas API treats the // miter limit as part of the canvas state, while the native Pathfinder API treats the miter limit // as part of the line join. Pathfinder's choice is more logical, because the miter limit is diff --git a/canvas/src/text.rs b/canvas/src/text.rs index 7e65eaf9..2e5b4f14 100644 --- a/canvas/src/text.rs +++ b/canvas/src/text.rs @@ -8,11 +8,12 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use crate::{CanvasRenderingContext2D, TextAlign}; +use crate::{CanvasRenderingContext2D, TextAlign, TextBaseline}; use font_kit::family_name::FamilyName; use font_kit::handle::Handle; use font_kit::hinting::HintingOptions; use font_kit::loaders::default::Font; +use font_kit::metrics::Metrics; use font_kit::properties::Properties; use font_kit::source::{Source, SystemSource}; use font_kit::sources::mem::MemSource; @@ -63,6 +64,17 @@ impl CanvasRenderingContext2D { TextAlign::Center => position.set_x(position.x() - layout.width() * 0.5), } + match self.current_state.text_baseline { + TextBaseline::Alphabetic => {} + TextBaseline::Top => position.set_y(position.y() + layout.ascent()), + TextBaseline::Middle => position.set_y(position.y() + layout.ascent() * 0.5), + TextBaseline::Bottom => position.set_y(position.y() + layout.descent()), + TextBaseline::Ideographic => { + position.set_y(position.y() + layout.ideographic_baseline()) + } + TextBaseline::Hanging => position.set_y(position.y() + layout.hanging_baseline()), + } + let transform = self.current_state.transform * Transform2F::from_translation(position); // TODO(pcwalton): Report errors. @@ -116,15 +128,35 @@ impl CanvasRenderingContext2D { self.set_font(font.expect("Didn't find the font!").load().unwrap()); } + #[inline] + pub fn font_size(&self) -> f32 { + self.current_state.font_size + } + #[inline] pub fn set_font_size(&mut self, new_font_size: f32) { self.current_state.font_size = new_font_size; } + #[inline] + pub fn text_align(&self) -> TextAlign { + self.current_state.text_align + } + #[inline] pub fn set_text_align(&mut self, new_text_align: TextAlign) { self.current_state.text_align = new_text_align; } + + #[inline] + pub fn text_baseline(&self) -> TextBaseline { + self.current_state.text_baseline + } + + #[inline] + pub fn set_text_baseline(&mut self, new_text_baseline: TextBaseline) { + self.current_state.text_baseline = new_text_baseline; + } } // TODO(pcwalton): Support other fields. @@ -174,6 +206,12 @@ impl CanvasFontContext { pub trait LayoutExt { fn width(&self) -> f32; + fn fold_metric(&self, get: G, fold: F) -> f32 where G: FnMut(&Metrics) -> f32, + F: FnMut(f32, f32) -> f32; + fn ascent(&self) -> f32; + fn descent(&self) -> f32; + fn hanging_baseline(&self) -> f32; + fn ideographic_baseline(&self) -> f32; } impl LayoutExt for Layout { @@ -189,4 +227,39 @@ impl LayoutExt for Layout { let scale_factor = self.size / font_metrics.units_per_em as f32; last_glyph.offset.x + glyph_rect.max_x() * scale_factor } + + fn fold_metric(&self, mut get: G, mut fold: F) -> f32 where G: FnMut(&Metrics) -> f32, + F: FnMut(f32, f32) -> f32 { + let (mut last_font_seen, mut value) = (None, 0.0); + for glyph in &self.glyphs { + if let Some(ref last_font_seen) = last_font_seen { + if Arc::ptr_eq(last_font_seen, &glyph.font.font) { + continue; + } + } + let font_metrics = glyph.font.font.metrics(); + let scale_factor = self.size / font_metrics.units_per_em as f32; + value = fold(value, get(&font_metrics) * scale_factor); + last_font_seen = Some(glyph.font.font.clone()); + } + value + } + + fn ascent(&self) -> f32 { + self.fold_metric(|metrics| metrics.ascent, f32::max) + } + + fn descent(&self) -> f32 { + self.fold_metric(|metrics| metrics.descent, f32::min) + } + + fn hanging_baseline(&self) -> f32 { + // TODO(pcwalton) + 0.0 + } + + fn ideographic_baseline(&self) -> f32 { + // TODO(pcwalton) + 0.0 + } } diff --git a/examples/canvas_nanovg/src/main.rs b/examples/canvas_nanovg/src/main.rs index b95ba5cc..c481796c 100644 --- a/examples/canvas_nanovg/src/main.rs +++ b/examples/canvas_nanovg/src/main.rs @@ -10,7 +10,8 @@ use arrayvec::ArrayVec; use image; -use pathfinder_canvas::{CanvasFontContext, CanvasRenderingContext2D, LineJoin, Path2D, TextAlign}; +use pathfinder_canvas::{CanvasFontContext, CanvasRenderingContext2D, LineJoin, Path2D}; +use pathfinder_canvas::{TextAlign, TextBaseline}; use pathfinder_color::{ColorF, ColorU}; use pathfinder_content::fill::FillRule; use pathfinder_content::gradient::Gradient; @@ -571,6 +572,7 @@ fn draw_window(canvas: &mut CanvasRenderingContext2D, title: &str, rect: RectF) // TODO(pcwalton): Bold text. canvas.set_font_size(15.0); canvas.set_text_align(TextAlign::Center); + canvas.set_text_baseline(TextBaseline::Middle); canvas.set_fill_style(ColorU::new(220, 220, 220, 160)); canvas.set_shadow_blur(2.0); canvas.set_shadow_offset(Vector2F::new(0.0, 1.0)); @@ -594,6 +596,8 @@ fn draw_search_box(canvas: &mut CanvasRenderingContext2D, text: &str, rect: Rect canvas.set_font_size(17.0); canvas.set_fill_style(ColorU::new(255, 255, 255, 64)); + canvas.set_text_align(TextAlign::Left); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(text, rect.origin() + Vector2F::splat(rect.height()) * Vector2F::new(1.05, 0.5)); } @@ -615,6 +619,8 @@ fn draw_dropdown(canvas: &mut CanvasRenderingContext2D, text: &str, rect: RectF) canvas.set_font_size(17.0); canvas.set_fill_style(ColorU::new(255, 255, 255, 160)); + canvas.set_text_align(TextAlign::Left); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(text, rect.origin() + Vector2F::splat(rect.height()) * Vector2F::new(0.3, 0.5)); } @@ -622,6 +628,8 @@ fn draw_dropdown(canvas: &mut CanvasRenderingContext2D, text: &str, rect: RectF) fn draw_label(canvas: &mut CanvasRenderingContext2D, text: &str, rect: RectF) { canvas.set_font_size(15.0); canvas.set_fill_style(ColorU::new(255, 255, 255, 128)); + canvas.set_text_align(TextAlign::Left); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(text, rect.origin() + Vector2F::new(0.0, rect.height() * 0.5)); } @@ -649,6 +657,7 @@ fn draw_text_edit_box(canvas: &mut CanvasRenderingContext2D, text: &str, rect: R canvas.set_font_size(17.0); canvas.set_fill_style(ColorU::new(255, 255, 255, 64)); canvas.set_text_align(TextAlign::Left); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(text, rect.origin() + Vector2F::splat(rect.height()) * Vector2F::new(0.3, 0.5)); } @@ -664,6 +673,7 @@ fn draw_numeric_edit_box(canvas: &mut CanvasRenderingContext2D, canvas.set_fill_style(ColorU::new(255, 255, 255, 64)); canvas.set_text_align(TextAlign::Right); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text( unit, rect.upper_right() + Vector2F::new(-0.3, 0.5) * Vector2F::splat(rect.height())); @@ -671,6 +681,7 @@ fn draw_numeric_edit_box(canvas: &mut CanvasRenderingContext2D, canvas.set_font_size(17.0); canvas.set_fill_style(ColorU::new(255, 255, 255, 128)); canvas.set_text_align(TextAlign::Right); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(value, rect.upper_right() + Vector2F::new(-unit_width - rect.height() * 0.5, rect.height() * 0.5)); @@ -681,6 +692,8 @@ fn draw_check_box(canvas: &mut CanvasRenderingContext2D, text: &str, rect: RectF canvas.set_font_size(15.0); canvas.set_fill_style(ColorU::new(255, 255, 255, 160)); + canvas.set_text_align(TextAlign::Left); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(text, rect.origin() + Vector2F::new(28.0, rect.height() * 0.5)); let check_box_rect = @@ -727,6 +740,7 @@ fn draw_button(canvas: &mut CanvasRenderingContext2D, text: &str, rect: RectF, c let text_origin = rect.center() + Vector2F::new(icon_width * 0.25 - text_width * 0.5, 0.0); canvas.set_fill_style(ColorU::new(0, 0, 0, 160)); canvas.set_text_align(TextAlign::Left); + canvas.set_text_baseline(TextBaseline::Middle); canvas.fill_text(text, text_origin - Vector2F::new(0.0, 1.0)); canvas.set_fill_style(ColorU::new(255, 255, 255, 160)); canvas.fill_text(text, text_origin);