diff --git a/Cargo.lock b/Cargo.lock index 8aa1411..2329c28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,6 +685,27 @@ dependencies = [ "libc", ] +[[package]] +name = "cosmic-text" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71" +dependencies = [ + "fontdb", + "libm", + "log", + "rangemap", + "rustc-hash", + "rustybuzz 0.11.0", + "self_cell", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.2.11" @@ -1929,6 +1950,25 @@ dependencies = [ "rustversion", ] +[[package]] +name = "etagere" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "306960881d6c46bd0dd6b7f07442a441418c08d0d3e63d8d080b0f64c6343e4e" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +dependencies = [ + "num-traits", +] + [[package]] name = "exr" version = "1.71.0" @@ -2416,6 +2456,17 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "glyphon" +version = "0.3.0" +source = "git+https://github.com/grovesNL/glyphon.git?rev=941309aed230d7110bfec0d4af502ecb4648cf90#941309aed230d7110bfec0d4af502ecb4648cf90" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "wgpu", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -3246,6 +3297,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -4392,6 +4452,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +[[package]] +name = "rangemap" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977b1e897f9d764566891689e642653e5ed90c6895106acd005eb4c1d0203991" + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -4814,6 +4880,23 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "rustybuzz" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.20.0", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.16" @@ -4947,6 +5030,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" + [[package]] name = "semver" version = "0.9.0" @@ -5350,6 +5439,12 @@ dependencies = [ "usvg", ] +[[package]] +name = "svg_fmt" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" + [[package]] name = "svgtypes" version = "0.12.0" @@ -5360,6 +5455,16 @@ dependencies = [ "siphasher 0.3.11", ] +[[package]] +name = "swash" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7c73c813353c347272919aa1af2885068b05e625e5532b43049e4f641ae77f" +dependencies = [ + "yazi", + "zeno", +] + [[package]] name = "swc_atoms" version = "0.6.4" @@ -5812,6 +5917,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -6290,6 +6404,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.22" @@ -6311,6 +6431,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-vo" version = "0.1.0" @@ -6412,7 +6538,7 @@ dependencies = [ "fontdb", "kurbo", "log", - "rustybuzz", + "rustybuzz 0.10.0", "unicode-bidi", "unicode-script", "unicode-vo", @@ -6490,6 +6616,7 @@ dependencies = [ "dssim", "env_logger", "futures-intrusive", + "glyphon", "image", "log", "pollster", @@ -7291,6 +7418,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/vega-wgpu-renderer/Cargo.toml b/vega-wgpu-renderer/Cargo.toml index 8b391cc..716026b 100644 --- a/vega-wgpu-renderer/Cargo.toml +++ b/vega-wgpu-renderer/Cargo.toml @@ -21,6 +21,7 @@ thiserror = "1.0.52" csscolorparser = "0.6.2" image = "0.24.7" futures-intrusive = "^0.5" +glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev="941309aed230d7110bfec0d4af502ecb4648cf90" } [dev-dependencies] dssim = "3.2.4" diff --git a/vega-wgpu-renderer/src/renderers/canvas.rs b/vega-wgpu-renderer/src/renderers/canvas.rs index a9b5804..6869c6a 100644 --- a/vega-wgpu-renderer/src/renderers/canvas.rs +++ b/vega-wgpu-renderer/src/renderers/canvas.rs @@ -1,12 +1,14 @@ use crate::error::VegaWgpuError; -use crate::renderers::mark::MarkRenderer; +use crate::renderers::mark::GeomMarkRenderer; use crate::renderers::rect::RectShader; use crate::renderers::rule::RuleShader; use crate::renderers::symbol::SymbolShader; +use crate::renderers::text::TextMarkRenderer; use crate::scene::rect::{RectInstance, RectMark}; use crate::scene::rule::RuleMark; use crate::scene::scene_graph::{SceneGraph, SceneGroup, SceneMark}; use crate::scene::symbol::{SymbolInstance, SymbolMark}; +use crate::scene::text::TextMark; use image::imageops::crop_imm; use wgpu::{ Adapter, Buffer, BufferAddress, BufferDescriptor, BufferUsages, CommandBuffer, CommandEncoder, @@ -23,15 +25,20 @@ use winit::window::Window; #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct CanvasUniform { - size: [f32; 2], + pub size: [f32; 2], filler: [f32; 2], // Pad to 16 bytes } +pub enum MarkRenderer { + Geom(GeomMarkRenderer), + Text(TextMarkRenderer), +} + pub trait Canvas { fn add_mark_renderer(&mut self, mark_renderer: MarkRenderer); fn clear_mark_renderer(&mut self); - fn device(&self) -> &Device; + fn queue(&self) -> &Queue; fn uniform(&self) -> &CanvasUniform; fn set_uniform(&mut self, uniform: CanvasUniform); @@ -41,36 +48,47 @@ pub trait Canvas { fn sample_count(&self) -> u32; fn add_symbol_mark(&mut self, mark: &SymbolMark) { - self.add_mark_renderer(MarkRenderer::new( + self.add_mark_renderer(MarkRenderer::Geom(GeomMarkRenderer::new( &self.device(), self.uniform().clone(), self.texture_format(), self.sample_count(), Box::new(SymbolShader::new(mark.shape)), mark.instances.as_slice(), - )); + ))); } fn add_rect_mark(&mut self, mark: &RectMark) { - self.add_mark_renderer(MarkRenderer::new( + self.add_mark_renderer(MarkRenderer::Geom(GeomMarkRenderer::new( &self.device(), self.uniform().clone(), self.texture_format(), self.sample_count(), Box::new(RectShader::new()), mark.instances.as_slice(), - )); + ))); } fn add_rule_mark(&mut self, mark: &RuleMark) { - self.add_mark_renderer(MarkRenderer::new( + self.add_mark_renderer(MarkRenderer::Geom(GeomMarkRenderer::new( &self.device(), self.uniform().clone(), self.texture_format(), self.sample_count(), Box::new(RuleShader::new()), mark.instances.as_slice(), - )); + ))); + } + + fn add_text_mark(&mut self, mark: &TextMark) { + self.add_mark_renderer(MarkRenderer::Text(TextMarkRenderer::new( + &self.device(), + &self.queue(), + self.uniform().clone(), + self.texture_format(), + self.sample_count(), + mark.instances.clone(), + ))); } fn add_group_mark(&mut self, group: &SceneGroup) { @@ -85,6 +103,9 @@ pub trait Canvas { SceneMark::Rule(mark) => { self.add_rule_mark(mark); } + SceneMark::Text(mark) => { + self.add_text_mark(mark); + } SceneMark::Group(group) => { self.add_group_mark(group); } @@ -336,12 +357,29 @@ impl WindowCanvas { }; let mut commands = vec![background_command]; - for mark in &self.marks { - let command = if self.sample_count > 1 { - mark.render(&self.device, &self.multisampled_framebuffer, Some(&view)) - } else { - mark.render(&self.device, &view, None) + for mark in &mut self.marks { + let command = match mark { + MarkRenderer::Geom(mark) => { + if self.sample_count > 1 { + mark.render(&self.device, &self.multisampled_framebuffer, Some(&view)) + } else { + mark.render(&self.device, &view, None) + } + } + MarkRenderer::Text(mark) => { + if self.sample_count > 1 { + mark.render( + &self.device, + &self.queue, + &self.multisampled_framebuffer, + Some(&view), + ) + } else { + mark.render(&self.device, &self.queue, &view, None) + } + } }; + commands.push(command); } @@ -365,6 +403,10 @@ impl Canvas for WindowCanvas { &self.device } + fn queue(&self) -> &Queue { + &self.queue + } + fn uniform(&self) -> &CanvasUniform { &self.uniform } @@ -492,16 +534,33 @@ impl PngCanvas { let mut commands = vec![background_command]; - for mark in &self.marks { - let command = if self.sample_count > 1 { - mark.render( - &self.device, - &self.multisampled_framebuffer, - Some(&self.texture_view), - ) - } else { - mark.render(&self.device, &self.texture_view, None) + for mark in &mut self.marks { + let command = match mark { + MarkRenderer::Geom(mark) => { + if self.sample_count > 1 { + mark.render( + &self.device, + &self.multisampled_framebuffer, + Some(&self.texture_view), + ) + } else { + mark.render(&self.device, &self.texture_view, None) + } + } + MarkRenderer::Text(mark) => { + if self.sample_count > 1 { + mark.render( + &self.device, + &self.queue, + &self.multisampled_framebuffer, + Some(&self.texture_view), + ) + } else { + mark.render(&self.device, &self.queue, &self.texture_view, None) + } + } }; + commands.push(command); } @@ -578,6 +637,10 @@ impl Canvas for PngCanvas { &self.device } + fn queue(&self) -> &Queue { + &self.queue + } + fn uniform(&self) -> &CanvasUniform { &self.uniform } diff --git a/vega-wgpu-renderer/src/renderers/mark.rs b/vega-wgpu-renderer/src/renderers/mark.rs index 1cd5aa2..4a5b51a 100644 --- a/vega-wgpu-renderer/src/renderers/mark.rs +++ b/vega-wgpu-renderer/src/renderers/mark.rs @@ -14,7 +14,7 @@ pub trait MarkShader { fn instance_desc(&self) -> wgpu::VertexBufferLayout<'static>; } -pub struct MarkRenderer { +pub struct GeomMarkRenderer { render_pipeline: wgpu::RenderPipeline, vertex_buffer: wgpu::Buffer, num_vertices: u32, @@ -27,7 +27,7 @@ pub struct MarkRenderer { uniform_bind_group: wgpu::BindGroup, } -impl MarkRenderer { +impl GeomMarkRenderer { pub fn new( device: &Device, uniform: CanvasUniform, diff --git a/vega-wgpu-renderer/src/renderers/mod.rs b/vega-wgpu-renderer/src/renderers/mod.rs index 2f681f2..649cdfe 100644 --- a/vega-wgpu-renderer/src/renderers/mod.rs +++ b/vega-wgpu-renderer/src/renderers/mod.rs @@ -3,4 +3,5 @@ pub mod mark; pub mod rect; pub mod rule; pub mod symbol; +mod text; pub mod vertex; diff --git a/vega-wgpu-renderer/src/renderers/text.rs b/vega-wgpu-renderer/src/renderers/text.rs new file mode 100644 index 0000000..2bf07fe --- /dev/null +++ b/vega-wgpu-renderer/src/renderers/text.rs @@ -0,0 +1,180 @@ +use crate::renderers::canvas::CanvasUniform; +use crate::renderers::mark::MarkShader; +use crate::scene::text::TextInstance; +use crate::specs::text::{TextAlignSpec, TextBaselineSpec}; +use glyphon::cosmic_text::Align; +use glyphon::{ + Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, + TextAtlas, TextBounds, TextRenderer, +}; +use wgpu::{ + CommandBuffer, CommandEncoderDescriptor, Device, MultisampleState, Operations, Queue, + RenderPassColorAttachment, RenderPassDescriptor, TextureFormat, TextureView, +}; + +pub struct TextMarkRenderer { + pub font_system: FontSystem, + pub cache: SwashCache, + pub atlas: TextAtlas, + pub text_renderer: TextRenderer, + pub instances: Vec, + pub uniform: CanvasUniform, +} + +impl TextMarkRenderer { + pub fn new( + device: &Device, + queue: &Queue, + uniform: CanvasUniform, + texture_format: TextureFormat, + sample_count: u32, + instances: Vec, + ) -> Self { + let mut font_system = FontSystem::new(); + let mut cache = SwashCache::new(); + let mut atlas = TextAtlas::new(device, queue, texture_format); + let mut text_renderer = TextRenderer::new( + &mut atlas, + &device, + MultisampleState { + count: sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + None, + ); + + Self { + font_system, + cache, + atlas, + text_renderer, + uniform, + instances, + } + } + + pub fn render( + &mut self, + device: &Device, + queue: &Queue, + texture_view: &TextureView, + resolve_target: Option<&TextureView>, + ) -> CommandBuffer { + // Collect buffer into a vector first so that they live as long as the text areas + // that reference them below + let buffers = self + .instances + .iter() + .map(|instance| { + let mut buffer = Buffer::new( + &mut self.font_system, + Metrics::new(instance.font_size, instance.font_size * 1.0), + ); + buffer.set_text( + &mut self.font_system, + &instance.text, + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + buffer.set_size( + &mut self.font_system, + self.uniform.size[0], + self.uniform.size[1], + ); + buffer.shape_until_scroll(&mut self.font_system); + + buffer + }) + .collect::>(); + + let areas = buffers + .iter() + .zip(&self.instances) + .map(|(buffer, instance)| { + let (width, height) = measure(&buffer); + + let left = match instance.align { + TextAlignSpec::Left => instance.position[0], + TextAlignSpec::Center => instance.position[0] - width / 2.0, + TextAlignSpec::Right => instance.position[0] - width, + }; + + let top = match instance.baseline { + TextBaselineSpec::Alphabetic => instance.position[1] - height, + TextBaselineSpec::Top => instance.position[1], + TextBaselineSpec::Middle => instance.position[1] - height * 0.56, + TextBaselineSpec::Bottom => instance.position[1] - height, + TextBaselineSpec::LineTop => todo!(), + TextBaselineSpec::LineBottom => todo!(), + }; + + TextArea { + buffer: &buffer, + left, + top, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: self.uniform.size[0] as i32, + bottom: self.uniform.size[1] as i32, + }, + default_color: Color::rgb( + (instance.color[0] * 255.0) as u8, + (instance.color[1] * 255.0) as u8, + (instance.color[2] * 255.0) as u8, + ), + } + }) + .collect::>(); + + self.text_renderer + .prepare( + &device, + &queue, + &mut self.font_system, + &mut self.atlas, + Resolution { + width: self.uniform.size[0] as u32, + height: self.uniform.size[1] as u32, + }, + areas, + &mut self.cache, + ) + .unwrap(); + + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("Text render"), + }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &texture_view, + resolve_target, + ops: Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + self.text_renderer.render(&self.atlas, &mut pass).unwrap(); + } + + encoder.finish() + } +} + +pub fn measure(buffer: &Buffer) -> (f32, f32) { + let (width, total_lines) = buffer + .layout_runs() + .fold((0.0, 0usize), |(width, total_lines), run| { + (run.line_w.max(width), total_lines + 1) + }); + (width, (total_lines as f32 * buffer.metrics().line_height)) +} diff --git a/vega-wgpu-renderer/src/scene/mod.rs b/vega-wgpu-renderer/src/scene/mod.rs index a0cf293..e031364 100644 --- a/vega-wgpu-renderer/src/scene/mod.rs +++ b/vega-wgpu-renderer/src/scene/mod.rs @@ -2,3 +2,4 @@ pub mod rect; pub mod rule; pub mod scene_graph; pub mod symbol; +pub mod text; diff --git a/vega-wgpu-renderer/src/scene/scene_graph.rs b/vega-wgpu-renderer/src/scene/scene_graph.rs index ff8c443..b61b929 100644 --- a/vega-wgpu-renderer/src/scene/scene_graph.rs +++ b/vega-wgpu-renderer/src/scene/scene_graph.rs @@ -2,6 +2,7 @@ use crate::error::VegaWgpuError; use crate::scene::rect::RectMark; use crate::scene::rule::RuleMark; use crate::scene::symbol::SymbolMark; +use crate::scene::text::TextMark; use crate::specs::group::GroupItemSpec; use crate::specs::mark::{MarkContainerSpec, MarkSpec}; use crate::specs::rect::RectItemSpec; @@ -20,6 +21,8 @@ pub trait SceneVisitor { fn visit_rect_mark(&mut self, mark: &RectMark, bounds: GroupBounds) -> Result<(), Self::Error>; fn visit_rule_mark(&mut self, mark: &RuleMark, bounds: GroupBounds) -> Result<(), Self::Error>; + + fn visit_text_mark(&mut self, mark: &TextMark, bounds: GroupBounds) -> Result<(), Self::Error>; } #[derive(Debug, Clone)] @@ -74,6 +77,7 @@ impl SceneGroup { SceneMark::Symbol(mark) => visitor.visit_symbol_mark(mark, self.bounds)?, SceneMark::Rect(mark) => visitor.visit_rect_mark(mark, self.bounds)?, SceneMark::Rule(mark) => visitor.visit_rule_mark(mark, self.bounds)?, + SceneMark::Text(mark) => visitor.visit_text_mark(mark, self.bounds)?, SceneMark::Group(group) => visitor.visit_group(group, self.bounds)?, } } @@ -104,7 +108,13 @@ impl SceneGroup { MarkSpec::Symbol(mark) => { vec![SceneMark::Symbol(SymbolMark::from_spec(mark, new_origin)?)] } - _ => unimplemented!(), + MarkSpec::Text(mark) => { + vec![SceneMark::Text(TextMark::from_spec(mark, new_origin)?)] + } + _ => { + println!("Mark type not yet supported: {:?}", item); + continue; + } }; group_marks.extend(item_marks); } @@ -127,6 +137,7 @@ pub enum SceneMark { Symbol(SymbolMark), Rect(RectMark), Rule(RuleMark), + Text(TextMark), Group(SceneGroup), } diff --git a/vega-wgpu-renderer/src/scene/text.rs b/vega-wgpu-renderer/src/scene/text.rs new file mode 100644 index 0000000..eb3856f --- /dev/null +++ b/vega-wgpu-renderer/src/scene/text.rs @@ -0,0 +1,83 @@ +use crate::error::VegaWgpuError; +use crate::specs::mark::MarkContainerSpec; +use crate::specs::symbol::{SymbolItemSpec, SymbolShape}; +use crate::specs::text::{ + FontStyleSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec, TextItemSpec, +}; + +#[derive(Debug, Clone)] +pub struct TextMark { + pub instances: Vec, + pub clip: bool, +} + +impl TextMark { + pub fn from_spec( + spec: &MarkContainerSpec, + origin: [f32; 2], + ) -> Result { + let instances = TextInstance::from_specs(spec.items.as_slice(), origin)?; + Ok(Self { + instances, + clip: spec.clip, + }) + } +} + +#[derive(Clone, Debug)] +pub struct TextInstance { + pub text: String, + pub position: [f32; 2], + pub color: [f32; 3], + pub opacity: f32, + pub align: TextAlignSpec, + pub angle: f32, + pub baseline: TextBaselineSpec, + pub dx: f32, + pub dy: f32, + pub font: String, + pub font_size: f32, + pub font_weight: FontWeightSpec, + pub font_style: FontStyleSpec, + pub limit: f32, +} + +impl TextInstance { + pub fn from_spec(item_spec: &TextItemSpec, origin: [f32; 2]) -> Result { + let color = if let Some(fill) = &item_spec.fill { + let c = csscolorparser::parse(fill)?; + [c.r as f32, c.g as f32, c.b as f32] + } else { + [0.0, 0.0, 0.0] + }; + Ok(Self { + text: item_spec.text.clone(), + position: [item_spec.x + origin[0], item_spec.y + origin[1]], + color, + align: item_spec.align.unwrap_or_default(), + angle: item_spec.angle.unwrap_or(0.0), + baseline: item_spec.baseline.unwrap_or_default(), + dx: item_spec.dx.unwrap_or(0.0), + dy: item_spec.dy.unwrap_or(0.0), + opacity: item_spec.fill_opacity.unwrap_or(1.0), + font: item_spec + .font + .clone() + .unwrap_or_else(|| "Liberation Sans".to_string()), + font_size: item_spec.fill_opacity.unwrap_or(12.0), + font_weight: item_spec.font_weight.unwrap_or_default(), + font_style: item_spec.font_style.unwrap_or_default(), + limit: item_spec.limit.unwrap_or(0.0), + }) + } + + pub fn from_specs( + item_specs: &[TextItemSpec], + origin: [f32; 2], + ) -> Result, VegaWgpuError> { + item_specs + .iter() + .map(|item| Self::from_spec(item, origin)) + .collect() + } +} diff --git a/vega-wgpu-renderer/src/specs/mark.rs b/vega-wgpu-renderer/src/specs/mark.rs index 460e535..ffa3857 100644 --- a/vega-wgpu-renderer/src/specs/mark.rs +++ b/vega-wgpu-renderer/src/specs/mark.rs @@ -2,6 +2,7 @@ use crate::specs::group::GroupItemSpec; use crate::specs::rect::RectItemSpec; use crate::specs::rule::RuleItemSpec; use crate::specs::symbol::SymbolItemSpec; +use crate::specs::text::TextItemSpec; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -21,7 +22,7 @@ pub enum MarkSpec { Rule(MarkContainerSpec), Shape, Symbol(MarkContainerSpec), - Text, + Text(MarkContainerSpec), Trail, } diff --git a/vega-wgpu-renderer/src/specs/mod.rs b/vega-wgpu-renderer/src/specs/mod.rs index df266d4..4662b06 100644 --- a/vega-wgpu-renderer/src/specs/mod.rs +++ b/vega-wgpu-renderer/src/specs/mod.rs @@ -4,6 +4,7 @@ pub mod mark; pub mod rect; pub mod rule; pub mod symbol; +pub mod text; use crate::specs::group::GroupItemSpec; use crate::specs::mark::MarkContainerSpec; diff --git a/vega-wgpu-renderer/src/specs/text.rs b/vega-wgpu-renderer/src/specs/text.rs new file mode 100644 index 0000000..9b142b3 --- /dev/null +++ b/vega-wgpu-renderer/src/specs/text.rs @@ -0,0 +1,77 @@ +use crate::specs::mark::MarkItemSpec; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TextItemSpec { + // Required + pub x: f32, + pub y: f32, + pub text: String, + + // Optional + pub align: Option, + pub angle: Option, + pub baseline: Option, + pub dx: Option, + pub dy: Option, + pub fill: Option, + pub fill_opacity: Option, + pub font: Option, + pub font_size: Option, + pub font_weight: Option, + pub font_style: Option, + pub limit: Option, +} + +impl MarkItemSpec for TextItemSpec {} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TextAlignSpec { + #[default] + Left, + Center, + Right, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum TextBaselineSpec { + Alphabetic, + Top, + Middle, + #[default] + Bottom, + LineTop, + LineBottom, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum FontWeightSpec { + Name(FontWeightNameSpec), + Number(f32), +} + +impl Default for FontWeightSpec { + fn default() -> Self { + Self::Name(FontWeightNameSpec::Normal) + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FontWeightNameSpec { + #[default] + Normal, + Bold, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FontStyleSpec { + #[default] + Normal, + Italic, +}