use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use encase::{ShaderType, UniformBuffer};
use glam::{Mat4, Vec2, Vec4};
use serde::{Deserialize, Serialize};
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
use fontdue::{Font, FontSettings};
use crate::render_traits::{AspectRatioMode, AspectRatioAlignmentMode, DrawToRasterGpu, DrawToRasterCpu, DrawToSvg, MarginParams, PickableLayer, PreparedLayer, UnitsMode, ViewParams};
use crate::render_types::{CpuContext, CpuRenderPass, PrepareResult, RenderResult};
use crate::render_types::GpuContext;
use crate::wgpu;
use crate::wgpu::util::DeviceExt; use crate::cache::{use_memo_internal_text_layer_data, CachedInternalTextLayerData};
use crate::two::shapes::{
TwoCircle, TwoElement, TwoGroup, TwoLine, TwoPath, TwoRectangle,
TwoColor, TwoText, TwoTextAlign, TwoTextBaseline
};
use crate::two::svg::{update_svg, SvgContext};
use crate::positioning::get_point_position;
use crate::log;
const FONT_BYTES: &[u8] = include_bytes!("../two/fonts/Inter-Bold.ttf").as_slice();
#[derive(Clone)]
struct FontAtlasCache {
font: Font,
atlas_texture: Option<wgpu::Texture>,
glyph_cache: HashMap<(char, u32), (fontdue::Metrics, Vec<u8>)>, }
thread_local! {
static FONT_ATLAS: RefCell<Option<FontAtlasCache>> = const { RefCell::new(None) };
}
fn get_or_init_font_atlas() -> FontAtlasCache {
FONT_ATLAS.with(|atlas| {
let mut atlas_ref = atlas.borrow_mut();
if let Some(ref cached_atlas) = *atlas_ref {
cached_atlas.clone()
} else {
let font = Font::from_bytes(FONT_BYTES, FontSettings::default()).expect("load font");
let cache = FontAtlasCache {
font,
atlas_texture: None,
glyph_cache: HashMap::new(),
};
*atlas_ref = Some(cache.clone());
cache
}
})
}
fn measure_text_width(font: &Font, text: &str, font_size: f32) -> f32 {
let mut layout: Layout = Layout::new(CoordinateSystem::PositiveYUp);
layout.reset(&LayoutSettings {
max_width: None,
max_height: None,
..LayoutSettings::default()
});
layout.append(&[font], &TextStyle::new(text, font_size, 0));
let glyphs = layout.glyphs();
if glyphs.is_empty() {
return 0.0;
}
let mut max_x = 0.0f32;
for glyph in glyphs {
let right_edge = glyph.x + glyph.width as f32;
max_x = max_x.max(right_edge);
}
max_x
}
fn calculate_text_position(font_size: f32, text_align: TextAlignMode, text_baseline: TextBaselineMode, text_width: f32) -> (f32, f32) {
let x = match text_align {
TextAlignMode::Start => 0.0,
TextAlignMode::Middle => 0.0 - text_width / 2.0,
TextAlignMode::End => 0.0 - text_width,
};
let y = match text_baseline {
TextBaselineMode::Top => font_size - font_size,
TextBaselineMode::Middle => font_size - font_size / 2.0,
TextBaselineMode::Alphabetic => font_size - font_size / 2.0, TextBaselineMode::Bottom => font_size,
};
(x, y)
}
fn parse_color(color: &TwoColor) -> [f32; 4] {
match color {
TwoColor::Rgb((r, g, b)) => {
let r = *r as f32 / 255.0;
let g = *g as f32 / 255.0;
let b = *b as f32 / 255.0;
[r, g, b, 1.0]
}
TwoColor::Rgba((r, g, b, a)) => {
let r = *r as f32 / 255.0;
let g = *g as f32 / 255.0;
let b = *b as f32 / 255.0;
let a = *a as f32 / 255.0;
[r, g, b, a]
}
}
}
const PADDING: usize = 1;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TextAlignMode {
Start,
Middle,
End,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TextBaselineMode {
Top,
Middle,
Bottom,
Alphabetic,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TextLayerParams {
pub layer_id: String,
pub bounds: Option<MarginParams>,
pub data_unit_mode_x: UnitsMode, pub data_unit_mode_y: UnitsMode, pub text_size: f32,
pub text_size_unit_mode: UnitsMode, pub text_align_mode: TextAlignMode,
pub text_baseline_mode: TextBaselineMode,
pub text_rotation: Option<f32>,
pub position_x: Arc<Vec<f32>>, pub position_y: Arc<Vec<f32>>,
pub text_vec: Arc<Vec<String>>,
}
pub type InternalTextLayerData = CachedInternalTextLayerData;
pub struct TextLayer {
view_params: ViewParams,
layer_params: TextLayerParams,
internal_data: Option<Arc<InternalTextLayerData>>,
}
impl TextLayer {
pub fn new(
view_params: ViewParams,
layer_params: TextLayerParams,
) -> Self {
if layer_params.text_size_unit_mode == UnitsMode::Data && (layer_params.data_unit_mode_x == UnitsMode::Pixels || layer_params.data_unit_mode_y == UnitsMode::Pixels) {
panic!("text_size_unit_mode cannot be 'data' when data_unit_mode is 'pixels'");
}
Self {
view_params,
layer_params,
internal_data: None,
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl PreparedLayer for TextLayer {
async fn prepare(&mut self, _gpu_context: Option<&GpuContext<'_>>) -> PrepareResult {
let n = self.layer_params.text_vec.len();
let font_size = self.layer_params.text_size;
let text_align_mode = self.layer_params.text_align_mode;
let text_baseline_mode = self.layer_params.text_baseline_mode;
let cache_keys: Vec<String> = vec![
self.layer_params.layer_id.clone(),
format!("{:?}", self.layer_params.text_vec), format!("{:?}", self.layer_params.position_x), format!("{:?}", self.layer_params.position_y), format!("{}", font_size),
format!("{:?}", text_align_mode),
format!("{:?}", text_baseline_mode),
];
let internal_data = use_memo_internal_text_layer_data(async || {
let font_atlas = get_or_init_font_atlas();
let mut layout = Layout::new(CoordinateSystem::PositiveYUp);
layout.reset(&LayoutSettings {
max_width: None,
max_height: None,
..LayoutSettings::default()
});
for text_str in self.layer_params.text_vec.iter() {
layout.append(
&[&font_atlas.font],
&TextStyle::new(text_str, font_size, 0),
);
}
let glyphs = layout.glyphs();
if glyphs.is_empty() {
return InternalTextLayerData {
atlas_data: Vec::new(),
all_instance_data: Vec::new(),
atlas_width: 0,
atlas_height: 0,
};
}
let mut atlas_width: usize = 0;
let mut atlas_height: usize = 0;
let mut rasters: Vec<(fontdue::Metrics, Vec<u8>)> = Vec::with_capacity(glyphs.len());
for g in glyphs {
let (metrics, bitmap) = font_atlas.font.rasterize_config(g.key);
atlas_width += 2 * PADDING + metrics.width.max(1);
atlas_height = atlas_height.max(2 * PADDING + metrics.height.max(1));
rasters.push((metrics, bitmap));
}
if atlas_width == 0 || atlas_height == 0 {
return InternalTextLayerData {
atlas_data: Vec::new(),
all_instance_data: Vec::new(),
atlas_width: 0,
atlas_height: 0,
};
}
let mut atlas: Vec<u8> = vec![0u8; atlas_width * atlas_height];
let mut x_cursor: usize = PADDING;
let mut all_instance_data: Vec<f32> = Vec::new();
let mut total_instances = 0u32;
for elem_i in 0..n {
let text_str = &self.layer_params.text_vec[elem_i];
let text_x_pos = self.layer_params.position_x[elem_i];
let text_y_pos = self.layer_params.position_y[elem_i];
let text_width = measure_text_width(
&font_atlas.font,
text_str,
font_size,
);
let (offset_x, offset_y) = calculate_text_position(
font_size,
text_align_mode,
text_baseline_mode,
text_width
);
let mut element_layout = Layout::new(CoordinateSystem::PositiveYUp);
element_layout.reset(&LayoutSettings {
max_width: None,
max_height: None,
..LayoutSettings::default()
});
element_layout.append(
&[&font_atlas.font],
&TextStyle::new(text_str, font_size, 0),
);
let element_glyphs = element_layout.glyphs();
let mut element_cursor = x_cursor;
for (i, g) in element_glyphs.iter().enumerate() {
let (m, bmp) = &rasters[total_instances as usize + i];
let gw = m.width.max(0);
let gh = m.height.max(0);
if gw > 0 && gh > 0 {
for row in 0..gh {
let src = &bmp[row * gw..row * gw + gw];
let dst_row = PADDING + row;
let dst_start = dst_row * atlas_width + element_cursor;
let dst_end = dst_start + gw;
let dst = &mut atlas[dst_start..dst_end];
dst.copy_from_slice(src);
}
}
let x_px = offset_x + g.x;
let y_px = offset_y + g.y;
let w_px = g.width as f32;
let h_px: f32 = g.height as f32;
let u0 = (element_cursor as f32) / (atlas_width as f32);
let v0 = (PADDING as f32) / (atlas_height as f32);
let u1 = ((element_cursor + gw) as f32) / (atlas_width as f32);
let v1 = ((PADDING + gh) as f32) / (atlas_height as f32);
if gw > 0 && gh > 0 {
all_instance_data.extend_from_slice(&[
text_x_pos, text_y_pos, x_px, y_px, w_px, h_px, u0, v0, u1, v1, ]);
}
element_cursor += gw + 2 * PADDING;
}
x_cursor = element_cursor;
total_instances += element_glyphs.len() as u32;
}
InternalTextLayerData {
atlas_data: atlas,
all_instance_data,
atlas_width,
atlas_height,
}
}, &cache_keys, self.view_params.cache_enabled).await;
self.internal_data = Some(internal_data);
return PrepareResult {
bailed_early: false,
}
}
}
#[derive(ShaderType, Debug)]
struct TextLayerUniforms {
layer_size: Vec2, camera_view: Mat4, data_unit_mode_x: u32, data_unit_mode_y: u32, text_size: f32,
text_size_unit_mode: u32, aspect_ratio_mode: u32, aspect_ratio_alignment_mode: u32, text_rotation: f32, color: Vec4,
}
pub async fn base_draw_text_layer(
gpu_context: &GpuContext<'_>, pass: &mut wgpu::RenderPass<'_>,
view_params: &ViewParams,
layer_params: &TextLayerParams,
internal_data: &InternalTextLayerData,
) {
let GpuContext { device, queue } = gpu_context;
let camera_view = view_params.camera_view.unwrap_or([
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]);
let bounds = if layer_params.bounds.is_none() {
&view_params.margins
} else {
&layer_params.bounds
};
let margin_top = if let Some(margin_params) = &bounds {
margin_params.margin_top.unwrap_or(0.0)
} else { 0.0 } as f64;
let margin_right = if let Some(margin_params) = &bounds {
margin_params.margin_right.unwrap_or(0.0)
} else { 0.0 } as f64;
let margin_bottom = if let Some(margin_params) = &bounds {
margin_params.margin_bottom.unwrap_or(0.0)
} else { 0.0 } as f64;
let margin_left = if let Some(margin_params) = &bounds {
margin_params.margin_left.unwrap_or(0.0)
} else { 0.0 } as f64;
let viewport_w = view_params.width as f32;
let viewport_h = view_params.height as f32;
let layer_w = viewport_w - (margin_left + margin_right) as f32;
let layer_h = viewport_h - (margin_top + margin_bottom) as f32;
let atlas = &internal_data.atlas_data;
let all_instance_data = &internal_data.all_instance_data;
let atlas_width = internal_data.atlas_width;
let atlas_height = internal_data.atlas_height;
const NUM_VALUES_PER_INSTANCE: usize = 10;
let instance_count: u32 = (all_instance_data.len() / NUM_VALUES_PER_INSTANCE) as u32;
let atlas_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Text Atlas"),
size: wgpu::Extent3d {
width: atlas_width as u32,
height: atlas_height as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
atlas_tex.as_image_copy(),
atlas,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(atlas_width as u32),
rows_per_image: Some(atlas_height as u32),
},
wgpu::Extent3d {
width: atlas_width as u32,
height: atlas_height as u32,
depth_or_array_layers: 1,
},
);
let atlas_view = atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Text Sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let instance_buffer = device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Text Instances"),
contents: bytemuck::cast_slice(all_instance_data),
usage: wgpu::BufferUsages::VERTEX,
});
let uniform_struct = TextLayerUniforms {
layer_size: Vec2::new(layer_w, layer_h), camera_view: Mat4::from_cols_array(&camera_view), data_unit_mode_x: match layer_params.data_unit_mode_x {
UnitsMode::Pixels => 0,
UnitsMode::Data => 1,
},
data_unit_mode_y: match layer_params.data_unit_mode_y {
UnitsMode::Pixels => 0,
UnitsMode::Data => 1,
},
text_size: layer_params.text_size,
text_size_unit_mode: match layer_params.text_size_unit_mode {
UnitsMode::Pixels => 0,
UnitsMode::Data => 1,
}, aspect_ratio_mode: match view_params.aspect_ratio_mode {
AspectRatioMode::Ignore => 0,
AspectRatioMode::Contain => 1,
AspectRatioMode::Cover => 2,
},
aspect_ratio_alignment_mode: match view_params.aspect_ratio_alignment_mode {
AspectRatioAlignmentMode::Center => 0,
AspectRatioAlignmentMode::Start => 1,
AspectRatioAlignmentMode::End => 2,
},
text_rotation: layer_params.text_rotation.unwrap_or(0.0),
color: Vec4::from([0.0, 0.0, 0.0, 1.0]), };
let mut buffer = UniformBuffer::new(Vec::<u8>::new());
buffer.write(&uniform_struct).unwrap();
let uniform_bytes = buffer.into_inner();
let uniform_buffer = device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Text Uniforms"),
contents: &uniform_bytes,
usage: wgpu::BufferUsages::UNIFORM,
});
let bind_group_layout = device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text BGL"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let bind_group = device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Text BG"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&atlas_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&atlas_sampler),
},
],
});
let shader = device
.create_shader_module(wgpu::include_wgsl!("shaders/text_layer.wgsl"));
let render_pipeline_layout = device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let vertex_buffers = [wgpu::VertexBufferLayout {
array_stride: (NUM_VALUES_PER_INSTANCE * std::mem::size_of::<f32>()) as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: (2 * std::mem::size_of::<f32>()) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: (6 * std::mem::size_of::<f32>()) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
],
}];
let render_pipeline = device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Text Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &vertex_buffers,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
cache: None,
multiview_mask: None,
});
pass.set_viewport(
margin_left as f32,
margin_top as f32,
viewport_w - (margin_left + margin_right) as f32,
viewport_h - (margin_top + margin_bottom) as f32,
0.0, 1.0, );
pass.set_scissor_rect(
margin_left as u32,
margin_top as u32,
(viewport_w - (margin_left + margin_right) as f32) as u32,
(viewport_h - (margin_top + margin_bottom) as f32) as u32,
);
pass.set_pipeline(&render_pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.set_vertex_buffer(0, instance_buffer.slice(..));
pass.draw(0..4, 0..instance_count);
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DrawToRasterGpu for TextLayer {
async fn draw(&self, gpu_context: &GpuContext<'_>, pass: &mut wgpu::RenderPass) {
let internal_data = self.internal_data.as_ref().expect("Internal data was not prepared. Call prepare() first.");
base_draw_text_layer(
gpu_context, pass,
&self.view_params,
&self.layer_params,
internal_data,
).await;
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DrawToRasterCpu for TextLayer {
async fn draw(&self, _cpu_context: &CpuContext<'_>, _pass: &mut CpuRenderPass) {}
}
pub fn base_draw_text_layer_svg(
view_params: &ViewParams,
layer_params: &TextLayerParams,
) -> Vec<TwoElement> {
let n = layer_params.text_vec.len();
let camera_view = view_params.camera_view.unwrap_or([
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]);
let bounds = if layer_params.bounds.is_none() {
&view_params.margins
} else {
&layer_params.bounds
};
let margin_top = if let Some(margin_params) = &bounds {
margin_params.margin_top.unwrap_or(0.0)
} else { 0.0 } as f64;
let margin_right = if let Some(margin_params) = &bounds {
margin_params.margin_right.unwrap_or(0.0)
} else { 0.0 } as f64;
let margin_bottom = if let Some(margin_params) = &bounds {
margin_params.margin_bottom.unwrap_or(0.0)
} else { 0.0 } as f64;
let margin_left = if let Some(margin_params) = &bounds {
margin_params.margin_left.unwrap_or(0.0)
} else { 0.0 } as f64;
let viewport_w = view_params.width as f32;
let viewport_h = view_params.height as f32;
let layer_w = viewport_w - (margin_left + margin_right) as f32;
let layer_h = viewport_h - (margin_top + margin_bottom) as f32;
let mut svg_elements: Vec<TwoElement> = Vec::with_capacity(n);
for i in 0..n {
let x = layer_params.position_x[i];
let y = layer_params.position_y[i];
let (px, py) = get_point_position(
x,
y,
layer_w,
layer_h,
&camera_view,
layer_params.data_unit_mode_x,
layer_params.data_unit_mode_y,
view_params.aspect_ratio_mode,
view_params.aspect_ratio_alignment_mode,
None,
);
svg_elements.push(TwoElement::Text(TwoText {
x: px as f64,
y: (layer_h - py) as f64,
width: 100.0, height: 100.0, text: layer_params.text_vec[i].clone(),
font: "Arial".to_string(), fontsize: layer_params.text_size as f64,
align: match layer_params.text_align_mode {
TextAlignMode::Start => TwoTextAlign::Start,
TextAlignMode::Middle => TwoTextAlign::Middle,
TextAlignMode::End => TwoTextAlign::End,
},
baseline: match layer_params.text_baseline_mode {
TextBaselineMode::Top => TwoTextBaseline::Top,
TextBaselineMode::Middle => TwoTextBaseline::Middle,
TextBaselineMode::Bottom => TwoTextBaseline::Bottom,
TextBaselineMode::Alphabetic => TwoTextBaseline::Alphabetic,
},
rotation: Some(layer_params.text_rotation.unwrap_or(0.0) as f64),
..Default::default()
}));
}
let layer_group_vec = vec![
TwoElement::Group(TwoGroup {
elements: svg_elements,
translate: Some((margin_left, margin_top)),
layer_id: Some(layer_params.layer_id.clone()),
clip_rect: Some((0.0, 0.0, layer_w as f64, layer_h as f64)),
..Default::default()
})
];
return layer_group_vec;
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DrawToSvg for TextLayer {
async fn draw(&self, ctx: &mut SvgContext) {
let svg_elements = base_draw_text_layer_svg(
&self.view_params,
&self.layer_params,
);
update_svg(ctx, &svg_elements);
}
}
inventory::submit! {
crate::registry::LayerRegistration {
layer_type_name: "TextLayer",
create_layer: |value, view_params| {
let params: TextLayerParams = serde_json::from_value(value).unwrap();
Box::new(TextLayer::new(view_params.clone(), params))
},
}
}
impl PickableLayer for TextLayer {}