From 6b0f0ff45483f51821d3c3f772579bdeacdae7ea Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Fri, 26 Dec 2025 13:35:52 -0700 Subject: [PATCH] vello cpu backend sed --- Cargo.toml | 17 +- examples/widget-gallery/Cargo.toml | 2 +- examples/widget-gallery/src/context_menu.rs | 33 +- examples/widget-gallery/src/draggable.rs | 7 +- src/app.rs | 21 + src/app_handle.rs | 4 +- src/context.rs | 54 +- src/id.rs | 4 +- src/lib.rs | 3 +- src/renderer.rs | 462 +++++++++++++--- src/view.rs | 8 +- src/view_state.rs | 7 +- src/views/text_input.rs | 36 +- src/window_handle.rs | 7 +- vello/src/lib.rs | 2 +- vello_cpu/Cargo.toml | 18 + vello_cpu/src/lib.rs | 585 ++++++++++++++++++++ 17 files changed, 1152 insertions(+), 118 deletions(-) create mode 100644 vello_cpu/Cargo.toml create mode 100644 vello_cpu/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5aa20d9a0..9bf2c8b19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "renderer", "vello", + "vello_cpu", "vger", "tiny_skia", "reactive", @@ -15,6 +16,7 @@ default-members = [ ".", "renderer", "vello", + "vello_cpu", "vger", "tiny_skia", "reactive", @@ -86,8 +88,9 @@ strum_macros = { workspace = true, optional = true } paste = "1.0" floem_renderer = { path = "renderer", version = "0.2.0" } floem_vello_renderer = { path = "vello", version = "0.2.0", optional = true } +floem_vello_cpu_renderer = { path = "vello_cpu", version = "0.2.0", optional = true } floem_vger_renderer = { path = "vger", version = "0.2.0", optional = true } -floem_tiny_skia_renderer = { path = "tiny_skia", version = "0.2.0" } +floem_tiny_skia_renderer = { path = "tiny_skia", version = "0.2.0", optional = true } floem_reactive = { path = "reactive", version = "0.2.0" } floem-editor-core = { path = "editor-core", version = "0.2.0", optional = true } copypasta = { version = "0.10", default-features = false, features = [ @@ -131,9 +134,17 @@ objc2-app-kit = { version = "0.3", features = [ ] } [features] -default = ["editor", "default-image-formats", "vger"] +default = ["editor", "default-image-formats", "vello", "vello_cpu"] + +# Renderers - enable any combination you want +# They are tried in order: vello -> vger -> vello_cpu -> tiny_skia vello = ["dep:floem_vello_renderer"] -vger = ["dep:floem_vger_renderer"] +vger = ["dep:floem_vger_renderer"] +vello_cpu = ["dep:floem_vello_cpu_renderer"] +tiny_skia = ["dep:floem_tiny_skia_renderer"] + +# Feature flag for capabilities - simple renderer (vger) has limited features +simple_renderer = [] serde = [ "dep:serde", "winit/serde", diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index 3013b5693..ae31bb6f2 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -floem = { path = "../..", features = ["rfd-async-std", "vello"] } +floem = { path = "../..", features = ["rfd-async-std", "vello", "vello_cpu"] } strum = { workspace = true } files = { path = "../files/", optional = true } stacks = { path = "../stacks/", optional = true } diff --git a/examples/widget-gallery/src/context_menu.rs b/examples/widget-gallery/src/context_menu.rs index d04de8824..54c4c27c9 100644 --- a/examples/widget-gallery/src/context_menu.rs +++ b/examples/widget-gallery/src/context_menu.rs @@ -1,11 +1,14 @@ use floem::{ + kurbo::Affine, menu::*, - prelude::ViewTuple, + prelude::{RwSignal, SignalGet, SignalUpdate, ViewTuple}, + reactive::Effect, views::{ButtonClass, Decorators}, - IntoView, + HasViewId, IntoView, }; pub fn menu_view() -> impl IntoView { + let transform = RwSignal::new(Affine::IDENTITY); let export_submenu = |m: SubMenu| { m.item("PDF", |i| i.action(|| println!("Exporting as PDF..."))) .item("PNG", |i| i.action(|| println!("Exporting as PNG..."))) @@ -40,15 +43,27 @@ pub fn menu_view() -> impl IntoView { }) }; - let transform_submenu = |m: SubMenu| { + let transform_submenu = move |m: SubMenu| { m.item("Rotate 90°", |i| { - i.action(|| println!("Rotating 90 degrees...")) + i.action(move || { + transform.update(|s| { + *s = s.then_rotate(90f64.to_radians()); + }) + }) }) .item("Flip Horizontal", |i| { - i.action(|| println!("Flipping horizontally...")) + i.action(move || { + transform.update(|s| { + *s *= Affine::FLIP_X; + }) + }) }) .item("Flip Vertical", |i| { - i.action(|| println!("Flipping vertically...")) + i.action(move || { + transform.update(|s| { + *s *= Affine::FLIP_Y; + }) + }) }) .separator() .item("Reset Transform", |i| { @@ -85,6 +100,12 @@ pub fn menu_view() -> impl IntoView { .class(ButtonClass) .style(|s| s.padding(10.0).border(1.0)) .context_menu(context_menu); + let id = context_button.view_id(); + + Effect::new(move |_| { + let transform = transform.get(); + id.set_transform(transform); + }); (popout_button, context_button) .v_stack() diff --git a/examples/widget-gallery/src/draggable.rs b/examples/widget-gallery/src/draggable.rs index 9a2dd1ea7..489416a30 100644 --- a/examples/widget-gallery/src/draggable.rs +++ b/examples/widget-gallery/src/draggable.rs @@ -1,4 +1,7 @@ -use floem::{prelude::*, style::CursorStyle}; +use floem::{ + prelude::{palette::css, *}, + style::CursorStyle, +}; fn sortable_item( name: &str, @@ -86,6 +89,6 @@ pub fn draggable_view() -> impl IntoView { move |item_id| *item_id, move |item_id| sortable_item(items[item_id], sortable_items, dragger_id, item_id), ) - .style(|s| s.flex_col().row_gap(5).padding(10)) + .style(|s| s.flex_col().row_gap(5).padding(10).color(css::BLACK)) .into_view() } diff --git a/src/app.rs b/src/app.rs index 354e45758..38da9e7c0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,6 +22,7 @@ use crate::{ clipboard::Clipboard, inspector::Capture, profiler::Profile, + renderer::RendererKind, view::IntoView, window::{WindowConfig, WindowCreation}, }; @@ -39,6 +40,7 @@ pub struct AppConfig { pub(crate) exit_on_close: bool, pub(crate) wgpu_features: wgpu::Features, pub(crate) global_theme_override: Option, + pub(crate) renderer_preference: RendererKind, } impl Default for AppConfig { @@ -47,6 +49,7 @@ impl Default for AppConfig { exit_on_close: !cfg!(target_os = "macos"), wgpu_features: wgpu::Features::default(), global_theme_override: None, + renderer_preference: RendererKind::Auto, } } } @@ -72,6 +75,24 @@ impl AppConfig { self.global_theme_override = Some(theme); self } + + /// Sets the preferred renderer type. + /// + /// The renderer preference determines which renderer backend to use. + /// If the preferred renderer fails to initialize, the system will fall back + /// to other available renderers in order of preference. + /// + /// # Examples + /// ```no_run + /// # use floem::{AppConfig, renderer::RendererKind}; + /// let config = AppConfig::default() + /// .renderer_preference(RendererKind::VelloCpu); + /// ``` + #[inline] + pub fn renderer_preference(mut self, renderer: RendererKind) -> Self { + self.renderer_preference = renderer; + self + } } /// Initializes and runs an application with a single window. diff --git a/src/app_handle.rs b/src/app_handle.rs index 0bb313261..55d7ca461 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -84,7 +84,8 @@ impl ApplicationHandle { } = &handle.paint_state { let (gpu_resources, surface) = rx.recv().unwrap().unwrap(); - let renderer = crate::renderer::Renderer::new( + let renderer = crate::renderer::Renderer::new_with_kind( + self.config.renderer_preference, window.clone(), gpu_resources.clone(), surface, @@ -548,6 +549,7 @@ impl ApplicationHandle { transparent, apply_default_theme, font_embolden, + self.config.renderer_preference, ); self.window_handles.insert(window_id, window_handle); } diff --git a/src/context.rs b/src/context.rs index 9341ec4fc..78244710c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,7 @@ use floem_reactive::Scope; use floem_renderer::Renderer as FloemRenderer; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; +use peniko::BlendMode; use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Shape, Size, Vec2}; use smallvec::SmallVec; use std::{ @@ -74,6 +75,7 @@ pub struct DragState { pub(crate) release_location: Option, } +#[derive(Debug)] pub(crate) enum FrameUpdate { Style(ViewId), Layout(ViewId), @@ -1000,9 +1002,11 @@ impl<'a> StyleCx<'a> { pub fn style_view(&mut self) { let view_id = self.current_view; - let view = view_id.view(); + let view_interact_state = self.get_interact_state(&view_id); let view_state = view_id.state(); + let view = view_id.view(); + { let mut view_state = view_state.borrow_mut(); if self.window_state.view_style_dirty.contains(&view_id) { @@ -1026,7 +1030,6 @@ impl<'a> StyleCx<'a> { } } - let view_interact_state = self.get_interact_state(&view_id); let view_class = view.borrow().view_class(); let (mut new_frame, classes_applied) = self.compute_combined( @@ -1086,18 +1089,16 @@ impl<'a> StyleCx<'a> { &mut new_frame, ); - if view_state.view_transform_props.read_explicit( + view_state.view_transform_props.read_explicit( &self.direct, &self.current, &self.now, &mut new_frame, - ) || new_frame - { - self.window_state.schedule_layout(view_id); - } + ); } - if new_frame { + // TODO: we should still be scheduling style here. the style pass should still run, we just shouldn't be repainting because of a hidden view. + if new_frame && !self.hidden { self.window_state.schedule_style(view_id); } @@ -1106,7 +1107,10 @@ impl<'a> StyleCx<'a> { let taffy_style = self.direct.clone().apply(layout_style).to_taffy_style(); if taffy_style != view_state.borrow().taffy_style { view_state.borrow_mut().taffy_style = taffy_style; - self.window_state.schedule_layout(view_id); + // TODO: we should still be requesting layout here. the layout should still run, we just shouldn't be repainting because of a hidden view. + if !self.hidden { + view_id.request_layout(); + } } view.borrow_mut().style_pass(self); @@ -1511,9 +1515,9 @@ pub struct PaintCx<'a> { pub(crate) pending_drag_paint: Option, pub gpu_resources: Option, pub window: Arc, - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] pub layer_count: usize, - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] pub saved_layer_counts: Vec, } @@ -1521,12 +1525,12 @@ impl PaintCx<'_> { pub fn save(&mut self) { self.saved_transforms.push(self.transform); self.saved_clips.push(self.clip); - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] self.saved_layer_counts.push(self.layer_count); } pub fn restore(&mut self) { - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] { let saved_count = self.saved_layer_counts.pop().unwrap_or_default(); while self.layer_count > saved_count { @@ -1541,7 +1545,7 @@ impl PaintCx<'_> { .renderer_mut() .set_transform(self.transform); - #[cfg(not(feature = "vello"))] + #[cfg(feature = "simple_renderer")] { if let Some(rect) = self.clip { self.paint_state.renderer_mut().clip(&rect); @@ -1596,12 +1600,24 @@ impl PaintCx<'_> { if !is_empty { let view_style_props = view_state.borrow().view_style_props.clone(); let layout_props = view_state.borrow().layout_props.clone(); + let alpha = view_style_props.opacity(); + if alpha != 1. { + self.push_layer( + BlendMode::default(), + alpha, + Affine::IDENTITY, + &size.to_rect(), + ); + } paint_bg(self, &view_style_props, size); view.borrow_mut().paint(self); paint_border(self, &layout_props, &view_style_props, size); - paint_outline(self, &view_style_props, size) + paint_outline(self, &view_style_props, size); + if alpha != 1. { + self.pop_layer(); + } } // Check if this view is being dragged and needs deferred painting if let Some(dragging) = self.window_state.dragging.as_ref() { @@ -1719,7 +1735,7 @@ impl PaintCx<'_> { /// Clip the drawing area to the given shape. pub fn clip(&mut self, shape: &impl Shape) { - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] { use peniko::Mix; @@ -1728,7 +1744,7 @@ impl PaintCx<'_> { self.clip = Some(shape.bounding_box().to_rounded_rect(0.0)); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "simple_renderer")] { let rect = if let Some(rect) = shape.as_rect() { rect.to_rounded_rect(0.0) @@ -1849,8 +1865,10 @@ impl PaintState { scale: f64, size: Size, font_embolden: f32, + renderer_preference: crate::renderer::RendererKind, ) -> Self { - let renderer = crate::renderer::Renderer::new( + let renderer = crate::renderer::Renderer::new_with_kind( + renderer_preference, window.clone(), gpu_resources, surface, diff --git a/src/id.rs b/src/id.rs index 24b2a36ae..b051ff70e 100644 --- a/src/id.rs +++ b/src/id.rs @@ -6,9 +6,9 @@ use std::{any::Any, cell::RefCell, rc::Rc}; -use peniko::kurbo::{Affine, Insets, Point, Rect, Size}; +use peniko::kurbo::{Insets, Point, Rect, Size}; use slotmap::new_key_type; -use taffy::{Layout, NodeId, TaffyTree}; +use taffy::{Display, Layout, NodeId, TaffyTree}; use winit::window::WindowId; use crate::{ diff --git a/src/lib.rs b/src/lib.rs index 48754561f..a65580977 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,7 +182,7 @@ mod app; #[cfg(target_os = "macos")] mod app_delegate; mod app_handle; -#[cfg(feature = "vello")] +#[cfg(not(feature = "simple_renderer"))] mod border_path_iter; mod clipboard; pub mod context; @@ -244,6 +244,7 @@ pub use imbl; pub use muda; pub use peniko; pub use peniko::kurbo; +pub use renderer::RendererKind; pub use screen_layout::ScreenLayout; pub use taffy; pub use ui_events; diff --git a/src/renderer.rs b/src/renderer.rs index 304dd0594..958062f28 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -53,31 +53,104 @@ use crate::kurbo::Point; use floem_renderer::Img; use floem_renderer::gpu_resources::GpuResources; use floem_renderer::text::LayoutRun; +#[cfg(feature = "tiny_skia")] use floem_tiny_skia_renderer::TinySkiaRenderer; +#[cfg(feature = "vello_cpu")] +use floem_vello_cpu_renderer::VelloCpuRenderer; #[cfg(feature = "vello")] use floem_vello_renderer::VelloRenderer; -#[cfg(not(feature = "vello"))] +#[cfg(feature = "vger")] use floem_vger_renderer::VgerRenderer; use peniko::BrushRef; use peniko::kurbo::{Affine, Rect, Shape, Size, Stroke}; use winit::window::Window; +/// Enum for selecting a specific renderer at runtime +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RendererKind { + /// High-performance GPU renderer with full feature support + Vello, + /// CPU-based renderer with full feature support, good for compatibility + VelloCpu, + /// Simple GPU renderer with limited features but good compatibility + Vger, + /// CPU-based renderer with good feature support, performance not as good as `VelloCpu` + TinySkia, + /// Auto-select based on availability and performance + #[default] + Auto, +} + +impl RendererKind { + /// Get all available renderer kinds based on enabled features + pub fn available() -> Vec { + let mut available = vec![Self::Auto]; + + #[cfg(feature = "vello")] + available.push(Self::Vello); + + #[cfg(feature = "vello_cpu")] + available.push(Self::VelloCpu); + + #[cfg(feature = "vger")] + available.push(Self::Vger); + + #[cfg(feature = "tiny_skia")] + available.push(Self::TinySkia); + + available + } + + /// Check if this renderer kind is available (feature is enabled) + pub fn is_available(self) -> bool { + match self { + Self::Auto => true, + #[cfg(feature = "vello")] + Self::Vello => true, + #[cfg(not(feature = "vello"))] + Self::Vello => false, + #[cfg(feature = "vello_cpu")] + Self::VelloCpu => true, + #[cfg(not(feature = "vello_cpu"))] + Self::VelloCpu => false, + #[cfg(feature = "vger")] + Self::Vger => true, + #[cfg(not(feature = "vger"))] + Self::Vger => false, + #[cfg(feature = "tiny_skia")] + Self::TinySkia => true, + #[cfg(not(feature = "tiny_skia"))] + Self::TinySkia => false, + } + } + + /// Whether this renderer has full feature support (vs simple renderer) + pub fn is_full_featured(self) -> bool { + match self { + Self::Auto => true, // Auto will prefer full-featured renderers + Self::Vello | Self::VelloCpu | Self::TinySkia => true, + Self::Vger => false, + } + } +} + #[allow(clippy::large_enum_variant)] pub enum Renderer { #[cfg(feature = "vello")] Vello(VelloRenderer), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + VelloCpu(VelloCpuRenderer>), + #[cfg(feature = "vger")] Vger(VgerRenderer), + #[cfg(feature = "tiny_skia")] TinySkia(TinySkiaRenderer>), /// Uninitialized renderer, used to allow the renderer to be created lazily /// All operations on this renderer are no-ops - Uninitialized { - scale: f64, - size: Size, - }, + Uninitialized { scale: f64, size: Size }, } impl Renderer { + /// Create a new renderer, trying renderers in preference order pub fn new( window: Arc, gpu_resources: GpuResources, @@ -85,66 +158,253 @@ impl Renderer { scale: f64, size: Size, font_embolden: f32, + ) -> Self { + Self::new_with_kind( + RendererKind::Auto, + window, + gpu_resources, + surface, + scale, + size, + font_embolden, + ) + } + + /// Create a new renderer with a specific kind preference + pub fn new_with_kind( + kind: RendererKind, + window: Arc, + gpu_resources: GpuResources, + surface: wgpu::Surface<'static>, + scale: f64, + size: Size, + font_embolden: f32, ) -> Self { let size = Size::new(size.width.max(1.0), size.height.max(1.0)); - let force_tiny_skia = std::env::var("FLOEM_FORCE_TINY_SKIA") + // Check for environment variable overrides + let env_override = if std::env::var("FLOEM_FORCE_VELLO") .ok() - .map(|val| val.as_str() == "1") - .unwrap_or(false); + .map(|v| v == "1") + .unwrap_or(false) + { + Some(RendererKind::Vello) + } else if std::env::var("FLOEM_FORCE_VGER") + .ok() + .map(|v| v == "1") + .unwrap_or(false) + { + Some(RendererKind::Vger) + } else if std::env::var("FLOEM_FORCE_VELLO_CPU") + .ok() + .map(|v| v == "1") + .unwrap_or(false) + { + Some(RendererKind::VelloCpu) + } else if std::env::var("FLOEM_FORCE_TINY_SKIA") + .ok() + .map(|v| v == "1") + .unwrap_or(false) + { + Some(RendererKind::TinySkia) + } else { + None + }; - #[cfg(feature = "vello")] - let vger_err = if !force_tiny_skia { - match VelloRenderer::new( + let preferred_kind = env_override.unwrap_or(kind); + let mut errors = Vec::new(); + + // Determine the order to try renderers + let try_order = if preferred_kind != RendererKind::Auto { + // Try preferred first, then fallback order + let mut order = vec![preferred_kind]; + for &kind in &[ + RendererKind::Vello, + RendererKind::Vger, + RendererKind::VelloCpu, + RendererKind::TinySkia, + ] { + if kind != preferred_kind { + order.push(kind); + } + } + order + } else { + vec![ + RendererKind::Vello, + RendererKind::Vger, + RendererKind::VelloCpu, + RendererKind::TinySkia, + ] + }; + + let gpu_renderers = [RendererKind::Vello, RendererKind::Vger]; + let cpu_renderers = [RendererKind::VelloCpu, RendererKind::TinySkia]; + + let mut surface_option = Some(surface); + + // Try renderers in the preferred order, respecting surface consumption + for &renderer_kind in &try_order { + if !renderer_kind.is_available() { + continue; + } + + if gpu_renderers.contains(&renderer_kind) { + // GPU renderer - needs surface + if let Some(surf) = surface_option.take() { + match Self::try_create_renderer_gpu( + renderer_kind, + window.clone(), + gpu_resources.clone(), + surf, + size, + scale, + font_embolden, + ) { + Ok(renderer) => return renderer, + Err(err) => { + // Surface is consumed on failure, can't try other GPU renderers + if env_override == Some(renderer_kind) { + panic!( + "Failed to create forced renderer {:?}: {}", + renderer_kind, err + ); + } + errors.push(format!("{:?}: {}", renderer_kind, err)); + } + } + } else { + errors.push(format!( + "{:?}: surface already consumed by previous GPU renderer", + renderer_kind + )); + } + } else if cpu_renderers.contains(&renderer_kind) { + // CPU renderer - doesn't need surface + match Self::try_create_renderer_cpu( + renderer_kind, + window.clone(), + size, + scale, + font_embolden, + ) { + Ok(renderer) => return renderer, + Err(err) => { + if env_override == Some(renderer_kind) { + panic!( + "Failed to create forced renderer {:?}: {}", + renderer_kind, err + ); + } + errors.push(format!("{:?}: {}", renderer_kind, err)); + } + } + } + } + + panic!("Failed to create any renderer:\n{}", errors.join("\n")); + } + + /// Helper method to try creating a GPU renderer (consumes surface) + fn try_create_renderer_gpu( + kind: RendererKind, + _window: Arc, + gpu_resources: GpuResources, + surface: wgpu::Surface<'static>, + size: Size, + scale: f64, + font_embolden: f32, + ) -> Result { + match kind { + #[cfg(feature = "vello")] + RendererKind::Vello => VelloRenderer::new( gpu_resources, surface, size.width as u32, size.height as u32, scale, font_embolden, - ) { - Ok(vger) => return Self::Vello(vger), - Err(err) => Some(err), + ) + .map(Self::Vello) + .map_err(|e| e.to_string()), + #[cfg(not(feature = "vello"))] + RendererKind::Vello => { + Err("Vello renderer not available (feature disabled)".to_string()) } - } else { - None - }; - #[cfg(not(feature = "vello"))] - let vger_err = if !force_tiny_skia { - match VgerRenderer::new( + #[cfg(feature = "vger")] + RendererKind::Vger => VgerRenderer::new( gpu_resources, surface, size.width as u32, size.height as u32, scale, font_embolden, - ) { - Ok(vger) => return Self::Vger(vger), - Err(err) => Some(err), + ) + .map(Self::Vger) + .map_err(|e| e.to_string()), + #[cfg(not(feature = "vger"))] + RendererKind::Vger => Err("Vger renderer not available (feature disabled)".to_string()), + + _ => Err(format!("Renderer {:?} is not a GPU renderer", kind)), + } + } + + /// Helper method to try creating a CPU renderer + fn try_create_renderer_cpu( + kind: RendererKind, + window: Arc, + size: Size, + scale: f64, + font_embolden: f32, + ) -> Result { + match kind { + #[cfg(feature = "vello_cpu")] + RendererKind::VelloCpu => VelloCpuRenderer::new( + window, + size.width as u32, + size.height as u32, + scale, + font_embolden, + ) + .map(Self::VelloCpu) + .map_err(|e| e.to_string()), + #[cfg(not(feature = "vello_cpu"))] + RendererKind::VelloCpu => { + Err("VelloCpu renderer not available (feature disabled)".to_string()) } - } else { - None - }; - let tiny_skia_err = match TinySkiaRenderer::new( - window, - size.width as u32, - size.height as u32, - scale, - font_embolden, - ) { - Ok(tiny_skia) => return Self::TinySkia(tiny_skia), - Err(err) => err, - }; + #[cfg(feature = "tiny_skia")] + RendererKind::TinySkia => TinySkiaRenderer::new( + window, + size.width as u32, + size.height as u32, + scale, + font_embolden, + ) + .map(Self::TinySkia) + .map_err(|e| e.to_string()), + #[cfg(not(feature = "tiny_skia"))] + RendererKind::TinySkia => { + Err("TinySkia renderer not available (feature disabled)".to_string()) + } - if !force_tiny_skia { - panic!( - "Failed to create VgerRenderer: {}\nFailed to create TinySkiaRenderer: {tiny_skia_err}", - vger_err.unwrap() - ); - } else { - panic!("Failed to create TinySkiaRenderer: {tiny_skia_err}"); + _ => Err(format!("Renderer {:?} is not a CPU renderer", kind)), + } + } + + /// Get the kind of renderer currently active + pub fn kind(&self) -> RendererKind { + match self { + #[cfg(feature = "vello")] + Self::Vello(_) => RendererKind::Vello, + #[cfg(feature = "vello_cpu")] + Self::VelloCpu(_) => RendererKind::VelloCpu, + #[cfg(feature = "vger")] + Self::Vger(_) => RendererKind::Vger, + #[cfg(feature = "tiny_skia")] + Self::TinySkia(_) => RendererKind::TinySkia, + Self::Uninitialized { .. } => RendererKind::Auto, // Not yet initialized } } @@ -153,8 +413,11 @@ impl Renderer { match self { #[cfg(feature = "vello")] Renderer::Vello(r) => r.resize(size.width as u32, size.height as u32, scale), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(r) => r.resize(size.width as u32, size.height as u32, scale), + #[cfg(feature = "vger")] Renderer::Vger(r) => r.resize(size.width as u32, size.height as u32, scale), + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(r) => r.resize(size.width as u32, size.height as u32, scale), Renderer::Uninitialized { .. } => {} } @@ -164,8 +427,11 @@ impl Renderer { match self { #[cfg(feature = "vello")] Renderer::Vello(r) => r.set_scale(scale), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(r) => r.set_scale(scale), + #[cfg(feature = "vger")] Renderer::Vger(r) => r.set_scale(scale), + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(r) => r.set_scale(scale), Renderer::Uninitialized { scale: old_scale, .. @@ -179,8 +445,11 @@ impl Renderer { match self { #[cfg(feature = "vello")] Renderer::Vello(r) => r.scale(), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(r) => r.scale(), + #[cfg(feature = "vger")] Renderer::Vger(r) => r.scale(), + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(r) => r.scale(), Renderer::Uninitialized { scale, .. } => *scale, } @@ -190,8 +459,11 @@ impl Renderer { match self { #[cfg(feature = "vello")] Renderer::Vello(r) => r.size(), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(r) => r.size(), + #[cfg(feature = "vger")] Renderer::Vger(r) => r.size(), + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(r) => r.size(), Renderer::Uninitialized { size, .. } => *size, } @@ -203,8 +475,11 @@ impl Renderer { match self { #[cfg(feature = "vello")] Self::Vello(r) => r.debug_info(), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Self::VelloCpu(r) => r.debug_info(), + #[cfg(feature = "vger")] Self::Vger(r) => r.debug_info(), + #[cfg(feature = "tiny_skia")] Self::TinySkia(r) => r.debug_info(), Self::Uninitialized { .. } => "Uninitialized".to_string(), } @@ -218,10 +493,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(r) => { r.begin(capture); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(r) => { + r.begin(capture); + } + #[cfg(feature = "vger")] Renderer::Vger(r) => { r.begin(capture); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(r) => { r.begin(capture); } @@ -235,10 +515,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.clip(shape); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.clip(shape); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.clip(shape); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.clip(shape); } @@ -252,10 +537,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.clear_clip(); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.clear_clip(); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.clear_clip(); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.clear_clip(); } @@ -274,10 +564,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.stroke(shape, brush, stroke); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.stroke(shape, brush, stroke); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.stroke(shape, brush, stroke); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.stroke(shape, brush, stroke); } @@ -296,10 +591,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.fill(path, brush, blur_radius); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.fill(path, brush, blur_radius); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.fill(path, brush, blur_radius); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.fill(path, brush, blur_radius); } @@ -319,10 +619,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.push_layer(blend, alpha, transform, clip); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.push_layer(blend, alpha, transform, clip); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.push_layer(blend, alpha, transform, clip); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => v.push_layer(blend, alpha, transform, clip), Renderer::Uninitialized { .. } => {} } @@ -334,10 +639,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.pop_layer(); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.pop_layer(); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.pop_layer(); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => v.pop_layer(), Renderer::Uninitialized { .. } => {} } @@ -353,10 +663,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.draw_text_with_layout(layout, pos); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.draw_text_with_layout(layout, pos); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.draw_text_with_layout(layout, pos); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.draw_text_with_layout(layout, pos); } @@ -370,10 +685,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.draw_img(img, rect); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.draw_img(img, rect); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.draw_img(img, rect); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.draw_img(img, rect); } @@ -392,10 +712,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.draw_svg(svg, rect, brush); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.draw_svg(svg, rect, brush); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.draw_svg(svg, rect, brush); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.draw_svg(svg, rect, brush); } @@ -409,10 +734,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.set_transform(transform); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.set_transform(transform); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.set_transform(transform); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.set_transform(transform); } @@ -426,10 +756,15 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vello(v) => { v.set_z_index(z_index); } - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(v) => { + v.set_z_index(z_index); + } + #[cfg(feature = "vger")] Renderer::Vger(v) => { v.set_z_index(z_index); } + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(v) => { v.set_z_index(z_index); } @@ -441,8 +776,11 @@ impl floem_renderer::Renderer for Renderer { match self { #[cfg(feature = "vello")] Renderer::Vello(r) => r.finish(), - #[cfg(not(feature = "vello"))] + #[cfg(feature = "vello_cpu")] + Renderer::VelloCpu(r) => r.finish(), + #[cfg(feature = "vger")] Renderer::Vger(r) => r.finish(), + #[cfg(feature = "tiny_skia")] Renderer::TinySkia(r) => r.finish(), Renderer::Uninitialized { .. } => None, } diff --git a/src/view.rs b/src/view.rs index e1cc02e3e..5e40065f9 100644 --- a/src/view.rs +++ b/src/view.rs @@ -716,7 +716,7 @@ fn paint_box_shadow( } } } -#[cfg(feature = "vello")] +#[cfg(not(feature = "simple_renderer"))] pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, size: Size) { use crate::{ border_path_iter::{BorderPath, BorderPathEvent}, @@ -768,7 +768,7 @@ pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, size: Size assert!(current_path.is_empty()); } -#[cfg(not(feature = "vello"))] +#[cfg(feature = "simple_renderer")] pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, size: Size) { let outline = &style.outline().0; if outline.width == 0. { @@ -785,7 +785,7 @@ pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, size: Size ); } -#[cfg(not(feature = "vello"))] +#[cfg(feature = "simple_renderer")] pub(crate) fn paint_border( cx: &mut PaintCx, layout_style: &LayoutProps, @@ -874,7 +874,7 @@ pub(crate) fn paint_border( } } -#[cfg(feature = "vello")] +#[cfg(not(feature = "simple_renderer"))] pub(crate) fn paint_border( cx: &mut PaintCx, layout_style: &LayoutProps, diff --git a/src/view_state.rs b/src/view_state.rs index 900ee3233..73f18c50b 100644 --- a/src/view_state.rs +++ b/src/view_state.rs @@ -1,3 +1,5 @@ +#[cfg(not(feature = "simple_renderer"))] +use crate::style::Opacity; use crate::{ ViewId, animate::Animation, @@ -71,7 +73,7 @@ impl Stack { } } -#[cfg(feature = "vello")] +#[cfg(not(feature = "simple_renderer"))] prop_extractor! { pub(crate) ViewStyleProps { pub border_radius: BorderRadiusProp, @@ -83,10 +85,11 @@ prop_extractor! { pub border_color: BorderColorProp, pub background: Background, pub shadow: BoxShadowProp, + pub opacity: Opacity, } } // removing outlines to make clippy happy about progress fields not being read -#[cfg(not(feature = "vello"))] +#[cfg(feature = "simple_renderer")] prop_extractor! { pub(crate) ViewStyleProps { pub border_radius: BorderRadiusProp, diff --git a/src/views/text_input.rs b/src/views/text_input.rs index d7d58d4a4..43e81cf24 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -1,5 +1,5 @@ #![deny(missing_docs)] -use crate::action::{exec_after, set_ime_allowed, set_ime_cursor_area}; +use crate::action::{TimerToken, exec_after, set_ime_allowed, set_ime_cursor_area}; use crate::event::{EventListener, EventPropagation}; use crate::id::ViewId; use crate::reactive::{Effect, RwSignal}; @@ -18,6 +18,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{peniko::color::palette, style::Style, view::View}; +use std::cell::Cell; use std::{any::Any, ops::Range}; use crate::text::{Attrs, AttrsList, FamilyOwned, TextLayout}; @@ -119,6 +120,7 @@ pub struct TextInput { last_cursor_action_on: Instant, window_origin: Option, last_ime_cursor_area: Option<(Point, Size)>, + cursor_blink_timer: Cell>, } /// Type of cursor movement in navigation. @@ -234,6 +236,7 @@ pub fn text_input(buffer: RwSignal) -> TextInput { on_enter: None, window_origin: None, last_ime_cursor_area: None, + cursor_blink_timer: Cell::new(None), } .on_event_stop(EventListener::FocusGained, move |_| { is_focused.set(true); @@ -1358,6 +1361,9 @@ impl View for TextInput { } fn paint(&mut self, cx: &mut crate::context::PaintCx) { + // Clear the timer token since this paint was triggered (possibly by the timer) + self.cursor_blink_timer.set(None); + let text_node = self.text_node.unwrap(); let node_layout = self .id @@ -1366,10 +1372,8 @@ impl View for TextInput { .layout(text_node) .cloned() .unwrap_or_default(); - let location = node_layout.location; let text_start_point = Point::new(location.x as f64, location.y as f64); - cx.save(); cx.clip(&self.id.get_content_rect()); cx.draw_text( @@ -1381,26 +1385,26 @@ impl View for TextInput { if let Some(preedit) = &self.preedit { let start_idx = self.cursor_glyph_idx; let end_idx = start_idx + preedit.text.len(); - let start_hit = self.text_buf.hit_position(start_idx); let start_x = location.x as f64 + start_hit.point.x - self.clip_start_x; let end_x = location.x as f64 + self.text_buf.hit_position(end_idx).point.x - self.clip_start_x; - let color = self.style.color().unwrap_or(palette::css::BLACK); let y = location.y as f64 + start_hit.glyph_ascent; - cx.fill( &Rect::new(start_x, y, end_x, y + 1.0), &Brush::Solid(color), 0.0, ); } - cx.restore(); // skip rendering selection / cursor if we don't have focus if !cx.window_state.is_focused(&self.id()) { + // Cancel any pending blink timer when we lose focus + if let Some(token) = self.cursor_blink_timer.take() { + token.cancel(); + } return; } @@ -1413,7 +1417,10 @@ impl View for TextInput { if has_selection { self.paint_selection_rect(&node_layout, cx); - // we can skip drawing a cursor and handling blink + // Cancel blink timer when we have a selection + if let Some(token) = self.cursor_blink_timer.take() { + token.cancel(); + } return; } @@ -1434,11 +1441,14 @@ impl View for TextInput { cx.fill(&cursor_rect, &cursor_color, 0.0); } - // request paint either way if we're attempting draw a cursor - let id = self.id(); - exec_after(Duration::from_millis(CURSOR_BLINK_INTERVAL_MS), move |_| { - id.request_paint(); - }); + // Only schedule a new blink timer if we don't already have one pending + if self.cursor_blink_timer.get().is_none() { + let id = self.id(); + let timer = exec_after(Duration::from_millis(CURSOR_BLINK_INTERVAL_MS), move |_| { + id.request_paint(); + }); + self.cursor_blink_timer.set(Some(timer)); + } } } diff --git a/src/window_handle.rs b/src/window_handle.rs index 2c0d669bf..324c815e1 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -90,6 +90,7 @@ pub(crate) struct WindowHandle { } impl WindowHandle { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( window: Box, gpu_resources: Option, @@ -98,6 +99,7 @@ impl WindowHandle { transparent: bool, apply_default_theme: bool, font_embolden: f32, + renderer_preference: crate::renderer::RendererKind, ) -> Self { let scope = Scope::new(); let window_id = window.id(); @@ -158,6 +160,7 @@ impl WindowHandle { scale, size.get_untracked() * scale, font_embolden, + renderer_preference, ) } else { let gpu_resources_rx = GpuResources::request( @@ -583,9 +586,9 @@ impl WindowHandle { pending_drag_paint: None, gpu_resources, window: self.window.clone(), - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] saved_layer_counts: Vec::new(), - #[cfg(feature = "vello")] + #[cfg(not(feature = "simple_renderer"))] layer_count: 0, }; cx.paint_state diff --git a/vello/src/lib.rs b/vello/src/lib.rs index 2267cf485..369f75de5 100644 --- a/vello/src/lib.rs +++ b/vello/src/lib.rs @@ -680,7 +680,7 @@ impl VelloRenderer { .draw_glyphs(&font) .font_size(run.font_size) .brush(run.color) - .hint(false) + .hint(true) .transform(transform) .draw( Fill::NonZero, diff --git a/vello_cpu/Cargo.toml b/vello_cpu/Cargo.toml new file mode 100644 index 000000000..bb8e24874 --- /dev/null +++ b/vello_cpu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "floem_vello_cpu_renderer" +version.workspace = true +edition = "2024" +repository = "https://github.com/lapce/floem" +license.workspace = true + +[dependencies] +peniko = { workspace = true } +wgpu = { workspace = true } +resvg = { workspace = true } + +anyhow = "1.0" +bytemuck = "1.0" +vello_cpu = { version = "0.0.4", default-features = false, features = ["std", "text"] } +raw-window-handle = { workspace = true } +floem_renderer = { path = "../renderer", version = "0.2.0" } +softbuffer = "0.4.1" diff --git a/vello_cpu/src/lib.rs b/vello_cpu/src/lib.rs new file mode 100644 index 000000000..250bbeec5 --- /dev/null +++ b/vello_cpu/src/lib.rs @@ -0,0 +1,585 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::Result; +use floem_renderer::text::fontdb::ID; +use floem_renderer::text::{FONT_SYSTEM, LayoutGlyph, LayoutRun}; +use floem_renderer::{Img, Renderer}; +use peniko::kurbo::Size; +use peniko::{ + Blob, BrushRef, Color, + color::palette, + kurbo::{Affine, Point, Rect, Shape, Stroke}, +}; +use peniko::{ImageAlphaType, ImageBrush, ImageData}; +use softbuffer::{Context, Surface}; +use std::num::NonZeroU32; +use vello_cpu::{ImageSource, Mask, PaintType, Pixmap, RenderContext}; + +thread_local! { +#[allow(clippy::type_complexity)] +static IMAGE_CACHE: RefCell, Arc>> = RefCell::new(HashMap::new()); +} + +trait BrushRefExt<'a> { + fn to_vello_cpu_paint(self) -> PaintType; +} + +impl<'a> BrushRefExt<'a> for BrushRef<'a> { + fn to_vello_cpu_paint(self) -> PaintType { + match self { + BrushRef::Solid(alpha_color) => PaintType::Solid(alpha_color), + BrushRef::Gradient(gradient) => PaintType::Gradient(gradient.clone()), + BrushRef::Image(image) => PaintType::Image(ImageBrush { + image: ImageSource::from_peniko_image_data(image.image), + sampler: image.sampler, + }), + } + } +} + +pub struct VelloCpuRenderer { + context: RenderContext, + surface: Surface, + width: u32, + height: u32, + window_scale: f64, + transform: Affine, + capture: bool, + font_cache: HashMap, +} + +impl + VelloCpuRenderer +where + W: Clone, +{ + pub fn new( + window: W, + width: u32, + height: u32, + scale: f64, + _font_embolden: f32, + ) -> Result { + let renderer = RenderContext::new(width as u16, height as u16); + + let context = Context::new(window.clone()) + .map_err(|err| anyhow::anyhow!("unable to create context: {}", err))?; + let mut surface = Surface::new(&context, window) + .map_err(|err| anyhow::anyhow!("unable to create surface: {}", err))?; + surface + .resize( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ) + .map_err(|_| anyhow::anyhow!("failed to resize surface"))?; + + Ok(Self { + context: renderer, + surface, + width, + height, + window_scale: scale, + transform: Affine::IDENTITY, + capture: false, + font_cache: HashMap::new(), + }) + } + + pub fn resize(&mut self, width: u32, height: u32, scale: f64) { + self.width = width; + self.height = height; + self.window_scale = scale; + self.context = RenderContext::new(width as u16, height as u16); + + let _ = self.surface.resize( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ); + } + + pub fn set_scale(&mut self, scale: f64) { + self.window_scale = scale; + } + + pub const fn scale(&self) -> f64 { + self.window_scale + } + + pub const fn size(&self) -> Size { + Size::new(self.width as f64, self.height as f64) + } +} + +impl Renderer + for VelloCpuRenderer +where + W: Clone, +{ + fn begin(&mut self, capture: bool) { + self.capture = capture; + self.transform = Affine::IDENTITY; + // Reset the renderer for a new frame + self.context = RenderContext::new(self.width as u16, self.height as u16); + } + + fn stroke<'b, 's>( + &mut self, + shape: &impl Shape, + brush: impl Into>, + stroke: &'s Stroke, + ) { + let brush_ref = brush.into(); + self.context + .set_transform(self.transform.then_scale(self.window_scale)); + self.context.set_paint(brush_ref.to_vello_cpu_paint()); + self.context.set_stroke(stroke.clone()); + // Note: stroke_path implementation would go here + // For now, we'll implement a basic stroke as fill + self.context.stroke_path(&shape.into_path(0.1)); + } + + fn fill<'b>(&mut self, path: &impl Shape, brush: impl Into>, blur_radius: f64) { + let brush_ref = brush.into(); + self.context + .set_transform(self.transform.then_scale(self.window_scale)); + self.context.set_paint(brush_ref.to_vello_cpu_paint()); + if blur_radius > 0. + && let Some(rect) = path.as_rect() + { + self.context + .fill_blurred_rounded_rect(&rect, blur_radius as f32, 1.); + } else if let Some(rect) = path.as_rect() { + self.context.fill_rect(&rect); + } else { + self.context.fill_path(&path.to_path(0.1)); + } + } + + fn push_layer( + &mut self, + blend: impl Into, + alpha: f32, + transform: Affine, + clip: &impl Shape, + ) { + self.transform *= transform; + self.context + .set_transform(self.transform.then_scale(self.window_scale)); + let blend = blend.into(); + self.context + .push_layer(Some(&clip.to_path(0.1)), Some(blend), Some(alpha), None); + } + + fn pop_layer(&mut self) { + self.context.pop_layer(); + } + + fn draw_text_with_layout<'b>( + &mut self, + layout: impl Iterator>, + pos: impl Into, + ) { + let pos: Point = pos.into(); + self.context.reset_transform(); + let transform = self + .transform + .pre_translate((pos.x, pos.y).into()) + .then_scale(self.window_scale); + + for line in layout { + let mut current_run: Option = None; + + for glyph in line.glyphs { + let color = glyph.color_opt.map_or(palette::css::BLACK, |c| { + Color::from_rgba8(c.r(), c.g(), c.b(), c.a()) + }); + let font_size = glyph.font_size; + let font_id = glyph.font_id; + let metadata = glyph.metadata; + + if current_run.as_ref().is_none_or(|run| { + run.color != color + || run.font_size != font_size + || run.font_id != font_id + || run.metadata != metadata + }) { + if let Some(run) = current_run.take() { + self.draw_glyph_run( + run, + transform.pre_translate((0., line.line_y.into()).into()), + ); + } + current_run = Some(GlyphRun { + color, + font_size, + font_id, + metadata, + glyphs: Vec::new(), + }); + } + + if let Some(run) = &mut current_run { + run.glyphs.push(glyph); + } + } + + if let Some(run) = current_run.take() { + self.draw_glyph_run( + run, + transform.pre_translate((0., line.line_y.into()).into()), + ); + } + } + } + + fn draw_img(&mut self, img: Img<'_>, rect: Rect) { + // TODO: use an image cache + let paint = PaintType::Image(ImageBrush { + image: ImageSource::from_peniko_image_data(&img.img.image), + sampler: img.img.sampler, + }); + self.context.set_paint(paint); + + let rect_width = rect.width().max(1.); + let rect_height = rect.height().max(1.); + + let scale_x = rect_width / img.img.image.width as f64; + let scale_y = rect_height / img.img.image.height as f64; + + let translate_x = rect.min_x(); + let translate_y = rect.min_y(); + + let transform = self.transform.then_scale(self.window_scale); + + self.context.set_paint_transform( + Affine::IDENTITY + .pre_scale_non_uniform(scale_x, scale_y) + .then_translate((translate_x, translate_y).into()), + ); + self.context.set_transform(transform); + + self.context.fill_rect(&rect); + self.context.reset_paint_transform(); + self.context.reset_paint_transform(); + } + + fn draw_svg<'b>( + &mut self, + svg: floem_renderer::Svg<'b>, + rect: Rect, + brush: Option>>, + ) { + let width = (rect.width() * self.window_scale).round() as u32; + let height = (rect.height() * self.window_scale).round() as u32; + + // Check cache first + let cached_pixmap = IMAGE_CACHE.with_borrow_mut(|ic| ic.get(svg.hash).cloned()); + + if let Some(pixmap) = cached_pixmap { + self.render_svg_with_brush(&pixmap, rect, brush); + return; + } + + // Store the hash before moving svg + let svg_hash = svg.hash.to_owned(); + + // Create vello_cpu pixmap directly and render SVG into it + let vello_pixmap = Arc::new(self.render_svg_to_pixmap(svg, width, height)); + + // Render the SVG + self.render_svg_with_brush(&vello_pixmap, rect, brush); + + // Cache the result + IMAGE_CACHE.with_borrow_mut(|ic| { + ic.insert(svg_hash, vello_pixmap); + }); + } + + fn set_transform(&mut self, transform: Affine) { + self.transform = transform; + } + + fn set_z_index(&mut self, _z_index: i32) {} + + fn clip(&mut self, _shape: &impl Shape) { + // Clipping in vello_cpu would need to be implemented + // This is a placeholder + } + + fn clear_clip(&mut self) { + // Clear clipping in vello_cpu + // This is a placeholder + } + + fn finish(&mut self) -> Option { + if self.capture { + self.render_capture_image() + } else { + // Render to display surface like tiny_skia does + let mut buffer = self + .surface + .buffer_mut() + .expect("failed to get the surface buffer"); + + // Create a pixmap to render into + let mut pixmap = Pixmap::new(self.width as u16, self.height as u16); + + // Flush the context and render to pixmap + self.context.flush(); + self.context.render_to_pixmap(&mut pixmap); + + // Copy from vello_cpu::Pixmap to the format specified by softbuffer::Buffer + for (out_pixel, pixel) in buffer.iter_mut().zip(pixmap.data().iter()) { + *out_pixel = ((pixel.r as u32) << 16) | ((pixel.g as u32) << 8) | (pixel.b as u32); + } + + buffer + .present() + .expect("failed to present the surface buffer"); + + None + } + } + + fn debug_info(&self) -> String { + format!("name: Vello CPU\nsize: {}x{}", self.width, self.height) + } +} + +impl + VelloCpuRenderer +where + W: Clone, +{ + fn render_svg_to_pixmap( + &self, + svg: floem_renderer::Svg<'_>, + width: u32, + height: u32, + ) -> Pixmap { + // Create a tiny_skia pixmap for resvg to render into + let mut tiny_skia_pixmap = match resvg::tiny_skia::Pixmap::new(width, height) { + Some(pixmap) => pixmap, + None => return Pixmap::new(width as u16, height as u16), // fallback to empty pixmap + }; + + // Calculate transform for scaling SVG to fit the target size + let svg_transform = resvg::tiny_skia::Transform::from_scale( + width as f32 / svg.tree.size().width(), + height as f32 / svg.tree.size().height(), + ); + + // Render SVG using resvg + resvg::render(svg.tree, svg_transform, &mut tiny_skia_pixmap.as_mut()); + + // Take ownership of the pixel data from tiny_skia and convert to vello_cpu format + let tiny_skia_data = tiny_skia_pixmap.take(); + self.convert_raw_data_to_vello_pixmap(tiny_skia_data, width as u16, height as u16) + } + + fn convert_raw_data_to_vello_pixmap( + &self, + raw_data: Vec, + width: u16, + height: u16, + ) -> Pixmap { + // Both tiny_skia and vello_cpu use premultiplied RGBA with 4 bytes per pixel + // Verify the data size matches expectations + let expected_len = (width as usize) * (height as usize) * 4; + if raw_data.len() != expected_len { + // Fallback to creating empty pixmap if size mismatch + return Pixmap::new(width, height); + } + + let mut vello_pixmap = Pixmap::new(width, height); + + // Try to do a direct memory copy using bytemuck if possible + let dst_slice = vello_pixmap.data_mut(); + + // Attempt to cast both slices to byte slices for memcpy-like operation + if let Ok(dst_bytes) = bytemuck::try_cast_slice_mut::<_, u8>(dst_slice) { + // Direct memory copy - fastest possible conversion + dst_bytes.copy_from_slice(&raw_data); + } else { + // Fallback: per-pixel conversion + for (src_chunk, dst_pixel) in raw_data.chunks_exact(4).zip(dst_slice.iter_mut()) { + dst_pixel.r = src_chunk[0]; + dst_pixel.g = src_chunk[1]; + dst_pixel.b = src_chunk[2]; + dst_pixel.a = src_chunk[3]; + } + } + + vello_pixmap + } + + fn render_svg_with_brush<'b>( + &mut self, + pixmap: &Arc, + rect: Rect, + brush: Option>>, + ) { + self.context + .set_transform(self.transform.then_scale(self.window_scale)); + + if let Some(brush) = brush { + // Create a temporary context sized to the SVG for recoloring + let recolored_pixmap = self.recolor_svg_pixmap(pixmap, brush); + + // Draw the recolored pixmap + let paint = PaintType::Image(ImageBrush { + image: ImageSource::Pixmap(recolored_pixmap), + sampler: peniko::ImageSampler::new(), + }); + self.context.set_paint(paint); + + let rect_width = rect.width().max(1.); + let rect_height = rect.height().max(1.); + let scale_x = rect_width / pixmap.width() as f64; + let scale_y = rect_height / pixmap.height() as f64; + let translate_x = rect.min_x(); + let translate_y = rect.min_y(); + + self.context.set_paint_transform( + Affine::IDENTITY + .pre_scale_non_uniform(scale_x, scale_y) + .then_translate((translate_x, translate_y).into()), + ); + self.context.fill_rect(&rect); + self.context.reset_paint_transform(); + } else { + // Render the SVG directly without recoloring + let paint = PaintType::Image(ImageBrush { + image: ImageSource::Pixmap(pixmap.clone()), + sampler: peniko::ImageSampler::new(), + }); + self.context.set_paint(paint); + + let rect_width = rect.width().max(1.); + let rect_height = rect.height().max(1.); + let scale_x = rect_width / pixmap.width() as f64; + let scale_y = rect_height / pixmap.height() as f64; + let translate_x = rect.min_x(); + let translate_y = rect.min_y(); + + self.context.set_paint_transform( + Affine::IDENTITY + .pre_scale_non_uniform(scale_x, scale_y) + .then_translate((translate_x, translate_y).into()), + ); + self.context.fill_rect(&rect); + self.context.reset_paint_transform(); + } + } + + fn recolor_svg_pixmap<'b>( + &self, + original_pixmap: &Arc, + brush: impl Into>, + ) -> Arc { + let width = original_pixmap.width(); + let height = original_pixmap.height(); + + // Create a temporary context sized to the SVG + let mut temp_context = RenderContext::new(width, height); + + // Set up the brush paint + let brush_ref = brush.into(); + temp_context.set_paint(brush_ref.to_vello_cpu_paint()); + + // Create mask from the original SVG pixmap + let mask = Mask::new_alpha(original_pixmap); + + // Apply the mask and fill the entire SVG area with the brush color + temp_context.push_mask_layer(mask); + temp_context.fill_rect(&Rect::from_origin_size( + (0.0, 0.0), + (width as f64, height as f64), + )); + temp_context.pop_layer(); + + // Render to a new pixmap + let mut result_pixmap = Pixmap::new(width, height); + temp_context.flush(); + temp_context.render_to_pixmap(&mut result_pixmap); + + Arc::new(result_pixmap) + } + + fn render_capture_image(&mut self) -> Option { + if !self.capture { + return None; + } + + let width = self.width as u16; + let height = self.height as u16; + + // Create a pixmap to render into + let mut target = Pixmap::new(width, height); + + // Flush the context and render to pixmap + self.context.flush(); + self.context.render_to_pixmap(&mut target); + + // Convert pixmap data to the format expected by ImageBrush + let data = target.data(); + let mut buffer = Vec::with_capacity(data.len() * 4); + for pixel in data { + buffer.extend_from_slice(&[pixel.r, pixel.g, pixel.b, pixel.a]); + } + + Some(peniko::ImageBrush::new(ImageData { + data: Blob::new(Arc::new(buffer)), + format: peniko::ImageFormat::Rgba8, + alpha_type: ImageAlphaType::AlphaPremultiplied, + width: self.width, + height: self.height, + })) + } + + fn get_font(&mut self, font_id: ID) -> peniko::FontData { + self.font_cache.get(&font_id).cloned().unwrap_or_else(|| { + let mut font_system = FONT_SYSTEM.lock(); + let font = font_system.get_font(font_id).unwrap(); + let face = font_system.db().face(font_id).unwrap(); + let font_data = font.data(); + let font_index = face.index; + drop(font_system); + let font = peniko::FontData::new(Blob::new(Arc::new(font_data.to_vec())), font_index); + self.font_cache.insert(font_id, font.clone()); + font + }) + } + + fn draw_glyph_run(&mut self, run: GlyphRun, transform: Affine) { + let font = self.get_font(run.font_id); + + // Set the paint color for the glyphs + self.context.set_paint(run.color); + self.context.set_transform(transform); + + // Convert glyphs to vello_cpu format + let glyphs = run.glyphs.iter().map(|glyph| vello_cpu::Glyph { + id: glyph.glyph_id as u32, + x: glyph.x, + y: glyph.y, + }); + + // Render glyphs using vello_cpu's glyph_run method + self.context + .glyph_run(&font) + .font_size(run.font_size) + .hint(true) + .fill_glyphs(glyphs); + } +} + +struct GlyphRun<'a> { + color: Color, + font_size: f32, + font_id: ID, + metadata: usize, + glyphs: Vec<&'a LayoutGlyph>, +}