Skip to content

Commit e003389

Browse files
authored
Desktop: Implement missing vello overlays (#3004)
* Implement fill overlay * Implement text rendering for overlays * Adjust y positioning
1 parent ef2fab3 commit e003389

File tree

2 files changed

+189
-23
lines changed

2 files changed

+189
-23
lines changed
Binary file not shown.

editor/src/messages/portfolio/document/overlays/utility_types_vello.rs

Lines changed: 189 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use core::f64::consts::{FRAC_PI_2, PI, TAU};
1010
use glam::{DAffine2, DVec2};
1111
use graphene_std::Color;
1212
use graphene_std::math::quad::Quad;
13+
use graphene_std::table::Table;
14+
use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path};
1315
use graphene_std::vector::click_target::ClickTargetType;
1416
use graphene_std::vector::{PointId, SegmentId, Vector};
1517
use std::collections::HashMap;
@@ -378,7 +380,8 @@ impl OverlayContext {
378380
}
379381

380382
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
381-
self.internal().text(text, font_color, background_color, transform, padding, pivot);
383+
let mut internal = self.internal();
384+
internal.text(text, font_color, background_color, transform, padding, pivot);
382385
}
383386

384387
pub fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option<String>) {
@@ -972,32 +975,195 @@ impl OverlayContextInternal {
972975
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
973976
/// Used by the fill tool to show the area to be filled.
974977
fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
975-
// TODO: Implement pattern fill in Vello
976-
// For now, just fill with a semi-transparent version of the color
978+
const PATTERN_WIDTH: u32 = 4;
979+
const PATTERN_HEIGHT: u32 = 4;
980+
981+
// Create a 4x4 pixel pattern with colored pixels at (0,0) and (2,2)
982+
// This matches the Canvas2D checkerboard pattern
983+
let mut data = vec![0u8; (PATTERN_WIDTH * PATTERN_HEIGHT * 4) as usize];
984+
let rgba = color.to_rgba8_srgb();
985+
986+
// ┌▄▄┬──┬──┬──┐
987+
// ├▀▀┼──┼──┼──┤
988+
// ├──┼──┼▄▄┼──┤
989+
// ├──┼──┼▀▀┼──┤
990+
// └──┴──┴──┴──┘
991+
// Set pixels at (0,0) and (2,2) to the specified color
992+
let pixels = [(0, 0), (2, 2)];
993+
for &(x, y) in &pixels {
994+
let index = ((y * PATTERN_WIDTH + x) * 4) as usize;
995+
data[index..index + 4].copy_from_slice(&rgba);
996+
}
997+
998+
let image = peniko::Image {
999+
data: data.into(),
1000+
format: peniko::ImageFormat::Rgba8,
1001+
width: PATTERN_WIDTH,
1002+
height: PATTERN_HEIGHT,
1003+
x_extend: peniko::Extend::Repeat,
1004+
y_extend: peniko::Extend::Repeat,
1005+
alpha: 1.0,
1006+
quality: peniko::ImageQuality::default(),
1007+
};
1008+
9771009
let path = self.push_path(subpaths, transform);
978-
let semi_transparent_color = color.with_alpha(0.5);
979-
980-
self.scene.fill(
981-
peniko::Fill::NonZero,
982-
self.get_transform(),
983-
peniko::Color::from_rgba8(
984-
(semi_transparent_color.r() * 255.) as u8,
985-
(semi_transparent_color.g() * 255.) as u8,
986-
(semi_transparent_color.b() * 255.) as u8,
987-
(semi_transparent_color.a() * 255.) as u8,
988-
),
989-
None,
990-
&path,
991-
);
992-
}
1010+
let brush = peniko::Brush::Image(image);
1011+
1012+
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
1013+
}
1014+
1015+
fn get_width(&self, text: &str) -> f64 {
1016+
// Use the actual text-to-path system to get precise text width
1017+
const FONT_SIZE: f64 = 12.0;
1018+
1019+
let typesetting = TypesettingConfig {
1020+
font_size: FONT_SIZE,
1021+
line_height_ratio: 1.2,
1022+
character_spacing: 0.0,
1023+
max_width: None,
1024+
max_height: None,
1025+
tilt: 0.0,
1026+
align: TextAlign::Left,
1027+
};
1028+
1029+
// Load Source Sans Pro font data
1030+
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
1031+
let font_blob = Some(load_font(FONT_DATA));
1032+
1033+
// Convert text to paths and calculate actual bounds
1034+
let text_table = to_path(text, font_blob, typesetting, false);
1035+
let text_bounds = self.calculate_text_bounds(&text_table);
1036+
text_bounds.width()
1037+
}
1038+
1039+
fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
1040+
// Use the proper text-to-path system for accurate text rendering
1041+
const FONT_SIZE: f64 = 12.0;
1042+
1043+
// Create typesetting configuration
1044+
let typesetting = TypesettingConfig {
1045+
font_size: FONT_SIZE,
1046+
line_height_ratio: 1.2,
1047+
character_spacing: 0.0,
1048+
max_width: None,
1049+
max_height: None,
1050+
tilt: 0.0,
1051+
align: TextAlign::Left, // We'll handle alignment manually via pivot
1052+
};
1053+
1054+
// Load Source Sans Pro font data
1055+
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
1056+
let font_blob = Some(load_font(FONT_DATA));
1057+
1058+
// Convert text to vector paths using the existing text system
1059+
let text_table = to_path(text, font_blob, typesetting, false);
1060+
// Calculate text bounds from the generated paths
1061+
let text_bounds = self.calculate_text_bounds(&text_table);
1062+
let text_width = text_bounds.width();
1063+
let text_height = text_bounds.height();
1064+
1065+
// Calculate position based on pivot
1066+
let mut position = DVec2::ZERO;
1067+
match pivot[0] {
1068+
Pivot::Start => position.x = padding,
1069+
Pivot::Middle => position.x = -text_width / 2.0,
1070+
Pivot::End => position.x = -padding - text_width,
1071+
}
1072+
match pivot[1] {
1073+
Pivot::Start => position.y = padding,
1074+
Pivot::Middle => position.y -= text_height * 0.5,
1075+
Pivot::End => position.y = -padding - text_height,
1076+
}
1077+
1078+
let text_transform = transform * DAffine2::from_translation(position);
1079+
let device_transform = self.get_transform();
1080+
let combined_transform = kurbo::Affine::new(text_transform.to_cols_array());
1081+
let vello_transform = device_transform * combined_transform;
1082+
1083+
// Draw background if specified
1084+
if let Some(bg_color) = background_color {
1085+
let bg_rect = kurbo::Rect::new(
1086+
text_bounds.min_x() - padding,
1087+
text_bounds.min_y() - padding,
1088+
text_bounds.max_x() + padding,
1089+
text_bounds.max_y() + padding,
1090+
);
1091+
self.scene.fill(peniko::Fill::NonZero, vello_transform, Self::parse_color(bg_color), None, &bg_rect);
1092+
}
1093+
1094+
// Render the actual text paths
1095+
self.render_text_paths(&text_table, font_color, vello_transform);
1096+
}
1097+
1098+
// Calculate bounds of text from vector table
1099+
fn calculate_text_bounds(&self, text_table: &Table<Vector>) -> kurbo::Rect {
1100+
let mut min_x = f64::INFINITY;
1101+
let mut min_y = f64::INFINITY;
1102+
let mut max_x = f64::NEG_INFINITY;
1103+
let mut max_y = f64::NEG_INFINITY;
1104+
1105+
for row in text_table.iter() {
1106+
// Use the existing segment_bezier_iter to get all bezier curves
1107+
for (_, bezier, _, _) in row.element.segment_bezier_iter() {
1108+
let transformed_bezier = bezier.apply_transformation(|point| row.transform.transform_point2(point));
1109+
1110+
// Add start and end points to bounds
1111+
let points = [transformed_bezier.start, transformed_bezier.end];
1112+
for point in points {
1113+
min_x = min_x.min(point.x);
1114+
min_y = min_y.min(point.y);
1115+
max_x = max_x.max(point.x);
1116+
max_y = max_y.max(point.y);
1117+
}
1118+
1119+
// Add handle points if they exist
1120+
match transformed_bezier.handles {
1121+
bezier_rs::BezierHandles::Quadratic { handle } => {
1122+
min_x = min_x.min(handle.x);
1123+
min_y = min_y.min(handle.y);
1124+
max_x = max_x.max(handle.x);
1125+
max_y = max_y.max(handle.y);
1126+
}
1127+
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
1128+
for handle in [handle_start, handle_end] {
1129+
min_x = min_x.min(handle.x);
1130+
min_y = min_y.min(handle.y);
1131+
max_x = max_x.max(handle.x);
1132+
max_y = max_y.max(handle.y);
1133+
}
1134+
}
1135+
_ => {}
1136+
}
1137+
}
1138+
}
9931139

994-
fn get_width(&self, _text: &str) -> f64 {
995-
// TODO: Implement proper text measurement in Vello
996-
0.
1140+
if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
1141+
kurbo::Rect::new(min_x, min_y, max_x, max_y)
1142+
} else {
1143+
// Fallback for empty text
1144+
kurbo::Rect::new(0.0, 0.0, 0.0, 12.0)
1145+
}
9971146
}
9981147

999-
fn text(&self, _text: &str, _font_color: &str, _background_color: Option<&str>, _transform: DAffine2, _padding: f64, _pivot: [Pivot; 2]) {
1000-
// TODO: Implement text rendering in Vello
1148+
// Render text paths to the vello scene using existing infrastructure
1149+
fn render_text_paths(&mut self, text_table: &Table<Vector>, font_color: &str, base_transform: kurbo::Affine) {
1150+
let color = Self::parse_color(font_color);
1151+
1152+
for row in text_table.iter() {
1153+
// Use the existing bezier_to_path infrastructure to convert Vector to BezPath
1154+
let mut path = BezPath::new();
1155+
let mut last_point = None;
1156+
1157+
for (_, bezier, start_id, end_id) in row.element.segment_bezier_iter() {
1158+
let move_to = last_point != Some(start_id);
1159+
last_point = Some(end_id);
1160+
1161+
self.bezier_to_path(bezier, row.transform.clone(), move_to, &mut path);
1162+
}
1163+
1164+
// Render the path
1165+
self.scene.fill(peniko::Fill::NonZero, base_transform, color, None, &path);
1166+
}
10011167
}
10021168

10031169
fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option<String>) {

0 commit comments

Comments
 (0)