diff --git a/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf b/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf new file mode 100644 index 0000000000..ffe27865aa Binary files /dev/null and b/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf differ diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index b24739359b..59c5b2657f 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -10,6 +10,8 @@ use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; use graphene_std::Color; use graphene_std::math::quad::Quad; +use graphene_std::table::Table; +use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path}; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::{PointId, SegmentId, Vector}; use std::collections::HashMap; @@ -378,7 +380,8 @@ impl OverlayContext { } pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { - self.internal().text(text, font_color, background_color, transform, padding, pivot); + let mut internal = self.internal(); + internal.text(text, font_color, background_color, transform, padding, pivot); } pub fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option) { @@ -972,32 +975,195 @@ impl OverlayContextInternal { /// Fills the area inside the path with a pattern. Assumes `color` is in gamma space. /// Used by the fill tool to show the area to be filled. fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color) { - // TODO: Implement pattern fill in Vello - // For now, just fill with a semi-transparent version of the color + const PATTERN_WIDTH: u32 = 4; + const PATTERN_HEIGHT: u32 = 4; + + // Create a 4x4 pixel pattern with colored pixels at (0,0) and (2,2) + // This matches the Canvas2D checkerboard pattern + let mut data = vec![0u8; (PATTERN_WIDTH * PATTERN_HEIGHT * 4) as usize]; + let rgba = color.to_rgba8_srgb(); + + // ┌▄▄┬──┬──┬──┐ + // ├▀▀┼──┼──┼──┤ + // ├──┼──┼▄▄┼──┤ + // ├──┼──┼▀▀┼──┤ + // └──┴──┴──┴──┘ + // Set pixels at (0,0) and (2,2) to the specified color + let pixels = [(0, 0), (2, 2)]; + for &(x, y) in &pixels { + let index = ((y * PATTERN_WIDTH + x) * 4) as usize; + data[index..index + 4].copy_from_slice(&rgba); + } + + let image = peniko::Image { + data: data.into(), + format: peniko::ImageFormat::Rgba8, + width: PATTERN_WIDTH, + height: PATTERN_HEIGHT, + x_extend: peniko::Extend::Repeat, + y_extend: peniko::Extend::Repeat, + alpha: 1.0, + quality: peniko::ImageQuality::default(), + }; + let path = self.push_path(subpaths, transform); - let semi_transparent_color = color.with_alpha(0.5); - - self.scene.fill( - peniko::Fill::NonZero, - self.get_transform(), - peniko::Color::from_rgba8( - (semi_transparent_color.r() * 255.) as u8, - (semi_transparent_color.g() * 255.) as u8, - (semi_transparent_color.b() * 255.) as u8, - (semi_transparent_color.a() * 255.) as u8, - ), - None, - &path, - ); - } + let brush = peniko::Brush::Image(image); + + self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path); + } + + fn get_width(&self, text: &str) -> f64 { + // Use the actual text-to-path system to get precise text width + const FONT_SIZE: f64 = 12.0; + + let typesetting = TypesettingConfig { + font_size: FONT_SIZE, + line_height_ratio: 1.2, + character_spacing: 0.0, + max_width: None, + max_height: None, + tilt: 0.0, + align: TextAlign::Left, + }; + + // Load Source Sans Pro font data + const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); + let font_blob = Some(load_font(FONT_DATA)); + + // Convert text to paths and calculate actual bounds + let text_table = to_path(text, font_blob, typesetting, false); + let text_bounds = self.calculate_text_bounds(&text_table); + text_bounds.width() + } + + fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { + // Use the proper text-to-path system for accurate text rendering + const FONT_SIZE: f64 = 12.0; + + // Create typesetting configuration + let typesetting = TypesettingConfig { + font_size: FONT_SIZE, + line_height_ratio: 1.2, + character_spacing: 0.0, + max_width: None, + max_height: None, + tilt: 0.0, + align: TextAlign::Left, // We'll handle alignment manually via pivot + }; + + // Load Source Sans Pro font data + const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); + let font_blob = Some(load_font(FONT_DATA)); + + // Convert text to vector paths using the existing text system + let text_table = to_path(text, font_blob, typesetting, false); + // Calculate text bounds from the generated paths + let text_bounds = self.calculate_text_bounds(&text_table); + let text_width = text_bounds.width(); + let text_height = text_bounds.height(); + + // Calculate position based on pivot + let mut position = DVec2::ZERO; + match pivot[0] { + Pivot::Start => position.x = padding, + Pivot::Middle => position.x = -text_width / 2.0, + Pivot::End => position.x = -padding - text_width, + } + match pivot[1] { + Pivot::Start => position.y = padding, + Pivot::Middle => position.y -= text_height * 0.5, + Pivot::End => position.y = -padding - text_height, + } + + let text_transform = transform * DAffine2::from_translation(position); + let device_transform = self.get_transform(); + let combined_transform = kurbo::Affine::new(text_transform.to_cols_array()); + let vello_transform = device_transform * combined_transform; + + // Draw background if specified + if let Some(bg_color) = background_color { + let bg_rect = kurbo::Rect::new( + text_bounds.min_x() - padding, + text_bounds.min_y() - padding, + text_bounds.max_x() + padding, + text_bounds.max_y() + padding, + ); + self.scene.fill(peniko::Fill::NonZero, vello_transform, Self::parse_color(bg_color), None, &bg_rect); + } + + // Render the actual text paths + self.render_text_paths(&text_table, font_color, vello_transform); + } + + // Calculate bounds of text from vector table + fn calculate_text_bounds(&self, text_table: &Table) -> kurbo::Rect { + let mut min_x = f64::INFINITY; + let mut min_y = f64::INFINITY; + let mut max_x = f64::NEG_INFINITY; + let mut max_y = f64::NEG_INFINITY; + + for row in text_table.iter() { + // Use the existing segment_bezier_iter to get all bezier curves + for (_, bezier, _, _) in row.element.segment_bezier_iter() { + let transformed_bezier = bezier.apply_transformation(|point| row.transform.transform_point2(point)); + + // Add start and end points to bounds + let points = [transformed_bezier.start, transformed_bezier.end]; + for point in points { + min_x = min_x.min(point.x); + min_y = min_y.min(point.y); + max_x = max_x.max(point.x); + max_y = max_y.max(point.y); + } + + // Add handle points if they exist + match transformed_bezier.handles { + bezier_rs::BezierHandles::Quadratic { handle } => { + min_x = min_x.min(handle.x); + min_y = min_y.min(handle.y); + max_x = max_x.max(handle.x); + max_y = max_y.max(handle.y); + } + bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { + for handle in [handle_start, handle_end] { + min_x = min_x.min(handle.x); + min_y = min_y.min(handle.y); + max_x = max_x.max(handle.x); + max_y = max_y.max(handle.y); + } + } + _ => {} + } + } + } - fn get_width(&self, _text: &str) -> f64 { - // TODO: Implement proper text measurement in Vello - 0. + if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() { + kurbo::Rect::new(min_x, min_y, max_x, max_y) + } else { + // Fallback for empty text + kurbo::Rect::new(0.0, 0.0, 0.0, 12.0) + } } - fn text(&self, _text: &str, _font_color: &str, _background_color: Option<&str>, _transform: DAffine2, _padding: f64, _pivot: [Pivot; 2]) { - // TODO: Implement text rendering in Vello + // Render text paths to the vello scene using existing infrastructure + fn render_text_paths(&mut self, text_table: &Table, font_color: &str, base_transform: kurbo::Affine) { + let color = Self::parse_color(font_color); + + for row in text_table.iter() { + // Use the existing bezier_to_path infrastructure to convert Vector to BezPath + let mut path = BezPath::new(); + let mut last_point = None; + + for (_, bezier, start_id, end_id) in row.element.segment_bezier_iter() { + let move_to = last_point != Some(start_id); + last_point = Some(end_id); + + self.bezier_to_path(bezier, row.transform.clone(), move_to, &mut path); + } + + // Render the path + self.scene.fill(peniko::Fill::NonZero, base_transform, color, None, &path); + } } fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option) {