1use std::cell::RefCell;
2use std::mem;
3use std::sync::Arc;
4use std::sync::mpsc::sync_channel;
5
6use anyhow::Result;
7use floem_renderer::gpu_resources::GpuResources;
8use floem_renderer::text::{Glyph, GlyphRunProps};
9use floem_renderer::{Img, Renderer, tiny_skia};
10use floem_vger_rs::{GlyphImage, Image, PaintIndex, PixelFormat, Vger};
11use peniko::kurbo::{Size, Stroke};
12use peniko::{Blob, ImageData, LinearGradientPosition};
13use peniko::{
14 BrushRef, Color, GradientKind,
15 kurbo::{Affine, Point, Rect, Shape},
16};
17use swash::FontRef;
18use swash::scale::{Render, ScaleContext, Source, StrikeWith};
19use swash::zeno::Format;
20use wgpu::{
21 Adapter, Device, DeviceType, Queue, StoreOp, Surface, SurfaceConfiguration, TextureFormat,
22};
23
24thread_local! {
25 static SCALE_CONTEXT: RefCell<ScaleContext> = RefCell::new(ScaleContext::new());
29}
30
31pub struct VgerRenderer {
32 device: Arc<Device>,
33 #[allow(unused)]
34 queue: Arc<Queue>,
35 surface: Surface<'static>,
36 vger: Vger,
37 alt_vger: Option<Vger>,
38 config: SurfaceConfiguration,
39 scale: f64,
40 transform: Affine,
41 clip: Option<Rect>,
42 capture: bool,
43 font_embolden: f32,
44 adapter: Adapter,
45}
46
47impl VgerRenderer {
48 pub fn new(
49 gpu_resources: GpuResources,
50 surface: wgpu::Surface<'static>,
51 width: u32,
52 height: u32,
53 scale: f64,
54 font_embolden: f32,
55 ) -> Result<Self> {
56 let GpuResources {
57 adapter,
58 device,
59 queue,
60 ..
61 } = gpu_resources;
62
63 if adapter.get_info().device_type == DeviceType::Cpu {
64 return Err(anyhow::anyhow!("only cpu adapter found"));
65 }
66
67 let mut required_downlevel_flags = wgpu::DownlevelFlags::empty();
68 required_downlevel_flags.set(wgpu::DownlevelFlags::VERTEX_STORAGE, true);
69
70 if !adapter
71 .get_downlevel_capabilities()
72 .flags
73 .contains(required_downlevel_flags)
74 {
75 return Err(anyhow::anyhow!(
76 "adapter doesn't support required downlevel flags"
77 ));
78 }
79
80 let device = Arc::new(device);
81 let queue = Arc::new(queue);
82
83 let surface_caps = surface.get_capabilities(&adapter);
84 let texture_format = surface_caps
85 .formats
86 .into_iter()
87 .find(|it| matches!(it, TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm))
88 .ok_or_else(|| anyhow::anyhow!("surface should support Rgba8Unorm or Bgra8Unorm"))?;
89
90 let latency = match adapter.get_info().backend {
91 wgpu::Backend::Vulkan => 2,
92 _ => 1,
93 };
94
95 let config = wgpu::SurfaceConfiguration {
96 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
97 format: texture_format,
98 width,
99 height,
100 present_mode: wgpu::PresentMode::AutoVsync,
101 alpha_mode: wgpu::CompositeAlphaMode::Auto,
102 view_formats: vec![],
103 desired_maximum_frame_latency: latency,
104 };
105 surface.configure(&device, &config);
106
107 let vger = floem_vger_rs::Vger::new(device.clone(), queue.clone(), texture_format);
108
109 Ok(Self {
110 device,
111 queue,
112 surface,
113 vger,
114 alt_vger: None,
115 scale,
116 config,
117 transform: Affine::IDENTITY,
118 clip: None,
119 capture: false,
120 font_embolden,
121 adapter,
122 })
123 }
124
125 pub fn resize(&mut self, width: u32, height: u32, scale: f64) {
126 if width != self.config.width || height != self.config.height {
127 self.config.width = width;
128 self.config.height = height;
129 self.surface.configure(&self.device, &self.config);
130 }
131 self.scale = scale;
132 }
133
134 pub fn set_scale(&mut self, scale: f64) {
135 self.scale = scale;
136 }
137
138 pub fn size(&self) -> Size {
139 Size::new(self.config.width as f64, self.config.height as f64)
140 }
141}
142
143impl VgerRenderer {
144 fn device_transform(&self) -> Affine {
145 self.transform
146 }
147
148 fn scale_components(&self) -> (f64, f64, f64) {
149 let coeffs = self.device_transform().as_coeffs();
150 let scale_x = coeffs[0].hypot(coeffs[1]);
151 let scale_y = coeffs[2].hypot(coeffs[3]);
152 let uniform = (scale_x + scale_y) * 0.5;
153 (scale_x, scale_y, uniform)
154 }
155
156 fn brush_to_paint<'b>(&mut self, brush: impl Into<BrushRef<'b>>) -> Option<PaintIndex> {
157 let paint = match brush.into() {
158 BrushRef::Solid(color) => self.vger.color_paint(vger_color(color)),
159 BrushRef::Gradient(g) => match g.kind {
160 GradientKind::Linear(LinearGradientPosition { start, end }) => {
161 let mut stops = g.stops.iter();
162 let first_stop = stops.next()?;
163 let second_stop = stops.next()?;
164 let inner_color = vger_color(first_stop.color.to_alpha_color());
165 let outer_color = vger_color(second_stop.color.to_alpha_color());
166 let start = floem_vger_rs::defs::LocalPoint::new(
167 start.x as f32 * first_stop.offset,
168 start.y as f32 * first_stop.offset,
169 );
170 let end = floem_vger_rs::defs::LocalPoint::new(
171 end.x as f32 * second_stop.offset,
172 end.y as f32 * second_stop.offset,
173 );
174 self.vger
175 .linear_gradient(start, end, inner_color, outer_color, 0.0)
176 }
177 GradientKind::Radial { .. } => return None,
178 GradientKind::Sweep { .. } => return None,
179 },
180 BrushRef::Image(_) => return None,
181 };
182 Some(paint)
183 }
184
185 fn vger_point(&self, point: Point) -> floem_vger_rs::defs::LocalPoint {
186 let coeffs = self.device_transform().as_coeffs();
187
188 let transformed_x = coeffs[0] * point.x + coeffs[2] * point.y + coeffs[4];
189 let transformed_y = coeffs[1] * point.x + coeffs[3] * point.y + coeffs[5];
190
191 floem_vger_rs::defs::LocalPoint::new(transformed_x as f32, transformed_y as f32)
192 }
193
194 fn vger_rect(&self, rect: Rect) -> floem_vger_rs::defs::LocalRect {
195 let origin = rect.origin();
196 let origin = self.vger_point(origin);
197
198 let end = Point::new(rect.x1, rect.y1);
199 let end = self.vger_point(end);
200
201 let size = (end - origin).to_size();
202 floem_vger_rs::defs::LocalRect::new(origin, size)
203 }
204
205 fn render_image(&mut self) -> Option<peniko::ImageBrush> {
206 let width_align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1;
207 let width = (self.config.width + width_align) & !width_align;
208 let height = self.config.height;
209 let texture_desc = wgpu::TextureDescriptor {
210 size: wgpu::Extent3d {
211 width: self.config.width,
212 height,
213 depth_or_array_layers: 1,
214 },
215 mip_level_count: 1,
216 sample_count: 1,
217 dimension: wgpu::TextureDimension::D2,
218 format: wgpu::TextureFormat::Rgba8Unorm,
219 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
220 label: Some("render_texture"),
221 view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
222 };
223 let texture = self.device.create_texture(&texture_desc);
224 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
225 let desc = wgpu::RenderPassDescriptor {
226 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
227 view: &view,
228 resolve_target: None,
229 ops: wgpu::Operations {
230 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
231 store: StoreOp::Store,
232 },
233 depth_slice: None,
234 })],
235 ..Default::default()
236 };
237
238 self.vger.encode(&desc);
239
240 let bytes_per_pixel = 4;
241 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
242 label: None,
243 size: (width as u64 * height as u64 * bytes_per_pixel),
244 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
245 mapped_at_creation: false,
246 });
247 let bytes_per_row = width * bytes_per_pixel as u32;
248 assert!(bytes_per_row.is_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT));
249
250 let mut encoder = self
251 .device
252 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
253 encoder.copy_texture_to_buffer(
254 texture.as_image_copy(),
255 wgpu::TexelCopyBufferInfo {
256 buffer: &buffer,
257 layout: wgpu::TexelCopyBufferLayout {
258 offset: 0,
259 bytes_per_row: Some(bytes_per_row),
260 rows_per_image: None,
261 },
262 },
263 texture_desc.size,
264 );
265 let command_buffer = encoder.finish();
266 self.queue.submit(Some(command_buffer));
267 self.device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
268
269 let slice = buffer.slice(..);
270 let (tx, rx) = sync_channel(1);
271 slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
272
273 loop {
274 if let Ok(r) = rx.try_recv() {
275 break r.ok()?;
276 }
277 if let wgpu::PollStatus::WaitSucceeded =
278 self.device.poll(wgpu::PollType::wait_indefinitely()).ok()?
279 {
280 rx.recv().ok()?.ok()?;
281 break;
282 }
283 }
284
285 let mut cropped_buffer = Vec::new();
286 let buffer: Vec<u8> = slice.get_mapped_range().to_owned();
287
288 let mut cursor = 0;
289 let row_size = self.config.width as usize * bytes_per_pixel as usize;
290 for _ in 0..height {
291 cropped_buffer.extend_from_slice(&buffer[cursor..(cursor + row_size)]);
292 cursor += bytes_per_row as usize;
293 }
294
295 Some(peniko::ImageBrush::new(ImageData {
296 data: Blob::new(Arc::new(cropped_buffer)),
297 format: peniko::ImageFormat::Rgba8,
298 alpha_type: peniko::ImageAlphaType::AlphaPremultiplied,
299 width: self.config.width,
300 height,
301 }))
302 }
304}
305
306impl Renderer for VgerRenderer {
307 fn begin(&mut self, capture: bool) {
308 if self.capture != capture {
310 self.capture = capture;
311 if self.alt_vger.is_none() {
312 self.alt_vger = Some(floem_vger_rs::Vger::new(
313 self.device.clone(),
314 self.queue.clone(),
315 TextureFormat::Rgba8Unorm,
316 ));
317 }
318 mem::swap(&mut self.vger, self.alt_vger.as_mut().unwrap())
319 }
320
321 self.transform = Affine::IDENTITY;
322 self.vger
323 .begin(self.config.width as f32, self.config.height as f32, 1.0);
324 }
325
326 fn stroke<'b, 's>(
327 &mut self,
328 shape: &impl Shape,
329 brush: impl Into<BrushRef<'b>>,
330 stroke: &'s Stroke,
331 ) {
332 let (_, _, scale) = self.scale_components();
333 let paint = match self.brush_to_paint(brush) {
334 Some(paint) => paint,
335 None => return,
336 };
337 let width = (stroke.width * scale).round() as f32;
338 if let Some(rect) = shape.as_rect() {
339 let min = rect.origin();
340 let max = min + rect.size().to_vec2();
341 self.vger.stroke_rect(
342 self.vger_point(min),
343 self.vger_point(max),
344 0.0,
345 width,
346 paint,
347 );
348 return;
349 } else if let Some(rect) = shape.as_rounded_rect() {
350 if rect.radii().top_left == rect.radii().top_right
351 && rect.radii().top_left == rect.radii().bottom_left
352 && rect.radii().top_left == rect.radii().bottom_right
353 {
354 let min = rect.origin();
355 let max = min + rect.rect().size().to_vec2();
356 let radius = (rect.radii().top_left * scale) as f32;
357 self.vger.stroke_rect(
358 self.vger_point(min),
359 self.vger_point(max),
360 radius,
361 width,
362 paint,
363 );
364 return;
365 }
366 } else if let Some(line) = shape.as_line() {
367 self.vger.stroke_segment(
368 self.vger_point(line.p0),
369 self.vger_point(line.p1),
370 width,
371 paint,
372 );
373 return;
374 } else if let Some(circle) = shape.as_circle() {
375 self.vger.stroke_arc(
376 self.vger_point(circle.center),
377 (circle.radius * scale) as f32,
378 width,
379 0.0,
380 std::f32::consts::PI,
381 paint,
382 );
383 return;
384 }
385
386 for segment in shape.path_segments(0.001) {
387 match segment {
388 peniko::kurbo::PathSeg::Line(ln) => self.vger.stroke_segment(
389 self.vger_point(ln.p0),
390 self.vger_point(ln.p1),
391 width,
392 paint,
393 ),
394 peniko::kurbo::PathSeg::Quad(bez) => {
395 self.vger.stroke_bezier(
396 self.vger_point(bez.p0),
397 self.vger_point(bez.p1),
398 self.vger_point(bez.p2),
399 width,
400 paint,
401 );
402 }
403
404 peniko::kurbo::PathSeg::Cubic(cubic) => {
405 let q1 = ((cubic.p1.to_vec2() + cubic.p2.to_vec2()) * 3.0
407 - cubic.p0.to_vec2()
408 - cubic.p3.to_vec2())
409 / 4.0;
410
411 self.vger.stroke_bezier(
412 self.vger_point(cubic.p0),
413 self.vger_point(q1.to_point()),
414 self.vger_point(cubic.p3),
415 width,
416 paint,
417 );
418 }
419 }
420 }
421 }
422
423 fn fill<'b>(&mut self, path: &impl Shape, brush: impl Into<BrushRef<'b>>, blur_radius: f64) {
424 let (_, _, scale) = self.scale_components();
425 let paint = match self.brush_to_paint(brush) {
426 Some(paint) => paint,
427 None => return,
428 };
429 if let Some(rect) = path.as_rect() {
430 self.vger.fill_rect(
431 self.vger_rect(rect),
432 0.0,
433 paint,
434 (blur_radius * scale) as f32,
435 );
436 return;
437 } else if let Some(rect) = path.as_rounded_rect() {
438 if rect.radii().top_left == rect.radii().top_right
439 && rect.radii().top_left == rect.radii().bottom_left
440 && rect.radii().top_left == rect.radii().bottom_right
441 {
442 self.vger.fill_rect(
444 self.vger_rect(rect.rect()),
445 (rect.radii().top_left * scale) as f32,
446 paint,
447 (blur_radius * scale) as f32,
448 );
449 return;
450 }
451 } else if let Some(circle) = path.as_circle() {
452 self.vger.fill_circle(
453 self.vger_point(circle.center),
454 (circle.radius * scale) as f32,
455 paint,
456 );
457 return;
458 }
459
460 let mut first = true;
461 for segment in path.path_segments(0.001) {
462 match segment {
463 peniko::kurbo::PathSeg::Line(line) => {
464 if first {
465 first = false;
466 self.vger.move_to(self.vger_point(line.p0));
467 }
468 self.vger
469 .quad_to(self.vger_point(line.p1), self.vger_point(line.p1));
470 }
471 peniko::kurbo::PathSeg::Quad(quad) => {
472 if first {
473 first = false;
474 self.vger.move_to(self.vger_point(quad.p0));
475 }
476 self.vger
477 .quad_to(self.vger_point(quad.p1), self.vger_point(quad.p2));
478 }
479 peniko::kurbo::PathSeg::Cubic(cubic) => {
480 if first {
481 first = false;
482 self.vger.move_to(self.vger_point(cubic.p0));
483 }
484
485 let q1 = ((cubic.p1.to_vec2() + cubic.p2.to_vec2()) * 3.0
487 - cubic.p0.to_vec2()
488 - cubic.p3.to_vec2())
489 / 4.0;
490 self.vger
491 .quad_to(self.vger_point(q1.to_point()), self.vger_point(cubic.p3));
492 }
493 }
494 }
495 self.vger.fill(paint);
496 }
497
498 fn draw_glyphs<'a>(
499 &mut self,
500 origin: Point,
501 props: &GlyphRunProps<'a>,
502 glyphs: impl Iterator<Item = Glyph> + 'a,
503 ) {
504 let font = &props.font;
505 let coeffs = props.transform.as_coeffs();
506 let pos = self.device_transform() * (origin + Point::new(coeffs[4], coeffs[5]).to_vec2());
507 let (_, _, scale) = self.scale_components();
509
510 let clip = self.clip;
511 let Some(font_ref) = FontRef::from_index(font.data.data(), font.index as usize) else {
512 return;
513 };
514 let font_blob_id = font.data.id();
515 let _color = match &props.brush {
516 peniko::Brush::Solid(color) => Color::from(*color),
517 _ => return,
518 };
519 let Some(paint) = self.brush_to_paint(props.brush) else {
520 return;
521 };
522 let skew = props
523 .glyph_transform
524 .map(|transform| transform.as_coeffs()[0].atan().to_degrees() as f32);
525 let embolden = scaled_embolden_strength(self.font_embolden, scale);
526
527 for glyph in glyphs {
528 let glyph_x = pos.x as f32 + glyph.x * scale as f32;
529 let glyph_y = pos.y as f32 + glyph.y * scale as f32;
530
531 if let Some(rect) = clip
532 && (glyph_x as f64) > rect.x1
533 {
534 break;
535 }
536
537 let scaled_font_size = (props.font_size * scale as f32).round() as u32;
538
539 let x_bin = ((glyph_x.fract() + 1.0).fract() * 4.0).min(3.0) as u8;
540 let y_bin = ((glyph_y.fract() + 1.0).fract() * 4.0).min(3.0) as u8;
541
542 let glyph_id = glyph.id as u16;
543 let scaled_size = props.font_size * scale as f32;
544 let coords = props.normalized_coords;
545
546 let synthesis_bits = skew.unwrap_or(0.0).to_bits() & 0xFFFF_FFFE;
547
548 self.vger.render_glyph(
549 glyph_x.floor(),
550 glyph_y.floor(),
551 font_blob_id,
552 glyph_id,
553 scaled_font_size,
554 (x_bin, y_bin),
555 synthesis_bits,
556 || {
557 let image = SCALE_CONTEXT.with_borrow_mut(|ctx| {
558 let mut scaler = ctx
559 .builder(font_ref)
560 .size(scaled_size)
561 .hint(props.hint)
562 .normalized_coords(coords)
563 .build();
564 let mut render = Render::new(&[
565 Source::ColorOutline(0),
566 Source::ColorBitmap(StrikeWith::BestFit),
567 Source::Outline,
568 ]);
569 render
570 .format(Format::Alpha)
571 .offset(swash::zeno::Vector::new(glyph_x.fract(), glyph_y.fract()))
572 .embolden(embolden);
573 if let Some(angle) = skew {
574 render.transform(Some(swash::zeno::Transform::skew(
575 swash::zeno::Angle::from_degrees(angle),
576 swash::zeno::Angle::ZERO,
577 )));
578 }
579 render.render(&mut scaler, glyph_id)
580 });
581 match image {
582 Some(img) => GlyphImage {
583 colored: img.content != swash::scale::image::Content::Mask,
584 data: img.data.into(),
585 width: img.placement.width,
586 height: img.placement.height,
587 left: img.placement.left,
588 top: img.placement.top,
589 },
590 None => GlyphImage {
591 data: Blob::new(Arc::new([])),
592 width: 0,
593 height: 0,
594 left: 0,
595 top: 0,
596 colored: false,
597 },
598 }
599 },
600 paint,
601 );
602 }
603 }
604
605 fn draw_img(&mut self, img: Img<'_>, rect: Rect) {
606 let transform = self.device_transform().as_coeffs();
607 let (scale_x, scale_y, _) = self.scale_components();
608
609 let origin = rect.origin();
610 let transformed_x = transform[0] * origin.x + transform[2] * origin.y + transform[4];
611 let transformed_y = transform[1] * origin.x + transform[3] * origin.y + transform[5];
612
613 let x = transformed_x.round() as f32;
614 let y = transformed_y.round() as f32;
615
616 let width = (rect.width() * scale_x.abs()).round().max(1.0) as u32;
617 let height = (rect.height() * scale_y.abs()).round().max(1.0) as u32;
618
619 self.vger.render_image(x, y, img.hash, width, height, || {
620 let data = img.img.image.data;
621 let (width, height) = (img.img.image.width, img.img.image.height);
622
623 Image {
624 width,
625 height,
626 data,
627 pixel_format: PixelFormat::Rgba,
628 }
629 });
630 }
631
632 fn draw_svg<'b>(
633 &mut self,
634 svg: floem_renderer::Svg<'b>,
635 rect: Rect,
636 brush: Option<impl Into<BrushRef<'b>>>,
637 ) {
638 let transform = self.device_transform().as_coeffs();
639 let (scale_x, scale_y, _) = self.scale_components();
640
641 let origin = rect.origin();
642 let transformed_x = transform[0] * origin.x + transform[2] * origin.y + transform[4];
643 let transformed_y = transform[1] * origin.x + transform[3] * origin.y + transform[5];
644
645 let x = transformed_x.round() as f32;
646 let y = transformed_y.round() as f32;
647
648 let width = (rect.width() * scale_x.abs()).round().max(1.0) as u32;
649 let height = (rect.height() * scale_y.abs()).round().max(1.0) as u32;
650
651 let paint = brush.and_then(|b| self.brush_to_paint(b));
652
653 self.vger.render_svg(
654 x,
655 y,
656 svg.hash,
657 width,
658 height,
659 || {
660 let mut img = tiny_skia::Pixmap::new(width, height).unwrap();
661
662 let svg_scale = (width as f32 / svg.tree.size().width())
663 .min(height as f32 / svg.tree.size().height());
664
665 let final_scale_x = svg_scale;
666 let final_scale_y = svg_scale;
667
668 let transform = tiny_skia::Transform::from_scale(final_scale_x, final_scale_y);
669
670 resvg::render(svg.tree, transform, &mut img.as_mut());
671
672 img.take()
673 },
674 paint,
675 );
676 }
677
678 fn set_transform(&mut self, transform: Affine) {
679 self.transform = transform;
680 }
681
682 fn set_z_index(&mut self, z_index: i32) {
683 self.vger.set_z_index(z_index);
684 }
685
686 fn clip(&mut self, shape: &impl Shape) {
687 let (rect, radius) = if let Some(rect) = shape.as_rect() {
688 (rect, 0.0)
689 } else if let Some(rect) = shape.as_rounded_rect() {
690 (rect.rect(), rect.radii().top_left)
691 } else {
692 (shape.bounding_box(), 0.0)
693 };
694
695 let (_, _, scale) = self.scale_components();
696 self.vger
697 .scissor(self.vger_rect(rect), (radius * scale) as f32);
698
699 let transformed_rect = self.device_transform().transform_rect_bbox(rect);
700
701 self.clip = Some(transformed_rect);
702 }
703
704 fn clear_clip(&mut self) {
705 self.vger.reset_scissor();
706 self.clip = None;
707 }
708
709 fn finish(&mut self) -> Option<peniko::ImageBrush> {
710 if self.capture {
711 self.render_image()
712 } else {
713 if let Ok(frame) = self.surface.get_current_texture() {
714 let texture_view = frame
715 .texture
716 .create_view(&wgpu::TextureViewDescriptor::default());
717 let desc = wgpu::RenderPassDescriptor {
718 label: None,
719 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
720 view: &texture_view,
721 resolve_target: None,
722 ops: wgpu::Operations {
723 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
724 store: StoreOp::Store,
725 },
726 depth_slice: None,
727 })],
728 depth_stencil_attachment: None,
729 timestamp_writes: None,
730 occlusion_query_set: None,
731 };
732
733 self.vger.encode(&desc);
734 frame.present();
735 }
736 None
737 }
738 }
739
740 fn push_layer(
741 &mut self,
742 _blend: impl Into<peniko::BlendMode>,
743 _alpha: f32,
744 _transform: Affine,
745 _clip: &impl Shape,
746 ) {
747 }
748
749 fn pop_layer(&mut self) {}
750
751 fn debug_info(&self) -> String {
752 use std::fmt::Write;
753
754 let mut out = String::new();
755 writeln!(out, "name: Vger").ok();
756 writeln!(out, "info: {:#?}", self.adapter.get_info()).ok();
757
758 out
759 }
760}
761
762fn vger_color(color: Color) -> floem_vger_rs::Color {
763 floem_vger_rs::Color {
764 r: color.components[0],
765 g: color.components[1],
766 b: color.components[2],
767 a: color.components[3],
768 }
769}
770
771fn scaled_embolden_strength(font_embolden: f32, scale: f64) -> f32 {
772 font_embolden * scale as f32
773}
774
775#[cfg(test)]
776mod tests {
777 use super::scaled_embolden_strength;
778
779 #[test]
780 fn embolden_strength_scales_with_raster_scale() {
781 assert!((scaled_embolden_strength(0.2, 1.5) - 0.3).abs() < f32::EPSILON);
782 assert_eq!(scaled_embolden_strength(0.2, 0.0), 0.0);
783 }
784}