From df77e5e78e7973343b94e1bc49d652ee44b911f7 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 18 Mar 2026 19:06:45 -0600 Subject: [PATCH 001/102] repaint benchmarks --- benches/painting.rs | 100 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/benches/painting.rs b/benches/painting.rs index 4bb0673c3..98afc9a00 100644 --- a/benches/painting.rs +++ b/benches/painting.rs @@ -11,12 +11,12 @@ use std::hint::black_box; -use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; use floem::headless::{HeadlessHarness, TestRoot}; use floem::prelude::*; use floem::style::FlexWrap; use floem::unit::Pct; -use floem::views::{Container, Decorators, Empty, Stack}; +use floem::views::{Container, Decorators, Empty, Label, ScrollExt, Stack}; // ============================================================================= // View tree creation helpers @@ -105,6 +105,29 @@ fn create_wide_tree_depth2(width: usize) -> impl IntoView { Stack::from_iter(children).style(|s| s.size(100.0, 100.0)) } +/// Create a large vertically scrollable list of labels. +fn create_scroll_label_list(n: usize) -> impl IntoView { + let rows: Vec<_> = (0..n) + .map(|i| { + Label::new(format!( + "Row {i:05} | benchmark label content for retained scroll repaint" + )) + .style(|s| { + s.width_full() + .height(24.0) + .padding_horiz(6.0) + .padding_vert(2.0) + }) + .into_any() + }) + .collect(); + + Stack::from_iter(rows) + .style(|s| s.flex_col()) + .scroll() + .style(|s| s.size(1000.0, 240.0)) +} + // ============================================================================= // Flat tree painting benchmarks // ============================================================================= @@ -285,6 +308,78 @@ fn bench_repaint_after_change(c: &mut Criterion) { group.finish(); } +// ============================================================================= +// Scroll repaint benchmarks +// ============================================================================= + +fn bench_scroll_repaint(c: &mut Criterion) { + let mut group = c.benchmark_group("scroll_repaint"); + + for n in [100, 1_000].iter() { + group.bench_with_input(BenchmarkId::new("initial_paint", n), n, |b, &n| { + b.iter_batched( + || { + let root = TestRoot::new(); + let view = create_scroll_label_list(n); + HeadlessHarness::new_with_size(root, view, 1000.0, 240.0) + }, + |mut harness| { + harness.paint(); + black_box(()); + }, + BatchSize::SmallInput, + ); + }); + + group.bench_with_input( + BenchmarkId::new("first_repaint_after_scroll", n), + n, + |b, &n| { + b.iter_batched( + || { + let root = TestRoot::new(); + let view = create_scroll_label_list(n); + let mut harness = HeadlessHarness::new_with_size(root, view, 1000.0, 240.0); + harness.paint(); + harness + }, + |mut harness| { + harness.scroll_down(160.0, 120.0, 48.0); + harness.paint(); + black_box(()); + }, + BatchSize::SmallInput, + ); + }, + ); + + group.bench_with_input( + BenchmarkId::new("steady_repaint_after_scroll", n), + n, + |b, &n| { + let root = TestRoot::new(); + let view = create_scroll_label_list(n); + let mut harness = HeadlessHarness::new_with_size(root, view, 320.0, 240.0); + harness.paint(); + let mut scroll_down = true; + + b.iter(|| { + if scroll_down { + harness.scroll_down(160.0, 120.0, 48.0); + } else { + harness.scroll_up(160.0, 120.0, 48.0); + } + scroll_down = !scroll_down; + harness.paint(); + black_box(()); + }); + }, + ); + } + + group.finish(); +} + criterion_group!( benches, bench_paint_flat_tree, @@ -293,6 +388,7 @@ criterion_group!( bench_paint_wide_tree, bench_paint_order_tracking, bench_repaint_after_change, + bench_scroll_repaint, ); criterion_main!(benches); From 71a70b1211f3d423e1a629aed5db54ed45c2eab0 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 17 Mar 2026 21:52:58 -0600 Subject: [PATCH 002/102] imaging: retained scene --- Cargo.lock | 10 + Cargo.toml | 2 + examples/widget-gallery/src/canvas.rs | 8 +- src/app/handle.rs | 7 +- src/box_tree.rs | 10 + src/context.rs | 3 +- src/headless.rs | 4 +- src/inspector/view.rs | 8 +- src/paint/display_list.rs | 603 ++++++++++++++++++++++++++ src/paint/mod.rs | 326 ++++++++++---- src/style/cx.rs | 4 +- src/style/transition.rs | 1 - src/style/values.rs | 1 - src/text/layout.rs | 6 +- src/view/mod.rs | 1 - src/views/editor/gutter.rs | 1 - src/views/editor/view.rs | 1 - src/views/img.rs | 4 +- src/views/label.rs | 1 - src/views/scroll.rs | 2 +- src/views/slider.rs | 3 +- src/views/svg.rs | 1 - src/views/text_input.rs | 7 +- src/views/toggle_button.rs | 2 +- src/window/handle.rs | 15 +- src/window/state.rs | 42 +- 26 files changed, 943 insertions(+), 130 deletions(-) create mode 100644 src/paint/display_list.rs diff --git a/Cargo.lock b/Cargo.lock index d61b933c0..ba1e8c794 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1557,6 +1557,7 @@ dependencies = [ "fluent-bundle", "futures", "image 0.25.9", + "imaging", "imbl", "indexmap", "lapce-xi-rope", @@ -2994,6 +2995,15 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "imaging" +version = "0.0.1" +source = "git+https://github.com/forest-rs/imaging?rev=0527cbabf0d6ea27e14a82955f4a582c57bdc440#0527cbabf0d6ea27e14a82955f4a582c57bdc440" +dependencies = [ + "kurbo", + "peniko", +] + [[package]] name = "imbl" version = "7.0.0" diff --git a/Cargo.toml b/Cargo.toml index 767b7d697..9e9cbfc80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ ui-events = { version = "0.3", features = ["kurbo"] } ui-events-winit = { path = "./ui-events-winit", package = "ui-events-floem-winit" } dpi = { version = "0.1.2", default-features = false } rustc-hash = "2.1" +imaging = { git = "https://github.com/forest-rs/imaging", rev = "0527cbabf0d6ea27e14a82955f4a582c57bdc440", package = "imaging" } [dependencies] slotmap = "1.0" @@ -74,6 +75,7 @@ sha2 = "0.10" bitflags = "2.9" indexmap = "2" rustc-hash.workspace = true +imaging.workspace = true smallvec = "1.15" educe = "0.6" taffy = { version = "0.9.2", features = ["grid"] } diff --git a/examples/widget-gallery/src/canvas.rs b/examples/widget-gallery/src/canvas.rs index bdf5077fd..9478b305b 100644 --- a/examples/widget-gallery/src/canvas.rs +++ b/examples/widget-gallery/src/canvas.rs @@ -48,7 +48,7 @@ pub fn canvas_view() -> impl IntoView { rounded.track(); let now = Instant::now(); if border_radius.borrow_mut().step(&now) { - cx.window_state.schedule_paint(cx.target_id.owning_id()); + cx.window_state.schedule_paint(cx.target_id); } cx.fill( &Rect::ZERO @@ -295,7 +295,7 @@ impl View for SatValuePicker { cx.stroke(&inner, css::BLACK, &Stroke::new(1.0)); } - cx.clear_clip(); + cx.pop_clip(); } } @@ -417,7 +417,7 @@ impl View for HuePicker { cx.clip(&rect_path); cx.stroke(&indicator_rect, css::WHITE, &Stroke::new(2.0)); cx.fill(&indicator_rect, css::BLACK, 0.); - cx.clear_clip(); + cx.pop_clip(); } } } @@ -539,7 +539,7 @@ impl View for OpacityPicker { cx.clip(&rect_path); cx.stroke(&indicator_rect, css::WHITE, &Stroke::new(2.0)); cx.fill(&indicator_rect, css::BLACK, 0.); - cx.clear_clip(); + cx.pop_clip(); } } } diff --git a/src/app/handle.rs b/src/app/handle.rs index 5fa117c2c..c40f9bd27 100644 --- a/src/app/handle.rs +++ b/src/app/handle.rs @@ -107,7 +107,10 @@ impl ApplicationHandle { *font_embolden, ); self.gpu_resources = Some(gpu_resources); - handle.paint_state = PaintState::Initialized { renderer }; + handle.paint_state = PaintState::Initialized { + renderer, + display_list: crate::paint::display_list::RetainedDisplayList::default(), + }; handle.gpu_resources = self.gpu_resources.clone(); handle.init_renderer(); } else { @@ -674,7 +677,7 @@ impl ApplicationHandle { any_work_remaining = true; } - if handle.window_state.request_paint && handle.can_render_now() { + if handle.window_state.has_pending_paint() && handle.can_render_now() { handle.window.request_redraw(); let frame_interval = handle .window diff --git a/src/box_tree.rs b/src/box_tree.rs index beb9e8506..e3a95ee18 100644 --- a/src/box_tree.rs +++ b/src/box_tree.rs @@ -303,6 +303,16 @@ impl BoxTree { } self.tree.remove(id); } + + pub fn element_ids_for_view(&self, view_id: ViewId) -> Vec { + self.metadata + .values() + .filter_map(|meta| { + (meta.element_id.owning_id() == view_id && self.tree.is_alive(meta.element_id.0)) + .then_some(meta.element_id) + }) + .collect() + } } impl Default for BoxTree { diff --git a/src/context.rs b/src/context.rs index 95e231023..5e3b3bd36 100644 --- a/src/context.rs +++ b/src/context.rs @@ -7,7 +7,6 @@ use crate::{ event::{EventPropagation, Phase}, platform::menu::Menu, style::recalc::StyleReason, - view::ViewId, }; pub type EventCallback = dyn FnMut(&mut EventCx) -> EventPropagation; @@ -272,7 +271,7 @@ pub(crate) enum FrameUpdate { Style(ElementId, StyleReason), Layout, BoxTreeCommit, - Paint(ViewId), + Paint(ElementId), } // Re-export EventCx from event module for backward compatibility diff --git a/src/headless.rs b/src/headless.rs index 694c6b5de..c5f68ac4e 100644 --- a/src/headless.rs +++ b/src/headless.rs @@ -420,14 +420,14 @@ impl HeadlessHarness { /// /// This is useful for verifying that style changes trigger repaints. pub fn paint_requested(&self) -> bool { - self.window_handle.window_state.request_paint + self.window_handle.window_state.has_pending_paint() } /// Clear the paint request flag. /// /// Call this before an operation to then check if it triggered a repaint. pub fn clear_paint_request(&mut self) { - self.window_handle.window_state.request_paint = false; + self.window_handle.window_state.clear_pending_paint(); } /// Check if a view has pending style changes. diff --git a/src/inspector/view.rs b/src/inspector/view.rs index ff67bc6e2..ecdbe64f0 100644 --- a/src/inspector/view.rs +++ b/src/inspector/view.rs @@ -737,7 +737,7 @@ impl View for InspectorImageView { self.contain_index - 1 }; if let Some(id) = self.contain_ids.get(self.contain_index).copied() { - cx.window_state.request_paint = true; + cx.window_state.request_paint(self.id); self.id.owning_id().request_paint(); update_select_view_id(id, &self.capture_view, false, self.datas); return EventPropagation::Stop; @@ -748,7 +748,7 @@ impl View for InspectorImageView { if !self.contain_ids.is_empty() { self.contain_index = (self.contain_index + 1) % self.contain_ids.len(); if let Some(id) = self.contain_ids.get(self.contain_index).copied() { - cx.window_state.request_paint = true; + cx.window_state.request_paint(self.id); self.id.owning_id().request_paint(); update_select_view_id(id, &self.capture_view, false, self.datas); return EventPropagation::Stop; @@ -768,7 +768,7 @@ impl View for InspectorImageView { .filter_map(|data_id| self.data_id_to_view.get(data_id).copied()), ); } - cx.window_state.request_paint = true; + cx.window_state.request_paint(self.id); self.contain_index = 0; self.capture_view .highlighted @@ -786,7 +786,7 @@ impl View for InspectorImageView { } self.contain_index = 0; if let Some(id) = self.contain_ids.last().copied() { - cx.window_state.request_paint = true; + cx.window_state.request_paint(self.id); self.id.owning_id().request_paint(); update_select_view_id(id, &self.capture_view, false, self.datas); return EventPropagation::Stop; diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs new file mode 100644 index 000000000..6929037a9 --- /dev/null +++ b/src/paint/display_list.rs @@ -0,0 +1,603 @@ +use std::sync::Arc; + +use floem_renderer::text::{Glyph, GlyphRunProps}; +use floem_renderer::{Img, Renderer as FloemRenderer, Svg, usvg}; +use imaging::{ + Clip, Composite, Draw, FillRule, Geometry, Glyph as SceneGlyph, GlyphRun, Group, StrokeStyle, +}; +use peniko::BrushRef; +use peniko::kurbo::{ + Affine, BezPath, Circle, Line, Point, Rect, RoundedRect, Shape, Stroke, +}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ElementId, paint::PaintOrPost}; + +/// Transform class describing when recorded content remains valid. +#[allow(dead_code)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(crate) enum TransformClass { + Exact, + TranslateOnly, + Orthonormal, + Affine, +} + +impl TransformClass { + pub(crate) fn supports(self, diff: Self) -> bool { + match self { + Self::Exact => matches!(diff, Self::Exact), + Self::TranslateOnly => matches!(diff, Self::Exact | Self::TranslateOnly), + Self::Orthonormal => !matches!(diff, Self::Affine), + Self::Affine => true, + } + } +} + +pub(crate) fn transform_diff_class(original: Affine, current: Affine) -> TransformClass { + if current == original { + return TransformClass::Exact; + } + + let o = original.as_coeffs(); + let c = current.as_coeffs(); + let same_linear = o[0] == c[0] && o[1] == c[1] && o[2] == c[2] && o[3] == c[3]; + if same_linear { + TransformClass::TranslateOnly + } else { + TransformClass::Affine + } +} + +#[derive(Clone, Debug)] +pub(crate) enum ShapeHint { + Rect(Rect), + RoundedRect(RoundedRect), + Line(Line), + Circle(Circle), +} + +#[derive(Clone)] +pub(crate) struct OwnedSvg { + pub tree: Arc, + pub hash: Arc<[u8]>, +} + +#[derive(Clone)] +pub(crate) enum DisplayCommand { + SetZIndex(i32), + PushClip { + clip: Clip, + hint: Option, + }, + PopClip, + PushLayer { + group: Group, + transform: Affine, + clip_hint: Option, + }, + PopLayer, + Draw { + draw: Draw, + hint: Option, + }, + DrawImage { + img: peniko::ImageBrush, + hash: Arc<[u8]>, + rect: Rect, + transform: Affine, + }, + DrawSvg { + svg: OwnedSvg, + rect: Rect, + transform: Affine, + brush: Option, + }, +} + +#[derive(Clone)] +pub(crate) struct ElementStage { + pub commands: Vec, + pub transform_class: TransformClass, +} + +impl Default for ElementStage { + fn default() -> Self { + Self { + commands: Vec::new(), + transform_class: TransformClass::Affine, + } + } +} + +#[derive(Clone, Copy)] +pub(crate) struct ElementSnapshot { + pub local_bounds: Rect, + pub clip: Option, + pub world_transform: Affine, +} + +#[derive(Clone, Default)] +pub(crate) struct ElementDisplayList { + pub paint: ElementStage, + pub post: ElementStage, + pub snapshot: Option, +} + +#[derive(Default)] +pub struct RetainedDisplayList { + paint_order: Vec, + elements: FxHashMap, +} + +impl RetainedDisplayList { + pub(crate) fn set_paint_order(&mut self, paint_order: Vec) { + self.paint_order = paint_order; + } + + pub(crate) fn paint_order(&self) -> &[PaintOrPost] { + &self.paint_order + } + + pub(crate) fn element_mut(&mut self, id: ElementId) -> &mut ElementDisplayList { + self.elements.entry(id).or_default() + } + + pub(crate) fn element(&self, id: ElementId) -> Option<&ElementDisplayList> { + self.elements.get(&id) + } + + pub(crate) fn needs_rerecord(&self, id: ElementId, snapshot: ElementSnapshot) -> bool { + let Some(element) = self.element(id) else { + return true; + }; + let Some(previous) = element.snapshot else { + return true; + }; + + if previous.local_bounds != snapshot.local_bounds || previous.clip != snapshot.clip { + return true; + } + + let diff = transform_diff_class(previous.world_transform, snapshot.world_transform); + !element.paint.transform_class.supports(diff) || !element.post.transform_class.supports(diff) + } + + pub(crate) fn retain_only(&mut self, ids: &FxHashSet) { + self.elements.retain(|id, _| ids.contains(id)); + } +} + +pub(crate) struct RecordingRenderer<'a> { + commands: &'a mut Vec, + transform: Affine, +} + +impl<'a> RecordingRenderer<'a> { + pub fn new(commands: &'a mut Vec) -> Self { + Self { + commands, + transform: Affine::IDENTITY, + } + } + + fn record_draw(&mut self, draw: Draw, hint: Option) { + self.commands.push(DisplayCommand::Draw { draw, hint }); + } +} + +impl RecordingRenderer<'_> { + pub fn set_transform(&mut self, transform: Affine) { + self.transform = transform; + } + + pub fn set_z_index(&mut self, z_index: i32) { + self.commands.push(DisplayCommand::SetZIndex(z_index)); + } + + pub fn clip(&mut self, shape: &impl Shape) { + let (shape, hint) = shape_to_geometry(shape); + self.commands.push(DisplayCommand::PushClip { + clip: Clip::Fill { + transform: self.transform, + shape, + fill_rule: FillRule::NonZero, + }, + hint, + }); + } + + pub fn clear_clip(&mut self) { + self.commands.push(DisplayCommand::PopClip); + } + + pub fn stroke<'b, 's>( + &mut self, + shape: &impl Shape, + brush: impl Into>, + stroke: &'s Stroke, + ) { + let (shape, hint) = shape_to_geometry(shape); + self.record_draw( + Draw::Stroke { + transform: self.transform, + stroke: StrokeStyle::clone(stroke), + paint: brush.into().to_owned(), + paint_transform: None, + shape, + composite: Composite::default(), + }, + hint, + ); + } + + pub fn fill<'b>( + &mut self, + shape: &impl Shape, + brush: impl Into>, + blur_radius: f64, + ) { + let brush = brush.into(); + if blur_radius > 0.0 + && let BrushRef::Solid(color) = brush + && let Some((rect, radius)) = blurred_rounded_rect(shape) + { + self.record_draw( + Draw::BlurredRoundedRect(imaging::BlurredRoundedRect { + transform: self.transform, + rect, + color, + radius, + std_dev: blur_radius, + composite: Composite::default(), + }), + shape_hint(shape), + ); + return; + } + + let (shape, hint) = shape_to_geometry(shape); + self.record_draw( + Draw::Fill { + transform: self.transform, + fill_rule: FillRule::NonZero, + paint: brush.to_owned(), + paint_transform: None, + shape, + composite: Composite::default(), + }, + hint, + ); + } + + pub fn push_layer( + &mut self, + blend: impl Into, + alpha: f32, + transform: Affine, + clip: &impl Shape, + ) { + let (shape, hint) = shape_to_geometry(clip); + self.commands.push(DisplayCommand::PushLayer { + group: Group { + clip: Some(Clip::Fill { + transform: self.transform, + shape, + fill_rule: FillRule::NonZero, + }), + filters: Vec::new(), + composite: Composite::new(blend.into(), alpha), + }, + transform, + clip_hint: hint, + }); + } + + pub fn pop_layer(&mut self) { + self.commands.push(DisplayCommand::PopLayer); + } + + pub fn draw_glyphs<'a>( + &mut self, + origin: Point, + props: &GlyphRunProps<'a>, + glyphs: impl Iterator + 'a, + ) { + let glyph_run = GlyphRun { + font: props.font.clone(), + transform: self.transform * Affine::translate((origin.x, origin.y)) * props.transform, + glyph_transform: props.glyph_transform, + font_size: props.font_size, + hint: props.hint, + normalized_coords: props.normalized_coords.to_vec(), + style: props.style.to_owned(), + glyphs: glyphs + .map(|glyph| SceneGlyph { + id: glyph.id, + x: glyph.x, + y: glyph.y, + }) + .collect(), + paint: props.brush.to_owned(), + composite: Composite::new(peniko::BlendMode::default(), props.brush_alpha), + }; + self.record_draw(Draw::GlyphRun(glyph_run), None); + } + + pub fn draw_svg<'b>( + &mut self, + svg: Svg<'b>, + rect: Rect, + brush: Option>>, + ) { + self.commands.push(DisplayCommand::DrawSvg { + svg: OwnedSvg { + tree: Arc::new(svg.tree.clone()), + hash: Arc::from(svg.hash.to_vec()), + }, + rect, + transform: self.transform, + brush: brush.map(|brush| brush.into().to_owned()), + }); + } + + pub fn draw_img(&mut self, img: Img<'_>, rect: Rect) { + self.commands.push(DisplayCommand::DrawImage { + img: img.img, + hash: Arc::from(img.hash.to_vec()), + rect, + transform: self.transform, + }); + } + +} + +pub(crate) fn replay_stage( + stage: &ElementStage, + renderer: &mut impl FloemRenderer, + base_transform: Affine, +) { + for command in &stage.commands { + match command { + DisplayCommand::SetZIndex(z_index) => renderer.set_z_index(*z_index), + DisplayCommand::PushClip { clip, hint } => replay_clip(renderer, clip, hint.clone(), base_transform), + DisplayCommand::PopClip => renderer.clear_clip(), + DisplayCommand::PushLayer { + group, + transform, + clip_hint, + } => { + let Some(Clip::Fill { + transform: clip_transform, + shape, + .. + }) = group.clip.as_ref() + else { + continue; + }; + renderer.set_transform(base_transform * *clip_transform); + match (clip_hint.clone(), shape) { + (Some(ShapeHint::Rect(rect)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &rect, + ), + (Some(ShapeHint::RoundedRect(rect)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &rect, + ), + (Some(ShapeHint::Line(line)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &line, + ), + (Some(ShapeHint::Circle(circle)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &circle, + ), + (None, Geometry::Rect(rect)) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + rect, + ), + (None, Geometry::RoundedRect(rect)) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + rect, + ), + (None, Geometry::Path(path)) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + path, + ), + } + } + DisplayCommand::PopLayer => renderer.pop_layer(), + DisplayCommand::Draw { draw, hint } => replay_draw(renderer, draw, hint.clone(), base_transform), + DisplayCommand::DrawImage { + img, + hash, + rect, + transform, + } => { + renderer.set_transform(base_transform * *transform); + renderer.draw_img( + Img { + img: img.clone(), + hash, + }, + *rect, + ); + } + DisplayCommand::DrawSvg { + svg, + rect, + transform, + brush, + } => { + renderer.set_transform(base_transform * *transform); + renderer.draw_svg( + Svg { + tree: svg.tree.as_ref(), + hash: svg.hash.as_ref(), + }, + *rect, + brush.as_ref(), + ); + } + } + } +} + +fn replay_clip(renderer: &mut impl FloemRenderer, clip: &Clip, hint: Option, base_transform: Affine) { + let Clip::Fill { + transform, shape, .. + } = clip + else { + return; + }; + renderer.set_transform(base_transform * *transform); + match (hint, shape) { + (Some(ShapeHint::Rect(rect)), _) => renderer.clip(&rect), + (Some(ShapeHint::RoundedRect(rect)), _) => renderer.clip(&rect), + (Some(ShapeHint::Line(line)), _) => renderer.clip(&line), + (Some(ShapeHint::Circle(circle)), _) => renderer.clip(&circle), + (None, Geometry::Rect(rect)) => renderer.clip(rect), + (None, Geometry::RoundedRect(rect)) => renderer.clip(rect), + (None, Geometry::Path(path)) => renderer.clip(path), + } +} + +fn replay_draw( + renderer: &mut impl FloemRenderer, + draw: &Draw, + hint: Option, + base_transform: Affine, +) { + match draw { + Draw::Fill { + transform, + paint, + shape, + .. + } => { + renderer.set_transform(base_transform * *transform); + match (hint, shape) { + (Some(ShapeHint::Rect(rect)), _) => renderer.fill(&rect, paint, 0.0), + (Some(ShapeHint::RoundedRect(rect)), _) => renderer.fill(&rect, paint, 0.0), + (Some(ShapeHint::Line(line)), _) => renderer.fill(&line, paint, 0.0), + (Some(ShapeHint::Circle(circle)), _) => renderer.fill(&circle, paint, 0.0), + (None, Geometry::Rect(rect)) => renderer.fill(rect, paint, 0.0), + (None, Geometry::RoundedRect(rect)) => renderer.fill(rect, paint, 0.0), + (None, Geometry::Path(path)) => renderer.fill(path, paint, 0.0), + } + } + Draw::Stroke { + transform, + stroke, + paint, + shape, + .. + } => { + renderer.set_transform(base_transform * *transform); + match (hint, shape) { + (Some(ShapeHint::Rect(rect)), _) => renderer.stroke(&rect, paint, stroke), + (Some(ShapeHint::RoundedRect(rect)), _) => renderer.stroke(&rect, paint, stroke), + (Some(ShapeHint::Line(line)), _) => renderer.stroke(&line, paint, stroke), + (Some(ShapeHint::Circle(circle)), _) => renderer.stroke(&circle, paint, stroke), + (None, Geometry::Rect(rect)) => renderer.stroke(rect, paint, stroke), + (None, Geometry::RoundedRect(rect)) => renderer.stroke(rect, paint, stroke), + (None, Geometry::Path(path)) => renderer.stroke(path, paint, stroke), + } + } + Draw::GlyphRun(run) => { + let props = GlyphRunProps { + font: run.font.clone(), + font_size: run.font_size, + hint: run.hint, + normalized_coords: &run.normalized_coords, + style: (&run.style).into(), + brush: (&run.paint).into(), + brush_alpha: run.composite.alpha, + transform: run.transform, + glyph_transform: run.glyph_transform, + }; + renderer.set_transform(base_transform); + renderer.draw_glyphs( + Point::ZERO, + &props, + run.glyphs.iter().map(|glyph| Glyph { + id: glyph.id, + style_index: 0, + x: glyph.x, + y: glyph.y, + advance: 0.0, + }), + ); + } + Draw::BlurredRoundedRect(rect) => { + renderer.set_transform(base_transform * rect.transform); + let shape = rect.rect.to_rounded_rect(rect.radius); + renderer.fill(&shape, rect.color, rect.std_dev); + } + } +} + +fn shape_to_geometry(shape: &impl Shape) -> (Geometry, Option) { + if let Some(rect) = shape.as_rect() { + return (Geometry::Rect(rect), Some(ShapeHint::Rect(rect))); + } + if let Some(rect) = shape.as_rounded_rect() { + return ( + Geometry::RoundedRect(rect), + Some(ShapeHint::RoundedRect(rect)), + ); + } + if let Some(line) = shape.as_line() { + return ( + Geometry::Path(path_from_shape(shape)), + Some(ShapeHint::Line(line)), + ); + } + if let Some(circle) = shape.as_circle() { + return ( + Geometry::Path(path_from_shape(shape)), + Some(ShapeHint::Circle(circle)), + ); + } + (Geometry::Path(path_from_shape(shape)), None) +} + +fn shape_hint(shape: &impl Shape) -> Option { + shape + .as_rect() + .map(ShapeHint::Rect) + .or_else(|| shape.as_rounded_rect().map(ShapeHint::RoundedRect)) + .or_else(|| shape.as_line().map(ShapeHint::Line)) + .or_else(|| shape.as_circle().map(ShapeHint::Circle)) +} + +fn path_from_shape(shape: &impl Shape) -> BezPath { + shape.to_path(0.1) +} + +fn blurred_rounded_rect(shape: &impl Shape) -> Option<(Rect, f64)> { + if let Some(rect) = shape.as_rect() { + return Some((rect, 0.0)); + } + let rect = shape.as_rounded_rect()?; + let radii = rect.radii(); + (radii.top_left == radii.top_right + && radii.top_left == radii.bottom_left + && radii.top_left == radii.bottom_right) + .then_some((rect.rect(), radii.top_left)) +} diff --git a/src/paint/mod.rs b/src/paint/mod.rs index c5a5801cc..73e2b564e 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -6,15 +6,14 @@ //! - [`Renderer`] - Backend renderer abstraction pub mod border_path_iter; +pub mod display_list; pub mod renderer; pub use border_path_iter::{BorderPath, BorderPathEvent}; pub use renderer::Renderer; -use floem_renderer::Renderer as FloemRenderer; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; use peniko::kurbo::{Affine, RoundedRect, Shape, Size}; -use std::ops::{Deref, DerefMut}; use std::sync::Arc; use understory_box_tree::NodeFlags; use winit::window::Window; @@ -30,6 +29,9 @@ use crate::view::ViewId; use crate::view::stacking::{StackingContextItem, collect_stacking_context_items_into}; use crate::view::{paint_bg, paint_border, paint_outline}; use crate::window::state::WindowState; +use display_list::{ + ElementSnapshot, RecordingRenderer, RetainedDisplayList, TransformClass, replay_stage, +}; std::thread_local! { /// Holds the ID of a View being painted very briefly if it is being rendered as @@ -125,7 +127,9 @@ pub struct GlobalPaintCx<'a> { pub struct PaintCx<'a> { /// Reference to global paint state pub window_state: &'a mut WindowState, - paint_state: &'a mut PaintState, + recorder: &'a mut RecordingRenderer<'a>, + uses_layer_clip: bool, + is_vger: bool, /// The target visual node being painted (CRITICAL for views with multiple visuals) pub target_id: ElementId, /// World transform for this visual node (from box tree) @@ -137,6 +141,7 @@ pub struct PaintCx<'a> { pub font_size_cx: FontSizeCx, } +#[derive(Clone, Copy)] pub(crate) enum PaintOrPost { Paint(ElementId), Post(ElementId), @@ -254,84 +259,172 @@ impl GlobalPaintCx<'_> { let paint_order = self.build_paint_order(root_element_id, &mut box_tree); drop(box_tree); - for id_or_pop in paint_order { + let active_ids = paint_order + .iter() + .map(|step| match *step { + PaintOrPost::Paint(id) | PaintOrPost::Post(id) => id, + }) + .collect(); + + self.paint_state + .display_list_mut() + .retain_only(&active_ids); + self.paint_state + .display_list_mut() + .set_paint_order(paint_order.clone()); + + let mut dirty_ids = self.window_state.take_dirty_paint_elements(); + let snapshots = { + let mut box_tree = self.window_state.box_tree.borrow_mut(); + active_ids + .iter() + .copied() + .map(|element_id| { + let snapshot = ElementSnapshot { + local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), + clip: box_tree.clipped_local_clip(element_id.0), + world_transform: box_tree + .get_or_compute_world_transform(element_id.0) + .unwrap_or_default(), + }; + (element_id, snapshot) + }) + .collect::>() + }; + + for (element_id, snapshot) in snapshots { + if self + .paint_state + .display_list() + .needs_rerecord(element_id, snapshot) + { + dirty_ids.insert(element_id); + } + } + for element_id in dirty_ids { + self.record_visual_node(element_id, false); + self.record_visual_node(element_id, true); + } + + let replay_order = self.paint_state.display_list().paint_order().to_vec(); + for id_or_pop in replay_order { match id_or_pop { PaintOrPost::Paint(element_id) => { // Record for testing if self.record_paint_order { record_paint(element_id.owning_id()); } - - // Create per-target PaintCx and paint this visual node - self.paint_visual_node(element_id, false); + self.replay_visual_node(element_id, false); } PaintOrPost::Post(element_id) => { - self.paint_visual_node(element_id, true); + self.replay_visual_node(element_id, true); } } } } - /// Paint a single visual node with its absolute transform - pub(crate) fn paint_visual_node(&mut self, element_id: ElementId, is_post: bool) { + fn element_base_transform(&mut self, element_id: ElementId) -> Affine { // Get state from box tree for this visual node let mut box_tree = self.window_state.box_tree.borrow_mut(); - let world_transform = box_tree + box_tree .get_or_compute_world_transform(element_id.0) - .unwrap_or_default(); + .unwrap_or_default() + } + + fn replay_visual_node(&mut self, element_id: ElementId, is_post: bool) { + let base_transform = self + .element_base_transform(element_id) + .then_scale(self.window_state.effective_scale()); + let Some(element) = self.paint_state.display_list().element(element_id) else { + return; + }; + let stage = if is_post { + element.post.clone() + } else { + element.paint.clone() + }; + let renderer = self.paint_state.renderer_mut(); + replay_stage(&stage, renderer, base_transform); + } + + /// Record a single visual node in local coordinates. + pub(crate) fn record_visual_node(&mut self, element_id: ElementId, is_post: bool) { + let mut box_tree = self.window_state.box_tree.borrow_mut(); let layout_rect_local = box_tree.local_bounds(element_id.0).unwrap_or_default(); let clip = box_tree.clipped_local_clip(element_id.0); + let snapshot = ElementSnapshot { + local_bounds: layout_rect_local, + clip, + world_transform: box_tree + .get_or_compute_world_transform(element_id.0) + .unwrap_or_default(), + }; drop(box_tree); - // Set absolute transform on renderer - let device_transform = world_transform.then_scale(self.window_state.effective_scale()); - self.paint_state - .renderer_mut() - .set_transform(device_transform); - let layout_rect = layout_rect_local; let view_id = element_id.owning_id(); let view = view_id.view(); let view_state = view_id.state(); + let mut commands = Vec::new(); + let mut recorder = RecordingRenderer::new(&mut commands); + let uses_layer_clip = self.paint_state.renderer().uses_layer_clip(); + let is_vger = self.paint_state.renderer().is_vger(); + let world_transform = self.element_base_transform(element_id); + let font_size_cx = view_state.borrow().layout_props.font_size_cx(); - // Create per-target PaintCx - let mut cx = PaintCx { - window_state: self.window_state, - paint_state: self.paint_state, - target_id: element_id, - world_transform, - layout_rect_local, - clip, - font_size_cx: view_state.borrow().layout_props.font_size_cx(), - }; - - if !is_post { - if element_id.is_view() { - let state = view_state.borrow(); - paint_bg(&mut cx, &state.view_style_props, layout_rect); - paint_border( - &mut cx, - &state.layout_props, - &state.view_style_props, - layout_rect, - ); - drop(state); - // Apply overflow clip (stays active through children) - if let Some(clip_shape) = clip { - cx.clip(&clip_shape); + { + // Create per-target PaintCx + let mut cx = PaintCx { + window_state: self.window_state, + recorder: &mut recorder, + uses_layer_clip, + is_vger, + target_id: element_id, + world_transform, + layout_rect_local, + clip, + font_size_cx, + }; + + if !is_post { + if element_id.is_view() { + let state = view_state.borrow(); + paint_bg(&mut cx, &state.view_style_props, layout_rect); + paint_border( + &mut cx, + &state.layout_props, + &state.view_style_props, + layout_rect, + ); + drop(state); + // Apply overflow clip (stays active through children) + if let Some(clip_shape) = clip { + cx.clip(&clip_shape); + } + } + view.borrow_mut().paint(&mut cx); + } else { + if element_id.is_view() && clip.is_some() { + cx.pop_clip(); + } + view.borrow_mut().post_paint(&mut cx); + if element_id.is_view() { + let state = view_state.borrow(); + paint_outline(&mut cx, &state.view_style_props, layout_rect); } - } - view.borrow_mut().paint(&mut cx); - } else { - if element_id.is_view() && clip.is_some() { - cx.clear_clip(); - } - view.borrow_mut().post_paint(&mut cx); - if element_id.is_view() { - let state = view_state.borrow(); - paint_outline(&mut cx, &state.view_style_props, layout_rect); } } + + let display_list = self.paint_state.display_list_mut(); + let element = display_list.element_mut(element_id); + let stage = if is_post { + &mut element.post + } else { + &mut element.paint + }; + stage.commands = commands; + stage.transform_class = TransformClass::Affine; + element.snapshot = Some(snapshot); } } @@ -354,25 +447,91 @@ impl PaintCx<'_> { /// Clip the drawing area (delegates to helper methods) pub fn clip(&mut self, shape: &impl Shape) { - if self.paint_state.renderer().uses_layer_clip() { + if self.uses_layer_clip { use peniko::Mix; self.push_layer(Mix::Normal, 1.0, Affine::IDENTITY, shape); } else { - self.paint_state.renderer_mut().clip(shape); + self.recorder.clip(shape); } } /// Clear clip - pub fn clear_clip(&mut self) { - if self.paint_state.renderer().uses_layer_clip() { + pub fn pop_clip(&mut self) { + if self.uses_layer_clip { self.pop_layer(); } else { - self.paint_state.renderer_mut().clear_clip(); + self.recorder.clear_clip(); } } // Note: get_transform/set_transform removed as Renderer doesn't expose transform() // Views that previously used save/restore should use clip/clear_clip instead + + pub fn stroke<'b, 's>( + &mut self, + shape: &impl Shape, + brush: impl Into>, + stroke: &'s peniko::kurbo::Stroke, + ) { + self.recorder.stroke(shape, brush, stroke); + } + + pub fn fill<'b>( + &mut self, + path: &impl peniko::kurbo::Shape, + brush: impl Into>, + blur_radius: f64, + ) { + self.recorder.fill(path, brush, blur_radius); + } + + pub fn push_layer( + &mut self, + blend: impl Into, + alpha: f32, + transform: Affine, + clip: &impl Shape, + ) { + self.recorder.push_layer(blend, alpha, transform, clip); + } + + pub fn pop_layer(&mut self) { + self.recorder.pop_layer(); + } + + pub fn draw_img(&mut self, img: floem_renderer::Img<'_>, rect: peniko::kurbo::Rect) { + self.recorder.draw_img(img, rect); + } + + pub fn draw_glyphs<'a>( + &mut self, + origin: peniko::kurbo::Point, + props: &floem_renderer::text::GlyphRunProps<'a>, + glyphs: impl Iterator + 'a, + ) { + self.recorder.draw_glyphs(origin, props, glyphs); + } + + pub fn draw_svg<'b>( + &mut self, + svg: floem_renderer::Svg<'b>, + rect: peniko::kurbo::Rect, + brush: Option>>, + ) { + self.recorder.draw_svg(svg, rect, brush); + } + + pub fn set_transform(&mut self, transform: Affine) { + self.recorder.set_transform(transform); + } + + pub fn set_z_index(&mut self, z_index: i32) { + self.recorder.set_z_index(z_index); + } + + pub fn is_vger(&self) -> bool { + self.is_vger + } } // TODO: should this be private? @@ -391,7 +550,10 @@ pub enum PaintState { renderer: Renderer, }, /// The renderer is initialized and ready to paint. - Initialized { renderer: Renderer }, + Initialized { + renderer: Renderer, + display_list: RetainedDisplayList, + }, } impl PaintState { @@ -425,48 +587,58 @@ impl PaintState { size, font_embolden, ); - Self::Initialized { renderer } + Self::Initialized { + renderer, + display_list: RetainedDisplayList::default(), + } } #[cfg(feature = "skia")] pub fn new_skia(window: Arc, scale: f64, size: Size, font_embolden: f32) -> Self { let renderer = Renderer::new_skia(window.clone(), scale, size, font_embolden); - Self::Initialized { renderer } + Self::Initialized { + renderer, + display_list: RetainedDisplayList::default(), + } } pub(crate) fn renderer(&self) -> &Renderer { match self { PaintState::PendingGpuResources { renderer, .. } => renderer, - PaintState::Initialized { renderer } => renderer, + PaintState::Initialized { renderer, .. } => renderer, } } pub(crate) fn renderer_mut(&mut self) -> &mut Renderer { match self { PaintState::PendingGpuResources { renderer, .. } => renderer, - PaintState::Initialized { renderer } => renderer, + PaintState::Initialized { renderer, .. } => renderer, } } - pub(crate) fn resize(&mut self, scale: f64, size: Size) { - self.renderer_mut().resize(scale, size); + pub(crate) fn display_list(&self) -> &RetainedDisplayList { + match self { + PaintState::PendingGpuResources { .. } => { + panic!("display list is unavailable before renderer initialization") + } + PaintState::Initialized { display_list, .. } => display_list, + } } - pub(crate) fn set_scale(&mut self, scale: f64) { - self.renderer_mut().set_scale(scale); + pub(crate) fn display_list_mut(&mut self) -> &mut RetainedDisplayList { + match self { + PaintState::PendingGpuResources { .. } => { + panic!("display list is unavailable before renderer initialization") + } + PaintState::Initialized { display_list, .. } => display_list, + } } -} -impl Deref for PaintCx<'_> { - type Target = Renderer; - - fn deref(&self) -> &Self::Target { - self.paint_state.renderer() + pub(crate) fn resize(&mut self, scale: f64, size: Size) { + self.renderer_mut().resize(scale, size); } -} -impl DerefMut for PaintCx<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.paint_state.renderer_mut() + pub(crate) fn set_scale(&mut self, scale: f64) { + self.renderer_mut().set_scale(scale); } } diff --git a/src/style/cx.rs b/src/style/cx.rs index d26dcfd6b..09bc83e03 100644 --- a/src/style/cx.rs +++ b/src/style/cx.rs @@ -537,10 +537,10 @@ impl<'a> StyleCx<'a> { // ───────────────────────────────────────────────────────────────────── // Phase 8.2: Update box tree visiblity dependent props (must happen after visibility phase override) // ───────────────────────────────────────────────────────────────────── + let element_id = vs.element_id; let focus_nav_changed; { let box_tree = view_id.box_tree(); - let element_id = vs.element_id; let box_tree = &mut box_tree.borrow_mut(); let old_flags = box_tree.flags(element_id.0).unwrap_or(NodeFlags::empty()); let old_focus = box_tree.focus_nav_meta(element_id.0).unwrap_or_default(); @@ -588,7 +588,7 @@ impl<'a> StyleCx<'a> { // Phase 8.3: request paint for view style changes if not hidden // ───────────────────────────────────────────────────────────────────── if !is_hidden_final && need_paint { - self.window_state.request_paint(view_id); + self.window_state.request_paint(element_id); } } } diff --git a/src/style/transition.rs b/src/style/transition.rs index 707b59659..01ad4635c 100644 --- a/src/style/transition.rs +++ b/src/style/transition.rs @@ -6,7 +6,6 @@ //! - [`DirectTransition`] - Standalone transition controller use floem_reactive::{RwSignal, SignalGet}; -use floem_renderer::Renderer; use peniko::color::palette; use peniko::kurbo::{self, Point, Stroke}; use std::rc::Rc; diff --git a/src/style/values.rs b/src/style/values.rs index b3a6aafd5..e4921fc9b 100644 --- a/src/style/values.rs +++ b/src/style/values.rs @@ -1,7 +1,6 @@ //! Core style property value trait and implementations. use floem_reactive::{RwSignal, SignalGet, SignalUpdate as _}; -use floem_renderer::Renderer; use floem_renderer::text::{FontWeight, LineHeightValue}; use peniko::color::{HueDirection, palette}; use peniko::kurbo::{self, Affine, Point, Stroke, Vec2}; diff --git a/src/text/layout.rs b/src/text/layout.rs index c488006ba..68202d913 100644 --- a/src/text/layout.rs +++ b/src/text/layout.rs @@ -6,7 +6,6 @@ use std::{ sync::LazyLock, }; -use floem_renderer::Renderer as _; use floem_renderer::text::{AttrsList, GlyphRunProps, TextBrush}; use parking_lot::Mutex; use parley::swash::{FontRef, scale::ScaleContext, zeno}; @@ -20,7 +19,6 @@ use peniko::{ kurbo::{Affine, Point, Size}, }; -use crate::paint::Renderer; /// Shared Parley font context used by Floem text layout construction. /// @@ -779,8 +777,8 @@ impl TextLayout { (min_y.is_finite() && max_y.is_finite()).then_some((min_y, max_y)) } - /// Draws the layout at the given origin using Floem's renderer wrapper. - pub fn draw(&self, renderer: &mut Renderer, origin: impl Into) { + /// Draws the layout into Floem's retained paint recorder. + pub fn draw(&self, renderer: &mut crate::paint::PaintCx<'_>, origin: impl Into) { let origin = origin.into(); for line in self.layout.lines() { for item in line.items() { diff --git a/src/view/mod.rs b/src/view/mod.rs index d2bfcf8df..5195f29ee 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -65,7 +65,6 @@ use std::hash::Hash; use std::rc::Rc; use crate::{ - Renderer, context::{EventCx, PaintCx, StyleCx, UpdateCx}, event::EventPropagation, style::{LayoutProps, Style, StyleClassRef}, diff --git a/src/views/editor/gutter.rs b/src/views/editor/gutter.rs index 1f676e8fc..f2d423290 100644 --- a/src/views/editor/gutter.rs +++ b/src/views/editor/gutter.rs @@ -1,7 +1,6 @@ use std::{cell::RefCell, rc::Rc}; use crate::{ - Renderer, context::PaintCx, peniko::kurbo::Point, prop, prop_extractor, diff --git a/src/views/editor/view.rs b/src/views/editor/view.rs index 5743317b6..7ec5e4528 100644 --- a/src/views/editor/view.rs +++ b/src/views/editor/view.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, ops::RangeInclusive, rc::Rc}; use crate::{ - Renderer, action::{set_ime_allowed, set_ime_cursor_area}, context::{LayoutChanged, LayoutChangedListener, PaintCx, UpdateCx, VisualChanged}, event::{ diff --git a/src/views/img.rs b/src/views/img.rs index 8a5154f9e..0d955f6fa 100644 --- a/src/views/img.rs +++ b/src/views/img.rs @@ -7,7 +7,7 @@ use peniko::{Blob, ImageAlphaType, ImageData, kurbo::Rect}; use sha2::{Digest, Sha256}; use crate::{ - Renderer, prop_extractor, + prop_extractor, style::{FontSizeCx, ObjectFit, ObjectPosition}, view::{LayoutNodeCx, MeasureFn, View, ViewId}, }; @@ -377,7 +377,7 @@ impl View for Img { dest_rect, ); if self.needs_clip() { - cx.clear_clip(); + cx.pop_clip(); } } } diff --git a/src/views/label.rs b/src/views/label.rs index daea0e671..e65c52491 100644 --- a/src/views/label.rs +++ b/src/views/label.rs @@ -20,7 +20,6 @@ use crate::{ views::editor::SelectionColor, }; use floem_reactive::UpdaterEffect; -use floem_renderer::Renderer; use peniko::{ Brush, color::palette::{self}, diff --git a/src/views/scroll.rs b/src/views/scroll.rs index df95f2db2..87ff2c443 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -17,7 +17,7 @@ use crate::prelude::EventListenerTrait; use crate::prelude::el::UpdatePhaseLayout; use crate::style::ScrollbarWidth; use crate::{ - BoxTree, ElementId, Renderer, + BoxTree, ElementId, context::{EventCx, PaintCx, StyleCx}, event::{Event, EventPropagation, Phase}, prop, prop_extractor, diff --git a/src/views/slider.rs b/src/views/slider.rs index beaac358f..cb33f8547 100644 --- a/src/views/slider.rs +++ b/src/views/slider.rs @@ -13,7 +13,6 @@ use crate::custom_event; use crate::event::CustomEvent; use crate::unit::FontSizeCx; use crate::{ - Renderer, context::{LayoutChanged, LayoutChangedListener}, event::{Event, EventPropagation, FocusEvent}, prelude::*, @@ -389,7 +388,7 @@ impl View for Slider { .unwrap_or(palette::css::TRANSPARENT.into()), 0., ); - cx.clear_clip(); + cx.pop_clip(); if let Some(color) = self.style.foreground() { cx.fill(&self.handle, &color, 0.); diff --git a/src/views/svg.rs b/src/views/svg.rs index 4c28d5d5c..166324a8a 100644 --- a/src/views/svg.rs +++ b/src/views/svg.rs @@ -2,7 +2,6 @@ use std::{cell::RefCell, rc::Rc}; use floem_reactive::Effect; use floem_renderer::{ - Renderer, usvg::{self, Tree}, }; use peniko::{ diff --git a/src/views/text_input.rs b/src/views/text_input.rs index d704622ba..5fa1fb2e0 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -15,7 +15,6 @@ use crate::{ }; use floem_reactive::{Effect, SignalGet, SignalTrack, SignalUpdate, SignalWith}; -use floem_renderer::Renderer; use taffy::Dimension; use ui_events::keyboard::{Key, KeyState, KeyboardEvent, Modifiers, NamedKey}; use ui_events::pointer::{PointerButton, PointerButtonEvent, PointerEvent}; @@ -1534,7 +1533,7 @@ impl View for TextInput { // skip rendering selection / cursor if we don't have focus if !cx.window_state.is_focused(self.id) { // Clear the custom clip we applied - cx.clear_clip(); + cx.pop_clip(); return; } @@ -1548,12 +1547,12 @@ impl View for TextInput { if has_selection { // Clear the custom clip we applied self.paint_selection_rect(cx); - cx.clear_clip(); + cx.pop_clip(); return; } // Clear the custom clip we applied - cx.clear_clip(); + cx.pop_clip(); // see if we should render the cursor let is_cursor_visible = (self.last_cursor_action_on.elapsed().as_millis() diff --git a/src/views/toggle_button.rs b/src/views/toggle_button.rs index 4aca86b19..ef8eae488 100644 --- a/src/views/toggle_button.rs +++ b/src/views/toggle_button.rs @@ -13,7 +13,7 @@ use crate::context::Phases; use crate::custom_event; use crate::event::listener::EventListenerTrait; use crate::{ - BoxTree, ElementId, Renderer, + BoxTree, ElementId, context::{EventCx, PaintCx, UpdateCx}, easing::Linear, event::{ diff --git a/src/window/handle.rs b/src/window/handle.rs index dc12ace02..3abe5311c 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -429,7 +429,7 @@ impl WindowHandle { let scale = self.window_state.effective_scale(); self.paint_state.set_scale(scale); self.event(Event::Window(WindowEvent::ScaleChanged(scale))); - self.window_state.request_paint = true; + self.window_state.request_paint(self.window_state.root_view_id); self.schedule_repaint(); } @@ -501,7 +501,7 @@ impl WindowHandle { self.layout(); self.commit_box_tree(); self.process_update_no_paint(); - self.window_state.request_paint = true; + self.window_state.request_paint(self.window_state.root_view_id); self.schedule_repaint(); } @@ -778,15 +778,14 @@ impl WindowHandle { pub(crate) fn render_frame(&mut self) { let renderer_ready = matches!(self.paint_state, PaintState::Initialized { .. }); - if self.window_state.request_paint && renderer_ready { - self.window_state.request_paint = false; + if self.window_state.has_pending_paint() && renderer_ready { self.paint(); self.last_presented_at = Instant::now(); } if self.live_resize_active() { - self.window_state.schedule_paint(self.id); - self.window_state.request_paint = true; + self.window_state + .schedule_paint(self.id.get_element_id()); self.schedule_repaint(); } else { self.live_resize_until = None; @@ -912,7 +911,7 @@ impl WindowHandle { } pub(crate) fn render_frame_if_due(&mut self, min_frame_interval: Duration) -> bool { - if !self.window_state.request_paint { + if !self.window_state.has_pending_paint() { return false; } if self.last_presented_at.elapsed() < min_frame_interval { @@ -1125,7 +1124,7 @@ impl WindowHandle { self.window_state.needs_box_tree_commit = true; } UpdateMessage::RequestPaint => { - cx.window_state.request_paint = true; + cx.window_state.request_paint(cx.window_state.root_view_id); } UpdateMessage::Focus(id) => { // because we do not call route, the processing messages event is not sent. diff --git a/src/window/state.rs b/src/window/state.rs index 97d3c5122..0bf0c5691 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -13,6 +13,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::{SmallVec, smallvec}; use taffy::{AvailableSpace, NodeId}; use ui_events::pointer::{PointerId, PointerInfo}; +use understory_box_tree::QueryFilter; use understory_event_state::{click::ClickState, focus::FocusState, hover::HoverState}; use understory_focus::{FocusEntry, FocusSpace}; use winit::cursor::CursorIcon; @@ -140,7 +141,7 @@ pub struct WindowState { pub(crate) responsive_selector_views: FxHashMap, pub(crate) disabled_selector_views: FxHashMap, pub(crate) selected_selector_views: FxHashMap, - pub(crate) request_paint: bool, + pub(crate) dirty_paint_elements: FxHashSet, pub(crate) drag_tracker: DragTracker, pub(crate) screen_size_bp: ScreenSizeBp, pub(crate) grid_bps: GridBreakpoints, @@ -202,6 +203,8 @@ impl WindowState { let layout_tree = VIEW_STORAGE.with_borrow_mut(|s| s.taffy.clone()); let root_layout_node = root_view_id.taffy_node(); + let root_element_id = root_view_id.get_element_id(); + Self { root_layout_node, root_view_id, @@ -214,8 +217,8 @@ impl WindowState { root_size: Size::ZERO, fixed_elements: FxHashSet::default(), screen_size_bp: ScreenSizeBp::Xs, - scheduled_updates: vec![FrameUpdate::Paint(root_view_id)], - request_paint: true, + scheduled_updates: vec![FrameUpdate::Paint(root_element_id)], + dirty_paint_elements: FxHashSet::from_iter([root_element_id]), style_dirty: Default::default(), responsive_selector_views: FxHashMap::default(), disabled_selector_views: FxHashMap::default(), @@ -1001,6 +1004,19 @@ impl WindowState { let damage = self.box_tree.borrow_mut().commit(); + if !damage.dirty_rects.is_empty() { + let dirty_elements: FxHashSet = { + let box_tree = self.box_tree.borrow(); + damage + .dirty_rects + .iter() + .flat_map(|rect| box_tree.intersect_rect(*rect, QueryFilter::new().visible())) + .filter_map(|node_id| box_tree.element_id_of(node_id)) + .collect() + }; + self.dirty_paint_elements.extend(dirty_elements); + } + let pointer = self.last_pointer; for damage_rect in &damage.dirty_rects { if damage_rect.contains(pointer.0) { @@ -1016,7 +1032,6 @@ impl WindowState { } if !damage.dirty_rects.is_empty() { self.invalidate_focus_nav_cache(); - self.request_paint = true; } self.needs_box_tree_commit = false; } @@ -1210,13 +1225,24 @@ impl WindowState { /// Requests that the paint pass will run for `id` on the next frame, and ensures new frame is /// scheduled to happen. - pub fn schedule_paint(&mut self, id: ViewId) { + pub fn schedule_paint(&mut self, id: ElementId) { self.scheduled_updates.push(FrameUpdate::Paint(id)); } - // `Id` is unused currently, but could be used to calculate damage regions. - pub fn request_paint(&mut self, _id: impl Into) { - self.request_paint = true; + pub fn request_paint(&mut self, id: impl Into) { + self.dirty_paint_elements.insert(id.into()); + } + + pub fn take_dirty_paint_elements(&mut self) -> FxHashSet { + std::mem::take(&mut self.dirty_paint_elements) + } + + pub fn has_pending_paint(&self) -> bool { + !self.dirty_paint_elements.is_empty() + } + + pub fn clear_pending_paint(&mut self) { + self.dirty_paint_elements.clear(); } pub(crate) fn update_screen_size_bp(&mut self, size: Size) { From 32a27b73218e58cd3dbbcba0339232878cfbb083 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 17 Mar 2026 23:21:55 -0600 Subject: [PATCH 003/102] transform invalidation --- src/app/handle.rs | 2 +- src/box_tree.rs | 24 +++++++++++++ src/message.rs | 2 +- src/paint/mod.rs | 80 +++++++++++++++++++++++++++++++++++++---- src/view/id.rs | 17 +++++++-- src/view/mod.rs | 2 +- src/views/img.rs | 1 + src/views/label.rs | 4 ++- src/views/scroll.rs | 12 +++---- src/views/text_input.rs | 3 +- src/window/handle.rs | 9 ++--- src/window/state.rs | 24 ++++++------- 12 files changed, 142 insertions(+), 38 deletions(-) diff --git a/src/app/handle.rs b/src/app/handle.rs index c40f9bd27..df7aba055 100644 --- a/src/app/handle.rs +++ b/src/app/handle.rs @@ -677,7 +677,7 @@ impl ApplicationHandle { any_work_remaining = true; } - if handle.window_state.has_pending_paint() && handle.can_render_now() { + if handle.window_state.has_pending_render() && handle.can_render_now() { handle.window.request_redraw(); let frame_interval = handle .window diff --git a/src/box_tree.rs b/src/box_tree.rs index e3a95ee18..86f7930b8 100644 --- a/src/box_tree.rs +++ b/src/box_tree.rs @@ -3,6 +3,7 @@ use std::ops::{Deref, DerefMut}; use rustc_hash::FxHashMap; use crate::ViewId; +use crate::paint::display_list::TransformClass; /// A visual identifier that represents a rectangle in the box tree. /// @@ -162,6 +163,7 @@ impl FocusNavMeta { pub struct ElementMeta { pub element_id: ElementId, pub focus: FocusNavMeta, + pub(crate) retained_transform_boundary: Option, } impl ElementMeta { @@ -178,6 +180,7 @@ impl ElementMeta { autofocus: false, enabled: true, }, + retained_transform_boundary: None, } } } @@ -242,6 +245,27 @@ impl BoxTree { true } + pub(crate) fn retained_transform_boundary( + &self, + id: understory_box_tree::NodeId, + ) -> Option { + self.element_meta(id) + .and_then(|meta| meta.retained_transform_boundary) + } + + pub(crate) fn set_retained_transform_boundary( + &mut self, + id: understory_box_tree::NodeId, + boundary: Option, + ) -> bool { + let Some(mut meta) = self.element_meta(id) else { + return false; + }; + meta.retained_transform_boundary = boundary; + self.metadata.insert(id, meta); + true + } + /// Returns `true` when this node can receive focus by any means. #[inline] pub fn is_focusable(&self, id: understory_box_tree::NodeId) -> bool { diff --git a/src/message.rs b/src/message.rs index 494ed3edf..6a95a1fb7 100644 --- a/src/message.rs +++ b/src/message.rs @@ -65,7 +65,7 @@ pub enum UpdateMessage { pointer_id: PointerId, }, WindowScale(f64), - RequestPaint, + RequestPaint(ElementId), RequestLayout, /// Request that the box tree be updated from the layout tree (full walk) and committed RequestBoxTreeUpdate, diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 73e2b564e..28462040a 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -14,6 +14,7 @@ pub use renderer::Renderer; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; use peniko::kurbo::{Affine, RoundedRect, Shape, Size}; +use rustc_hash::FxHashSet; use std::sync::Arc; use understory_box_tree::NodeFlags; use winit::window::Window; @@ -30,7 +31,8 @@ use crate::view::stacking::{StackingContextItem, collect_stacking_context_items_ use crate::view::{paint_bg, paint_border, paint_outline}; use crate::window::state::WindowState; use display_list::{ - ElementSnapshot, RecordingRenderer, RetainedDisplayList, TransformClass, replay_stage, + ElementSnapshot, RecordingRenderer, RetainedDisplayList, TransformClass, + replay_stage, transform_diff_class, }; std::thread_local! { @@ -212,6 +214,66 @@ pub(crate) fn collect_visual_order( } impl GlobalPaintCx<'_> { + fn collect_retained_subtree_descendants( + &mut self, + active_ids: &FxHashSet, + explicit_dirty: &FxHashSet, + ) -> FxHashSet { + let mut reusable_descendants = FxHashSet::default(); + let display_list = self.paint_state.display_list(); + + let mut box_tree = self.window_state.box_tree.borrow_mut(); + let mut stack = Vec::new(); + + for &element_id in active_ids { + let Some(boundary) = box_tree.retained_transform_boundary(element_id.0) else { + continue; + }; + + if explicit_dirty.contains(&element_id) { + continue; + } + + let snapshot = ElementSnapshot { + local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), + clip: box_tree.local_clip(element_id.0).flatten(), + world_transform: box_tree + .get_or_compute_world_transform(element_id.0) + .unwrap_or_default(), + }; + + let Some(previous) = display_list.element(element_id).and_then(|e| e.snapshot) else { + continue; + }; + let diff = transform_diff_class(previous.world_transform, snapshot.world_transform); + if previous.local_bounds != snapshot.local_bounds + || previous.clip != snapshot.clip + || !boundary.supports(diff) + { + continue; + } + + stack.clear(); + stack.extend(box_tree.children_of(element_id.0).iter().copied()); + while let Some(node_id) = stack.pop() { + let Some(descendant) = box_tree.element_id_of(node_id) else { + continue; + }; + if !active_ids.contains(&descendant) + || explicit_dirty.contains(&descendant) + || display_list.element(descendant).is_none() + { + stack.extend(box_tree.children_of(node_id).iter().copied()); + continue; + } + reusable_descendants.insert(descendant); + stack.extend(box_tree.children_of(node_id).iter().copied()); + } + } + + reusable_descendants + } + /// Build explicit paint order for entire view tree. /// /// Returns a flat list of VisualIds in paint order (back-to-front, respecting z-index). @@ -266,14 +328,14 @@ impl GlobalPaintCx<'_> { }) .collect(); - self.paint_state - .display_list_mut() - .retain_only(&active_ids); + self.paint_state.display_list_mut().retain_only(&active_ids); self.paint_state .display_list_mut() .set_paint_order(paint_order.clone()); let mut dirty_ids = self.window_state.take_dirty_paint_elements(); + let reusable_descendants = + self.collect_retained_subtree_descendants(&active_ids, &dirty_ids); let snapshots = { let mut box_tree = self.window_state.box_tree.borrow_mut(); active_ids @@ -282,7 +344,7 @@ impl GlobalPaintCx<'_> { .map(|element_id| { let snapshot = ElementSnapshot { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), - clip: box_tree.clipped_local_clip(element_id.0), + clip: box_tree.local_clip(element_id.0).flatten(), world_transform: box_tree .get_or_compute_world_transform(element_id.0) .unwrap_or_default(), @@ -293,6 +355,9 @@ impl GlobalPaintCx<'_> { }; for (element_id, snapshot) in snapshots { + if reusable_descendants.contains(&element_id) { + continue; + } if self .paint_state .display_list() @@ -351,7 +416,10 @@ impl GlobalPaintCx<'_> { pub(crate) fn record_visual_node(&mut self, element_id: ElementId, is_post: bool) { let mut box_tree = self.window_state.box_tree.borrow_mut(); let layout_rect_local = box_tree.local_bounds(element_id.0).unwrap_or_default(); - let clip = box_tree.clipped_local_clip(element_id.0); + // Retained identity uses the element's own local clip. Do not use + // `clipped_local_clip()` here: that value includes ancestor/world clipping + // and would make the retained entry depend on transient parent state. + let clip = box_tree.local_clip(element_id.0).flatten(); let snapshot = ElementSnapshot { local_bounds: layout_rect_local, clip, diff --git a/src/view/id.rs b/src/view/id.rs index 25d2d8000..9d7cee5bc 100644 --- a/src/view/id.rs +++ b/src/view/id.rs @@ -24,6 +24,7 @@ thread_local! { use crate::context::EventCallbackConfig; use crate::event::listener::EventListenerKey; use crate::event::{RouteKind, listener}; +use crate::paint::display_list::TransformClass; use crate::style::recalc::StyleReason; use crate::view::LayoutTree; use crate::window::handle::get_current_view; @@ -842,7 +843,7 @@ impl ViewId { )); self.request_layout(); self.request_box_tree_commit(); - self.add_update_message(UpdateMessage::RequestPaint); + self.add_update_message(UpdateMessage::RequestPaint(self.get_element_id())); } /// Request that this view have it's layout pass run @@ -881,7 +882,7 @@ impl ViewId { /// Request that this view have it's paint pass run pub fn request_paint(&self) { - self.add_update_message(UpdateMessage::RequestPaint); + self.add_update_message(UpdateMessage::RequestPaint(self.get_element_id())); } /// request that this node be styled again @@ -1326,6 +1327,18 @@ impl ViewId { true } + /// Mark an owned element as a retained transform boundary. + pub(crate) fn set_retained_transform_boundary_for_element( + &self, + element_id: ElementId, + boundary: Option, + ) -> bool { + let box_tree = self.box_tree(); + box_tree + .borrow_mut() + .set_retained_transform_boundary(element_id.0, boundary) + } + /// Set whether an owned element can receive focus at all. /// /// Clearing focusability also clears keyboard navigation. diff --git a/src/view/mod.rs b/src/view/mod.rs index 5195f29ee..3b7d65a31 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1128,7 +1128,7 @@ impl View for Box { } fn post_paint(&mut self, cx: &mut PaintCx) { - (**self).paint(cx) + (**self).post_paint(cx) } } diff --git a/src/views/img.rs b/src/views/img.rs index 0d955f6fa..25913f418 100644 --- a/src/views/img.rs +++ b/src/views/img.rs @@ -358,6 +358,7 @@ impl View for Img { // object_fit changed, update taffy layout self.set_taffy_layout(); self.id.request_layout(); + cx.window_state.request_paint(self.id); } } diff --git a/src/views/label.rs b/src/views/label.rs index e65c52491..7111c4b03 100644 --- a/src/views/label.rs +++ b/src/views/label.rs @@ -495,11 +495,13 @@ impl View for Label { fn update(&mut self, cx: &mut UpdateCx, state: Box) { if state.is::() && let Ok(state) = state.downcast::() + && *state != self.label { self.label = *state; self.layout_data.borrow_mut().clear_overflow_state(); self.set_text_layout(); - cx.window_state.schedule_layout(); + self.id.request_layout(); + cx.window_state.request_paint(self.id); } } diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 87ff2c443..b988e43aa 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -585,6 +585,10 @@ impl Scroll { let child = child.into_any(); let child_id = child.id(); id.add_child(child); + id.set_retained_transform_boundary_for_element( + child_id.get_element_id(), + Some(crate::paint::display_list::TransformClass::TranslateOnly), + ); // we need to first set the clip rect to zero so that virtual items don't set a large initial size id.set_box_tree_clip(Some(RoundedRect::from_rect(Rect::ZERO, 0.))); @@ -975,7 +979,6 @@ impl View for Scroll { .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() { - cx.window_state.request_paint(self.id); } return result.propagation; } @@ -986,7 +989,6 @@ impl View for Scroll { .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() { - cx.window_state.request_paint(self.id); } return result.propagation; } @@ -997,7 +999,6 @@ impl View for Scroll { .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() { - cx.window_state.request_paint(self.id); } return result.propagation; } @@ -1008,7 +1009,6 @@ impl View for Scroll { .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() { - cx.window_state.request_paint(self.id); } return result.propagation; } @@ -1029,10 +1029,6 @@ impl View for Scroll { let change = self.apply_scroll_delta(delta); - if change.is_some() { - cx.window_state.request_paint(self.id); - } - return if self.scroll_style.propagate_pointer_wheel() && change.is_none() { EventPropagation::Continue } else { diff --git a/src/views/text_input.rs b/src/views/text_input.rs index 5fa1fb2e0..65217f49b 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -1220,7 +1220,7 @@ impl View for TextInput { format!("TextInput: {:?}", self.buffer.get_untracked()).into() } - fn update(&mut self, _cx: &mut UpdateCx, state: Box) { + fn update(&mut self, cx: &mut UpdateCx, state: Box) { if state.is::() { if let Ok(token) = state.downcast::() && *token == self.cursor_blink_timer @@ -1244,6 +1244,7 @@ impl View for TextInput { if text_updated { self.update_text_layout(); self.id.request_layout(); + cx.window_state.request_paint(self.id); } } else { eprintln!("downcast failed"); diff --git a/src/window/handle.rs b/src/window/handle.rs index 3abe5311c..6145f6bf2 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -778,8 +778,9 @@ impl WindowHandle { pub(crate) fn render_frame(&mut self) { let renderer_ready = matches!(self.paint_state, PaintState::Initialized { .. }); - if self.window_state.has_pending_paint() && renderer_ready { + if self.window_state.has_pending_render() && renderer_ready { self.paint(); + self.window_state.clear_pending_damage(); self.last_presented_at = Instant::now(); } @@ -911,7 +912,7 @@ impl WindowHandle { } pub(crate) fn render_frame_if_due(&mut self, min_frame_interval: Duration) -> bool { - if !self.window_state.has_pending_paint() { + if !self.window_state.has_pending_render() { return false; } if self.last_presented_at.elapsed() < min_frame_interval { @@ -1123,8 +1124,8 @@ impl WindowHandle { UpdateMessage::RequestBoxTreeCommit => { self.window_state.needs_box_tree_commit = true; } - UpdateMessage::RequestPaint => { - cx.window_state.request_paint(cx.window_state.root_view_id); + UpdateMessage::RequestPaint(id) => { + cx.window_state.request_paint(id); } UpdateMessage::Focus(id) => { // because we do not call route, the processing messages event is not sent. diff --git a/src/window/state.rs b/src/window/state.rs index 0bf0c5691..9ffbd5196 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -13,7 +13,6 @@ use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::{SmallVec, smallvec}; use taffy::{AvailableSpace, NodeId}; use ui_events::pointer::{PointerId, PointerInfo}; -use understory_box_tree::QueryFilter; use understory_event_state::{click::ClickState, focus::FocusState, hover::HoverState}; use understory_focus::{FocusEntry, FocusSpace}; use winit::cursor::CursorIcon; @@ -142,6 +141,7 @@ pub struct WindowState { pub(crate) disabled_selector_views: FxHashMap, pub(crate) selected_selector_views: FxHashMap, pub(crate) dirty_paint_elements: FxHashSet, + pub(crate) pending_damage_rects: Vec, pub(crate) drag_tracker: DragTracker, pub(crate) screen_size_bp: ScreenSizeBp, pub(crate) grid_bps: GridBreakpoints, @@ -219,6 +219,7 @@ impl WindowState { screen_size_bp: ScreenSizeBp::Xs, scheduled_updates: vec![FrameUpdate::Paint(root_element_id)], dirty_paint_elements: FxHashSet::from_iter([root_element_id]), + pending_damage_rects: Vec::new(), style_dirty: Default::default(), responsive_selector_views: FxHashMap::default(), disabled_selector_views: FxHashMap::default(), @@ -1004,18 +1005,7 @@ impl WindowState { let damage = self.box_tree.borrow_mut().commit(); - if !damage.dirty_rects.is_empty() { - let dirty_elements: FxHashSet = { - let box_tree = self.box_tree.borrow(); - damage - .dirty_rects - .iter() - .flat_map(|rect| box_tree.intersect_rect(*rect, QueryFilter::new().visible())) - .filter_map(|node_id| box_tree.element_id_of(node_id)) - .collect() - }; - self.dirty_paint_elements.extend(dirty_elements); - } + self.pending_damage_rects.extend(damage.dirty_rects.iter().copied()); let pointer = self.last_pointer; for damage_rect in &damage.dirty_rects { @@ -1241,10 +1231,18 @@ impl WindowState { !self.dirty_paint_elements.is_empty() } + pub fn has_pending_render(&self) -> bool { + !self.dirty_paint_elements.is_empty() || !self.pending_damage_rects.is_empty() + } + pub fn clear_pending_paint(&mut self) { self.dirty_paint_elements.clear(); } + pub fn clear_pending_damage(&mut self) { + self.pending_damage_rects.clear(); + } + pub(crate) fn update_screen_size_bp(&mut self, size: Size) { let bp = self.grid_bps.get_width_bp(size.width); if bp != self.screen_size_bp { From 019533b25c7e6737ac02b4c863f58e615e9b6e39 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 18 Mar 2026 10:33:21 -0600 Subject: [PATCH 004/102] fix drag --- Cargo.lock | 10 +++++----- Cargo.toml | 10 +++++----- src/paint/display_list.rs | 23 +++++++++++++++-------- src/paint/mod.rs | 4 ++-- src/text/layout.rs | 1 - src/views/scroll.rs | 12 ++++-------- src/views/svg.rs | 4 +--- src/window/handle.rs | 9 +++++---- src/window/state.rs | 30 +++++++++++++++++++++--------- 9 files changed, 58 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba1e8c794..a411cdc80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6376,7 +6376,7 @@ dependencies = [ [[package]] name = "understory_box_tree" version = "0.0.1" -source = "git+https://github.com/jrmoulton/understory?rev=3acd6cde8c5c#3acd6cde8c5c642442f14b02d893b7e1068e41eb" +source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "bitflags 2.11.0", "kurbo", @@ -6386,7 +6386,7 @@ dependencies = [ [[package]] name = "understory_event_state" version = "0.1.0" -source = "git+https://github.com/jrmoulton/understory?rev=3acd6cde8c5c#3acd6cde8c5c642442f14b02d893b7e1068e41eb" +source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "kurbo", ] @@ -6394,7 +6394,7 @@ dependencies = [ [[package]] name = "understory_focus" version = "0.1.0" -source = "git+https://github.com/jrmoulton/understory?rev=3acd6cde8c5c#3acd6cde8c5c642442f14b02d893b7e1068e41eb" +source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "kurbo", ] @@ -6402,7 +6402,7 @@ dependencies = [ [[package]] name = "understory_index" version = "0.0.1" -source = "git+https://github.com/jrmoulton/understory?rev=3acd6cde8c5c#3acd6cde8c5c642442f14b02d893b7e1068e41eb" +source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "hashbrown 0.16.1", "smallvec", @@ -6411,7 +6411,7 @@ dependencies = [ [[package]] name = "understory_virtual_list" version = "0.1.0" -source = "git+https://github.com/jrmoulton/understory?rev=3acd6cde8c5c#3acd6cde8c5c642442f14b02d893b7e1068e41eb" +source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" [[package]] name = "unic-langid" diff --git a/Cargo.toml b/Cargo.toml index 9e9cbfc80..3c9fb6b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,11 +116,11 @@ fluent-bundle = { version = "0.16", optional = true } unic-langid = { version = "0.9", optional = true } sys-locale = { version = "0.3.2", optional = true } ouroboros = { version = "0.18", optional = true } -understory_box_tree = { git = "https://github.com/jrmoulton/understory", rev = "3acd6cde8c5c", package = "understory_box_tree" } -understory_event_state = { git = "https://github.com/jrmoulton/understory", rev = "3acd6cde8c5c", package = "understory_event_state" } -understory_index = { git = "https://github.com/jrmoulton/understory", rev = "3acd6cde8c5c", package = "understory_index" } -understory_focus = { git = "https://github.com/jrmoulton/understory", rev = "3acd6cde8c5c", package = "understory_focus" } -understory_virtual_list = { git = "https://github.com/jrmoulton/understory", rev = "3acd6cde8c5c", package = "understory_virtual_list" } +understory_box_tree = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_box_tree" } +understory_event_state = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_event_state" } +understory_index = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_index" } +understory_focus = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_focus" } +understory_virtual_list = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_virtual_list" } [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index 6929037a9..2a429c4d4 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -6,9 +6,7 @@ use imaging::{ Clip, Composite, Draw, FillRule, Geometry, Glyph as SceneGlyph, GlyphRun, Group, StrokeStyle, }; use peniko::BrushRef; -use peniko::kurbo::{ - Affine, BezPath, Circle, Line, Point, Rect, RoundedRect, Shape, Stroke, -}; +use peniko::kurbo::{Affine, BezPath, Circle, Line, Point, Rect, RoundedRect, Shape, Stroke}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ElementId, paint::PaintOrPost}; @@ -160,7 +158,8 @@ impl RetainedDisplayList { } let diff = transform_diff_class(previous.world_transform, snapshot.world_transform); - !element.paint.transform_class.supports(diff) || !element.post.transform_class.supports(diff) + !element.paint.transform_class.supports(diff) + || !element.post.transform_class.supports(diff) } pub(crate) fn retain_only(&mut self, ids: &FxHashSet) { @@ -349,7 +348,6 @@ impl RecordingRenderer<'_> { transform: self.transform, }); } - } pub(crate) fn replay_stage( @@ -360,7 +358,9 @@ pub(crate) fn replay_stage( for command in &stage.commands { match command { DisplayCommand::SetZIndex(z_index) => renderer.set_z_index(*z_index), - DisplayCommand::PushClip { clip, hint } => replay_clip(renderer, clip, hint.clone(), base_transform), + DisplayCommand::PushClip { clip, hint } => { + replay_clip(renderer, clip, hint.clone(), base_transform) + } DisplayCommand::PopClip => renderer.clear_clip(), DisplayCommand::PushLayer { group, @@ -422,7 +422,9 @@ pub(crate) fn replay_stage( } } DisplayCommand::PopLayer => renderer.pop_layer(), - DisplayCommand::Draw { draw, hint } => replay_draw(renderer, draw, hint.clone(), base_transform), + DisplayCommand::Draw { draw, hint } => { + replay_draw(renderer, draw, hint.clone(), base_transform) + } DisplayCommand::DrawImage { img, hash, @@ -458,7 +460,12 @@ pub(crate) fn replay_stage( } } -fn replay_clip(renderer: &mut impl FloemRenderer, clip: &Clip, hint: Option, base_transform: Affine) { +fn replay_clip( + renderer: &mut impl FloemRenderer, + clip: &Clip, + hint: Option, + base_transform: Affine, +) { let Clip::Fill { transform, shape, .. } = clip diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 28462040a..8b9a54b0c 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -31,8 +31,8 @@ use crate::view::stacking::{StackingContextItem, collect_stacking_context_items_ use crate::view::{paint_bg, paint_border, paint_outline}; use crate::window::state::WindowState; use display_list::{ - ElementSnapshot, RecordingRenderer, RetainedDisplayList, TransformClass, - replay_stage, transform_diff_class, + ElementSnapshot, RecordingRenderer, RetainedDisplayList, TransformClass, replay_stage, + transform_diff_class, }; std::thread_local! { diff --git a/src/text/layout.rs b/src/text/layout.rs index 68202d913..67bbd2080 100644 --- a/src/text/layout.rs +++ b/src/text/layout.rs @@ -19,7 +19,6 @@ use peniko::{ kurbo::{Affine, Point, Size}, }; - /// Shared Parley font context used by Floem text layout construction. /// /// This is exposed so callers that need to register or inspect fonts can work diff --git a/src/views/scroll.rs b/src/views/scroll.rs index b988e43aa..687c8de36 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -978,8 +978,7 @@ impl View for Scroll { && self .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() - { - } + {} return result.propagation; } if cx.target == self.h_handle.element_id { @@ -988,8 +987,7 @@ impl View for Scroll { && self .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() - { - } + {} return result.propagation; } if cx.target == self.v_track.element_id { @@ -998,8 +996,7 @@ impl View for Scroll { && self .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() - { - } + {} return result.propagation; } if cx.target == self.h_track.element_id { @@ -1008,8 +1005,7 @@ impl View for Scroll { && self .apply_scroll_delta(new_offset - self.scroll_offset) .is_some() - { - } + {} return result.propagation; } } diff --git a/src/views/svg.rs b/src/views/svg.rs index 166324a8a..59a41abbe 100644 --- a/src/views/svg.rs +++ b/src/views/svg.rs @@ -1,9 +1,7 @@ use std::{cell::RefCell, rc::Rc}; use floem_reactive::Effect; -use floem_renderer::{ - usvg::{self, Tree}, -}; +use floem_renderer::usvg::{self, Tree}; use peniko::{ Brush, GradientKind, LinearGradientPosition, kurbo::{Point, Size}, diff --git a/src/window/handle.rs b/src/window/handle.rs index 6145f6bf2..b51072fa2 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -429,7 +429,8 @@ impl WindowHandle { let scale = self.window_state.effective_scale(); self.paint_state.set_scale(scale); self.event(Event::Window(WindowEvent::ScaleChanged(scale))); - self.window_state.request_paint(self.window_state.root_view_id); + self.window_state + .request_paint(self.window_state.root_view_id); self.schedule_repaint(); } @@ -501,7 +502,8 @@ impl WindowHandle { self.layout(); self.commit_box_tree(); self.process_update_no_paint(); - self.window_state.request_paint(self.window_state.root_view_id); + self.window_state + .request_paint(self.window_state.root_view_id); self.schedule_repaint(); } @@ -785,8 +787,7 @@ impl WindowHandle { } if self.live_resize_active() { - self.window_state - .schedule_paint(self.id.get_element_id()); + self.window_state.schedule_paint(self.id.get_element_id()); self.schedule_repaint(); } else { self.live_resize_until = None; diff --git a/src/window/state.rs b/src/window/state.rs index 9ffbd5196..32871a1af 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -953,14 +953,25 @@ impl WindowState { .local_bounds(dragging_preview.element_id.0) .unwrap_or_default(); - // Get current world transform and update natural position (detects layout changes) - let current_transform = self - .box_tree - .borrow_mut() - .get_or_compute_world_transform(dragging_preview.element_id.0) - .unwrap_or(Affine::IDENTITY); - - let natural_position = dragging.update_and_get_natural_position(current_transform); + // Reconstruct the authored world transform from the node's local transform and its + // parent's current world transform. This intentionally ignores any active + // set_world_position override on the dragged element itself so the release animation + // targets the real layout position instead of the preview override. + let natural_transform = { + let mut box_tree = self.box_tree.borrow_mut(); + let element_id = dragging_preview.element_id.0; + let local_transform = box_tree + .local_transform(element_id) + .unwrap_or(Affine::IDENTITY); + let parent_world_transform = box_tree + .parent_of(element_id) + .and_then(|parent| box_tree.get_or_compute_world_transform(parent)) + .unwrap_or(Affine::IDENTITY); + + parent_world_transform * local_transform + }; + + let natural_position = dragging.update_and_get_natural_position(natural_transform); // Calculate the drag point offset (where user grabbed within the element) let drag_point_offset = Point::new( @@ -1005,7 +1016,8 @@ impl WindowState { let damage = self.box_tree.borrow_mut().commit(); - self.pending_damage_rects.extend(damage.dirty_rects.iter().copied()); + self.pending_damage_rects + .extend(damage.dirty_rects.iter().copied()); let pointer = self.last_pointer; for damage_rect in &damage.dirty_rects { From 0aff75ee4d60511847a07d2d86ea8772ece18374 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 18 Mar 2026 17:54:45 -0600 Subject: [PATCH 005/102] request paint on set local bounds --- Cargo.lock | 5 ----- Cargo.toml | 16 +++++++++++----- src/box_tree.rs | 18 ++++++++++++++++++ src/inspector/view.rs | 2 +- src/view/id.rs | 6 +----- src/view/storage.rs | 11 +++++++++++ src/views/resizable.rs | 4 +--- src/views/scroll.rs | 12 ++++-------- src/views/toggle_button.rs | 9 +++------ src/window/state.rs | 20 ++++++++++---------- 10 files changed, 60 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a411cdc80..eb27b6c3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6376,7 +6376,6 @@ dependencies = [ [[package]] name = "understory_box_tree" version = "0.0.1" -source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "bitflags 2.11.0", "kurbo", @@ -6386,7 +6385,6 @@ dependencies = [ [[package]] name = "understory_event_state" version = "0.1.0" -source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "kurbo", ] @@ -6394,7 +6392,6 @@ dependencies = [ [[package]] name = "understory_focus" version = "0.1.0" -source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "kurbo", ] @@ -6402,7 +6399,6 @@ dependencies = [ [[package]] name = "understory_index" version = "0.0.1" -source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" dependencies = [ "hashbrown 0.16.1", "smallvec", @@ -6411,7 +6407,6 @@ dependencies = [ [[package]] name = "understory_virtual_list" version = "0.1.0" -source = "git+https://github.com/jrmoulton/understory?rev=0ae67e1bf2a8#0ae67e1bf2a80acef00149fd717868e9418a08ad" [[package]] name = "unic-langid" diff --git a/Cargo.toml b/Cargo.toml index 3c9fb6b50..8b8eb04ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,11 +116,17 @@ fluent-bundle = { version = "0.16", optional = true } unic-langid = { version = "0.9", optional = true } sys-locale = { version = "0.3.2", optional = true } ouroboros = { version = "0.18", optional = true } -understory_box_tree = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_box_tree" } -understory_event_state = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_event_state" } -understory_index = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_index" } -understory_focus = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_focus" } -understory_virtual_list = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_virtual_list" } +# understory_box_tree = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_box_tree" } +# understory_event_state = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_event_state" } +# understory_index = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_index" } +# understory_focus = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_focus" } +# understory_virtual_list = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_virtual_list" } + +understory_box_tree = { path = "../understory/understory_box_tree", package = "understory_box_tree" } +understory_event_state = { path = "../understory/understory_event_state", package = "understory_event_state" } +understory_index = { path = "../understory/understory_index", package = "understory_index" } +understory_focus = { path = "../understory/understory_focus", package = "understory_focus" } +understory_virtual_list = { path = "../understory/understory_virtual_list", package = "understory_virtual_list" } [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } diff --git a/src/box_tree.rs b/src/box_tree.rs index 86f7930b8..896a0a6a5 100644 --- a/src/box_tree.rs +++ b/src/box_tree.rs @@ -1,9 +1,13 @@ use std::ops::{Deref, DerefMut}; +use peniko::kurbo::{Rect, RoundedRect}; use rustc_hash::FxHashMap; use crate::ViewId; +use crate::action::add_update_message; +use crate::message::UpdateMessage; use crate::paint::display_list::TransformClass; +use crate::view::VIEW_STORAGE; /// A visual identifier that represents a rectangle in the box tree. /// @@ -60,6 +64,20 @@ impl ElementId { pub fn is_view(&self) -> bool { self.2 } + + pub fn set_local_clip(&self, clip: Option) { + let box_tree = VIEW_STORAGE.with_borrow(|s| s.existing_box_tree(self.owning_id())); + if box_tree.borrow_mut().set_local_clip(self.0, clip) { + add_update_message(UpdateMessage::RequestPaint(*self)); + } + } + + pub fn set_local_bounds(&self, rect: Rect) { + let box_tree = VIEW_STORAGE.with_borrow(|s| s.existing_box_tree(self.owning_id())); + if box_tree.borrow_mut().set_local_bounds(self.0, rect) { + add_update_message(UpdateMessage::RequestPaint(*self)); + } + } } /// Per-element focus navigation metadata kept in the box tree. diff --git a/src/inspector/view.rs b/src/inspector/view.rs index ecdbe64f0..36058c891 100644 --- a/src/inspector/view.rs +++ b/src/inspector/view.rs @@ -675,7 +675,6 @@ fn register_capture_elements( let box_tree = owner_id.box_tree(); let mut bt = box_tree.borrow_mut(); bt.reparent(element.0, Some(parent_element.0)); - bt.set_local_bounds(element.0, rect); bt.set_flags(element.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE); bt.set_element_meta( element.0, @@ -684,6 +683,7 @@ fn register_capture_elements( ))), ); drop(bt); + element.set_local_bounds(rect); element_to_data_id.insert(element, captured.id_data_str.clone()); next_parent = element; } diff --git a/src/view/id.rs b/src/view/id.rs index 9d7cee5bc..dc1c0b698 100644 --- a/src/view/id.rs +++ b/src/view/id.rs @@ -782,11 +782,7 @@ impl ViewId { /// set the clip rectange in local coordinates in the box tree pub fn set_box_tree_clip(&self, clip: Option) { - let element_id = self.get_element_id(); - VIEW_STORAGE.with_borrow_mut(|s| { - let box_tree = s.box_tree(*self); - box_tree.borrow_mut().set_local_clip(element_id.0, clip) - }) + self.get_element_id().set_local_clip(clip); } /// Returns true if this view is hidden. diff --git a/src/view/storage.rs b/src/view/storage.rs index db029207f..9f6bf504b 100644 --- a/src/view/storage.rs +++ b/src/view/storage.rs @@ -142,6 +142,17 @@ impl ViewStorage { .clone() } + pub(crate) fn existing_box_tree(&self, view_id: ViewId) -> Rc> { + let root = self + .root + .get(view_id) + .expect("all view ids are created with a root"); + self.box_tree + .get(root) + .expect("box tree should exist for live view") + .clone() + } + pub(crate) fn state(&mut self, id: ViewId) -> Rc> { if !self.view_ids.contains_key(id) { // if view_ids doesn't have this view id, that means it's been cleaned up, diff --git a/src/views/resizable.rs b/src/views/resizable.rs index 53b47e543..80ba8c86a 100644 --- a/src/views/resizable.rs +++ b/src/views/resizable.rs @@ -211,9 +211,7 @@ impl Handle { } }; - self.box_tree - .borrow_mut() - .set_local_bounds(self.element_id.0, new_rect); + self.element_id.set_local_bounds(new_rect); self.box_tree .borrow_mut() .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE); diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 687c8de36..fa42bee0c 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -107,7 +107,7 @@ impl ScrollHandle { let resolved = cx.resolve_nested_maps(Style::new(), &[Handle::class_ref()], self.element_id); if self.style.read_style_for(cx, &resolved, self.element_id) { - self.element_id.owning_id().request_paint(); + cx.window_state.request_paint(self.element_id); } } @@ -241,9 +241,7 @@ impl ScrollHandle { } }; - self.box_tree - .borrow_mut() - .set_local_bounds(self.element_id.0, rect); + self.element_id.set_local_bounds(rect); self.box_tree .borrow_mut() .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE); @@ -329,7 +327,7 @@ impl ScrollTrack { fn style(&mut self, cx: &mut StyleCx) { let resolved = cx.resolve_nested_maps(Style::new(), &[Track::class_ref()], self.element_id); if self.style.read_style_for(cx, &resolved, self.element_id) { - self.element_id.owning_id().request_paint(); + cx.window_state.request_paint(self.element_id); } } @@ -428,9 +426,7 @@ impl ScrollTrack { } }; - self.box_tree - .borrow_mut() - .set_local_bounds(self.element_id.0, rect); + self.element_id.set_local_bounds(rect); self.box_tree .borrow_mut() .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE); diff --git a/src/views/toggle_button.rs b/src/views/toggle_button.rs index ef8eae488..524ea3314 100644 --- a/src/views/toggle_button.rs +++ b/src/views/toggle_button.rs @@ -2,7 +2,7 @@ //! A toggle button widget. An example can be found in [widget-gallery/button](https://github.com/lapce/floem/tree/main/examples/widget-gallery) //! in the floem examples. -use std::{cell::RefCell, rc::Rc, time::Duration}; +use std::time::Duration; use floem_reactive::{Effect, SignalGet, SignalUpdate}; use peniko::Brush; @@ -13,7 +13,7 @@ use crate::context::Phases; use crate::custom_event; use crate::event::listener::EventListenerTrait; use crate::{ - BoxTree, ElementId, + ElementId, context::{EventCx, PaintCx, UpdateCx}, easing::Linear, event::{ @@ -60,7 +60,6 @@ custom_event!(ToggleChanged, bool, ToggleChanged::extract_inner); struct Handle { element_id: ElementId, - box_tree: Rc>, position: f64, parent_id: ViewId, dragged: bool, @@ -72,7 +71,6 @@ impl Handle { Self { parent_id, element_id: parent_id.create_child_element_id(1), - box_tree: parent_id.box_tree(), position: 0.0, dragged: false, moved_on_down: false, @@ -93,8 +91,7 @@ impl Handle { self.position + radius, size.height, ); - let mut bt = self.box_tree.borrow_mut(); - bt.set_local_bounds(self.element_id.0, rect); + self.element_id.set_local_bounds(rect); } fn snap(&mut self, state: bool, size: Size, radius: f64, inset: f64) { diff --git a/src/window/state.rs b/src/window/state.rs index 32871a1af..867019b1a 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -901,10 +901,11 @@ impl WindowState { let props = compute_view_box_properties(s, view_id, layout, parent_scroll); // Update box tree - let mut box_tree = self.box_tree.borrow_mut(); - box_tree.set_local_bounds(props.element_id.0, props.local_rect); - box_tree.set_local_clip(props.element_id.0, props.clip); - box_tree.set_local_transform(props.element_id.0, props.local_transform); + props.element_id.set_local_bounds(props.local_rect); + props.element_id.set_local_clip(props.clip); + self.box_tree + .borrow_mut() + .set_local_transform(props.element_id.0, props.local_transform); } } }); @@ -1487,12 +1488,11 @@ fn compute_absolute_transforms_and_boxes( let props = compute_view_box_properties(s, view_id, layout, parent_scroll); // Update box tree - { - let mut box_tree = box_tree.borrow_mut(); - box_tree.set_local_bounds(props.element_id.0, props.local_rect); - box_tree.set_local_clip(props.element_id.0, props.clip); - box_tree.set_local_transform(props.element_id.0, props.local_transform); - } + props.element_id.set_local_bounds(props.local_rect); + props.element_id.set_local_clip(props.clip); + box_tree + .borrow_mut() + .set_local_transform(props.element_id.0, props.local_transform); // Recurse with this view's scroll offset if let Some(children) = children { From 53ebb095cdd9e5d29d1695e38e0c081dc9b5d19e Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 18 Mar 2026 14:10:27 -0600 Subject: [PATCH 006/102] fix clip --- src/paint/display_list.rs | 1 + src/paint/mod.rs | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index 2a429c4d4..f60297454 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -112,6 +112,7 @@ impl Default for ElementStage { pub(crate) struct ElementSnapshot { pub local_bounds: Rect, pub clip: Option, + pub effective_clip: Option, pub world_transform: Affine, } diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 8b9a54b0c..0de04e69a 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -113,6 +113,21 @@ fn record_paint(id: ViewId) { }); } +fn effective_clip_not_loosened( + recorded: Option, + current: Option, +) -> bool { + match (recorded, current) { + (None, _) => true, + (Some(_), None) => false, + (Some(recorded), Some(current)) => { + recorded == current + || (recorded.radii() == current.radii() + && recorded.rect().contains_rect(current.rect())) + } + } +} + /// Global paint context - holds shared state for entire paint pass /// Similar to GlobalEventCx in event dispatch pub struct GlobalPaintCx<'a> { @@ -237,6 +252,7 @@ impl GlobalPaintCx<'_> { let snapshot = ElementSnapshot { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), clip: box_tree.local_clip(element_id.0).flatten(), + effective_clip: box_tree.clipped_local_clip(element_id.0), world_transform: box_tree .get_or_compute_world_transform(element_id.0) .unwrap_or_default(), @@ -248,6 +264,7 @@ impl GlobalPaintCx<'_> { let diff = transform_diff_class(previous.world_transform, snapshot.world_transform); if previous.local_bounds != snapshot.local_bounds || previous.clip != snapshot.clip + || !effective_clip_not_loosened(previous.effective_clip, snapshot.effective_clip) || !boundary.supports(diff) { continue; @@ -345,6 +362,7 @@ impl GlobalPaintCx<'_> { let snapshot = ElementSnapshot { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), clip: box_tree.local_clip(element_id.0).flatten(), + effective_clip: box_tree.clipped_local_clip(element_id.0), world_transform: box_tree .get_or_compute_world_transform(element_id.0) .unwrap_or_default(), @@ -419,10 +437,12 @@ impl GlobalPaintCx<'_> { // Retained identity uses the element's own local clip. Do not use // `clipped_local_clip()` here: that value includes ancestor/world clipping // and would make the retained entry depend on transient parent state. - let clip = box_tree.local_clip(element_id.0).flatten(); + let local_clip = box_tree.local_clip(element_id.0).flatten(); + let effective_clip = box_tree.clipped_local_clip(element_id.0); let snapshot = ElementSnapshot { local_bounds: layout_rect_local, - clip, + clip: local_clip, + effective_clip, world_transform: box_tree .get_or_compute_world_transform(element_id.0) .unwrap_or_default(), @@ -450,7 +470,7 @@ impl GlobalPaintCx<'_> { target_id: element_id, world_transform, layout_rect_local, - clip, + clip: local_clip, font_size_cx, }; @@ -466,13 +486,13 @@ impl GlobalPaintCx<'_> { ); drop(state); // Apply overflow clip (stays active through children) - if let Some(clip_shape) = clip { + if let Some(clip_shape) = effective_clip { cx.clip(&clip_shape); } } view.borrow_mut().paint(&mut cx); } else { - if element_id.is_view() && clip.is_some() { + if element_id.is_view() && effective_clip.is_some() { cx.pop_clip(); } view.borrow_mut().post_paint(&mut cx); From 261e1ce634adca569798e6f53f5a06a09c28b227 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 18 Mar 2026 18:46:29 -0600 Subject: [PATCH 007/102] move display list to window state --- src/app/handle.rs | 5 +--- src/paint/mod.rs | 56 +++++++++++---------------------------------- src/window/state.rs | 3 +++ 3 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/app/handle.rs b/src/app/handle.rs index df7aba055..589947b3a 100644 --- a/src/app/handle.rs +++ b/src/app/handle.rs @@ -107,10 +107,7 @@ impl ApplicationHandle { *font_embolden, ); self.gpu_resources = Some(gpu_resources); - handle.paint_state = PaintState::Initialized { - renderer, - display_list: crate::paint::display_list::RetainedDisplayList::default(), - }; + handle.paint_state = PaintState::Initialized { renderer }; handle.gpu_resources = self.gpu_resources.clone(); handle.init_renderer(); } else { diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 0de04e69a..5ad36d09b 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -31,8 +31,7 @@ use crate::view::stacking::{StackingContextItem, collect_stacking_context_items_ use crate::view::{paint_bg, paint_border, paint_outline}; use crate::window::state::WindowState; use display_list::{ - ElementSnapshot, RecordingRenderer, RetainedDisplayList, TransformClass, replay_stage, - transform_diff_class, + ElementSnapshot, RecordingRenderer, TransformClass, replay_stage, transform_diff_class, }; std::thread_local! { @@ -235,7 +234,7 @@ impl GlobalPaintCx<'_> { explicit_dirty: &FxHashSet, ) -> FxHashSet { let mut reusable_descendants = FxHashSet::default(); - let display_list = self.paint_state.display_list(); + let display_list = &self.window_state.display_list; let mut box_tree = self.window_state.box_tree.borrow_mut(); let mut stack = Vec::new(); @@ -345,9 +344,9 @@ impl GlobalPaintCx<'_> { }) .collect(); - self.paint_state.display_list_mut().retain_only(&active_ids); - self.paint_state - .display_list_mut() + self.window_state.display_list.retain_only(&active_ids); + self.window_state + .display_list .set_paint_order(paint_order.clone()); let mut dirty_ids = self.window_state.take_dirty_paint_elements(); @@ -361,7 +360,7 @@ impl GlobalPaintCx<'_> { .map(|element_id| { let snapshot = ElementSnapshot { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), - clip: box_tree.local_clip(element_id.0).flatten(), + clip: box_tree.clipped_local_clip(element_id.0), effective_clip: box_tree.clipped_local_clip(element_id.0), world_transform: box_tree .get_or_compute_world_transform(element_id.0) @@ -376,11 +375,7 @@ impl GlobalPaintCx<'_> { if reusable_descendants.contains(&element_id) { continue; } - if self - .paint_state - .display_list() - .needs_rerecord(element_id, snapshot) - { + if self.window_state.display_list.needs_rerecord(element_id, snapshot) { dirty_ids.insert(element_id); } } @@ -389,7 +384,7 @@ impl GlobalPaintCx<'_> { self.record_visual_node(element_id, true); } - let replay_order = self.paint_state.display_list().paint_order().to_vec(); + let replay_order = self.window_state.display_list.paint_order().to_vec(); for id_or_pop in replay_order { match id_or_pop { PaintOrPost::Paint(element_id) => { @@ -418,7 +413,7 @@ impl GlobalPaintCx<'_> { let base_transform = self .element_base_transform(element_id) .then_scale(self.window_state.effective_scale()); - let Some(element) = self.paint_state.display_list().element(element_id) else { + let Some(element) = self.window_state.display_list.element(element_id) else { return; }; let stage = if is_post { @@ -434,10 +429,7 @@ impl GlobalPaintCx<'_> { pub(crate) fn record_visual_node(&mut self, element_id: ElementId, is_post: bool) { let mut box_tree = self.window_state.box_tree.borrow_mut(); let layout_rect_local = box_tree.local_bounds(element_id.0).unwrap_or_default(); - // Retained identity uses the element's own local clip. Do not use - // `clipped_local_clip()` here: that value includes ancestor/world clipping - // and would make the retained entry depend on transient parent state. - let local_clip = box_tree.local_clip(element_id.0).flatten(); + let local_clip = box_tree.clipped_local_clip(element_id.0); let effective_clip = box_tree.clipped_local_clip(element_id.0); let snapshot = ElementSnapshot { local_bounds: layout_rect_local, @@ -503,8 +495,7 @@ impl GlobalPaintCx<'_> { } } - let display_list = self.paint_state.display_list_mut(); - let element = display_list.element_mut(element_id); + let element = self.window_state.display_list.element_mut(element_id); let stage = if is_post { &mut element.post } else { @@ -640,7 +631,6 @@ pub enum PaintState { /// The renderer is initialized and ready to paint. Initialized { renderer: Renderer, - display_list: RetainedDisplayList, }, } @@ -677,7 +667,6 @@ impl PaintState { ); Self::Initialized { renderer, - display_list: RetainedDisplayList::default(), } } @@ -686,39 +675,20 @@ impl PaintState { let renderer = Renderer::new_skia(window.clone(), scale, size, font_embolden); Self::Initialized { renderer, - display_list: RetainedDisplayList::default(), } } pub(crate) fn renderer(&self) -> &Renderer { match self { PaintState::PendingGpuResources { renderer, .. } => renderer, - PaintState::Initialized { renderer, .. } => renderer, + PaintState::Initialized { renderer } => renderer, } } pub(crate) fn renderer_mut(&mut self) -> &mut Renderer { match self { PaintState::PendingGpuResources { renderer, .. } => renderer, - PaintState::Initialized { renderer, .. } => renderer, - } - } - - pub(crate) fn display_list(&self) -> &RetainedDisplayList { - match self { - PaintState::PendingGpuResources { .. } => { - panic!("display list is unavailable before renderer initialization") - } - PaintState::Initialized { display_list, .. } => display_list, - } - } - - pub(crate) fn display_list_mut(&mut self) -> &mut RetainedDisplayList { - match self { - PaintState::PendingGpuResources { .. } => { - panic!("display list is unavailable before renderer initialization") - } - PaintState::Initialized { display_list, .. } => display_list, + PaintState::Initialized { renderer } => renderer, } } diff --git a/src/window/state.rs b/src/window/state.rs index 867019b1a..d1e97747b 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -27,6 +27,7 @@ use crate::{ event::{DragTracker, Event, WindowEvent, clear_hit_test_cache}, layout::responsive::{GridBreakpoints, ScreenSizeBp}, message::UpdateMessage, + paint::display_list::RetainedDisplayList, style::{CursorStyle, Style, StyleSelector, theme::default_theme}, view::{LayoutNodeCx, MeasureCx, VIEW_STORAGE, ViewId}, }; @@ -100,6 +101,7 @@ fn build_focus_space_for_scope<'a>( pub struct WindowState { pub(crate) layout_tree: Rc>>, pub(crate) box_tree: Rc>, + pub(crate) display_list: RetainedDisplayList, /// Per-pointer capture tracking inspired by Chromium's PointerEventManager. /// Maps pointer IDs to the view that has captured that pointer. @@ -210,6 +212,7 @@ impl WindowState { root_view_id, layout_tree, box_tree, + display_list: RetainedDisplayList::default(), pointer_capture_target: PointerCaptureMap::new(), pending_pointer_capture_target: PointerCaptureMap::new(), os_scale, From e2d519c2f024697b0869d7780f5ec85433970ee3 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 18 Mar 2026 23:15:59 -0600 Subject: [PATCH 008/102] paint debug and bump understory --- Cargo.lock | 5 +++ Cargo.toml | 22 ++++++------- benches/painting.rs | 4 ++- src/box_tree.rs | 24 +++++++++++--- src/event/dispatch.rs | 4 +-- src/event/drag_state.rs | 2 +- src/headless.rs | 7 ++++- src/paint/mod.rs | 62 +++++++++++++++++++++---------------- src/view/id.rs | 8 ++--- src/views/scroll.rs | 44 ++++++++++++++++++++++++-- src/window/state.rs | 21 ++++++++----- tests/scroll_paint_stats.rs | 38 +++++++++++++++++++++++ 12 files changed, 180 insertions(+), 61 deletions(-) create mode 100644 tests/scroll_paint_stats.rs diff --git a/Cargo.lock b/Cargo.lock index eb27b6c3a..b4df8fb1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6376,6 +6376,7 @@ dependencies = [ [[package]] name = "understory_box_tree" version = "0.0.1" +source = "git+https://github.com/jrmoulton/understory?rev=2c2abb8c4bd9#2c2abb8c4bd9bcca1168e51cbc614d8cfd2d3910" dependencies = [ "bitflags 2.11.0", "kurbo", @@ -6385,6 +6386,7 @@ dependencies = [ [[package]] name = "understory_event_state" version = "0.1.0" +source = "git+https://github.com/jrmoulton/understory?rev=2c2abb8c4bd9#2c2abb8c4bd9bcca1168e51cbc614d8cfd2d3910" dependencies = [ "kurbo", ] @@ -6392,6 +6394,7 @@ dependencies = [ [[package]] name = "understory_focus" version = "0.1.0" +source = "git+https://github.com/jrmoulton/understory?rev=2c2abb8c4bd9#2c2abb8c4bd9bcca1168e51cbc614d8cfd2d3910" dependencies = [ "kurbo", ] @@ -6399,6 +6402,7 @@ dependencies = [ [[package]] name = "understory_index" version = "0.0.1" +source = "git+https://github.com/jrmoulton/understory?rev=2c2abb8c4bd9#2c2abb8c4bd9bcca1168e51cbc614d8cfd2d3910" dependencies = [ "hashbrown 0.16.1", "smallvec", @@ -6407,6 +6411,7 @@ dependencies = [ [[package]] name = "understory_virtual_list" version = "0.1.0" +source = "git+https://github.com/jrmoulton/understory?rev=2c2abb8c4bd9#2c2abb8c4bd9bcca1168e51cbc614d8cfd2d3910" [[package]] name = "unic-langid" diff --git a/Cargo.toml b/Cargo.toml index 8b8eb04ec..62966bec9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,17 +116,17 @@ fluent-bundle = { version = "0.16", optional = true } unic-langid = { version = "0.9", optional = true } sys-locale = { version = "0.3.2", optional = true } ouroboros = { version = "0.18", optional = true } -# understory_box_tree = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_box_tree" } -# understory_event_state = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_event_state" } -# understory_index = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_index" } -# understory_focus = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_focus" } -# understory_virtual_list = { git = "https://github.com/jrmoulton/understory", rev = "0ae67e1bf2a8", package = "understory_virtual_list" } - -understory_box_tree = { path = "../understory/understory_box_tree", package = "understory_box_tree" } -understory_event_state = { path = "../understory/understory_event_state", package = "understory_event_state" } -understory_index = { path = "../understory/understory_index", package = "understory_index" } -understory_focus = { path = "../understory/understory_focus", package = "understory_focus" } -understory_virtual_list = { path = "../understory/understory_virtual_list", package = "understory_virtual_list" } +understory_box_tree = { git = "https://github.com/jrmoulton/understory", rev = "2c2abb8c4bd9", package = "understory_box_tree" } +understory_event_state = { git = "https://github.com/jrmoulton/understory", rev = "2c2abb8c4bd9", package = "understory_event_state" } +understory_index = { git = "https://github.com/jrmoulton/understory", rev = "2c2abb8c4bd9", package = "understory_index" } +understory_focus = { git = "https://github.com/jrmoulton/understory", rev = "2c2abb8c4bd9", package = "understory_focus" } +understory_virtual_list = { git = "https://github.com/jrmoulton/understory", rev = "2c2abb8c4bd9", package = "understory_virtual_list" } + +# understory_box_tree = { path = "../understory/understory_box_tree", package = "understory_box_tree" } +# understory_event_state = { path = "../understory/understory_event_state", package = "understory_event_state" } +# understory_index = { path = "../understory/understory_index", package = "understory_index" } +# understory_focus = { path = "../understory/understory_focus", package = "understory_focus" } +# understory_virtual_list = { path = "../understory/understory_virtual_list", package = "understory_virtual_list" } [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } diff --git a/benches/painting.rs b/benches/painting.rs index 98afc9a00..cd6e52d6c 100644 --- a/benches/painting.rs +++ b/benches/painting.rs @@ -321,7 +321,8 @@ fn bench_scroll_repaint(c: &mut Criterion) { || { let root = TestRoot::new(); let view = create_scroll_label_list(n); - HeadlessHarness::new_with_size(root, view, 1000.0, 240.0) + let mut harness = HeadlessHarness::new_with_size(root, view, 1000.0, 240.0); + harness }, |mut harness| { harness.paint(); @@ -360,6 +361,7 @@ fn bench_scroll_repaint(c: &mut Criterion) { let root = TestRoot::new(); let view = create_scroll_label_list(n); let mut harness = HeadlessHarness::new_with_size(root, view, 320.0, 240.0); + harness.rebuild(); harness.paint(); let mut scroll_down = true; diff --git a/src/box_tree.rs b/src/box_tree.rs index 896a0a6a5..ea42fb393 100644 --- a/src/box_tree.rs +++ b/src/box_tree.rs @@ -65,19 +65,35 @@ impl ElementId { self.2 } - pub fn set_local_clip(&self, clip: Option) { + fn set_local_clip_inner(&self, clip: Option, request_paint: bool) { let box_tree = VIEW_STORAGE.with_borrow(|s| s.existing_box_tree(self.owning_id())); - if box_tree.borrow_mut().set_local_clip(self.0, clip) { + if box_tree.borrow_mut().set_local_clip(self.0, clip) && request_paint { add_update_message(UpdateMessage::RequestPaint(*self)); } } - pub fn set_local_bounds(&self, rect: Rect) { + pub fn set_local_clip(&self, clip: Option) { + self.set_local_clip_inner(clip, true); + } + + pub(crate) fn set_local_clip_without_paint(&self, clip: Option) { + self.set_local_clip_inner(clip, false); + } + + fn set_local_bounds_inner(&self, rect: Rect, request_paint: bool) { let box_tree = VIEW_STORAGE.with_borrow(|s| s.existing_box_tree(self.owning_id())); - if box_tree.borrow_mut().set_local_bounds(self.0, rect) { + if box_tree.borrow_mut().set_local_bounds(self.0, rect) && request_paint { add_update_message(UpdateMessage::RequestPaint(*self)); } } + + pub fn set_local_bounds(&self, rect: Rect) { + self.set_local_bounds_inner(rect, true); + } + + pub(crate) fn set_local_bounds_without_paint(&self, rect: Rect) { + self.set_local_bounds_inner(rect, false); + } } /// Per-element focus navigation metadata kept in the box tree. diff --git a/src/event/dispatch.rs b/src/event/dispatch.rs index ed46233ac..9dcb6b8f2 100644 --- a/src/event/dispatch.rs +++ b/src/event/dispatch.rs @@ -834,7 +834,7 @@ impl RouteCx<'_, '_> { .window_state .box_tree .borrow_mut() - .get_or_compute_world_transform(dispatch_step.target_element_id.0) + .world_transform(dispatch_step.target_element_id.0) else { // we are storing pending events so it's possible that a node becomes stale. if it // did, don't panic, just continue. @@ -1265,7 +1265,7 @@ impl RouteCx<'_, '_> { .window_state .box_tree .borrow_mut() - .get_or_compute_world_bounds(hit.owning_id().get_element_id().0) + .world_bounds(hit.owning_id().get_element_id().0) .unwrap_or_default(); let bottom_left = Point::new(bounds.x0, bounds.y1); show_context_menu(menu(), Some(bottom_left)); diff --git a/src/event/drag_state.rs b/src/event/drag_state.rs index 51e7cee2a..869763fc2 100644 --- a/src/event/drag_state.rs +++ b/src/event/drag_state.rs @@ -421,7 +421,7 @@ impl DragTracker { // Calculate percentage based on custom_pct or initial click position // Get world bounds at drag start to know where user clicked let start_world_bounds = box_tree - .get_or_compute_world_bounds(pending.element_id.0) + .world_bounds(pending.element_id.0) .expect("drag source went stale during threshold check"); let click_offset_x = start_pos.x - start_world_bounds.x0; diff --git a/src/headless.rs b/src/headless.rs index c5f68ac4e..1a0292009 100644 --- a/src/headless.rs +++ b/src/headless.rs @@ -30,7 +30,8 @@ use crate::context::InteractionState; use crate::event::Event; use crate::event::path::hit_test; use crate::paint::{ - clear_paint_order, disable_paint_order_tracking, enable_paint_order_tracking, get_paint_order, + PaintStats, clear_paint_order, disable_paint_order_tracking, enable_paint_order_tracking, + get_paint_order, }; use crate::style::{Style, StyleSelector}; use crate::view::IntoView; @@ -535,6 +536,10 @@ impl HeadlessHarness { pub fn paint(&mut self) { self.window_handle.paint(); } + + pub fn last_paint_stats(&self) -> PaintStats { + self.window_handle.window_state.last_paint_stats + } } impl Drop for HeadlessHarness { diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 5ad36d09b..870d87965 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -71,6 +71,15 @@ impl PaintOrderTracker { } } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct PaintStats { + pub active_ids: usize, + pub explicit_dirty_ids: usize, + pub reusable_descendants: usize, + pub rerecord_ids: usize, + pub replay_steps: usize, +} + /// Enable paint order tracking. When enabled, all painted ViewIds are recorded in order. pub fn enable_paint_order_tracking() { PAINT_ORDER_TRACKER.with(|tracker| { @@ -183,7 +192,7 @@ pub(crate) fn collect_visual_order( } box_tree - .get_or_compute_world_bounds(element_id.0) + .world_bounds(element_id.0) .is_none_or(|bounds| bounds.area() != 0.0) }; @@ -236,7 +245,7 @@ impl GlobalPaintCx<'_> { let mut reusable_descendants = FxHashSet::default(); let display_list = &self.window_state.display_list; - let mut box_tree = self.window_state.box_tree.borrow_mut(); + let box_tree = self.window_state.box_tree.borrow(); let mut stack = Vec::new(); for &element_id in active_ids { @@ -252,9 +261,7 @@ impl GlobalPaintCx<'_> { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), clip: box_tree.local_clip(element_id.0).flatten(), effective_clip: box_tree.clipped_local_clip(element_id.0), - world_transform: box_tree - .get_or_compute_world_transform(element_id.0) - .unwrap_or_default(), + world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), }; let Some(previous) = display_list.element(element_id).and_then(|e| e.snapshot) else { @@ -350,10 +357,11 @@ impl GlobalPaintCx<'_> { .set_paint_order(paint_order.clone()); let mut dirty_ids = self.window_state.take_dirty_paint_elements(); + let explicit_dirty_ids = dirty_ids.len(); let reusable_descendants = self.collect_retained_subtree_descendants(&active_ids, &dirty_ids); let snapshots = { - let mut box_tree = self.window_state.box_tree.borrow_mut(); + let box_tree = self.window_state.box_tree.borrow(); active_ids .iter() .copied() @@ -362,9 +370,7 @@ impl GlobalPaintCx<'_> { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), clip: box_tree.clipped_local_clip(element_id.0), effective_clip: box_tree.clipped_local_clip(element_id.0), - world_transform: box_tree - .get_or_compute_world_transform(element_id.0) - .unwrap_or_default(), + world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), }; (element_id, snapshot) }) @@ -375,16 +381,28 @@ impl GlobalPaintCx<'_> { if reusable_descendants.contains(&element_id) { continue; } - if self.window_state.display_list.needs_rerecord(element_id, snapshot) { + if self + .window_state + .display_list + .needs_rerecord(element_id, snapshot) + { dirty_ids.insert(element_id); } } + let rerecord_ids = dirty_ids.len(); for element_id in dirty_ids { self.record_visual_node(element_id, false); self.record_visual_node(element_id, true); } let replay_order = self.window_state.display_list.paint_order().to_vec(); + self.window_state.last_paint_stats = PaintStats { + active_ids: active_ids.len(), + explicit_dirty_ids, + reusable_descendants: reusable_descendants.len(), + rerecord_ids, + replay_steps: replay_order.len(), + }; for id_or_pop in replay_order { match id_or_pop { PaintOrPost::Paint(element_id) => { @@ -403,10 +421,8 @@ impl GlobalPaintCx<'_> { fn element_base_transform(&mut self, element_id: ElementId) -> Affine { // Get state from box tree for this visual node - let mut box_tree = self.window_state.box_tree.borrow_mut(); - box_tree - .get_or_compute_world_transform(element_id.0) - .unwrap_or_default() + let box_tree = self.window_state.box_tree.borrow(); + box_tree.world_transform(element_id.0).unwrap_or_default() } fn replay_visual_node(&mut self, element_id: ElementId, is_post: bool) { @@ -427,7 +443,7 @@ impl GlobalPaintCx<'_> { /// Record a single visual node in local coordinates. pub(crate) fn record_visual_node(&mut self, element_id: ElementId, is_post: bool) { - let mut box_tree = self.window_state.box_tree.borrow_mut(); + let box_tree = self.window_state.box_tree.borrow_mut(); let layout_rect_local = box_tree.local_bounds(element_id.0).unwrap_or_default(); let local_clip = box_tree.clipped_local_clip(element_id.0); let effective_clip = box_tree.clipped_local_clip(element_id.0); @@ -435,9 +451,7 @@ impl GlobalPaintCx<'_> { local_bounds: layout_rect_local, clip: local_clip, effective_clip, - world_transform: box_tree - .get_or_compute_world_transform(element_id.0) - .unwrap_or_default(), + world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), }; drop(box_tree); @@ -629,9 +643,7 @@ pub enum PaintState { renderer: Renderer, }, /// The renderer is initialized and ready to paint. - Initialized { - renderer: Renderer, - }, + Initialized { renderer: Renderer }, } impl PaintState { @@ -665,17 +677,13 @@ impl PaintState { size, font_embolden, ); - Self::Initialized { - renderer, - } + Self::Initialized { renderer } } #[cfg(feature = "skia")] pub fn new_skia(window: Arc, scale: f64, size: Size, font_embolden: f32) -> Self { let renderer = Renderer::new_skia(window.clone(), scale, size, font_embolden); - Self::Initialized { - renderer, - } + Self::Initialized { renderer } } pub(crate) fn renderer(&self) -> &Renderer { diff --git a/src/view/id.rs b/src/view/id.rs index dc1c0b698..ebf17344b 100644 --- a/src/view/id.rs +++ b/src/view/id.rs @@ -599,7 +599,7 @@ impl ViewId { VIEW_STORAGE.with_borrow_mut(|s| { s.box_tree(*self) .borrow_mut() - .get_or_compute_world_transform(element_id.0) + .world_transform(element_id.0) .unwrap_or_default() }) } @@ -614,7 +614,7 @@ impl ViewId { VIEW_STORAGE.with_borrow_mut(|s| { s.box_tree(*self) .borrow_mut() - .get_or_compute_world_bounds(element_id.0) + .world_bounds(element_id.0) .unwrap_or_default() }) } @@ -642,7 +642,7 @@ impl ViewId { .with_borrow_mut(|s| { s.box_tree(*self) .borrow_mut() - .get_or_compute_world_bounds(element_id.0) + .world_bounds(element_id.0) .unwrap_or_default() }) .origin() @@ -1407,7 +1407,7 @@ impl ViewId { VIEW_STORAGE.with_borrow_mut(|s| { s.box_tree(*self) .borrow_mut() - .get_or_compute_world_transform(element_id.0) + .world_transform(element_id.0) .unwrap_or_default() * Point::ZERO }) diff --git a/src/views/scroll.rs b/src/views/scroll.rs index fa42bee0c..49e20a5fd 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -687,6 +687,43 @@ impl Scroll { /// internal methods impl Scroll { + fn clamp_scroll_offset_to_content(&mut self) -> bool { + let viewport_size = self.id.get_content_rect_local().size(); + let content_size = self.child.get_layout_rect_local().size(); + + let mut max_scroll = + (content_size.to_vec2() - viewport_size.to_vec2()).max_by_component(Vec2::ZERO); + + if !matches!(self.scroll_style.overflow_x(), taffy::Overflow::Scroll) { + max_scroll.x = 0.0; + } + if !matches!(self.scroll_style.overflow_y(), taffy::Overflow::Scroll) { + max_scroll.y = 0.0; + } + + let old_scroll_offset = self.scroll_offset; + self.scroll_offset = self + .scroll_offset + .max_by_component(Vec2::ZERO) + .min_by_component(max_scroll); + let changed = old_scroll_offset != self.scroll_offset; + let translation_changed = self.id.set_child_translation(self.scroll_offset); + + if translation_changed { + self.id.route_event( + Event::new_custom(ScrollChanged { + offset: self.scroll_offset, + }), + RouteKind::Directed { + target: self.id.get_element_id(), + phases: crate::context::Phases::TARGET, + }, + ); + } + + changed + } + /// this applies a delta, set the viewport in the window state and returns the delta that was actually applied /// /// If the delta is positive, the view will scroll down, negative will scroll up. @@ -799,7 +836,7 @@ impl Scroll { fn do_scroll_to_element(&mut self, scroll_to: ScrollTo) -> EventPropagation { let child_element_id = self.child.get_element_id(); let box_tree = self.id.box_tree(); - let mut box_tree = box_tree.borrow_mut(); + let box_tree = box_tree.borrow(); let Some(target_local_rect) = scroll_to .rect @@ -809,10 +846,10 @@ impl Scroll { }; let target_transform = box_tree - .get_or_compute_world_transform(scroll_to.id.0) + .world_transform(scroll_to.id.0) .unwrap_or(Affine::IDENTITY); let child_transform = box_tree - .get_or_compute_world_transform(child_element_id.0) + .world_transform(child_element_id.0) .unwrap_or(Affine::IDENTITY); let target_world_rect = target_transform.transform_rect_bbox(target_local_rect); @@ -959,6 +996,7 @@ impl View for Scroll { fn event(&mut self, cx: &mut EventCx) -> EventPropagation { // in order to use this we had to set `id.has_layout_listener`. if UpdatePhaseLayout::extract(&cx.event).is_some() { + self.clamp_scroll_offset_to_content(); self.set_positions(); return EventPropagation::Stop; } diff --git a/src/window/state.rs b/src/window/state.rs index d1e97747b..cfaf67ac1 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -27,6 +27,7 @@ use crate::{ event::{DragTracker, Event, WindowEvent, clear_hit_test_cache}, layout::responsive::{GridBreakpoints, ScreenSizeBp}, message::UpdateMessage, + paint::PaintStats, paint::display_list::RetainedDisplayList, style::{CursorStyle, Style, StyleSelector, theme::default_theme}, view::{LayoutNodeCx, MeasureCx, VIEW_STORAGE, ViewId}, @@ -102,6 +103,7 @@ pub struct WindowState { pub(crate) layout_tree: Rc>>, pub(crate) box_tree: Rc>, pub(crate) display_list: RetainedDisplayList, + pub(crate) last_paint_stats: PaintStats, /// Per-pointer capture tracking inspired by Chromium's PointerEventManager. /// Maps pointer IDs to the view that has captured that pointer. @@ -213,6 +215,7 @@ impl WindowState { layout_tree, box_tree, display_list: RetainedDisplayList::default(), + last_paint_stats: PaintStats::default(), pointer_capture_target: PointerCaptureMap::new(), pending_pointer_capture_target: PointerCaptureMap::new(), os_scale, @@ -904,8 +907,10 @@ impl WindowState { let props = compute_view_box_properties(s, view_id, layout, parent_scroll); // Update box tree - props.element_id.set_local_bounds(props.local_rect); - props.element_id.set_local_clip(props.clip); + props + .element_id + .set_local_bounds_without_paint(props.local_rect); + props.element_id.set_local_clip_without_paint(props.clip); self.box_tree .borrow_mut() .set_local_transform(props.element_id.0, props.local_transform); @@ -962,14 +967,14 @@ impl WindowState { // set_world_position override on the dragged element itself so the release animation // targets the real layout position instead of the preview override. let natural_transform = { - let mut box_tree = self.box_tree.borrow_mut(); + let box_tree = self.box_tree.borrow(); let element_id = dragging_preview.element_id.0; let local_transform = box_tree .local_transform(element_id) .unwrap_or(Affine::IDENTITY); let parent_world_transform = box_tree .parent_of(element_id) - .and_then(|parent| box_tree.get_or_compute_world_transform(parent)) + .and_then(|parent| box_tree.world_transform(parent)) .unwrap_or(Affine::IDENTITY); parent_world_transform * local_transform @@ -1114,7 +1119,7 @@ impl WindowState { let mut tree = self.box_tree.borrow_mut(); for (overlay_element_id, logical_parent_element_id, base_local_transform) in overlay_data { let parent_world = tree - .get_or_compute_world_transform(logical_parent_element_id.0) + .world_transform(logical_parent_element_id.0) .unwrap_or(Affine::IDENTITY); tree.set_local_transform(overlay_element_id.0, parent_world * base_local_transform); } @@ -1491,8 +1496,10 @@ fn compute_absolute_transforms_and_boxes( let props = compute_view_box_properties(s, view_id, layout, parent_scroll); // Update box tree - props.element_id.set_local_bounds(props.local_rect); - props.element_id.set_local_clip(props.clip); + props + .element_id + .set_local_bounds_without_paint(props.local_rect); + props.element_id.set_local_clip_without_paint(props.clip); box_tree .borrow_mut() .set_local_transform(props.element_id.0, props.local_transform); diff --git a/tests/scroll_paint_stats.rs b/tests/scroll_paint_stats.rs new file mode 100644 index 000000000..6b0556942 --- /dev/null +++ b/tests/scroll_paint_stats.rs @@ -0,0 +1,38 @@ +use floem::headless::{HeadlessHarness, TestRoot}; +use floem::prelude::*; +use floem::views::{Decorators, Label, ScrollExt, Stack}; + +fn create_scroll_label_list(n: usize) -> impl IntoView { + let rows: Vec<_> = (0..n) + .map(|i| { + Label::new(format!( + "Row {i:05} | benchmark label content for retained scroll repaint" + )) + .style(|s| s.width_full().height(24.0).padding_horiz(6.0).padding_vert(2.0)) + .into_any() + }) + .collect(); + + Stack::from_iter(rows) + .style(|s| s.flex_col().width_full()) + .scroll() + .style(|s| s.size(320.0, 240.0)) +} + +#[test] +fn print_scroll_paint_stats_after_rebuild() { + let root = TestRoot::new(); + let view = create_scroll_label_list(1_000); + let mut harness = HeadlessHarness::new_with_size(root, view, 320.0, 240.0); + + harness.rebuild(); + harness.paint(); + println!("initial after rebuild: {:?}", harness.last_paint_stats()); + + harness.scroll_down(160.0, 120.0, 48.0); + harness.paint(); + println!( + "after first scroll after rebuild: {:?}", + harness.last_paint_stats() + ); +} From 16726b94193063f970081d3845a825a755ba2167 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 15:18:56 -0600 Subject: [PATCH 009/102] widget gal canvas request paint --- examples/widget-gallery/src/canvas.rs | 196 ++++++++++++++------------ src/window/state.rs | 2 +- 2 files changed, 110 insertions(+), 88 deletions(-) diff --git a/examples/widget-gallery/src/canvas.rs b/examples/widget-gallery/src/canvas.rs index 9478b305b..366fc94ef 100644 --- a/examples/widget-gallery/src/canvas.rs +++ b/examples/widget-gallery/src/canvas.rs @@ -1,10 +1,13 @@ -use std::{cell::RefCell, rc::Rc, time::Instant}; +use std::{cell::RefCell, rc::Rc, time::Duration, time::Instant}; use floem::{ ViewId, context::{EventCx, LayoutChanged, LayoutChangedListener, PaintCx}, - easing::Spring, - event::{CustomEvent, Event, EventPropagation}, + easing::{Linear, Spring}, + event::{ + CustomEvent, DragConfig, DragEvent, DragSourceEvent, Event, EventPropagation, + PointerCaptureEvent, + }, kurbo::{Affine, Circle, Point, Rect, Shape, Size, Stroke}, peniko::{ Gradient, Mix, @@ -151,7 +154,6 @@ pub struct SatValuePicker { current_color: AlphaColor, on_change: Option>, point: Point, - track: bool, } impl SatValuePicker { pub fn new(color: impl Fn() -> Color + 'static) -> Self { @@ -164,7 +166,6 @@ impl SatValuePicker { current_color: color.convert(), on_change: None, point: Point::ZERO, - track: false, } } @@ -196,6 +197,13 @@ impl SatValuePicker { fn post_layout(&mut self, new_layout: &LayoutChanged) { self.size = new_layout.new_box.size(); } + + fn set_from_point(&mut self, point: Point) -> Color { + self.id.request_paint(); + self.current_color = self.position_to_hwb(point); + self.point = point; + self.current_color.convert() + } } impl View for SatValuePicker { fn id(&self) -> ViewId { @@ -205,6 +213,7 @@ impl View for SatValuePicker { fn update(&mut self, _cx: &mut floem::context::UpdateCx, state: Box) { if let Ok(color) = state.downcast::() { self.current_color = color.convert(); + self.id.request_paint(); } } @@ -212,36 +221,35 @@ impl View for SatValuePicker { if let Some(new_layout) = LayoutChangedListener::extract(&cx.event) { self.post_layout(new_layout); } - if let Some(on_change) = &self.on_change { - match &cx.event { - Event::Pointer(PointerEvent::Down(PointerButtonEvent { - state, pointer, .. - })) => { - self.current_color = self.position_to_hwb(state.logical_point()); - self.point = state.logical_point(); - on_change(self.current_color.convert()); - self.track = true; - if let Some(pointer_id) = pointer.pointer_id { - cx.request_pointer_capture(pointer_id); - } - } - Event::Pointer(PointerEvent::Up(PointerButtonEvent { state, .. })) => { - self.current_color = self.position_to_hwb(state.logical_point()); - self.point = state.logical_point(); - on_change(self.current_color.convert()); - self.track = false; - } - Event::Pointer(PointerEvent::Move(pu)) => { - if self.track { - self.current_color = self.position_to_hwb(pu.current.logical_point()); - self.point = pu.current.logical_point(); - on_change(self.current_color.convert()); - } - } - _ => { - return EventPropagation::Continue; + let updated_color = match &cx.event { + Event::Pointer(PointerEvent::Down(PointerButtonEvent { state, pointer, .. })) => { + if let Some(pointer_id) = pointer.pointer_id { + cx.window_state.set_pointer_capture(pointer_id, self.id); } + Some(self.set_from_point(state.logical_point())) + } + Event::PointerCapture(PointerCaptureEvent::Gained(drag)) => { + cx.start_drag(*drag, DragConfig::new(0., Duration::ZERO, Linear), false); + None + } + Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => { + Some(self.set_from_point(dme.current_state.logical_point())) } + Event::Drag(DragEvent::Source(DragSourceEvent::End(de))) => { + Some(self.set_from_point(de.current_state.logical_point())) + } + Event::Drag(DragEvent::Source(DragSourceEvent::Cancel(dce))) => { + Some(self.set_from_point(dce.current_state.logical_point())) + } + _ => { + return EventPropagation::Continue; + } + }; + + if let Some(color) = updated_color + && let Some(on_change) = &self.on_change + { + on_change(color); } EventPropagation::Stop } @@ -304,7 +312,6 @@ pub struct HuePicker { size: Size, current_color: AlphaColor, on_change: Option>, - track: bool, } impl HuePicker { @@ -317,7 +324,6 @@ impl HuePicker { size: Size::ZERO, current_color: color.convert(), on_change: None, - track: false, } } @@ -339,6 +345,12 @@ impl HuePicker { fn post_layout(&mut self, new_layout: &LayoutChanged) { self.size = new_layout.new_box.size(); } + + fn set_from_point(&mut self, point: Point) -> Color { + self.id.request_paint(); + self.current_color = self.position_to_hsl(point); + self.current_color.convert() + } } impl View for HuePicker { @@ -349,6 +361,7 @@ impl View for HuePicker { fn update(&mut self, _cx: &mut floem::context::UpdateCx, state: Box) { if let Ok(color) = state.downcast::() { self.current_color = color.convert(); + self.id.request_paint(); } } @@ -356,33 +369,35 @@ impl View for HuePicker { if let Some(new_layout) = LayoutChangedListener::extract(&cx.event) { self.post_layout(new_layout); } - if let Some(on_change) = &self.on_change { - match &cx.event { - Event::Pointer(PointerEvent::Down(PointerButtonEvent { - state, pointer, .. - })) => { - self.current_color = self.position_to_hsl(state.logical_point()); - on_change(self.current_color.convert()); - self.track = true; - if let Some(pointer_id) = pointer.pointer_id { - cx.request_pointer_capture(pointer_id); - } - } - Event::Pointer(PointerEvent::Up(PointerButtonEvent { state, .. })) => { - self.current_color = self.position_to_hsl(state.logical_point()); - on_change(self.current_color.convert()); - self.track = false; - } - Event::Pointer(PointerEvent::Move(pu)) => { - if self.track { - self.current_color = self.position_to_hsl(pu.current.logical_point()); - on_change(self.current_color.convert()); - } - } - _ => { - return EventPropagation::Continue; + let updated_color = match &cx.event { + Event::Pointer(PointerEvent::Down(PointerButtonEvent { state, pointer, .. })) => { + if let Some(pointer_id) = pointer.pointer_id { + cx.window_state.set_pointer_capture(pointer_id, self.id); } + Some(self.set_from_point(state.logical_point())) + } + Event::PointerCapture(PointerCaptureEvent::Gained(drag)) => { + cx.start_drag(*drag, DragConfig::new(0., Duration::ZERO, Linear), false); + None + } + Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => { + Some(self.set_from_point(dme.current_state.logical_point())) + } + Event::Drag(DragEvent::Source(DragSourceEvent::End(de))) => { + Some(self.set_from_point(de.current_state.logical_point())) + } + Event::Drag(DragEvent::Source(DragSourceEvent::Cancel(dce))) => { + Some(self.set_from_point(dce.current_state.logical_point())) } + _ => { + return EventPropagation::Continue; + } + }; + + if let Some(color) = updated_color + && let Some(on_change) = &self.on_change + { + on_change(color); } EventPropagation::Stop } @@ -427,7 +442,6 @@ pub struct OpacityPicker { size: Size, current_color: Color, on_change: Option>, - track: bool, } impl OpacityPicker { @@ -440,7 +454,6 @@ impl OpacityPicker { size: Size::ZERO, current_color: color, on_change: None, - track: false, } } @@ -458,6 +471,12 @@ impl OpacityPicker { fn post_layout(&mut self, layout_changed: &LayoutChanged) { self.size = layout_changed.new_box.size(); } + + fn set_from_point(&mut self, point: Point) -> Color { + self.id.request_paint(); + self.current_color = self.position_to_alpha(point); + self.current_color + } } impl View for OpacityPicker { @@ -468,6 +487,7 @@ impl View for OpacityPicker { fn update(&mut self, _cx: &mut floem::context::UpdateCx, state: Box) { if let Ok(color) = state.downcast::() { self.current_color = *color; + self.id.request_paint(); } } @@ -475,33 +495,35 @@ impl View for OpacityPicker { if let Some(new_layout) = LayoutChangedListener::extract(&cx.event) { self.post_layout(new_layout); } - if let Some(on_change) = &self.on_change { - match &cx.event { - Event::Pointer(PointerEvent::Down(PointerButtonEvent { - state, pointer, .. - })) => { - self.current_color = self.position_to_alpha(state.logical_point()); - on_change(self.current_color); - self.track = true; - if let Some(pointer_id) = pointer.pointer_id { - cx.request_pointer_capture(pointer_id); - } - } - Event::Pointer(PointerEvent::Up(PointerButtonEvent { state, .. })) => { - self.current_color = self.position_to_alpha(state.logical_point()); - on_change(self.current_color); - self.track = false; - } - Event::Pointer(PointerEvent::Move(pu)) => { - if self.track { - self.current_color = self.position_to_alpha(pu.current.logical_point()); - on_change(self.current_color); - } - } - _ => { - return EventPropagation::Continue; + let updated_color = match &cx.event { + Event::Pointer(PointerEvent::Down(PointerButtonEvent { state, pointer, .. })) => { + if let Some(pointer_id) = pointer.pointer_id { + cx.window_state.set_pointer_capture(pointer_id, self.id); } + Some(self.set_from_point(state.logical_point())) + } + Event::PointerCapture(PointerCaptureEvent::Gained(drag)) => { + cx.start_drag(*drag, DragConfig::new(0., Duration::ZERO, Linear), false); + None } + Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => { + Some(self.set_from_point(dme.current_state.logical_point())) + } + Event::Drag(DragEvent::Source(DragSourceEvent::End(de))) => { + Some(self.set_from_point(de.current_state.logical_point())) + } + Event::Drag(DragEvent::Source(DragSourceEvent::Cancel(dce))) => { + Some(self.set_from_point(dce.current_state.logical_point())) + } + _ => { + return EventPropagation::Continue; + } + }; + + if let Some(color) = updated_color + && let Some(on_change) = &self.on_change + { + on_change(color); } EventPropagation::Stop } diff --git a/src/window/state.rs b/src/window/state.rs index cfaf67ac1..c4a60b71d 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -528,7 +528,7 @@ impl WindowState { /// Note: Unlike the web API, this doesn't validate that the pointer is active /// (has button pressed). The caller should ensure this constraint if needed. #[inline] - pub(crate) fn set_pointer_capture( + pub fn set_pointer_capture( &mut self, pointer_id: PointerId, target: impl Into, From 3d286a4484e7ec9507556b53d9ec8d51bb0ad667 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 16:46:38 -0600 Subject: [PATCH 010/102] move towards blink --- src/paint/display_list.rs | 502 ++++++++++++++++++++++++++++++-------- src/paint/mod.rs | 63 +---- 2 files changed, 411 insertions(+), 154 deletions(-) diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index f60297454..bfbea4f70 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -30,6 +30,17 @@ impl TransformClass { Self::Affine => true, } } + + pub(crate) fn combine(self, other: Self) -> Self { + use TransformClass::{Affine, Exact, Orthonormal, TranslateOnly}; + + match (self, other) { + (Exact, x) | (x, Exact) => x, + (TranslateOnly, _) | (_, TranslateOnly) => TranslateOnly, + (Orthonormal, _) | (_, Orthonormal) => Orthonormal, + (Affine, Affine) => Affine, + } + } } pub(crate) fn transform_diff_class(original: Affine, current: Affine) -> TransformClass { @@ -95,19 +106,51 @@ pub(crate) enum DisplayCommand { #[derive(Clone)] pub(crate) struct ElementStage { - pub commands: Vec, + pub chunks: Vec, pub transform_class: TransformClass, } impl Default for ElementStage { fn default() -> Self { Self { - commands: Vec::new(), - transform_class: TransformClass::Affine, + chunks: Vec::new(), + transform_class: TransformClass::Exact, } } } +impl ElementStage { + pub(crate) fn set_commands(&mut self, commands: Vec) { + self.chunks = chunk_display_commands(commands); + self.transform_class = self + .chunks + .iter() + .map(|chunk| chunk.transform_class) + .fold(TransformClass::Exact, TransformClass::combine); + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct PaintPropertyState { + pub z_index: i32, + pub clip_depth: u32, + pub layer_depth: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PaintChunkKind { + Boundary, + Draw, +} + +#[derive(Clone)] +pub(crate) struct PaintChunk { + pub kind: PaintChunkKind, + pub properties: PaintPropertyState, + pub commands: Vec, + pub transform_class: TransformClass, +} + #[derive(Clone, Copy)] pub(crate) struct ElementSnapshot { pub local_bounds: Rect, @@ -116,6 +159,23 @@ pub(crate) struct ElementSnapshot { pub world_transform: Affine, } +impl ElementSnapshot { + pub(crate) fn from_box_tree(box_tree: &crate::BoxTree, element_id: ElementId) -> Self { + Self { + local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), + clip: box_tree.local_clip(element_id.0).flatten(), + effective_clip: box_tree.clipped_local_clip(element_id.0), + world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), + } + } + + pub(crate) fn supports_reuse(self, current: Self) -> bool { + self.local_bounds == current.local_bounds + && self.clip == current.clip + && effective_clip_not_loosened(self.effective_clip, current.effective_clip) + } +} + #[derive(Clone, Default)] pub(crate) struct ElementDisplayList { pub paint: ElementStage, @@ -154,7 +214,7 @@ impl RetainedDisplayList { return true; }; - if previous.local_bounds != snapshot.local_bounds || previous.clip != snapshot.clip { + if !previous.supports_reuse(snapshot) { return true; } @@ -356,107 +416,217 @@ pub(crate) fn replay_stage( renderer: &mut impl FloemRenderer, base_transform: Affine, ) { - for command in &stage.commands { + let mut current_z_index = None; + let mut current_transform = None; + + for chunk in &stage.chunks { + if current_z_index != Some(chunk.properties.z_index) { + renderer.set_z_index(chunk.properties.z_index); + current_z_index = Some(chunk.properties.z_index); + } + for command in &chunk.commands { + match command { + DisplayCommand::SetZIndex(_) => {} + DisplayCommand::PushClip { clip, hint } => { + replay_clip(renderer, clip, hint.clone(), base_transform, &mut current_transform) + } + DisplayCommand::PopClip => renderer.clear_clip(), + DisplayCommand::PushLayer { + group, + transform, + clip_hint, + } => { + let Some(Clip::Fill { + transform: clip_transform, + shape, + .. + }) = group.clip.as_ref() + else { + continue; + }; + set_transform_if_needed( + renderer, + base_transform * *clip_transform, + &mut current_transform, + ); + match (clip_hint.clone(), shape) { + (Some(ShapeHint::Rect(rect)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &rect, + ), + (Some(ShapeHint::RoundedRect(rect)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &rect, + ), + (Some(ShapeHint::Line(line)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &line, + ), + (Some(ShapeHint::Circle(circle)), _) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + &circle, + ), + (None, Geometry::Rect(rect)) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + rect, + ), + (None, Geometry::RoundedRect(rect)) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + rect, + ), + (None, Geometry::Path(path)) => renderer.push_layer( + group.composite.blend, + group.composite.alpha, + *transform, + path, + ), + } + } + DisplayCommand::PopLayer => renderer.pop_layer(), + DisplayCommand::Draw { draw, hint } => { + replay_draw( + renderer, + draw, + hint.clone(), + base_transform, + &mut current_transform, + ) + } + DisplayCommand::DrawImage { + img, + hash, + rect, + transform, + } => { + set_transform_if_needed( + renderer, + base_transform * *transform, + &mut current_transform, + ); + renderer.draw_img( + Img { + img: img.clone(), + hash, + }, + *rect, + ); + } + DisplayCommand::DrawSvg { + svg, + rect, + transform, + brush, + } => { + set_transform_if_needed( + renderer, + base_transform * *transform, + &mut current_transform, + ); + renderer.draw_svg( + Svg { + tree: svg.tree.as_ref(), + hash: svg.hash.as_ref(), + }, + *rect, + brush.as_ref(), + ); + } + } + } + } +} + +fn chunk_display_commands(commands: Vec) -> Vec { + let mut chunks = Vec::new(); + let mut properties = PaintPropertyState::default(); + + for command in commands { match command { - DisplayCommand::SetZIndex(z_index) => renderer.set_z_index(*z_index), - DisplayCommand::PushClip { clip, hint } => { - replay_clip(renderer, clip, hint.clone(), base_transform) + DisplayCommand::SetZIndex(z_index) => { + properties.z_index = z_index; } - DisplayCommand::PopClip => renderer.clear_clip(), - DisplayCommand::PushLayer { - group, - transform, - clip_hint, - } => { - let Some(Clip::Fill { - transform: clip_transform, - shape, - .. - }) = group.clip.as_ref() - else { - continue; - }; - renderer.set_transform(base_transform * *clip_transform); - match (clip_hint.clone(), shape) { - (Some(ShapeHint::Rect(rect)), _) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - &rect, - ), - (Some(ShapeHint::RoundedRect(rect)), _) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - &rect, - ), - (Some(ShapeHint::Line(line)), _) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - &line, - ), - (Some(ShapeHint::Circle(circle)), _) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - &circle, - ), - (None, Geometry::Rect(rect)) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - rect, - ), - (None, Geometry::RoundedRect(rect)) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - rect, - ), - (None, Geometry::Path(path)) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - path, - ), - } + DisplayCommand::PushClip { .. } => { + push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); + properties.clip_depth += 1; } - DisplayCommand::PopLayer => renderer.pop_layer(), - DisplayCommand::Draw { draw, hint } => { - replay_draw(renderer, draw, hint.clone(), base_transform) + DisplayCommand::PopClip => { + properties.clip_depth = properties.clip_depth.saturating_sub(1); + push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); } - DisplayCommand::DrawImage { - img, - hash, - rect, - transform, - } => { - renderer.set_transform(base_transform * *transform); - renderer.draw_img( - Img { - img: img.clone(), - hash, - }, - *rect, - ); + DisplayCommand::PushLayer { .. } => { + push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); + properties.layer_depth += 1; } - DisplayCommand::DrawSvg { - svg, - rect, - transform, - brush, - } => { - renderer.set_transform(base_transform * *transform); - renderer.draw_svg( - Svg { - tree: svg.tree.as_ref(), - hash: svg.hash.as_ref(), - }, - *rect, - brush.as_ref(), - ); + DisplayCommand::PopLayer => { + properties.layer_depth = properties.layer_depth.saturating_sub(1); + push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); } + command => { + let transform_class = command_transform_class(&command); + match chunks.last_mut() { + Some(PaintChunk { + kind: PaintChunkKind::Draw, + properties: chunk_properties, + commands: chunk_commands, + transform_class: chunk_transform_class, + }) if *chunk_properties == properties => { + *chunk_transform_class = + chunk_transform_class.combine(transform_class); + chunk_commands.push(command); + } + _ => chunks.push(PaintChunk { + kind: PaintChunkKind::Draw, + properties, + commands: vec![command], + transform_class, + }), + } + } + } + } + + chunks +} + +fn push_boundary_chunk( + chunks: &mut Vec, + properties: PaintPropertyState, + transform_class: TransformClass, + command: DisplayCommand, +) { + chunks.push(PaintChunk { + kind: PaintChunkKind::Boundary, + properties, + commands: vec![command], + transform_class, + }); +} + +fn command_transform_class(command: &DisplayCommand) -> TransformClass { + match command { + DisplayCommand::SetZIndex(_) + | DisplayCommand::PushClip { .. } + | DisplayCommand::PopClip + | DisplayCommand::PushLayer { .. } + | DisplayCommand::PopLayer => TransformClass::Affine, + DisplayCommand::Draw { draw, .. } => match draw { + Draw::Fill { .. } | Draw::Stroke { .. } => TransformClass::Affine, + Draw::GlyphRun(_) | Draw::BlurredRoundedRect(_) => TransformClass::TranslateOnly, + }, + DisplayCommand::DrawImage { .. } | DisplayCommand::DrawSvg { .. } => { + TransformClass::TranslateOnly } } } @@ -466,6 +636,7 @@ fn replay_clip( clip: &Clip, hint: Option, base_transform: Affine, + current_transform: &mut Option, ) { let Clip::Fill { transform, shape, .. @@ -473,7 +644,7 @@ fn replay_clip( else { return; }; - renderer.set_transform(base_transform * *transform); + set_transform_if_needed(renderer, base_transform * *transform, current_transform); match (hint, shape) { (Some(ShapeHint::Rect(rect)), _) => renderer.clip(&rect), (Some(ShapeHint::RoundedRect(rect)), _) => renderer.clip(&rect), @@ -490,6 +661,7 @@ fn replay_draw( draw: &Draw, hint: Option, base_transform: Affine, + current_transform: &mut Option, ) { match draw { Draw::Fill { @@ -498,7 +670,7 @@ fn replay_draw( shape, .. } => { - renderer.set_transform(base_transform * *transform); + set_transform_if_needed(renderer, base_transform * *transform, current_transform); match (hint, shape) { (Some(ShapeHint::Rect(rect)), _) => renderer.fill(&rect, paint, 0.0), (Some(ShapeHint::RoundedRect(rect)), _) => renderer.fill(&rect, paint, 0.0), @@ -516,7 +688,7 @@ fn replay_draw( shape, .. } => { - renderer.set_transform(base_transform * *transform); + set_transform_if_needed(renderer, base_transform * *transform, current_transform); match (hint, shape) { (Some(ShapeHint::Rect(rect)), _) => renderer.stroke(&rect, paint, stroke), (Some(ShapeHint::RoundedRect(rect)), _) => renderer.stroke(&rect, paint, stroke), @@ -539,7 +711,7 @@ fn replay_draw( transform: run.transform, glyph_transform: run.glyph_transform, }; - renderer.set_transform(base_transform); + set_transform_if_needed(renderer, base_transform, current_transform); renderer.draw_glyphs( Point::ZERO, &props, @@ -553,13 +725,135 @@ fn replay_draw( ); } Draw::BlurredRoundedRect(rect) => { - renderer.set_transform(base_transform * rect.transform); + set_transform_if_needed(renderer, base_transform * rect.transform, current_transform); let shape = rect.rect.to_rounded_rect(rect.radius); renderer.fill(&shape, rect.color, rect.std_dev); } } } +fn set_transform_if_needed( + renderer: &mut impl FloemRenderer, + transform: Affine, + current_transform: &mut Option, +) { + if current_transform != &Some(transform) { + renderer.set_transform(transform); + *current_transform = Some(transform); + } +} + +fn effective_clip_not_loosened( + recorded: Option, + current: Option, +) -> bool { + match (recorded, current) { + (None, _) => true, + (Some(_), None) => false, + (Some(recorded), Some(current)) => { + recorded == current + || (recorded.radii() == current.radii() + && recorded.rect().contains_rect(current.rect())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use peniko::Color; + + #[test] + fn stage_groups_adjacent_draws_with_matching_properties() { + let rect = Rect::new(0.0, 0.0, 10.0, 10.0); + let mut stage = ElementStage::default(); + stage.set_commands(vec![ + DisplayCommand::SetZIndex(7), + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: FillRule::NonZero, + paint: Color::BLACK.into(), + paint_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + hint: Some(ShapeHint::Rect(rect)), + }, + DisplayCommand::Draw { + draw: Draw::Stroke { + transform: Affine::IDENTITY, + stroke: StrokeStyle::new(1.0), + paint: Color::BLACK.into(), + paint_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + hint: Some(ShapeHint::Rect(rect)), + }, + ]); + + assert_eq!(stage.chunks.len(), 1); + assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); + assert_eq!(stage.chunks[0].properties.z_index, 7); + assert_eq!(stage.chunks[0].commands.len(), 2); + assert_eq!(stage.transform_class, TransformClass::Affine); + } + + #[test] + fn stage_tracks_clip_depth_across_boundary_chunks() { + let rect = Rect::new(0.0, 0.0, 10.0, 10.0); + let mut stage = ElementStage::default(); + stage.set_commands(vec![ + DisplayCommand::PushClip { + clip: Clip::Fill { + transform: Affine::IDENTITY, + shape: Geometry::Rect(rect), + fill_rule: FillRule::NonZero, + }, + hint: Some(ShapeHint::Rect(rect)), + }, + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: FillRule::NonZero, + paint: Color::BLACK.into(), + paint_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + hint: Some(ShapeHint::Rect(rect)), + }, + DisplayCommand::PopClip, + ]); + + assert_eq!(stage.chunks.len(), 3); + assert_eq!(stage.chunks[0].kind, PaintChunkKind::Boundary); + assert_eq!(stage.chunks[1].kind, PaintChunkKind::Draw); + assert_eq!(stage.chunks[1].properties.clip_depth, 1); + assert_eq!(stage.chunks[2].kind, PaintChunkKind::Boundary); + assert_eq!(stage.chunks[2].properties.clip_depth, 0); + } + + #[test] + fn blurred_draws_downgrade_stage_transform_retention() { + let mut stage = ElementStage::default(); + stage.set_commands(vec![DisplayCommand::Draw { + draw: Draw::BlurredRoundedRect(imaging::BlurredRoundedRect { + transform: Affine::IDENTITY, + rect: Rect::new(0.0, 0.0, 10.0, 10.0), + color: Color::BLACK, + radius: 4.0, + std_dev: 6.0, + composite: Composite::default(), + }), + hint: None, + }]); + + assert_eq!(stage.transform_class, TransformClass::TranslateOnly); + } +} + fn shape_to_geometry(shape: &impl Shape) -> (Geometry, Option) { if let Some(rect) = shape.as_rect() { return (Geometry::Rect(rect), Some(ShapeHint::Rect(rect))); diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 870d87965..7bdb62f19 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -30,9 +30,7 @@ use crate::view::ViewId; use crate::view::stacking::{StackingContextItem, collect_stacking_context_items_into}; use crate::view::{paint_bg, paint_border, paint_outline}; use crate::window::state::WindowState; -use display_list::{ - ElementSnapshot, RecordingRenderer, TransformClass, replay_stage, transform_diff_class, -}; +use display_list::{ElementSnapshot, RecordingRenderer, replay_stage, transform_diff_class}; std::thread_local! { /// Holds the ID of a View being painted very briefly if it is being rendered as @@ -121,21 +119,6 @@ fn record_paint(id: ViewId) { }); } -fn effective_clip_not_loosened( - recorded: Option, - current: Option, -) -> bool { - match (recorded, current) { - (None, _) => true, - (Some(_), None) => false, - (Some(recorded), Some(current)) => { - recorded == current - || (recorded.radii() == current.radii() - && recorded.rect().contains_rect(current.rect())) - } - } -} - /// Global paint context - holds shared state for entire paint pass /// Similar to GlobalEventCx in event dispatch pub struct GlobalPaintCx<'a> { @@ -257,22 +240,13 @@ impl GlobalPaintCx<'_> { continue; } - let snapshot = ElementSnapshot { - local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), - clip: box_tree.local_clip(element_id.0).flatten(), - effective_clip: box_tree.clipped_local_clip(element_id.0), - world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), - }; + let snapshot = ElementSnapshot::from_box_tree(&box_tree, element_id); let Some(previous) = display_list.element(element_id).and_then(|e| e.snapshot) else { continue; }; let diff = transform_diff_class(previous.world_transform, snapshot.world_transform); - if previous.local_bounds != snapshot.local_bounds - || previous.clip != snapshot.clip - || !effective_clip_not_loosened(previous.effective_clip, snapshot.effective_clip) - || !boundary.supports(diff) - { + if !previous.supports_reuse(snapshot) || !boundary.supports(diff) { continue; } @@ -365,15 +339,7 @@ impl GlobalPaintCx<'_> { active_ids .iter() .copied() - .map(|element_id| { - let snapshot = ElementSnapshot { - local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), - clip: box_tree.clipped_local_clip(element_id.0), - effective_clip: box_tree.clipped_local_clip(element_id.0), - world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), - }; - (element_id, snapshot) - }) + .map(|element_id| (element_id, ElementSnapshot::from_box_tree(&box_tree, element_id))) .collect::>() }; @@ -389,6 +355,9 @@ impl GlobalPaintCx<'_> { dirty_ids.insert(element_id); } } + if std::env::var("FLOEM_TRACE_RERECORD").ok().as_deref() == Some("1") { + eprintln!("dirty_ids={dirty_ids:?}"); + } let rerecord_ids = dirty_ids.len(); for element_id in dirty_ids { self.record_visual_node(element_id, false); @@ -433,26 +402,21 @@ impl GlobalPaintCx<'_> { return; }; let stage = if is_post { - element.post.clone() + &element.post } else { - element.paint.clone() + &element.paint }; let renderer = self.paint_state.renderer_mut(); - replay_stage(&stage, renderer, base_transform); + replay_stage(stage, renderer, base_transform); } /// Record a single visual node in local coordinates. pub(crate) fn record_visual_node(&mut self, element_id: ElementId, is_post: bool) { let box_tree = self.window_state.box_tree.borrow_mut(); let layout_rect_local = box_tree.local_bounds(element_id.0).unwrap_or_default(); - let local_clip = box_tree.clipped_local_clip(element_id.0); + let local_clip = box_tree.local_clip(element_id.0).flatten(); let effective_clip = box_tree.clipped_local_clip(element_id.0); - let snapshot = ElementSnapshot { - local_bounds: layout_rect_local, - clip: local_clip, - effective_clip, - world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), - }; + let snapshot = ElementSnapshot::from_box_tree(&box_tree, element_id); drop(box_tree); let layout_rect = layout_rect_local; @@ -515,8 +479,7 @@ impl GlobalPaintCx<'_> { } else { &mut element.paint }; - stage.commands = commands; - stage.transform_class = TransformClass::Affine; + stage.set_commands(commands); element.snapshot = Some(snapshot); } } From 2bc15f0766f1a1b5fcbe2762588daa3c26f92716 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 17:12:15 -0600 Subject: [PATCH 011/102] paint property tree --- src/paint/display_list.rs | 290 +++++++++++++++++++++++++++++++++++--- src/paint/mod.rs | 3 - 2 files changed, 267 insertions(+), 26 deletions(-) diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index bfbea4f70..586de3dc4 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -107,6 +107,7 @@ pub(crate) enum DisplayCommand { #[derive(Clone)] pub(crate) struct ElementStage { pub chunks: Vec, + pub property_tree: PaintPropertyTree, pub transform_class: TransformClass, } @@ -114,27 +115,35 @@ impl Default for ElementStage { fn default() -> Self { Self { chunks: Vec::new(), - transform_class: TransformClass::Exact, + property_tree: PaintPropertyTree::default(), + transform_class: TransformClass::Affine, } } } impl ElementStage { pub(crate) fn set_commands(&mut self, commands: Vec) { - self.chunks = chunk_display_commands(commands); - self.transform_class = self - .chunks - .iter() - .map(|chunk| chunk.transform_class) - .fold(TransformClass::Exact, TransformClass::combine); + let (chunks, property_tree) = chunk_display_commands(commands); + self.chunks = chunks; + self.property_tree = property_tree; + self.transform_class = if self.chunks.is_empty() { + TransformClass::Affine + } else { + self.chunks + .iter() + .map(|chunk| chunk.transform_class) + .fold(TransformClass::Exact, TransformClass::combine) + }; } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct PaintPropertyState { pub z_index: i32, - pub clip_depth: u32, - pub layer_depth: u32, + pub transform_id: TransformNodeId, + pub clip_id: ClipNodeId, + pub effect_id: EffectNodeId, + pub scroll_id: ScrollNodeId, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -143,6 +152,88 @@ pub(crate) enum PaintChunkKind { Draw, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub(crate) struct TransformNodeId(pub u32); + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub(crate) struct ClipNodeId(pub u32); + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub(crate) struct EffectNodeId(pub u32); + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub(crate) struct ScrollNodeId(pub u32); + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub(crate) struct TransformNode { + pub parent: Option, + pub transform: Affine, +} + +#[allow(dead_code)] +#[derive(Clone)] +pub(crate) struct ClipNode { + pub parent: Option, + pub transform_id: TransformNodeId, + pub clip: Clip, + pub hint: Option, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct EffectNode { + pub parent: Option, + pub blend: peniko::BlendMode, + pub alpha: f32, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct ScrollNode { + pub parent: Option, + pub translation: Affine, +} + +#[allow(dead_code)] +#[derive(Clone)] +pub(crate) struct PaintPropertyTree { + pub transforms: Vec, + pub clips: Vec, + pub effects: Vec, + pub scrolls: Vec, +} + +impl Default for PaintPropertyTree { + fn default() -> Self { + Self { + transforms: vec![TransformNode { + parent: None, + transform: Affine::IDENTITY, + }], + clips: vec![ClipNode { + parent: None, + transform_id: TransformNodeId(0), + clip: Clip::Fill { + transform: Affine::IDENTITY, + shape: Geometry::Rect(Rect::ZERO), + fill_rule: FillRule::NonZero, + }, + hint: Some(ShapeHint::Rect(Rect::ZERO)), + }], + effects: vec![EffectNode { + parent: None, + blend: peniko::BlendMode::default(), + alpha: 1.0, + }], + scrolls: vec![ScrollNode { + parent: None, + translation: Affine::IDENTITY, + }], + } + } +} + #[derive(Clone)] pub(crate) struct PaintChunk { pub kind: PaintChunkKind, @@ -548,32 +639,100 @@ pub(crate) fn replay_stage( } } -fn chunk_display_commands(commands: Vec) -> Vec { +fn chunk_display_commands(commands: Vec) -> (Vec, PaintPropertyTree) { let mut chunks = Vec::new(); let mut properties = PaintPropertyState::default(); + let mut property_tree = PaintPropertyTree::default(); + let mut transform_intern = FxHashMap::default(); + transform_intern.insert(transform_key(Affine::IDENTITY), TransformNodeId(0)); + let mut clip_stack: Vec = Vec::new(); + let mut effect_stack: Vec = vec![EffectNodeId(0)]; for command in commands { match command { DisplayCommand::SetZIndex(z_index) => { properties.z_index = z_index; } - DisplayCommand::PushClip { .. } => { - push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); - properties.clip_depth += 1; + DisplayCommand::PushClip { clip, hint } => { + let transform_id = intern_transform(clip_transform(&clip), &mut property_tree, &mut transform_intern); + let clip_id = ClipNodeId(property_tree.clips.len() as u32); + property_tree.clips.push(ClipNode { + parent: clip_stack.last().copied(), + transform_id, + clip, + hint: hint.clone(), + }); + clip_stack.push(clip_id); + properties.clip_id = clip_id; + push_boundary_chunk( + &mut chunks, + properties, + command_transform_class(&DisplayCommand::PushClip { + clip: property_tree.clips[clip_id.0 as usize].clip.clone(), + hint: property_tree.clips[clip_id.0 as usize].hint.clone(), + }), + DisplayCommand::PushClip { + clip: property_tree.clips[clip_id.0 as usize].clip.clone(), + hint: property_tree.clips[clip_id.0 as usize].hint.clone(), + }, + ); } DisplayCommand::PopClip => { - properties.clip_depth = properties.clip_depth.saturating_sub(1); - push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); + clip_stack.pop(); + properties.clip_id = clip_stack.last().copied().unwrap_or_default(); + push_boundary_chunk( + &mut chunks, + properties, + command_transform_class(&DisplayCommand::PopClip), + DisplayCommand::PopClip, + ); } - DisplayCommand::PushLayer { .. } => { - push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); - properties.layer_depth += 1; + DisplayCommand::PushLayer { + group, + transform, + clip_hint, + } => { + let effect_id = EffectNodeId(property_tree.effects.len() as u32); + property_tree.effects.push(EffectNode { + parent: effect_stack.last().copied(), + blend: group.composite.blend, + alpha: group.composite.alpha, + }); + effect_stack.push(effect_id); + properties.effect_id = effect_id; + properties.transform_id = + intern_transform(transform, &mut property_tree, &mut transform_intern); + push_boundary_chunk( + &mut chunks, + properties, + command_transform_class(&DisplayCommand::PushLayer { + group: group.clone(), + transform, + clip_hint: clip_hint.clone(), + }), + DisplayCommand::PushLayer { + group, + transform, + clip_hint, + }, + ); } DisplayCommand::PopLayer => { - properties.layer_depth = properties.layer_depth.saturating_sub(1); - push_boundary_chunk(&mut chunks, properties, command_transform_class(&command), command); + effect_stack.pop(); + properties.effect_id = effect_stack.last().copied().unwrap_or_default(); + push_boundary_chunk( + &mut chunks, + properties, + command_transform_class(&DisplayCommand::PopLayer), + DisplayCommand::PopLayer, + ); } command => { + properties.transform_id = intern_transform( + command_affine(&command), + &mut property_tree, + &mut transform_intern, + ); let transform_class = command_transform_class(&command); match chunks.last_mut() { Some(PaintChunk { @@ -597,7 +756,7 @@ fn chunk_display_commands(commands: Vec) -> Vec { } } - chunks + (chunks, property_tree) } fn push_boundary_chunk( @@ -631,6 +790,54 @@ fn command_transform_class(command: &DisplayCommand) -> TransformClass { } } +fn command_affine(command: &DisplayCommand) -> Affine { + match command { + DisplayCommand::SetZIndex(_) | DisplayCommand::PopClip | DisplayCommand::PopLayer => { + Affine::IDENTITY + } + DisplayCommand::PushClip { clip, .. } => clip_transform(clip), + DisplayCommand::PushLayer { transform, .. } => *transform, + DisplayCommand::Draw { draw, .. } => match draw { + Draw::Fill { transform, .. } | Draw::Stroke { transform, .. } => *transform, + Draw::GlyphRun(run) => run.transform, + Draw::BlurredRoundedRect(rect) => rect.transform, + }, + DisplayCommand::DrawImage { transform, .. } | DisplayCommand::DrawSvg { transform, .. } => { + *transform + } + } +} + +fn clip_transform(clip: &Clip) -> Affine { + match clip { + Clip::Fill { transform, .. } => *transform, + Clip::Stroke { transform, .. } => *transform, + } +} + +fn intern_transform( + transform: Affine, + property_tree: &mut PaintPropertyTree, + intern: &mut FxHashMap<[u64; 6], TransformNodeId>, +) -> TransformNodeId { + let key = transform_key(transform); + if let Some(id) = intern.get(&key).copied() { + return id; + } + + let id = TransformNodeId(property_tree.transforms.len() as u32); + property_tree.transforms.push(TransformNode { + parent: Some(TransformNodeId(0)), + transform, + }); + intern.insert(key, id); + id +} + +fn transform_key(transform: Affine) -> [u64; 6] { + transform.as_coeffs().map(f64::to_bits) +} + fn replay_clip( renderer: &mut impl FloemRenderer, clip: &Clip, @@ -830,9 +1037,46 @@ mod tests { assert_eq!(stage.chunks.len(), 3); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Boundary); assert_eq!(stage.chunks[1].kind, PaintChunkKind::Draw); - assert_eq!(stage.chunks[1].properties.clip_depth, 1); + assert_ne!(stage.chunks[1].properties.clip_id, ClipNodeId(0)); assert_eq!(stage.chunks[2].kind, PaintChunkKind::Boundary); - assert_eq!(stage.chunks[2].properties.clip_depth, 0); + assert_eq!(stage.chunks[2].properties.clip_id, ClipNodeId(0)); + } + + #[test] + fn stage_splits_draw_chunks_on_transform_state() { + let rect = Rect::new(0.0, 0.0, 10.0, 10.0); + let mut stage = ElementStage::default(); + stage.set_commands(vec![ + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: FillRule::NonZero, + paint: Color::BLACK.into(), + paint_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + hint: Some(ShapeHint::Rect(rect)), + }, + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::translate((5.0, 0.0)), + fill_rule: FillRule::NonZero, + paint: Color::BLACK.into(), + paint_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + hint: Some(ShapeHint::Rect(rect)), + }, + ]); + + assert_eq!(stage.chunks.len(), 2); + assert_ne!( + stage.chunks[0].properties.transform_id, + stage.chunks[1].properties.transform_id + ); + assert_eq!(stage.property_tree.transforms.len(), 2); } #[test] diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 7bdb62f19..41a51ea41 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -355,9 +355,6 @@ impl GlobalPaintCx<'_> { dirty_ids.insert(element_id); } } - if std::env::var("FLOEM_TRACE_RERECORD").ok().as_deref() == Some("1") { - eprintln!("dirty_ids={dirty_ids:?}"); - } let rerecord_ids = dirty_ids.len(); for element_id in dirty_ids { self.record_visual_node(element_id, false); From e0a699a96d96928e8ae6172d24d9521c8c01b8aa Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 17:18:45 -0600 Subject: [PATCH 012/102] damage readiness --- src/paint/display_list.rs | 186 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index 586de3dc4..17dfabd48 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -135,6 +135,15 @@ impl ElementStage { .fold(TransformClass::Exact, TransformClass::combine) }; } + + #[allow(dead_code)] + pub(crate) fn chunk_indices_for_damage(&self, damage: &[Rect]) -> Vec { + self.chunks + .iter() + .enumerate() + .filter_map(|(index, chunk)| chunk.intersects_damage(damage).then_some(index)) + .collect() + } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -239,9 +248,32 @@ pub(crate) struct PaintChunk { pub kind: PaintChunkKind, pub properties: PaintPropertyState, pub commands: Vec, + pub bounds: Option, + pub metadata: PaintChunkMetadata, pub transform_class: TransformClass, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct PaintChunkMetadata { + pub has_text: bool, + pub has_raster_image: bool, + pub has_vector_image: bool, + pub has_blur: bool, + pub requires_layer: bool, +} + +impl PaintChunkMetadata { + fn merge(self, other: Self) -> Self { + Self { + has_text: self.has_text || other.has_text, + has_raster_image: self.has_raster_image || other.has_raster_image, + has_vector_image: self.has_vector_image || other.has_vector_image, + has_blur: self.has_blur || other.has_blur, + requires_layer: self.requires_layer || other.requires_layer, + } + } +} + #[derive(Clone, Copy)] pub(crate) struct ElementSnapshot { pub local_bounds: Rect, @@ -267,6 +299,16 @@ impl ElementSnapshot { } } +impl PaintChunk { + #[allow(dead_code)] + pub(crate) fn intersects_damage(&self, damage: &[Rect]) -> bool { + let Some(bounds) = self.bounds else { + return true; + }; + damage.iter().any(|rect| rect.intersect(bounds).area() > 0.0) + } +} + #[derive(Clone, Default)] pub(crate) struct ElementDisplayList { pub paint: ElementStage, @@ -734,21 +776,29 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa &mut transform_intern, ); let transform_class = command_transform_class(&command); + let bounds = command_bounds(&command); + let metadata = command_metadata(&command); match chunks.last_mut() { Some(PaintChunk { kind: PaintChunkKind::Draw, properties: chunk_properties, commands: chunk_commands, + bounds: chunk_bounds, + metadata: chunk_metadata, transform_class: chunk_transform_class, }) if *chunk_properties == properties => { *chunk_transform_class = chunk_transform_class.combine(transform_class); + *chunk_bounds = union_rects(*chunk_bounds, bounds); + *chunk_metadata = chunk_metadata.merge(metadata); chunk_commands.push(command); } _ => chunks.push(PaintChunk { kind: PaintChunkKind::Draw, properties, commands: vec![command], + bounds, + metadata, transform_class, }), } @@ -769,6 +819,8 @@ fn push_boundary_chunk( kind: PaintChunkKind::Boundary, properties, commands: vec![command], + bounds: None, + metadata: PaintChunkMetadata::default(), transform_class, }); } @@ -790,6 +842,100 @@ fn command_transform_class(command: &DisplayCommand) -> TransformClass { } } +fn command_bounds(command: &DisplayCommand) -> Option { + match command { + DisplayCommand::SetZIndex(_) + | DisplayCommand::PushClip { .. } + | DisplayCommand::PopClip + | DisplayCommand::PushLayer { .. } + | DisplayCommand::PopLayer => None, + DisplayCommand::Draw { draw, .. } => draw_bounds(draw), + DisplayCommand::DrawImage { rect, .. } | DisplayCommand::DrawSvg { rect, .. } => Some(*rect), + } +} + +fn draw_bounds(draw: &Draw) -> Option { + match draw { + Draw::Fill { shape, .. } => Some(geometry_bounds(shape)), + Draw::Stroke { shape, stroke, .. } => { + let bounds = geometry_bounds(shape); + let inset = stroke.width / 2.0; + Some(bounds.inflate(inset, inset)) + } + Draw::GlyphRun(run) => glyph_run_bounds(run), + Draw::BlurredRoundedRect(rect) => Some(rect.rect.inflate(rect.std_dev * 3.0, rect.std_dev * 3.0)), + } +} + +fn geometry_bounds(geometry: &Geometry) -> Rect { + match geometry { + Geometry::Rect(rect) => *rect, + Geometry::RoundedRect(rect) => rect.rect(), + Geometry::Path(path) => path.bounding_box(), + } +} + +fn glyph_run_bounds(run: &GlyphRun) -> Option { + let mut glyphs = run.glyphs.iter(); + let first = glyphs.next()?; + let mut rect = Rect::new( + first.x as f64, + (first.y - run.font_size) as f64, + (first.x + run.font_size) as f64, + first.y as f64, + ); + for glyph in glyphs { + rect = rect.union(Rect::new( + glyph.x as f64, + (glyph.y - run.font_size) as f64, + (glyph.x + run.font_size) as f64, + glyph.y as f64, + )); + } + Some(rect) +} + +fn command_metadata(command: &DisplayCommand) -> PaintChunkMetadata { + match command { + DisplayCommand::SetZIndex(_) + | DisplayCommand::PushClip { .. } + | DisplayCommand::PopClip + | DisplayCommand::PopLayer => PaintChunkMetadata::default(), + DisplayCommand::PushLayer { .. } => PaintChunkMetadata { + requires_layer: true, + ..PaintChunkMetadata::default() + }, + DisplayCommand::Draw { draw, .. } => match draw { + Draw::Fill { .. } | Draw::Stroke { .. } => PaintChunkMetadata::default(), + Draw::GlyphRun(_) => PaintChunkMetadata { + has_text: true, + ..PaintChunkMetadata::default() + }, + Draw::BlurredRoundedRect(_) => PaintChunkMetadata { + has_blur: true, + ..PaintChunkMetadata::default() + }, + }, + DisplayCommand::DrawImage { .. } => PaintChunkMetadata { + has_raster_image: true, + ..PaintChunkMetadata::default() + }, + DisplayCommand::DrawSvg { .. } => PaintChunkMetadata { + has_vector_image: true, + ..PaintChunkMetadata::default() + }, + } +} + +fn union_rects(lhs: Option, rhs: Option) -> Option { + match (lhs, rhs) { + (Some(lhs), Some(rhs)) => Some(lhs.union(rhs)), + (Some(lhs), None) => Some(lhs), + (None, Some(rhs)) => Some(rhs), + (None, None) => None, + } +} + fn command_affine(command: &DisplayCommand) -> Affine { match command { DisplayCommand::SetZIndex(_) | DisplayCommand::PopClip | DisplayCommand::PopLayer => { @@ -1095,6 +1241,46 @@ mod tests { }]); assert_eq!(stage.transform_class, TransformClass::TranslateOnly); + assert_eq!(stage.chunks[0].metadata.has_blur, true); + assert!(stage.chunks[0].bounds.is_some()); + } + + #[test] + fn stage_damage_query_filters_chunks_by_bounds() { + let rect = Rect::new(0.0, 0.0, 10.0, 10.0); + let mut stage = ElementStage::default(); + stage.set_commands(vec![ + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: FillRule::NonZero, + paint: Color::BLACK.into(), + paint_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + hint: Some(ShapeHint::Rect(rect)), + }, + DisplayCommand::DrawImage { + img: peniko::ImageBrush::new(peniko::ImageData { + data: peniko::Blob::new(Arc::new(vec![255, 255, 255, 255])), + format: peniko::ImageFormat::Rgba8, + alpha_type: peniko::ImageAlphaType::Alpha, + width: 1, + height: 1, + }), + hash: Arc::from([1_u8].as_slice()), + rect: Rect::new(40.0, 40.0, 50.0, 50.0), + transform: Affine::IDENTITY, + }, + ]); + + let damage = [Rect::new(1.0, 1.0, 5.0, 5.0)]; + let chunks = stage.chunk_indices_for_damage(&damage); + assert_eq!(chunks, vec![0]); + assert_eq!(stage.chunks.len(), 1); + assert!(stage.chunks[0].metadata.has_raster_image); + assert_eq!(stage.chunks[0].bounds, Some(Rect::new(0.0, 0.0, 50.0, 50.0))); } } From 05248446c543f21c5cab9906630eedc24ca1a44f Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 17:25:30 -0600 Subject: [PATCH 013/102] damage aware replay --- benches/painting.rs | 3 +- src/paint/display_list.rs | 473 +++++++++++++++++++++++++++++++++----- src/paint/mod.rs | 74 ++++-- 3 files changed, 473 insertions(+), 77 deletions(-) diff --git a/benches/painting.rs b/benches/painting.rs index cd6e52d6c..7885695a7 100644 --- a/benches/painting.rs +++ b/benches/painting.rs @@ -321,8 +321,7 @@ fn bench_scroll_repaint(c: &mut Criterion) { || { let root = TestRoot::new(); let view = create_scroll_label_list(n); - let mut harness = HeadlessHarness::new_with_size(root, view, 1000.0, 240.0); - harness + HeadlessHarness::new_with_size(root, view, 1000.0, 240.0) }, |mut harness| { harness.paint(); diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index 17dfabd48..d91053314 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -1,3 +1,154 @@ +//! Retained paint artifact storage and replay. +//! +//! This module holds Floem's retained display-list representation. The current design +//! is intentionally moving away from a purely "record a flat command stream per +//! element and replay it blindly" model toward a more Blink-like paint-artifact model: +//! element-local recording, explicit property state, chunking, and eventually +//! selective replay/layerization/compositing. +//! +//! ## Current architecture +//! +//! The core retained object is [`RetainedDisplayList`]. It stores: +//! - A frame-wide paint order of [`PaintOrPost`] entries, which preserves traversal +//! order and the split between pre-child paint and post-child paint. +//! - Per-element retained stages in [`ElementDisplayList`]. +//! - For each stage, a chunked representation in [`ElementStage`]. +//! +//! Each [`ElementStage`] is recorded in the element's local coordinate space. Recording +//! local geometry is important because it makes artifacts reusable across transform +//! changes such as scrolling or ancestor movement. The stage is then compiled into: +//! - [`PaintChunk`]s, which are adjacent runs of draw work sharing the same property +//! state. +//! - A [`PaintPropertyTree`], which currently tracks transform, clip, effect, and +//! scroll state ids referenced by those chunks. +//! +//! The chunk/property split is the key architectural step away from the older model. +//! The old flat command list encoded state transitions directly in the command stream. +//! The current structure instead treats chunks as "draw work under property state X", +//! which is much closer to the way Blink's paint chunks and property trees work. +//! +//! ## Recording model +//! +//! [`RecordingRenderer`] is the adapter used during paint recording. Views still paint +//! through a familiar imperative API (`fill`, `stroke`, `draw_glyphs`, `draw_img`, +//! `draw_svg`, `push_layer`, etc.), but the retained layer no longer treats that +//! imperative stream as the final artifact format. +//! +//! Recording proceeds in two steps: +//! 1. Views emit [`DisplayCommand`]s in local space. +//! 2. [`ElementStage::set_commands`] compiles those commands into chunks plus property +//! state. +//! +//! At the moment, this compilation does a few useful things: +//! - Infers [`TransformClass`] from the actual recorded content instead of assuming +//! every stage is always affine-sensitive. +//! - Coalesces adjacent draw commands that share the same property state. +//! - Tracks chunk bounds and simple chunk metadata. +//! - Moves stage-local clips out of the replay command stream and into clip property +//! ids. +//! +//! ## Clip model +//! +//! Clip handling is deliberately split in two: +//! - Stage-local clips are represented in the property tree and applied by clip id +//! during replay. +//! - View-owned overflow clips are replayed by traversal code in `paint/mod.rs` so +//! they remain active across descendant element replay. +//! +//! This split exists because stage-local clip transitions are local to one retained +//! artifact, while overflow clips affect the replay of descendant artifacts and cannot +//! be modeled as a purely local stage concern. +//! +//! Another important detail is that the retained snapshot only uses the element's +//! intrinsic local clip as artifact identity. We explicitly do **not** use the +//! ancestor-accumulated effective clip as a rerecord key anymore. That avoids the +//! old bug where scrolling changed the accumulated clip and invalidated reusable +//! artifacts even though the local recording itself was unchanged. +//! +//! Some local clips can extend to infinity on visible axes. Those clips are still +//! represented locally for identity purposes, but replay sanitizes them against the +//! current render surface before sending them to the renderer. This keeps retained +//! reuse decoupled from accumulated clip state without ever handing infinite clip +//! geometry to raster backends. +//! +//! ## Replay model +//! +//! Replay happens in [`replay_stage`]. The stage walks chunks, diffs the currently +//! applied clip chain against the chunk's target [`ClipNodeId`], and mutates renderer +//! state to match before replaying the chunk's draw commands. +//! +//! A few optimizations are already in place: +//! - Redundant `set_transform` calls are skipped. +//! - Redundant `set_z_index` calls are skipped. +//! - Stage-local clips are diffed by property id rather than replaying recorded +//! `PushClip` / `PopClip` commands. +//! - Chunk metadata and bounds are available for future scheduler/compositor work. +//! +//! However, replay is still fundamentally full-frame/full-scene today. Backends such +//! as Vello and tiny-skia rebuild their scene/recording from scratch on every frame. +//! That means chunk-level damage filtering is currently staged but not active as a +//! drawing optimization, because skipping a chunk in a fresh frame would simply make +//! that content disappear. +//! +//! ## Retention and invalidation +//! +//! [`ElementSnapshot`] captures the element-local state used to decide whether an +//! artifact can be reused: +//! - local bounds +//! - local clip +//! - world transform +//! +//! Retention is refined by [`TransformClass`], which describes what transform changes +//! are safe for a recorded artifact. This is especially useful for scroll/content +//! reuse where translation-only movement should not force rerecord. +//! +//! The retained subtree optimization in `paint/mod.rs` builds on top of this by +//! allowing transform boundaries to mark subtrees as reusable under certain transform +//! changes, while still replaying them in the correct frame order. +//! +//! ## Current optimizations +//! +//! The main optimizations already implemented in this module are: +//! - Element-local retained recording. +//! - Stage compilation into chunks. +//! - Property-tree ids for transforms, clips, and effects. +//! - Transform sensitivity inference. +//! - Adjacent draw coalescing by property state. +//! - Chunk bounds and metadata collection. +//! - Property-driven stage-local clip replay. +//! - Infinite clip sanitization at replay time. +//! +//! These changes are mostly architectural. They make later performance work possible +//! without forcing a second format rewrite. +//! +//! ## Where this is going +//! +//! The intended direction is a true retained paint artifact system that can support: +//! - Chunk-level damage-driven replay. +//! - Layer promotion based on chunk/property metadata. +//! - Tiling and partial raster. +//! - Parallel paint artifact construction. +//! - Parallel raster/composite for independent tiles or layers. +//! - Stronger separation between paint, raster, and compositing. +//! +//! In practical terms, the major remaining steps are: +//! 1. Introduce a real retained surface/compositor model so undamaged content can +//! survive across frames. +//! 2. Promote damage filtering from "artifact metadata" to "actual replay/raster +//! decision making". +//! 3. Expand property trees so clip/effect/transform/scroll state is not just +//! recorded, but also reusable across layer/tile boundaries. +//! 4. Teach chunk metadata to drive layerization decisions, especially for blur, +//! text, images, and isolated compositing effects. +//! 5. Add explicit raster/composite invalidation so rerecord, reraster, and replay +//! become separate decisions rather than one combined paint decision. +//! 6. Eventually move from a single-threaded replay model toward chunk/tile/layer +//! scheduling that can exploit parallelism safely. +//! +//! Until those steps land, this module should be read as a staging layer for the +//! future architecture: the artifact format is becoming compositor-friendly before +//! the compositor itself exists. + use std::sync::Arc; use floem_renderer::text::{Glyph, GlyphRunProps}; @@ -6,7 +157,7 @@ use imaging::{ Clip, Composite, Draw, FillRule, Geometry, Glyph as SceneGlyph, GlyphRun, Group, StrokeStyle, }; use peniko::BrushRef; -use peniko::kurbo::{Affine, BezPath, Circle, Line, Point, Rect, RoundedRect, Shape, Stroke}; +use peniko::kurbo::{Affine, BezPath, Circle, Line, Point, Rect, RoundedRect, Shape, Size, Stroke}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ElementId, paint::PaintOrPost}; @@ -141,7 +292,11 @@ impl ElementStage { self.chunks .iter() .enumerate() - .filter_map(|(index, chunk)| chunk.intersects_damage(damage).then_some(index)) + .filter_map(|(index, chunk)| { + chunk + .intersects_damage(damage, &self.property_tree) + .then_some(index) + }) .collect() } } @@ -278,7 +433,6 @@ impl PaintChunkMetadata { pub(crate) struct ElementSnapshot { pub local_bounds: Rect, pub clip: Option, - pub effective_clip: Option, pub world_transform: Affine, } @@ -287,26 +441,33 @@ impl ElementSnapshot { Self { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), clip: box_tree.local_clip(element_id.0).flatten(), - effective_clip: box_tree.clipped_local_clip(element_id.0), world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), } } pub(crate) fn supports_reuse(self, current: Self) -> bool { - self.local_bounds == current.local_bounds - && self.clip == current.clip - && effective_clip_not_loosened(self.effective_clip, current.effective_clip) + self.local_bounds == current.local_bounds && self.clip == current.clip } } impl PaintChunk { #[allow(dead_code)] - pub(crate) fn intersects_damage(&self, damage: &[Rect]) -> bool { - let Some(bounds) = self.bounds else { + pub(crate) fn intersects_damage( + &self, + damage: &[Rect], + property_tree: &PaintPropertyTree, + ) -> bool { + let Some(bounds) = self.visible_bounds(property_tree) else { return true; }; damage.iter().any(|rect| rect.intersect(bounds).area() > 0.0) } + + fn visible_bounds(&self, property_tree: &PaintPropertyTree) -> Option { + clip_bounds_for_id(property_tree, self.properties.clip_id) + .map(|clip_bounds| bounds_intersection(self.bounds, Some(clip_bounds))) + .unwrap_or(self.bounds) + } } #[derive(Clone, Default)] @@ -548,22 +709,39 @@ pub(crate) fn replay_stage( stage: &ElementStage, renderer: &mut impl FloemRenderer, base_transform: Affine, + render_size: Size, + local_damage: Option<&[Rect]>, ) { let mut current_z_index = None; let mut current_transform = None; - - for chunk in &stage.chunks { + let mut current_clip_stack: Vec = Vec::new(); + // This stays wired through the replay path even though full-scene replay is still active. + // Once the renderer/compositor can preserve undamaged content across frames, the stage can + // switch from "replay every chunk" to "replay only intersecting chunks" without changing the + // artifact format again. + let chunk_indices = local_damage.map(|damage| stage.chunk_indices_for_damage(damage)); + + for (index, chunk) in stage.chunks.iter().enumerate() { + if let Some(indices) = &chunk_indices && !indices.contains(&index) { + continue; + } if current_z_index != Some(chunk.properties.z_index) { renderer.set_z_index(chunk.properties.z_index); current_z_index = Some(chunk.properties.z_index); } + apply_clip_state( + renderer, + &stage.property_tree, + chunk.properties.clip_id, + base_transform, + render_size, + &mut current_transform, + &mut current_clip_stack, + ); for command in &chunk.commands { match command { DisplayCommand::SetZIndex(_) => {} - DisplayCommand::PushClip { clip, hint } => { - replay_clip(renderer, clip, hint.clone(), base_transform, &mut current_transform) - } - DisplayCommand::PopClip => renderer.clear_clip(), + DisplayCommand::PushClip { .. } | DisplayCommand::PopClip => {} DisplayCommand::PushLayer { group, transform, @@ -634,6 +812,7 @@ pub(crate) fn replay_stage( draw, hint.clone(), base_transform, + render_size, &mut current_transform, ) } @@ -679,6 +858,28 @@ pub(crate) fn replay_stage( } } } + + apply_clip_state( + renderer, + &stage.property_tree, + ClipNodeId(0), + base_transform, + render_size, + &mut current_transform, + &mut current_clip_stack, + ); +} + +pub(crate) fn replay_view_clip( + renderer: &mut impl FloemRenderer, + clip: RoundedRect, + base_transform: Affine, + render_size: Size, +) { + let final_transform = base_transform; + renderer.set_transform(final_transform); + let clip = constrain_infinite_rounded_rect(clip, final_transform, render_size); + renderer.clip(&clip); } fn chunk_display_commands(commands: Vec) -> (Vec, PaintPropertyTree) { @@ -706,28 +907,10 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa }); clip_stack.push(clip_id); properties.clip_id = clip_id; - push_boundary_chunk( - &mut chunks, - properties, - command_transform_class(&DisplayCommand::PushClip { - clip: property_tree.clips[clip_id.0 as usize].clip.clone(), - hint: property_tree.clips[clip_id.0 as usize].hint.clone(), - }), - DisplayCommand::PushClip { - clip: property_tree.clips[clip_id.0 as usize].clip.clone(), - hint: property_tree.clips[clip_id.0 as usize].hint.clone(), - }, - ); } DisplayCommand::PopClip => { clip_stack.pop(); properties.clip_id = clip_stack.last().copied().unwrap_or_default(); - push_boundary_chunk( - &mut chunks, - properties, - command_transform_class(&DisplayCommand::PopClip), - DisplayCommand::PopClip, - ); } DisplayCommand::PushLayer { group, @@ -936,6 +1119,47 @@ fn union_rects(lhs: Option, rhs: Option) -> Option { } } +fn bounds_intersection(lhs: Option, rhs: Option) -> Option { + match (lhs, rhs) { + (Some(lhs), Some(rhs)) => { + let intersection = lhs.intersect(rhs); + (intersection.area() > 0.0).then_some(intersection) + } + (Some(lhs), None) => Some(lhs), + (None, Some(rhs)) => Some(rhs), + (None, None) => None, + } +} + +fn clip_bounds_for_id(property_tree: &PaintPropertyTree, clip_id: ClipNodeId) -> Option { + if clip_id == ClipNodeId(0) { + return None; + } + + let mut current = Some(clip_id); + let mut bounds = None; + while let Some(id) = current { + if id == ClipNodeId(0) { + break; + } + let node = property_tree.clips.get(id.0 as usize)?; + bounds = bounds_intersection(bounds, clip_node_bounds(node)); + current = node.parent; + } + bounds +} + +fn clip_node_bounds(node: &ClipNode) -> Option { + match (&node.hint, &node.clip) { + (Some(ShapeHint::Rect(rect)), _) => Some(*rect), + (Some(ShapeHint::RoundedRect(rect)), _) => Some(rect.rect()), + (Some(ShapeHint::Circle(circle)), _) => Some(circle.bounding_box()), + (Some(ShapeHint::Line(line)), _) => Some(line.bounding_box()), + (None, Clip::Fill { shape, .. }) => Some(geometry_bounds(shape)), + (None, Clip::Stroke { shape, .. }) => Some(geometry_bounds(shape)), + } +} + fn command_affine(command: &DisplayCommand) -> Affine { match command { DisplayCommand::SetZIndex(_) | DisplayCommand::PopClip | DisplayCommand::PopLayer => { @@ -984,21 +1208,30 @@ fn transform_key(transform: Affine) -> [u64; 6] { transform.as_coeffs().map(f64::to_bits) } -fn replay_clip( +fn replay_clip_node( renderer: &mut impl FloemRenderer, - clip: &Clip, - hint: Option, + clip_node: &ClipNode, + property_tree: &PaintPropertyTree, base_transform: Affine, + render_size: Size, current_transform: &mut Option, ) { - let Clip::Fill { - transform, shape, .. - } = clip + let Clip::Fill { shape, .. } = &clip_node.clip else { + return; + }; + let Some(transform) = property_tree + .transforms + .get(clip_node.transform_id.0 as usize) + .map(|node| node.transform) else { return; }; - set_transform_if_needed(renderer, base_transform * *transform, current_transform); - match (hint, shape) { + let final_transform = base_transform * transform; + set_transform_if_needed(renderer, final_transform, current_transform); + match ( + sanitize_clip_hint(clip_node.hint.clone(), final_transform, render_size), + shape, + ) { (Some(ShapeHint::Rect(rect)), _) => renderer.clip(&rect), (Some(ShapeHint::RoundedRect(rect)), _) => renderer.clip(&rect), (Some(ShapeHint::Line(line)), _) => renderer.clip(&line), @@ -1009,11 +1242,71 @@ fn replay_clip( } } +fn apply_clip_state( + renderer: &mut impl FloemRenderer, + property_tree: &PaintPropertyTree, + target_clip_id: ClipNodeId, + base_transform: Affine, + render_size: Size, + current_transform: &mut Option, + current_clip_stack: &mut Vec, +) { + // Stage-local clips are now driven by property ids instead of recorded Push/Pop commands. + // Replay diffs the active clip chain against the target chunk state and mutates the renderer + // clip stack to match. + let target_stack = clip_chain(property_tree, target_clip_id); + let shared_prefix = current_clip_stack + .iter() + .zip(target_stack.iter()) + .take_while(|(lhs, rhs)| lhs == rhs) + .count(); + + for _ in shared_prefix..current_clip_stack.len() { + renderer.clear_clip(); + } + current_clip_stack.truncate(shared_prefix); + + for clip_id in target_stack.into_iter().skip(shared_prefix) { + let Some(node) = property_tree.clips.get(clip_id.0 as usize) else { + continue; + }; + replay_clip_node( + renderer, + node, + property_tree, + base_transform, + render_size, + current_transform, + ); + current_clip_stack.push(clip_id); + } +} + +fn clip_chain(property_tree: &PaintPropertyTree, clip_id: ClipNodeId) -> Vec { + let mut chain = Vec::new(); + let mut current = Some(clip_id); + + while let Some(id) = current { + if id == ClipNodeId(0) { + break; + } + chain.push(id); + current = property_tree + .clips + .get(id.0 as usize) + .and_then(|node| node.parent); + } + + chain.reverse(); + chain +} + fn replay_draw( renderer: &mut impl FloemRenderer, draw: &Draw, hint: Option, base_transform: Affine, + _render_size: Size, current_transform: &mut Option, ) { match draw { @@ -1096,22 +1389,68 @@ fn set_transform_if_needed( } } -fn effective_clip_not_loosened( - recorded: Option, - current: Option, -) -> bool { - match (recorded, current) { - (None, _) => true, - (Some(_), None) => false, - (Some(recorded), Some(current)) => { - recorded == current - || (recorded.radii() == current.radii() - && recorded.rect().contains_rect(current.rect())) - } +fn sanitize_clip_hint( + hint: Option, + transform: Affine, + render_size: Size, +) -> Option { + match hint { + Some(ShapeHint::Rect(rect)) => Some(ShapeHint::Rect(constrain_infinite_rect( + rect, + transform, + render_size, + ))), + Some(ShapeHint::RoundedRect(rect)) => Some(ShapeHint::RoundedRect( + constrain_infinite_rounded_rect(rect, transform, render_size), + )), + other => other, } } +fn constrain_infinite_rounded_rect( + rect: RoundedRect, + transform: Affine, + render_size: Size, +) -> RoundedRect { + let constrained = constrain_infinite_rect(rect.rect(), transform, render_size); + RoundedRect::from_rect(constrained, rect.radii()) +} + +fn constrain_infinite_rect(rect: Rect, transform: Affine, render_size: Size) -> Rect { + if rect.x0.is_finite() && rect.x1.is_finite() && rect.y0.is_finite() && rect.y1.is_finite() { + return rect; + } + + let viewport = Rect::from_origin_size(Point::ZERO, render_size); + let inverse = transform.inverse(); + let local_viewport = inverse.transform_rect_bbox(viewport); + + Rect::new( + if rect.x0.is_finite() { + rect.x0 + } else { + local_viewport.x0 + }, + if rect.y0.is_finite() { + rect.y0 + } else { + local_viewport.y0 + }, + if rect.x1.is_finite() { + rect.x1 + } else { + local_viewport.x1 + }, + if rect.y1.is_finite() { + rect.y1 + } else { + local_viewport.y1 + }, + ) +} + #[cfg(test)] +#[allow(clippy::items_after_test_module)] mod tests { use super::*; use peniko::Color; @@ -1154,7 +1493,7 @@ mod tests { } #[test] - fn stage_tracks_clip_depth_across_boundary_chunks() { + fn stage_tracks_clip_state_without_boundary_chunks() { let rect = Rect::new(0.0, 0.0, 10.0, 10.0); let mut stage = ElementStage::default(); stage.set_commands(vec![ @@ -1180,12 +1519,10 @@ mod tests { DisplayCommand::PopClip, ]); - assert_eq!(stage.chunks.len(), 3); - assert_eq!(stage.chunks[0].kind, PaintChunkKind::Boundary); - assert_eq!(stage.chunks[1].kind, PaintChunkKind::Draw); - assert_ne!(stage.chunks[1].properties.clip_id, ClipNodeId(0)); - assert_eq!(stage.chunks[2].kind, PaintChunkKind::Boundary); - assert_eq!(stage.chunks[2].properties.clip_id, ClipNodeId(0)); + assert_eq!(stage.chunks.len(), 1); + assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); + assert_ne!(stage.chunks[0].properties.clip_id, ClipNodeId(0)); + assert_eq!(stage.property_tree.clips.len(), 2); } #[test] @@ -1241,7 +1578,7 @@ mod tests { }]); assert_eq!(stage.transform_class, TransformClass::TranslateOnly); - assert_eq!(stage.chunks[0].metadata.has_blur, true); + assert!(stage.chunks[0].metadata.has_blur); assert!(stage.chunks[0].bounds.is_some()); } @@ -1282,6 +1619,20 @@ mod tests { assert!(stage.chunks[0].metadata.has_raster_image); assert_eq!(stage.chunks[0].bounds, Some(Rect::new(0.0, 0.0, 50.0, 50.0))); } + + #[test] + fn infinite_clip_is_constrained_to_render_bounds() { + let constrained = constrain_infinite_rect( + Rect::new(f64::NEG_INFINITY, 10.0, f64::INFINITY, 20.0), + Affine::IDENTITY, + Size::new(200.0, 100.0), + ); + + assert_eq!(constrained.x0, 0.0); + assert_eq!(constrained.x1, 200.0); + assert_eq!(constrained.y0, 10.0); + assert_eq!(constrained.y1, 20.0); + } } fn shape_to_geometry(shape: &impl Shape) -> (Geometry, Option) { diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 41a51ea41..20e1a0dcb 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -12,6 +12,7 @@ pub mod renderer; pub use border_path_iter::{BorderPath, BorderPathEvent}; pub use renderer::Renderer; +use floem_renderer::Renderer as _; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; use peniko::kurbo::{Affine, RoundedRect, Shape, Size}; use rustc_hash::FxHashSet; @@ -220,6 +221,24 @@ pub(crate) fn collect_visual_order( } impl GlobalPaintCx<'_> { + fn replay_element_overflow_clip(&mut self, element_id: ElementId) { + let box_tree = self.window_state.box_tree.borrow(); + let Some(clip) = box_tree.local_clip(element_id.0).flatten() else { + return; + }; + drop(box_tree); + + let base_transform = self + .element_base_transform(element_id) + .then_scale(self.window_state.effective_scale()); + let render_size = self.paint_state.renderer().size(); + let renderer = self.paint_state.renderer_mut(); + + // Overflow clip must be replayed at traversal time rather than recorded into the + // element stage so it can stay active across descendant element replay. + display_list::replay_view_clip(renderer, clip, base_transform, render_size); + } + fn collect_retained_subtree_descendants( &mut self, active_ids: &FxHashSet, @@ -376,10 +395,28 @@ impl GlobalPaintCx<'_> { if self.record_paint_order { record_paint(element_id.owning_id()); } - self.replay_visual_node(element_id, false); + if element_id.is_view() { + self.replay_element_overflow_clip(element_id); + } + // Damage-aware chunk replay is intentionally disabled here for now. + // Every backend rebuilds a fresh frame/scene on `begin()`, so skipping + // undamaged chunks would drop previously visible content and cause flashing. + self.replay_visual_node(element_id, false, None); } PaintOrPost::Post(element_id) => { - self.replay_visual_node(element_id, true); + self.replay_visual_node(element_id, true, None); + if element_id.is_view() { + let has_clip = self + .window_state + .box_tree + .borrow() + .local_clip(element_id.0) + .flatten() + .is_some(); + if has_clip { + self.paint_state.renderer_mut().clear_clip(); + } + } } } } @@ -391,7 +428,12 @@ impl GlobalPaintCx<'_> { box_tree.world_transform(element_id.0).unwrap_or_default() } - fn replay_visual_node(&mut self, element_id: ElementId, is_post: bool) { + fn replay_visual_node( + &mut self, + element_id: ElementId, + is_post: bool, + damage_rects: Option<&[peniko::kurbo::Rect]>, + ) { let base_transform = self .element_base_transform(element_id) .then_scale(self.window_state.effective_scale()); @@ -403,16 +445,27 @@ impl GlobalPaintCx<'_> { } else { &element.paint }; + let local_damage = damage_rects.map(|rects| { + let inverse = base_transform.inverse(); + rects.iter() + .map(|rect| inverse.transform_rect_bbox(*rect)) + .collect::>() + }); + let render_size = self.paint_state.renderer().size(); let renderer = self.paint_state.renderer_mut(); - replay_stage(stage, renderer, base_transform); + replay_stage( + stage, + renderer, + base_transform, + render_size, + local_damage.as_deref(), + ); } /// Record a single visual node in local coordinates. pub(crate) fn record_visual_node(&mut self, element_id: ElementId, is_post: bool) { let box_tree = self.window_state.box_tree.borrow_mut(); let layout_rect_local = box_tree.local_bounds(element_id.0).unwrap_or_default(); - let local_clip = box_tree.local_clip(element_id.0).flatten(); - let effective_clip = box_tree.clipped_local_clip(element_id.0); let snapshot = ElementSnapshot::from_box_tree(&box_tree, element_id); drop(box_tree); @@ -437,7 +490,7 @@ impl GlobalPaintCx<'_> { target_id: element_id, world_transform, layout_rect_local, - clip: local_clip, + clip: snapshot.clip, font_size_cx, }; @@ -452,16 +505,9 @@ impl GlobalPaintCx<'_> { layout_rect, ); drop(state); - // Apply overflow clip (stays active through children) - if let Some(clip_shape) = effective_clip { - cx.clip(&clip_shape); - } } view.borrow_mut().paint(&mut cx); } else { - if element_id.is_view() && effective_clip.is_some() { - cx.pop_clip(); - } view.borrow_mut().post_paint(&mut cx); if element_id.is_view() { let state = view_state.borrow(); From 2e013384f990a9810c4574d2be7ac83bf98e1591 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 19:04:02 -0600 Subject: [PATCH 014/102] compositor module --- src/compositor/mod.rs | 337 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 6 + src/window/state.rs | 70 ++++++++- 3 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 src/compositor/mod.rs diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs new file mode 100644 index 000000000..49c39a71f --- /dev/null +++ b/src/compositor/mod.rs @@ -0,0 +1,337 @@ +//! Compositor-facing API and retained compositor state. +//! +//! This module is the public entry point for composition-level concepts that sit +//! above paint recording and renderer backends. Paint/display-list code is focused +//! on describing Floem-drawn content. The compositor is responsible for deciding +//! how that content, external surfaces, timing information, and presentation are +//! orchestrated into frames. +//! +//! The immediate motivation for this module is external media, especially video. +//! A video editor needs to hand Floem an externally produced surface or texture, +//! mark that content ready, and let Floem drive the frame lifecycle from there. +//! The compositor is the right place to express that workflow because it owns: +//! - layer identity +//! - frame requests +//! - presentation timing +//! - the boundary between retained paint artifacts and final composition +//! +//! ## Current scope +//! +//! The compositor module currently provides: +//! - stable ids for layers and external surfaces +//! - descriptors for layer kinds and external surface metadata +//! - a retained registry of compositor layers and external surfaces +//! - a frame request queue with reasons, including "external surface ready" +//! - frame timing information that can be queried by higher-level code +//! +//! This is an API and state-model introduction. It does not yet own final platform +//! presentation or backend-specific composition. That work will come later when +//! Floem's renderer/compositor split is made explicit. +//! +//! ## Intended workflow +//! +//! The intended usage is: +//! 1. Application registers an external surface or texture source. +//! 2. Application associates it with one or more compositor layers. +//! 3. External producer signals that a frame is ready. +//! 4. Floem schedules a frame through the normal update/paint/composite path. +//! 5. The compositor decides what must be repainted, rerasterized, or recomposited. +//! 6. Floem presents and updates timing information. +//! +//! ## Where this is going +//! +//! This module is meant to grow into: +//! - a retained layer tree +//! - external surface integration for GPU/native surfaces +//! - frame pacing / present feedback APIs +//! - compositor scheduling and layer dirtiness +//! - platform/backend composition hooks +//! - the owner of "texture ready" style frame requests +//! +//! In other words, this module is the start of Floem's compositor boundary. + +use std::time::{Duration, Instant}; + +use peniko::BlendMode; +use peniko::kurbo::{Rect, Size}; +use rustc_hash::FxHashMap; + +/// Stable identifier for a compositor layer. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CompositorLayerId(u64); + +impl CompositorLayerId { + /// Returns the raw numeric identifier. + pub const fn as_u64(self) -> u64 { + self.0 + } +} + +/// Stable identifier for an externally managed surface. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExternalSurfaceId(u64); + +impl ExternalSurfaceId { + /// Returns the raw numeric identifier. + pub const fn as_u64(self) -> u64 { + self.0 + } +} + +/// Pixel format hint for an external surface. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ExternalPixelFormat { + #[default] + Unknown, + Rgba8Unorm, + Bgra8Unorm, + Rgba16Float, + Nv12, + P010, + Custom(u32), +} + +/// Alpha handling for an external surface. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ExternalAlphaMode { + #[default] + Unknown, + Opaque, + Straight, + Premultiplied, +} + +/// Color-space hint for an external surface. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ExternalColorSpace { + #[default] + Unknown, + Srgb, + DisplayP3, + Rec709, + Rec2020, + Custom(u32), +} + +/// Opaque handle type describing how an external surface is represented. +/// +/// This is intentionally backend-agnostic for now. Platform- or backend-specific +/// composition code can interpret these handles later without forcing the public +/// compositor API to depend on a single graphics backend. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ExternalSurfaceHandle { + #[default] + Unspecified, + Opaque(u64), +} + +/// Metadata describing an externally managed compositing surface. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct ExternalSurfaceDescriptor { + pub size: Size, + pub pixel_format: ExternalPixelFormat, + pub alpha_mode: ExternalAlphaMode, + pub color_space: ExternalColorSpace, + pub frame_rate_hint: Option, +} + +/// How a compositor layer gets its contents. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum CompositorLayerKind { + /// Layer contents come from normal Floem paint artifacts. + #[default] + FloemPainted, + /// Layer contents come from a registered external surface. + ExternalSurface { + surface_id: ExternalSurfaceId, + }, + /// Layer combines an external surface with Floem-drawn overlays or underlays. + Mixed { + surface_id: ExternalSurfaceId, + }, +} + +/// Retained descriptor for a compositor layer. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct CompositorLayerDescriptor { + pub kind: CompositorLayerKind, + pub bounds: Rect, + pub z_index: i32, + pub opacity: f32, + pub blend_mode: BlendMode, + pub isolated: bool, +} + +/// Why the compositor wants a frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FrameRequestReason { + AppRequested, + Animation, + Resize, + Damage, + ExternalSurfaceReady(ExternalSurfaceId), + LayerDirty(CompositorLayerId), +} + +/// Presentation timing state exposed by the compositor. +#[derive(Debug, Default, Clone, Copy)] +pub struct CompositorTiming { + pub frame_interval: Option, + pub predicted_next_present: Option, + pub last_present_started: Option, + pub last_present_completed: Option, + pub frame_number: u64, +} + +#[derive(Debug, Clone, Copy)] +pub struct ExternalSurfaceState { + pub handle: ExternalSurfaceHandle, + pub descriptor: ExternalSurfaceDescriptor, + pub latest_ready_at: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct CompositorLayerState { + pub descriptor: CompositorLayerDescriptor, + pub dirty: bool, +} + +/// Retained compositor registry and frame-request state. +#[derive(Debug, Default)] +pub struct Compositor { + next_layer_id: u64, + next_surface_id: u64, + layers: FxHashMap, + external_surfaces: FxHashMap, + pending_frame_reasons: Vec, + timing: CompositorTiming, +} + +impl Compositor { + /// Registers a new externally managed surface and returns its id. + pub fn register_external_surface( + &mut self, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ) -> ExternalSurfaceId { + let id = ExternalSurfaceId(self.next_surface_id); + self.next_surface_id += 1; + self.external_surfaces.insert( + id, + ExternalSurfaceState { + handle, + descriptor, + latest_ready_at: None, + }, + ); + id + } + + /// Updates the metadata for an existing external surface. + pub fn update_external_surface( + &mut self, + id: ExternalSurfaceId, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ) -> bool { + let Some(surface) = self.external_surfaces.get_mut(&id) else { + return false; + }; + surface.handle = handle; + surface.descriptor = descriptor; + true + } + + /// Returns the retained state for an external surface. + pub fn external_surface(&self, id: ExternalSurfaceId) -> Option<&ExternalSurfaceState> { + self.external_surfaces.get(&id) + } + + /// Registers a compositor layer and returns its id. + pub fn register_layer( + &mut self, + descriptor: CompositorLayerDescriptor, + ) -> CompositorLayerId { + let id = CompositorLayerId(self.next_layer_id); + self.next_layer_id += 1; + self.layers.insert( + id, + CompositorLayerState { + descriptor, + dirty: true, + }, + ); + id + } + + /// Updates the retained descriptor for a compositor layer. + pub fn update_layer( + &mut self, + id: CompositorLayerId, + descriptor: CompositorLayerDescriptor, + ) -> bool { + let Some(layer) = self.layers.get_mut(&id) else { + return false; + }; + layer.descriptor = descriptor; + layer.dirty = true; + true + } + + /// Returns the retained state for a compositor layer. + pub fn layer(&self, id: CompositorLayerId) -> Option<&CompositorLayerState> { + self.layers.get(&id) + } + + /// Marks a layer dirty and schedules a compositor frame. + pub fn mark_layer_dirty(&mut self, id: CompositorLayerId) { + if let Some(layer) = self.layers.get_mut(&id) { + layer.dirty = true; + self.pending_frame_reasons + .push(FrameRequestReason::LayerDirty(id)); + } + } + + /// Notifies the compositor that a new frame is ready on an external surface. + pub fn notify_external_surface_ready(&mut self, id: ExternalSurfaceId) -> bool { + let Some(surface) = self.external_surfaces.get_mut(&id) else { + return false; + }; + surface.latest_ready_at = Some(Instant::now()); + self.pending_frame_reasons + .push(FrameRequestReason::ExternalSurfaceReady(id)); + true + } + + /// Requests a compositor frame for an explicit reason. + pub fn request_frame(&mut self, reason: FrameRequestReason) { + self.pending_frame_reasons.push(reason); + } + + /// Returns whether a compositor frame has been requested. + pub fn has_pending_frame(&self) -> bool { + !self.pending_frame_reasons.is_empty() + } + + /// Drains and returns pending frame reasons. + pub fn take_pending_frame_reasons(&mut self) -> Vec { + std::mem::take(&mut self.pending_frame_reasons) + } + + /// Clears the dirty bit for all retained layers. + pub fn clear_layer_dirtiness(&mut self) { + for layer in self.layers.values_mut() { + layer.dirty = false; + } + } + + /// Returns the most recent compositor timing state. + pub const fn timing(&self) -> &CompositorTiming { + &self.timing + } + + /// Updates compositor timing state after presentation work has advanced. + pub fn update_timing(&mut self, timing: CompositorTiming) { + self.timing = timing; + } +} diff --git a/src/lib.rs b/src/lib.rs index ec549b13f..80720946c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,6 +180,7 @@ pub mod action; pub mod animate; mod app; mod box_tree; +pub mod compositor; pub mod context; pub mod event; pub mod ext_event; @@ -222,6 +223,11 @@ pub mod receiver_signal { pub use box_tree::{BoxTree, ElementId, ElementMeta, FocusNavMeta}; pub(crate) use box_tree::{bump_focus_nav_meta_revision, focus_nav_meta_revision}; +pub use compositor::{ + Compositor, CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, + CompositorTiming, ExternalAlphaMode, ExternalColorSpace, ExternalPixelFormat, + ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, +}; pub use app::{AppConfig, AppEvent, Application, launch, quit_app, reopen}; pub use floem_reactive as reactive; diff --git a/src/window/state.rs b/src/window/state.rs index c4a60b71d..2e3b31e2b 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -2,6 +2,10 @@ use std::{cell::RefCell, collections::HashMap}; use crate::{ action::exec_after_animation_frame, + compositor::{ + Compositor, CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, + ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, + }, inspector::CaptureState, platform::menu_types::MenuId, style::{StyleSelectors, recalc::StyleReason}, @@ -102,6 +106,7 @@ fn build_focus_space_for_scope<'a>( pub struct WindowState { pub(crate) layout_tree: Rc>>, pub(crate) box_tree: Rc>, + pub(crate) compositor: Compositor, pub(crate) display_list: RetainedDisplayList, pub(crate) last_paint_stats: PaintStats, @@ -214,6 +219,7 @@ impl WindowState { root_view_id, layout_tree, box_tree, + compositor: Compositor::default(), display_list: RetainedDisplayList::default(), last_paint_stats: PaintStats::default(), pointer_capture_target: PointerCaptureMap::new(), @@ -1244,6 +1250,58 @@ impl WindowState { self.dirty_paint_elements.insert(id.into()); } + pub fn register_compositor_layer( + &mut self, + descriptor: CompositorLayerDescriptor, + ) -> CompositorLayerId { + self.compositor.register_layer(descriptor) + } + + pub fn update_compositor_layer( + &mut self, + id: CompositorLayerId, + descriptor: CompositorLayerDescriptor, + ) -> bool { + self.compositor.update_layer(id, descriptor) + } + + pub fn mark_compositor_layer_dirty(&mut self, id: CompositorLayerId) { + self.compositor.mark_layer_dirty(id); + } + + pub fn register_external_surface( + &mut self, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ) -> ExternalSurfaceId { + self.compositor.register_external_surface(handle, descriptor) + } + + pub fn update_external_surface( + &mut self, + id: ExternalSurfaceId, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ) -> bool { + self.compositor.update_external_surface(id, handle, descriptor) + } + + pub fn notify_external_surface_ready(&mut self, id: ExternalSurfaceId) -> bool { + self.compositor.notify_external_surface_ready(id) + } + + pub fn request_compositor_frame(&mut self, reason: FrameRequestReason) { + self.compositor.request_frame(reason); + } + + pub const fn compositor_timing(&self) -> &CompositorTiming { + self.compositor.timing() + } + + pub fn update_compositor_timing(&mut self, timing: CompositorTiming) { + self.compositor.update_timing(timing); + } + pub fn take_dirty_paint_elements(&mut self) -> FxHashSet { std::mem::take(&mut self.dirty_paint_elements) } @@ -1253,7 +1311,9 @@ impl WindowState { } pub fn has_pending_render(&self) -> bool { - !self.dirty_paint_elements.is_empty() || !self.pending_damage_rects.is_empty() + !self.dirty_paint_elements.is_empty() + || !self.pending_damage_rects.is_empty() + || self.compositor.has_pending_frame() } pub fn clear_pending_paint(&mut self) { @@ -1264,6 +1324,14 @@ impl WindowState { self.pending_damage_rects.clear(); } + pub fn take_pending_compositor_frame_reasons(&mut self) -> Vec { + self.compositor.take_pending_frame_reasons() + } + + pub fn clear_compositor_layer_dirtiness(&mut self) { + self.compositor.clear_layer_dirtiness(); + } + pub(crate) fn update_screen_size_bp(&mut self, size: Size) { let bp = self.grid_bps.get_width_bp(size.width); if bp != self.screen_size_bp { From ba6e25691ca03c51c36f005403e30902a868ba9e Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 20:00:01 -0600 Subject: [PATCH 015/102] pull in subduction --- Cargo.lock | 19 +++ Cargo.toml | 2 + src/compositor/backend.rs | 55 ++++++++ src/compositor/mod.rs | 81 ++++++++++- src/compositor/subduction.rs | 257 +++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/window/state.rs | 9 ++ 7 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 src/compositor/backend.rs create mode 100644 src/compositor/subduction.rs diff --git a/Cargo.lock b/Cargo.lock index b4df8fb1d..3d98ec059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,7 @@ dependencies = [ "smallvec", "strum", "strum_macros", + "subduction_core", "sys-locale", "taffy", "tokio", @@ -3238,6 +3239,7 @@ checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" dependencies = [ "arrayvec", "euclid", + "libm", "serde", "smallvec", ] @@ -5725,6 +5727,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "subduction_core" +version = "0.0.1" +source = "git+https://github.com/forest-rs/subduction.git?rev=942a0d4d1a65f855c2a77aa10d01be132196d4bd#942a0d4d1a65f855c2a77aa10d01be132196d4bd" +dependencies = [ + "kurbo", + "understory_dirty", +] + [[package]] name = "svg_fmt" version = "0.4.5" @@ -6383,6 +6394,14 @@ dependencies = [ "understory_index", ] +[[package]] +name = "understory_dirty" +version = "0.1.0" +source = "git+https://github.com/endoli/understory.git?rev=e66fecdc521b57b76bb3b28ddc70ce3944926664#e66fecdc521b57b76bb3b28ddc70ce3944926664" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "understory_event_state" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 62966bec9..27713b514 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ floem_vger_renderer = { path = "vger", version = "0.2.0", optional = true } floem_tiny_skia_renderer = { path = "tiny_skia", version = "0.2.0" } floem_reactive = { path = "reactive", version = "0.2.0" } floem-editor-core = { path = "editor-core", version = "0.2.0", optional = true } +subduction_core = { git = "https://github.com/forest-rs/subduction.git", rev = "942a0d4d1a65f855c2a77aa10d01be132196d4bd", package = "subduction_core", optional = true, features = ["std"] } copypasta = { version = "0.10", default-features = false, features = [ "wayland", "x11", @@ -194,6 +195,7 @@ tokio = ["dep:tokio"] # rfd (file dialog) async runtime crossbeam = ["dep:crossbeam", "floem_renderer/crossbeam"] localization = ["dep:fluent-bundle", "dep:unic-langid", "dep:sys-locale", "dep:ouroboros"] +subduction = ["dep:subduction_core"] [dev-dependencies] criterion = { version = "0.8.1", features = ["html_reports"] } diff --git a/src/compositor/backend.rs b/src/compositor/backend.rs new file mode 100644 index 000000000..4f967d362 --- /dev/null +++ b/src/compositor/backend.rs @@ -0,0 +1,55 @@ +//! Backend contract for Floem's compositor layer. +//! +//! Floem keeps compositor identity, frame requests, and app-facing policy in +//! [`crate::compositor`]. Backends sit underneath that layer and mirror the +//! retained compositor state into a concrete presentation engine such as +//! `subduction`. + +use super::{ + CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, ExternalSurfaceDescriptor, + ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, +}; + +/// Backend interface for mirroring Floem compositor state into a concrete +/// presentation/composition engine. +pub trait CompositorBackend { + /// Human-readable backend name for diagnostics. + fn name(&self) -> &'static str; + + /// Registers a new external surface with the backend. + fn register_external_surface( + &mut self, + id: ExternalSurfaceId, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ); + + /// Updates an existing external surface. + fn update_external_surface( + &mut self, + id: ExternalSurfaceId, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ); + + /// Registers a compositor layer with the backend. + fn register_layer(&mut self, id: CompositorLayerId, descriptor: CompositorLayerDescriptor); + + /// Updates an existing compositor layer. + fn update_layer(&mut self, id: CompositorLayerId, descriptor: CompositorLayerDescriptor); + + /// Marks a compositor layer dirty. + fn mark_layer_dirty(&mut self, id: CompositorLayerId); + + /// Signals that a new frame is ready on an external surface. + fn notify_external_surface_ready(&mut self, id: ExternalSurfaceId); + + /// Signals that Floem has queued a compositor frame. + fn request_frame(&mut self, reason: FrameRequestReason); + + /// Clears layer dirty bits after a frame has been consumed. + fn clear_layer_dirtiness(&mut self); + + /// Updates compositor timing after presentation has advanced. + fn update_timing(&mut self, timing: CompositorTiming); +} diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index 49c39a71f..a7063f1bb 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -50,12 +50,18 @@ //! //! In other words, this module is the start of Floem's compositor boundary. +pub mod backend; +#[cfg(feature = "subduction")] +pub mod subduction; + use std::time::{Duration, Instant}; use peniko::BlendMode; use peniko::kurbo::{Rect, Size}; use rustc_hash::FxHashMap; +use self::backend::CompositorBackend; + /// Stable identifier for a compositor layer. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct CompositorLayerId(u64); @@ -197,7 +203,7 @@ pub struct CompositorLayerState { } /// Retained compositor registry and frame-request state. -#[derive(Debug, Default)] +#[derive(Default)] pub struct Compositor { next_layer_id: u64, next_surface_id: u64, @@ -205,9 +211,51 @@ pub struct Compositor { external_surfaces: FxHashMap, pending_frame_reasons: Vec, timing: CompositorTiming, + backend: Option>, +} + +impl std::fmt::Debug for Compositor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Compositor") + .field("next_layer_id", &self.next_layer_id) + .field("next_surface_id", &self.next_surface_id) + .field("layers", &self.layers) + .field("external_surfaces", &self.external_surfaces) + .field("pending_frame_reasons", &self.pending_frame_reasons) + .field("timing", &self.timing) + .field("backend_name", &self.backend_name()) + .finish() + } } impl Compositor { + /// Installs a compositor backend and synchronizes the retained state into it. + pub fn install_backend(&mut self, mut backend: Box) { + for (&id, surface) in &self.external_surfaces { + backend.register_external_surface(id, surface.handle, surface.descriptor); + } + + for (&id, layer) in &self.layers { + backend.register_layer(id, layer.descriptor); + if layer.dirty { + backend.mark_layer_dirty(id); + } + } + + for &reason in &self.pending_frame_reasons { + backend.request_frame(reason); + } + + backend.update_timing(self.timing); + self.backend = Some(backend); + } + + /// Returns the installed backend name, if any. + #[must_use] + pub fn backend_name(&self) -> Option<&'static str> { + self.backend.as_ref().map(|backend| backend.name()) + } + /// Registers a new externally managed surface and returns its id. pub fn register_external_surface( &mut self, @@ -224,6 +272,9 @@ impl Compositor { latest_ready_at: None, }, ); + if let Some(backend) = self.backend.as_mut() { + backend.register_external_surface(id, handle, descriptor); + } id } @@ -239,6 +290,9 @@ impl Compositor { }; surface.handle = handle; surface.descriptor = descriptor; + if let Some(backend) = self.backend.as_mut() { + backend.update_external_surface(id, handle, descriptor); + } true } @@ -261,6 +315,10 @@ impl Compositor { dirty: true, }, ); + if let Some(backend) = self.backend.as_mut() { + backend.register_layer(id, descriptor); + backend.mark_layer_dirty(id); + } id } @@ -275,6 +333,10 @@ impl Compositor { }; layer.descriptor = descriptor; layer.dirty = true; + if let Some(backend) = self.backend.as_mut() { + backend.update_layer(id, descriptor); + backend.mark_layer_dirty(id); + } true } @@ -289,6 +351,10 @@ impl Compositor { layer.dirty = true; self.pending_frame_reasons .push(FrameRequestReason::LayerDirty(id)); + if let Some(backend) = self.backend.as_mut() { + backend.mark_layer_dirty(id); + backend.request_frame(FrameRequestReason::LayerDirty(id)); + } } } @@ -300,12 +366,19 @@ impl Compositor { surface.latest_ready_at = Some(Instant::now()); self.pending_frame_reasons .push(FrameRequestReason::ExternalSurfaceReady(id)); + if let Some(backend) = self.backend.as_mut() { + backend.notify_external_surface_ready(id); + backend.request_frame(FrameRequestReason::ExternalSurfaceReady(id)); + } true } /// Requests a compositor frame for an explicit reason. pub fn request_frame(&mut self, reason: FrameRequestReason) { self.pending_frame_reasons.push(reason); + if let Some(backend) = self.backend.as_mut() { + backend.request_frame(reason); + } } /// Returns whether a compositor frame has been requested. @@ -323,6 +396,9 @@ impl Compositor { for layer in self.layers.values_mut() { layer.dirty = false; } + if let Some(backend) = self.backend.as_mut() { + backend.clear_layer_dirtiness(); + } } /// Returns the most recent compositor timing state. @@ -333,5 +409,8 @@ impl Compositor { /// Updates compositor timing state after presentation work has advanced. pub fn update_timing(&mut self, timing: CompositorTiming) { self.timing = timing; + if let Some(backend) = self.backend.as_mut() { + backend.update_timing(timing); + } } } diff --git a/src/compositor/subduction.rs b/src/compositor/subduction.rs new file mode 100644 index 000000000..eb861ccf5 --- /dev/null +++ b/src/compositor/subduction.rs @@ -0,0 +1,257 @@ +//! `subduction` backend adapter for Floem's compositor state. +//! +//! This adapter keeps Floem's compositor API backend-agnostic while mirroring +//! retained layers and surfaces into `subduction_core`'s `LayerStore`. +//! +//! The current integration scope is intentionally narrow: +//! - stable Floem compositor ids are mapped to `subduction` layer/surface ids +//! - layer descriptors are mirrored into `LayerStore` topology and properties +//! - external surfaces are mapped onto `subduction` `SurfaceId`s +//! - frame reasons and timing are retained so the future presenter/frame loop +//! can consume them from one place +//! +//! This is enough to establish the architectural seam. It is not yet full +//! presentation integration. + +use peniko::kurbo::{Affine, Rect, RoundedRect}; +use rustc_hash::FxHashMap; +use subduction_core::{ + layer::{ClipShape, FrameChanges, LayerFlags, LayerId, LayerStore, SurfaceId}, + transform::Transform3d, +}; + +use super::{ + CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, + ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, + backend::CompositorBackend, +}; + +/// `subduction`-backed compositor mirror. +#[derive(Debug)] +pub struct SubductionCompositorBackend { + store: LayerStore, + root_layer: LayerId, + layer_ids: FxHashMap, + layer_descriptors: FxHashMap, + surface_ids: FxHashMap, + surface_descriptors: FxHashMap, + surface_handles: FxHashMap, + next_surface_id: u32, + pending_frame_reasons: Vec, + timing: CompositorTiming, + needs_reorder: bool, +} + +impl Default for SubductionCompositorBackend { + fn default() -> Self { + Self::new() + } +} + +impl SubductionCompositorBackend { + /// Creates a new backend adapter with a synthetic root layer. + #[must_use] + pub fn new() -> Self { + let mut store = LayerStore::new(); + let root_layer = store.create_layer(); + + Self { + store, + root_layer, + layer_ids: FxHashMap::default(), + layer_descriptors: FxHashMap::default(), + surface_ids: FxHashMap::default(), + surface_descriptors: FxHashMap::default(), + surface_handles: FxHashMap::default(), + next_surface_id: 1, + pending_frame_reasons: Vec::new(), + timing: CompositorTiming::default(), + needs_reorder: false, + } + } + + /// Returns the mirrored `subduction` layer store. + #[must_use] + pub const fn store(&self) -> &LayerStore { + &self.store + } + + /// Returns the current compositor timing mirrored into the backend. + #[must_use] + pub const fn timing(&self) -> &CompositorTiming { + &self.timing + } + + /// Returns the `subduction` surface id mapped from a Floem external surface. + #[must_use] + pub fn surface_id_for(&self, id: ExternalSurfaceId) -> Option { + self.surface_ids.get(&id).copied() + } + + /// Evaluates the mirrored `subduction` layer store. + pub fn evaluate(&mut self) -> FrameChanges { + self.sync_order_if_needed(); + self.store.evaluate() + } + + fn register_surface_id(&mut self, id: ExternalSurfaceId) -> SurfaceId { + if let Some(surface_id) = self.surface_ids.get(&id).copied() { + return surface_id; + } + + let surface_id = SurfaceId(self.next_surface_id); + self.next_surface_id += 1; + self.surface_ids.insert(id, surface_id); + surface_id + } + + fn ensure_layer_id(&mut self, id: CompositorLayerId) -> LayerId { + if let Some(layer_id) = self.layer_ids.get(&id).copied() { + return layer_id; + } + + let layer_id = self.store.create_layer(); + self.store.add_child(self.root_layer, layer_id); + self.layer_ids.insert(id, layer_id); + self.needs_reorder = true; + layer_id + } + + fn apply_layer_descriptor(&mut self, id: CompositorLayerId, descriptor: CompositorLayerDescriptor) { + let layer_id = self.ensure_layer_id(id); + self.layer_descriptors.insert(id, descriptor); + + // Floem layer bounds are rects; `subduction` splits that into a + // translation (rect origin) and a local size (rect dimensions). + let bounds = descriptor.bounds; + self.store + .set_transform(layer_id, Transform3d::from(Affine::translate(bounds.origin().to_vec2()))); + self.store.set_bounds(layer_id, bounds.size()); + self.store.set_opacity(layer_id, descriptor.opacity); + self.store + .set_clip(layer_id, clip_shape_for_bounds(bounds, descriptor.isolated)); + self.store + .set_flags(layer_id, LayerFlags { hidden: descriptor.opacity <= 0.0 }); + self.store + .set_content(layer_id, self.content_for_kind(descriptor.kind)); + self.needs_reorder = true; + } + + fn content_for_kind(&self, kind: CompositorLayerKind) -> Option { + match kind { + CompositorLayerKind::FloemPainted => None, + CompositorLayerKind::ExternalSurface { surface_id } + | CompositorLayerKind::Mixed { surface_id } => self.surface_ids.get(&surface_id).copied(), + } + } + + fn sync_order_if_needed(&mut self) { + if !self.needs_reorder { + return; + } + + let mut ordered_layers: Vec<_> = self.layer_descriptors.iter().map(|(&id, &descriptor)| (id, descriptor)).collect(); + ordered_layers.sort_by_key(|(id, descriptor)| (descriptor.z_index, id.as_u64())); + + for (id, _) in ordered_layers { + let Some(layer_id) = self.layer_ids.get(&id).copied() else { + continue; + }; + + if self.store.parent(layer_id).is_some() { + self.store.remove_from_parent(layer_id); + } + self.store.add_child(self.root_layer, layer_id); + } + + self.needs_reorder = false; + } +} + +impl CompositorBackend for SubductionCompositorBackend { + fn name(&self) -> &'static str { + "subduction" + } + + fn register_external_surface( + &mut self, + id: ExternalSurfaceId, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ) { + self.register_surface_id(id); + self.surface_handles.insert(id, handle); + self.surface_descriptors.insert(id, descriptor); + } + + fn update_external_surface( + &mut self, + id: ExternalSurfaceId, + handle: ExternalSurfaceHandle, + descriptor: ExternalSurfaceDescriptor, + ) { + self.register_surface_id(id); + self.surface_handles.insert(id, handle); + self.surface_descriptors.insert(id, descriptor); + + let affected_layers: Vec<_> = self + .layer_descriptors + .iter() + .filter_map(|(&layer_id, descriptor)| match descriptor.kind { + CompositorLayerKind::ExternalSurface { surface_id } + | CompositorLayerKind::Mixed { surface_id } + if surface_id == id => + { + Some((layer_id, *descriptor)) + } + _ => None, + }) + .collect(); + + for (layer_id, descriptor) in affected_layers { + self.apply_layer_descriptor(layer_id, descriptor); + } + } + + fn register_layer(&mut self, id: CompositorLayerId, descriptor: CompositorLayerDescriptor) { + self.apply_layer_descriptor(id, descriptor); + } + + fn update_layer(&mut self, id: CompositorLayerId, descriptor: CompositorLayerDescriptor) { + self.apply_layer_descriptor(id, descriptor); + } + + fn mark_layer_dirty(&mut self, _id: CompositorLayerId) {} + + fn notify_external_surface_ready(&mut self, id: ExternalSurfaceId) { + self.pending_frame_reasons + .push(FrameRequestReason::ExternalSurfaceReady(id)); + } + + fn request_frame(&mut self, reason: FrameRequestReason) { + self.pending_frame_reasons.push(reason); + } + + fn clear_layer_dirtiness(&mut self) {} + + fn update_timing(&mut self, timing: CompositorTiming) { + self.timing = timing; + } +} + +fn clip_shape_for_bounds(bounds: Rect, isolated: bool) -> Option { + if !isolated || !bounds.is_finite() { + return None; + } + + let width = bounds.width(); + let height = bounds.height(); + if width <= 0.0 || height <= 0.0 { + return None; + } + + // `subduction` currently exposes only per-layer clips, so use the layer's + // local bounds as an isolation clip when requested. + Some(ClipShape::RoundedRect(RoundedRect::from_rect(bounds.with_origin((0.0, 0.0)), 0.0))) +} + diff --git a/src/lib.rs b/src/lib.rs index 80720946c..aece88fac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,11 +223,14 @@ pub mod receiver_signal { pub use box_tree::{BoxTree, ElementId, ElementMeta, FocusNavMeta}; pub(crate) use box_tree::{bump_focus_nav_meta_revision, focus_nav_meta_revision}; +pub use compositor::backend::CompositorBackend; pub use compositor::{ Compositor, CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, ExternalAlphaMode, ExternalColorSpace, ExternalPixelFormat, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, }; +#[cfg(feature = "subduction")] +pub use compositor::subduction::SubductionCompositorBackend; pub use app::{AppConfig, AppEvent, Application, launch, quit_app, reopen}; pub use floem_reactive as reactive; diff --git a/src/window/state.rs b/src/window/state.rs index 2e3b31e2b..3f6ae3c06 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -5,6 +5,7 @@ use crate::{ compositor::{ Compositor, CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, + backend::CompositorBackend, }, inspector::CaptureState, platform::menu_types::MenuId, @@ -1294,6 +1295,14 @@ impl WindowState { self.compositor.request_frame(reason); } + pub fn install_compositor_backend(&mut self, backend: Box) { + self.compositor.install_backend(backend); + } + + pub fn compositor_backend_name(&self) -> Option<&'static str> { + self.compositor.backend_name() + } + pub const fn compositor_timing(&self) -> &CompositorTiming { self.compositor.timing() } From bfb81113658dfedcab38bfed61a593e3d84cc0ab Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 20:10:01 -0600 Subject: [PATCH 016/102] subduction wgpu --- Cargo.lock | 11 ++++++ Cargo.toml | 3 +- examples/widget-gallery/Cargo.toml | 2 +- src/app/handle.rs | 15 +++++--- src/compositor/backend.rs | 31 +++++++++++++++++ src/compositor/mod.rs | 56 ++++++++++++++++++++++++++++++ src/compositor/subduction.rs | 39 ++++++++++++++++++++- src/window/handle.rs | 50 +++++++++++++++++++++++++- src/window/state.rs | 30 ++++++++++++++++ 9 files changed, 228 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d98ec059..5ea1e5b45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,7 @@ dependencies = [ "smallvec", "strum", "strum_macros", + "subduction_backend_wgpu", "subduction_core", "sys-locale", "taffy", @@ -5727,6 +5728,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "subduction_backend_wgpu" +version = "0.0.1" +source = "git+https://github.com/forest-rs/subduction.git?rev=942a0d4d1a65f855c2a77aa10d01be132196d4bd#942a0d4d1a65f855c2a77aa10d01be132196d4bd" +dependencies = [ + "bytemuck", + "subduction_core", + "wgpu", +] + [[package]] name = "subduction_core" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 27713b514..415728a78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ floem_tiny_skia_renderer = { path = "tiny_skia", version = "0.2.0" } floem_reactive = { path = "reactive", version = "0.2.0" } floem-editor-core = { path = "editor-core", version = "0.2.0", optional = true } subduction_core = { git = "https://github.com/forest-rs/subduction.git", rev = "942a0d4d1a65f855c2a77aa10d01be132196d4bd", package = "subduction_core", optional = true, features = ["std"] } +subduction_backend_wgpu = { git = "https://github.com/forest-rs/subduction.git", rev = "942a0d4d1a65f855c2a77aa10d01be132196d4bd", package = "subduction_backend_wgpu", optional = true } copypasta = { version = "0.10", default-features = false, features = [ "wayland", "x11", @@ -195,7 +196,7 @@ tokio = ["dep:tokio"] # rfd (file dialog) async runtime crossbeam = ["dep:crossbeam", "floem_renderer/crossbeam"] localization = ["dep:fluent-bundle", "dep:unic-langid", "dep:sys-locale", "dep:ouroboros"] -subduction = ["dep:subduction_core"] +subduction = ["dep:subduction_core", "dep:subduction_backend_wgpu"] [dev-dependencies] criterion = { version = "0.8.1", features = ["html_reports"] } diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index c7698529f..406d4c073 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -floem = { path = "../..", features = ["skia"] } +floem = { path = "../..", features = ["skia", "subduction"] } strum = { workspace = true } files = { path = "../files/", optional = true } diff --git a/src/app/handle.rs b/src/app/handle.rs index 589947b3a..21b413e21 100644 --- a/src/app/handle.rs +++ b/src/app/handle.rs @@ -677,11 +677,16 @@ impl ApplicationHandle { if handle.window_state.has_pending_render() && handle.can_render_now() { handle.window.request_redraw(); let frame_interval = handle - .window - .current_monitor() - .and_then(|m| m.current_video_mode()) - .and_then(|v| v.refresh_rate_millihertz()) - .map(|mhz| Duration::from_nanos(1_000_000_000_000 / mhz.get() as u64)) + .window_state + .compositor_preferred_frame_interval() + .or_else(|| { + handle + .window + .current_monitor() + .and_then(|m| m.current_video_mode()) + .and_then(|v| v.refresh_rate_millihertz()) + .map(|mhz| Duration::from_nanos(1_000_000_000_000 / mhz.get() as u64)) + }) .unwrap_or(Duration::from_millis(8)); handle.render_frame_if_due(frame_interval); } diff --git a/src/compositor/backend.rs b/src/compositor/backend.rs index 4f967d362..7a0013657 100644 --- a/src/compositor/backend.rs +++ b/src/compositor/backend.rs @@ -5,6 +5,10 @@ //! retained compositor state into a concrete presentation engine such as //! `subduction`. +use std::time::{Duration, Instant}; + +use floem_renderer::gpu_resources::GpuResources; + use super::{ CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, @@ -52,4 +56,31 @@ pub trait CompositorBackend { /// Updates compositor timing after presentation has advanced. fn update_timing(&mut self, timing: CompositorTiming); + + /// Attaches shared wgpu resources so the backend can prepare any presenter- + /// side resources against Floem's device and queue. + fn attach_wgpu_presenter( + &mut self, + _gpu_resources: &GpuResources, + _output_format: wgpu::TextureFormat, + _output_size: (u32, u32), + ) { + } + + /// Called before Floem paints a frame. + fn begin_frame(&mut self, _output_size: (u32, u32), _started_at: Instant) {} + + /// Called after Floem completes and presents a frame. + fn finish_frame( + &mut self, + _output_size: (u32, u32), + _started_at: Instant, + _completed_at: Instant, + ) { + } + + /// Returns a backend-specific frame interval hint when available. + fn preferred_frame_interval(&self) -> Option { + None + } } diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index a7063f1bb..bbe7dd797 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -56,6 +56,7 @@ pub mod subduction; use std::time::{Duration, Instant}; +use floem_renderer::gpu_resources::GpuResources; use peniko::BlendMode; use peniko::kurbo::{Rect, Size}; use rustc_hash::FxHashMap; @@ -250,12 +251,33 @@ impl Compositor { self.backend = Some(backend); } + /// Attaches shared wgpu presentation resources to the installed backend. + pub fn attach_wgpu_presenter( + &mut self, + gpu_resources: &GpuResources, + output_format: wgpu::TextureFormat, + output_size: (u32, u32), + ) { + if let Some(backend) = self.backend.as_mut() { + backend.attach_wgpu_presenter(gpu_resources, output_format, output_size); + } + } + /// Returns the installed backend name, if any. #[must_use] pub fn backend_name(&self) -> Option<&'static str> { self.backend.as_ref().map(|backend| backend.name()) } + /// Returns the preferred compositor frame interval if a backend can provide one. + #[must_use] + pub fn preferred_frame_interval(&self) -> Option { + self.backend + .as_ref() + .and_then(|backend| backend.preferred_frame_interval()) + .or(self.timing.frame_interval) + } + /// Registers a new externally managed surface and returns its id. pub fn register_external_surface( &mut self, @@ -413,4 +435,38 @@ impl Compositor { backend.update_timing(timing); } } + + /// Marks the start of a compositor-driven frame. + pub fn begin_frame(&mut self, output_size: (u32, u32), started_at: Instant) { + let mut timing = self.timing; + timing.last_present_started = Some(started_at); + timing.predicted_next_present = timing.frame_interval.map(|interval| started_at + interval); + self.update_timing(timing); + + if let Some(backend) = self.backend.as_mut() { + backend.begin_frame(output_size, started_at); + } + } + + /// Marks the completion of a compositor-driven frame. + pub fn finish_frame( + &mut self, + output_size: (u32, u32), + started_at: Instant, + completed_at: Instant, + ) { + let mut timing = self.timing; + if let Some(last_completed) = timing.last_present_completed { + timing.frame_interval = Some(completed_at.saturating_duration_since(last_completed)); + } + timing.last_present_started = Some(started_at); + timing.last_present_completed = Some(completed_at); + timing.frame_number = timing.frame_number.saturating_add(1); + timing.predicted_next_present = timing.frame_interval.map(|interval| completed_at + interval); + self.update_timing(timing); + + if let Some(backend) = self.backend.as_mut() { + backend.finish_frame(output_size, started_at, completed_at); + } + } } diff --git a/src/compositor/subduction.rs b/src/compositor/subduction.rs index eb861ccf5..e96a8c66b 100644 --- a/src/compositor/subduction.rs +++ b/src/compositor/subduction.rs @@ -16,9 +16,11 @@ use peniko::kurbo::{Affine, Rect, RoundedRect}; use rustc_hash::FxHashMap; use subduction_core::{ + backend::Presenter as _, layer::{ClipShape, FrameChanges, LayerFlags, LayerId, LayerStore, SurfaceId}, transform::Transform3d, }; +use subduction_backend_wgpu::WgpuPresenter; use super::{ CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, @@ -40,6 +42,7 @@ pub struct SubductionCompositorBackend { pending_frame_reasons: Vec, timing: CompositorTiming, needs_reorder: bool, + presenter: Option, } impl Default for SubductionCompositorBackend { @@ -67,6 +70,7 @@ impl SubductionCompositorBackend { pending_frame_reasons: Vec::new(), timing: CompositorTiming::default(), needs_reorder: false, + presenter: None, } } @@ -237,6 +241,40 @@ impl CompositorBackend for SubductionCompositorBackend { fn update_timing(&mut self, timing: CompositorTiming) { self.timing = timing; } + + fn attach_wgpu_presenter( + &mut self, + gpu_resources: &floem_renderer::gpu_resources::GpuResources, + output_format: wgpu::TextureFormat, + output_size: (u32, u32), + ) { + let default_layer_size = ( + output_size.0.clamp(1, 2048), + output_size.1.clamp(1, 2048), + ); + self.presenter = Some(WgpuPresenter::new( + gpu_resources.device.clone(), + gpu_resources.queue.clone(), + output_format, + output_size, + default_layer_size, + )); + } + + fn begin_frame(&mut self, output_size: (u32, u32), _started_at: std::time::Instant) { + let has_presenter = self.presenter.is_some(); + if has_presenter { + let changes = self.evaluate(); + if let Some(presenter) = self.presenter.as_mut() { + presenter.resize_output(output_size.0, output_size.1); + presenter.apply(&self.store, &changes); + } + } + } + + fn preferred_frame_interval(&self) -> Option { + self.timing.frame_interval + } } fn clip_shape_for_bounds(bounds: Rect, isolated: bool) -> Option { @@ -254,4 +292,3 @@ fn clip_shape_for_bounds(bounds: Rect, isolated: bool) -> Option { // local bounds as an isolation clip when requested. Some(ClipShape::RoundedRect(RoundedRect::from_rect(bounds.with_origin((0.0, 0.0)), 0.0))) } - diff --git a/src/window/handle.rs b/src/window/handle.rs index b51072fa2..05e921ca3 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -383,6 +383,7 @@ impl WindowHandle { let size = LogicalSize::new(rect.width(), rect.height()); self.size(Size::new(size.width, size.height)); } + self.attach_compositor_presenter(); // Now that the renderer is initialized, draw the first frame self.render_frame(); self.window.set_visible(true); @@ -781,9 +782,19 @@ impl WindowHandle { pub(crate) fn render_frame(&mut self) { let renderer_ready = matches!(self.paint_state, PaintState::Initialized { .. }); if self.window_state.has_pending_render() && renderer_ready { + let output_size = renderer_output_size(self.paint_state.renderer().size()); + let frame_started_at = Instant::now(); + self.window_state + .begin_compositor_frame(output_size, frame_started_at); self.paint(); + let frame_completed_at = Instant::now(); + self.window_state.finish_compositor_frame( + output_size, + frame_started_at, + frame_completed_at, + ); self.window_state.clear_pending_damage(); - self.last_presented_at = Instant::now(); + self.last_presented_at = frame_completed_at; } if self.live_resize_active() { @@ -1464,6 +1475,22 @@ impl WindowHandle { self.window.request_redraw(); } + fn attach_compositor_presenter(&mut self) { + let Some(gpu_resources) = self.gpu_resources.as_ref() else { + return; + }; + + let Some(output_format) = compositor_output_format(gpu_resources, self.window.clone()) else { + return; + }; + + self.window_state.attach_compositor_wgpu_presenter( + gpu_resources, + output_format, + renderer_output_size(self.paint_state.renderer().size()), + ); + } + pub(crate) fn destroy(&mut self) { self.event(Event::Window(WindowEvent::Closed)); self.scope.dispose(); @@ -1667,6 +1694,27 @@ impl WindowHandle { } } +fn renderer_output_size(size: Size) -> (u32, u32) { + ( + size.width.max(1.0).round() as u32, + size.height.max(1.0).round() as u32, + ) +} + +fn compositor_output_format( + gpu_resources: &GpuResources, + window: Arc, +) -> Option { + let surface = gpu_resources.instance.create_surface(window).ok()?; + let caps = surface.get_capabilities(&gpu_resources.adapter); + caps.formats.into_iter().find(|format| { + matches!( + format, + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm + ) + }) +} + pub(crate) fn get_current_view() -> ViewId { CURRENT_RUNNING_VIEW_HANDLE .with(|running| *running.borrow()) diff --git a/src/window/state.rs b/src/window/state.rs index 3f6ae3c06..d785d1d40 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -1,4 +1,5 @@ use std::{cell::RefCell, collections::HashMap}; +use std::time::{Duration, Instant}; use crate::{ action::exec_after_animation_frame, @@ -13,6 +14,7 @@ use crate::{ view::ViewStorage, }; +use floem_renderer::gpu_resources::GpuResources; use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::{SmallVec, smallvec}; @@ -1299,10 +1301,24 @@ impl WindowState { self.compositor.install_backend(backend); } + pub fn attach_compositor_wgpu_presenter( + &mut self, + gpu_resources: &GpuResources, + output_format: wgpu::TextureFormat, + output_size: (u32, u32), + ) { + self.compositor + .attach_wgpu_presenter(gpu_resources, output_format, output_size); + } + pub fn compositor_backend_name(&self) -> Option<&'static str> { self.compositor.backend_name() } + pub fn compositor_preferred_frame_interval(&self) -> Option { + self.compositor.preferred_frame_interval() + } + pub const fn compositor_timing(&self) -> &CompositorTiming { self.compositor.timing() } @@ -1341,6 +1357,20 @@ impl WindowState { self.compositor.clear_layer_dirtiness(); } + pub fn begin_compositor_frame(&mut self, output_size: (u32, u32), started_at: Instant) { + self.compositor.begin_frame(output_size, started_at); + } + + pub fn finish_compositor_frame( + &mut self, + output_size: (u32, u32), + started_at: Instant, + completed_at: Instant, + ) { + self.compositor + .finish_frame(output_size, started_at, completed_at); + } + pub(crate) fn update_screen_size_bp(&mut self, size: Size) { let bp = self.grid_bps.get_width_bp(size.width); if bp != self.screen_size_bp { From 4a1e59bc9d0289e5fac96c7bb63583b4c2744ded Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 20:17:55 -0600 Subject: [PATCH 017/102] raster + composite split working great --- examples/widget-gallery/Cargo.toml | 2 +- src/box_tree.rs | 25 ++- src/compositor/backend.rs | 19 ++ src/compositor/mod.rs | 198 ++++++++++++++++++- src/compositor/subduction.rs | 96 +++++++-- src/paint/display_list.rs | 102 +++++++++- src/paint/mod.rs | 90 +++++++-- src/paint/renderer.rs | 42 ++++ src/view/id.rs | 15 +- src/views/scroll.rs | 5 +- src/window/handle.rs | 213 ++++++++++++++++++++ src/window/state.rs | 185 ++++++++++++++++- vello/src/lib.rs | 307 ++++++++++++++++++++++++++--- 13 files changed, 1223 insertions(+), 76 deletions(-) diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index 406d4c073..20feb9321 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -floem = { path = "../..", features = ["skia", "subduction"] } +floem = { path = "../..", features = ["vello", "subduction"] } strum = { workspace = true } files = { path = "../files/", optional = true } diff --git a/src/box_tree.rs b/src/box_tree.rs index ea42fb393..988ca57e6 100644 --- a/src/box_tree.rs +++ b/src/box_tree.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashMap; use crate::ViewId; use crate::action::add_update_message; use crate::message::UpdateMessage; -use crate::paint::display_list::TransformClass; +use crate::paint::display_list::{CompositorPromotionHint, TransformClass}; use crate::view::VIEW_STORAGE; /// A visual identifier that represents a rectangle in the box tree. @@ -198,6 +198,7 @@ pub struct ElementMeta { pub element_id: ElementId, pub focus: FocusNavMeta, pub(crate) retained_transform_boundary: Option, + pub(crate) compositor_promotion_hint: Option, } impl ElementMeta { @@ -215,6 +216,7 @@ impl ElementMeta { enabled: true, }, retained_transform_boundary: None, + compositor_promotion_hint: None, } } } @@ -300,6 +302,27 @@ impl BoxTree { true } + pub(crate) fn compositor_promotion_hint( + &self, + id: understory_box_tree::NodeId, + ) -> Option { + self.element_meta(id) + .and_then(|meta| meta.compositor_promotion_hint) + } + + pub(crate) fn set_compositor_promotion_hint( + &mut self, + id: understory_box_tree::NodeId, + hint: Option, + ) -> bool { + let Some(mut meta) = self.element_meta(id) else { + return false; + }; + meta.compositor_promotion_hint = hint; + self.metadata.insert(id, meta); + true + } + /// Returns `true` when this node can receive focus by any means. #[inline] pub fn is_focusable(&self, id: understory_box_tree::NodeId) -> bool { diff --git a/src/compositor/backend.rs b/src/compositor/backend.rs index 7a0013657..991b2d350 100644 --- a/src/compositor/backend.rs +++ b/src/compositor/backend.rs @@ -8,12 +8,16 @@ use std::time::{Duration, Instant}; use floem_renderer::gpu_resources::GpuResources; +use peniko::kurbo::Rect; use super::{ CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, }; +pub type FloemPaintedSurfaceVisitor<'a> = + dyn FnMut(CompositorLayerId, Rect, (u32, u32), &wgpu::TextureView) + 'a; + /// Backend interface for mirroring Floem compositor state into a concrete /// presentation/composition engine. pub trait CompositorBackend { @@ -79,8 +83,23 @@ pub trait CompositorBackend { ) { } + /// Applies retained layer updates so compositor-owned Floem surfaces exist + /// before the paint phase tries to rasterize into them. + fn prepare_floem_surfaces(&mut self, _output_size: (u32, u32)) {} + /// Returns a backend-specific frame interval hint when available. fn preferred_frame_interval(&self) -> Option { None } + + /// Iterates compositor-owned surfaces that expect Floem to rasterize content + /// into them before final composition. + fn for_each_floem_painted_surface( + &self, + _visit: &mut FloemPaintedSurfaceVisitor<'_>, + ) { + } + + /// Composites the current backend layer tree into the provided output view. + fn composite_to_output(&mut self, _output: &wgpu::TextureView) {} } diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index bbe7dd797..8a9086078 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -61,7 +61,8 @@ use peniko::BlendMode; use peniko::kurbo::{Rect, Size}; use rustc_hash::FxHashMap; -use self::backend::CompositorBackend; +use self::backend::{CompositorBackend, FloemPaintedSurfaceVisitor}; +use crate::ElementId; /// Stable identifier for a compositor layer. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -108,6 +109,18 @@ pub enum ExternalAlphaMode { Premultiplied, } +/// Alpha handling for compositor layer contents. +/// +/// This is separate from [`ExternalAlphaMode`] because compositor layers are the +/// unit the backend actually composites. A Floem-painted layer may be straight- +/// alpha even when an external producer uses a different convention. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CompositorAlphaMode { + #[default] + Premultiplied, + Straight, +} + /// Color-space hint for an external surface. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExternalColorSpace { @@ -164,7 +177,9 @@ pub struct CompositorLayerDescriptor { pub kind: CompositorLayerKind, pub bounds: Rect, pub z_index: i32, + pub compositing_depth: u32, pub opacity: f32, + pub alpha_mode: CompositorAlphaMode, pub blend_mode: BlendMode, pub isolated: bool, } @@ -203,6 +218,27 @@ pub struct CompositorLayerState { pub dirty: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FloemPaintedSurfaceRole { + Root, + Overlay, + Promoted(ElementId), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct ResolvedPromotedLayer { + pub element_id: ElementId, + pub bounds: Rect, + pub raster_clip: Option, + pub z_index: i32, + pub compositing_depth: u32, + pub isolated: bool, + pub alpha_mode: CompositorAlphaMode, +} + +pub(crate) type FloemSurfaceRoleVisitor<'a> = + dyn FnMut(FloemPaintedSurfaceRole, Rect, (u32, u32), &wgpu::TextureView) + 'a; + /// Retained compositor registry and frame-request state. #[derive(Default)] pub struct Compositor { @@ -210,6 +246,10 @@ pub struct Compositor { next_surface_id: u64, layers: FxHashMap, external_surfaces: FxHashMap, + promoted_layers: FxHashMap, + promoted_layer_raster_clips: FxHashMap, + root_layer: Option, + overlay_layer: Option, pending_frame_reasons: Vec, timing: CompositorTiming, backend: Option>, @@ -222,6 +262,10 @@ impl std::fmt::Debug for Compositor { .field("next_surface_id", &self.next_surface_id) .field("layers", &self.layers) .field("external_surfaces", &self.external_surfaces) + .field("promoted_layers", &self.promoted_layers) + .field("promoted_layer_raster_clips", &self.promoted_layer_raster_clips) + .field("root_layer", &self.root_layer) + .field("overlay_layer", &self.overlay_layer) .field("pending_frame_reasons", &self.pending_frame_reasons) .field("timing", &self.timing) .field("backend_name", &self.backend_name()) @@ -263,6 +307,146 @@ impl Compositor { } } + /// Synchronizes compositor-owned Floem layers from retained display-list + /// layer candidates. + pub(crate) fn sync_promoted_layers(&mut self, promoted_layers: &[ResolvedPromotedLayer]) { + let mut active = FxHashMap::default(); + self.promoted_layer_raster_clips.clear(); + + for promoted in promoted_layers { + let descriptor = CompositorLayerDescriptor { + kind: CompositorLayerKind::FloemPainted, + bounds: promoted.bounds, + z_index: promoted.z_index, + compositing_depth: promoted.compositing_depth, + opacity: 1.0, + alpha_mode: promoted.alpha_mode, + blend_mode: BlendMode::default(), + isolated: promoted.isolated, + }; + + let layer_id = if let Some(layer_id) = self.promoted_layers.get(&promoted.element_id).copied() { + let _ = self.update_layer(layer_id, descriptor); + layer_id + } else { + let layer_id = self.register_layer(descriptor); + self.promoted_layers.insert(promoted.element_id, layer_id); + layer_id + }; + + active.insert(promoted.element_id, layer_id); + if let Some(raster_clip) = promoted.raster_clip { + self.promoted_layer_raster_clips + .insert(promoted.element_id, raster_clip); + } + } + + let stale: Vec<_> = self + .promoted_layers + .iter() + .filter_map(|(&element_id, &layer_id)| (!active.contains_key(&element_id)).then_some((element_id, layer_id))) + .collect(); + + for (element_id, layer_id) in stale { + let _ = self.update_layer( + layer_id, + CompositorLayerDescriptor { + kind: CompositorLayerKind::FloemPainted, + bounds: Rect::ZERO, + z_index: 0, + compositing_depth: 0, + opacity: 0.0, + alpha_mode: CompositorAlphaMode::Straight, + blend_mode: BlendMode::default(), + isolated: false, + }, + ); + self.promoted_layers.remove(&element_id); + } + } + + pub(crate) fn ensure_root_layer(&mut self, bounds: Rect) -> CompositorLayerId { + let descriptor = CompositorLayerDescriptor { + kind: CompositorLayerKind::FloemPainted, + bounds, + z_index: i32::MIN, + compositing_depth: 0, + opacity: 1.0, + alpha_mode: CompositorAlphaMode::Straight, + blend_mode: BlendMode::default(), + isolated: false, + }; + + if let Some(layer_id) = self.root_layer { + let _ = self.update_layer(layer_id, descriptor); + layer_id + } else { + let layer_id = self.register_layer(descriptor); + self.root_layer = Some(layer_id); + layer_id + } + } + + pub(crate) fn ensure_overlay_layer(&mut self, bounds: Rect) -> CompositorLayerId { + let descriptor = CompositorLayerDescriptor { + kind: CompositorLayerKind::FloemPainted, + bounds, + z_index: i32::MAX, + compositing_depth: 0, + opacity: 1.0, + alpha_mode: CompositorAlphaMode::Straight, + blend_mode: BlendMode::default(), + isolated: false, + }; + + if let Some(layer_id) = self.overlay_layer { + let _ = self.update_layer(layer_id, descriptor); + layer_id + } else { + let layer_id = self.register_layer(descriptor); + self.overlay_layer = Some(layer_id); + layer_id + } + } + + pub(crate) fn for_each_floem_painted_surface( + &self, + visit: &mut FloemSurfaceRoleVisitor<'_>, + ) { + let Some(backend) = self.backend.as_ref() else { + return; + }; + + let mut visit_backend: &mut FloemPaintedSurfaceVisitor<'_> = + &mut |layer_id, bounds, size, view| { + let role = if self.root_layer == Some(layer_id) { + Some(FloemPaintedSurfaceRole::Root) + } else if self.overlay_layer == Some(layer_id) { + Some(FloemPaintedSurfaceRole::Overlay) + } else { + self.promoted_layers + .iter() + .find_map(|(&element_id, &promoted_layer_id)| { + (promoted_layer_id == layer_id) + .then_some(FloemPaintedSurfaceRole::Promoted(element_id)) + }) + }; + + if let Some(role) = role { + visit(role, bounds, size, view); + } + }; + backend.for_each_floem_painted_surface(&mut visit_backend); + } + + pub(crate) fn promoted_layer_ids(&self) -> Vec { + self.promoted_layers.keys().copied().collect() + } + + pub(crate) fn promoted_layer_raster_clip(&self, element_id: ElementId) -> Option { + self.promoted_layer_raster_clips.get(&element_id).copied() + } + /// Returns the installed backend name, if any. #[must_use] pub fn backend_name(&self) -> Option<&'static str> { @@ -448,6 +632,12 @@ impl Compositor { } } + pub(crate) fn prepare_floem_surfaces(&mut self, output_size: (u32, u32)) { + if let Some(backend) = self.backend.as_mut() { + backend.prepare_floem_surfaces(output_size); + } + } + /// Marks the completion of a compositor-driven frame. pub fn finish_frame( &mut self, @@ -469,4 +659,10 @@ impl Compositor { backend.finish_frame(output_size, started_at, completed_at); } } + + pub(crate) fn composite_to_output(&mut self, output: &wgpu::TextureView) { + if let Some(backend) = self.backend.as_mut() { + backend.composite_to_output(output); + } + } } diff --git a/src/compositor/subduction.rs b/src/compositor/subduction.rs index e96a8c66b..cc40e44f4 100644 --- a/src/compositor/subduction.rs +++ b/src/compositor/subduction.rs @@ -23,9 +23,9 @@ use subduction_core::{ use subduction_backend_wgpu::WgpuPresenter; use super::{ - CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, + CompositorAlphaMode, CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, - backend::CompositorBackend, + backend::{CompositorBackend, FloemPaintedSurfaceVisitor}, }; /// `subduction`-backed compositor mirror. @@ -35,6 +35,7 @@ pub struct SubductionCompositorBackend { root_layer: LayerId, layer_ids: FxHashMap, layer_descriptors: FxHashMap, + floem_surface_ids: FxHashMap, surface_ids: FxHashMap, surface_descriptors: FxHashMap, surface_handles: FxHashMap, @@ -63,6 +64,7 @@ impl SubductionCompositorBackend { root_layer, layer_ids: FxHashMap::default(), layer_descriptors: FxHashMap::default(), + floem_surface_ids: FxHashMap::default(), surface_ids: FxHashMap::default(), surface_descriptors: FxHashMap::default(), surface_handles: FxHashMap::default(), @@ -128,34 +130,69 @@ impl SubductionCompositorBackend { // Floem layer bounds are rects; `subduction` splits that into a // translation (rect origin) and a local size (rect dimensions). let bounds = descriptor.bounds; + let is_empty = !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0; self.store .set_transform(layer_id, Transform3d::from(Affine::translate(bounds.origin().to_vec2()))); - self.store.set_bounds(layer_id, bounds.size()); + self.store.set_bounds( + layer_id, + if is_empty { + (1.0, 1.0).into() + } else { + bounds.size() + }, + ); self.store.set_opacity(layer_id, descriptor.opacity); self.store .set_clip(layer_id, clip_shape_for_bounds(bounds, descriptor.isolated)); self.store - .set_flags(layer_id, LayerFlags { hidden: descriptor.opacity <= 0.0 }); - self.store - .set_content(layer_id, self.content_for_kind(descriptor.kind)); + .set_flags(layer_id, LayerFlags { hidden: descriptor.opacity <= 0.0 || is_empty }); + let content = self.content_for_kind(id, descriptor.kind); + self.store.set_content(layer_id, content); + if matches!(descriptor.alpha_mode, CompositorAlphaMode::Straight) { + // The current `subduction_backend_wgpu` presenter composites all layers as + // premultiplied-alpha textures. Retaining the alpha mode here lets Floem + // describe the true contract now so backend work can make composition + // alpha-mode-aware instead of relying on this implicit assumption. + } self.needs_reorder = true; } - fn content_for_kind(&self, kind: CompositorLayerKind) -> Option { + fn content_for_kind(&mut self, id: CompositorLayerId, kind: CompositorLayerKind) -> Option { match kind { - CompositorLayerKind::FloemPainted => None, + CompositorLayerKind::FloemPainted => Some(self.floem_surface_id_for(id)), CompositorLayerKind::ExternalSurface { surface_id } | CompositorLayerKind::Mixed { surface_id } => self.surface_ids.get(&surface_id).copied(), } } + fn floem_surface_id_for(&mut self, id: CompositorLayerId) -> SurfaceId { + if let Some(surface_id) = self.floem_surface_ids.get(&id).copied() { + return surface_id; + } + + let surface_id = SurfaceId(self.next_surface_id); + self.next_surface_id += 1; + self.floem_surface_ids.insert(id, surface_id); + surface_id + } + fn sync_order_if_needed(&mut self) { if !self.needs_reorder { return; } - let mut ordered_layers: Vec<_> = self.layer_descriptors.iter().map(|(&id, &descriptor)| (id, descriptor)).collect(); - ordered_layers.sort_by_key(|(id, descriptor)| (descriptor.z_index, id.as_u64())); + let mut ordered_layers: Vec<_> = self + .layer_descriptors + .iter() + .map(|(&id, &descriptor)| (id, descriptor)) + .collect(); + ordered_layers.sort_by_key(|(id, descriptor)| { + ( + descriptor.z_index, + descriptor.compositing_depth, + id.as_u64(), + ) + }); for (id, _) in ordered_layers { let Some(layer_id) = self.layer_ids.get(&id).copied() else { @@ -262,6 +299,14 @@ impl CompositorBackend for SubductionCompositorBackend { } fn begin_frame(&mut self, output_size: (u32, u32), _started_at: std::time::Instant) { + self.prepare_floem_surfaces(output_size); + } + + fn preferred_frame_interval(&self) -> Option { + self.timing.frame_interval + } + + fn prepare_floem_surfaces(&mut self, output_size: (u32, u32)) { let has_presenter = self.presenter.is_some(); if has_presenter { let changes = self.evaluate(); @@ -272,8 +317,35 @@ impl CompositorBackend for SubductionCompositorBackend { } } - fn preferred_frame_interval(&self) -> Option { - self.timing.frame_interval + fn for_each_floem_painted_surface( + &self, + visit: &mut FloemPaintedSurfaceVisitor<'_>, + ) { + let Some(presenter) = self.presenter.as_ref() else { + return; + }; + + for (&layer_id, &surface_id) in &self.floem_surface_ids { + let Some(descriptor) = self.layer_descriptors.get(&layer_id).copied() else { + continue; + }; + let Some(view) = presenter.texture_for_surface(surface_id) else { + continue; + }; + let size = ( + descriptor.bounds.width().max(1.0).round() as u32, + descriptor.bounds.height().max(1.0).round() as u32, + ); + visit(layer_id, descriptor.bounds, size, view); + } + } + + fn composite_to_output(&mut self, output: &wgpu::TextureView) { + let Some(presenter) = self.presenter.as_mut() else { + return; + }; + let command_buffer = presenter.composite(&self.store, output); + presenter.queue().submit([command_buffer]); } } diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index d91053314..d5463b812 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -194,6 +194,18 @@ impl TransformClass { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum CompositorPromotionHint { + ScrollContent, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct CompositorLayerCandidate { + pub promotion_hint: CompositorPromotionHint, + pub bounds: Rect, + pub clip: Option, +} + pub(crate) fn transform_diff_class(original: Affine, current: Affine) -> TransformClass { if current == original { return TransformClass::Exact; @@ -260,6 +272,7 @@ pub(crate) struct ElementStage { pub chunks: Vec, pub property_tree: PaintPropertyTree, pub transform_class: TransformClass, + pub layer_candidate: Option, } impl Default for ElementStage { @@ -268,15 +281,21 @@ impl Default for ElementStage { chunks: Vec::new(), property_tree: PaintPropertyTree::default(), transform_class: TransformClass::Affine, + layer_candidate: None, } } } impl ElementStage { - pub(crate) fn set_commands(&mut self, commands: Vec) { + pub(crate) fn set_commands( + &mut self, + commands: Vec, + layer_candidate: Option, + ) { let (chunks, property_tree) = chunk_display_commands(commands); self.chunks = chunks; self.property_tree = property_tree; + self.layer_candidate = layer_candidate.clone(); self.transform_class = if self.chunks.is_empty() { TransformClass::Affine } else { @@ -285,6 +304,12 @@ impl ElementStage { .map(|chunk| chunk.transform_class) .fold(TransformClass::Exact, TransformClass::combine) }; + + if let Some(layer_candidate) = layer_candidate { + for chunk in &mut self.chunks { + chunk.metadata.promotion_hint = Some(layer_candidate.promotion_hint); + } + } } #[allow(dead_code)] @@ -415,6 +440,7 @@ pub(crate) struct PaintChunkMetadata { pub has_vector_image: bool, pub has_blur: bool, pub requires_layer: bool, + pub promotion_hint: Option, } impl PaintChunkMetadata { @@ -425,15 +451,17 @@ impl PaintChunkMetadata { has_vector_image: self.has_vector_image || other.has_vector_image, has_blur: self.has_blur || other.has_blur, requires_layer: self.requires_layer || other.requires_layer, + promotion_hint: self.promotion_hint.or(other.promotion_hint), } } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct ElementSnapshot { pub local_bounds: Rect, pub clip: Option, pub world_transform: Affine, + pub promotion_hint: Option, } impl ElementSnapshot { @@ -442,11 +470,22 @@ impl ElementSnapshot { local_bounds: box_tree.local_bounds(element_id.0).unwrap_or_default(), clip: box_tree.local_clip(element_id.0).flatten(), world_transform: box_tree.world_transform(element_id.0).unwrap_or_default(), + promotion_hint: box_tree.compositor_promotion_hint(element_id.0), } } pub(crate) fn supports_reuse(self, current: Self) -> bool { - self.local_bounds == current.local_bounds && self.clip == current.clip + self.local_bounds == current.local_bounds + && self.clip == current.clip + && self.promotion_hint == current.promotion_hint + } + + pub(crate) fn layer_candidate(self) -> Option { + Some(CompositorLayerCandidate { + promotion_hint: self.promotion_hint?, + bounds: self.local_bounds, + clip: self.clip, + }) } } @@ -477,6 +516,14 @@ pub(crate) struct ElementDisplayList { pub snapshot: Option, } +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct PromotedLayerCandidate { + pub element_id: ElementId, + pub candidate: CompositorLayerCandidate, + pub snapshot: ElementSnapshot, + pub z_index: i32, +} + #[derive(Default)] pub struct RetainedDisplayList { paint_order: Vec, @@ -500,7 +547,7 @@ impl RetainedDisplayList { self.elements.get(&id) } - pub(crate) fn needs_rerecord(&self, id: ElementId, snapshot: ElementSnapshot) -> bool { + pub(crate) fn needs_stage_rerecord(&self, id: ElementId, snapshot: ElementSnapshot) -> bool { let Some(element) = self.element(id) else { return true; }; @@ -520,6 +567,33 @@ impl RetainedDisplayList { pub(crate) fn retain_only(&mut self, ids: &FxHashSet) { self.elements.retain(|id, _| ids.contains(id)); } + + pub(crate) fn promoted_layer_candidates(&self) -> Vec { + self.elements + .iter() + .filter_map(|(&element_id, element)| { + let snapshot = element.snapshot?; + let candidate = element + .paint + .layer_candidate + .clone() + .or_else(|| snapshot.layer_candidate())?; + let z_index = element + .paint + .chunks + .first() + .map(|chunk| chunk.properties.z_index) + .unwrap_or_default(); + + Some(PromotedLayerCandidate { + element_id, + candidate, + snapshot, + z_index, + }) + }) + .collect() + } } pub(crate) struct RecordingRenderer<'a> { @@ -1416,7 +1490,11 @@ fn constrain_infinite_rounded_rect( RoundedRect::from_rect(constrained, rect.radii()) } -fn constrain_infinite_rect(rect: Rect, transform: Affine, render_size: Size) -> Rect { +pub(crate) fn constrain_rect_to_render_bounds( + rect: Rect, + transform: Affine, + render_size: Size, +) -> Rect { if rect.x0.is_finite() && rect.x1.is_finite() && rect.y0.is_finite() && rect.y1.is_finite() { return rect; } @@ -1449,6 +1527,10 @@ fn constrain_infinite_rect(rect: Rect, transform: Affine, render_size: Size) -> ) } +fn constrain_infinite_rect(rect: Rect, transform: Affine, render_size: Size) -> Rect { + constrain_rect_to_render_bounds(rect, transform, render_size) +} + #[cfg(test)] #[allow(clippy::items_after_test_module)] mod tests { @@ -1483,7 +1565,7 @@ mod tests { }, hint: Some(ShapeHint::Rect(rect)), }, - ]); + ], None); assert_eq!(stage.chunks.len(), 1); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); @@ -1517,7 +1599,7 @@ mod tests { hint: Some(ShapeHint::Rect(rect)), }, DisplayCommand::PopClip, - ]); + ], None); assert_eq!(stage.chunks.len(), 1); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); @@ -1552,7 +1634,7 @@ mod tests { }, hint: Some(ShapeHint::Rect(rect)), }, - ]); + ], None); assert_eq!(stage.chunks.len(), 2); assert_ne!( @@ -1575,7 +1657,7 @@ mod tests { composite: Composite::default(), }), hint: None, - }]); + }], None); assert_eq!(stage.transform_class, TransformClass::TranslateOnly); assert!(stage.chunks[0].metadata.has_blur); @@ -1610,7 +1692,7 @@ mod tests { rect: Rect::new(40.0, 40.0, 50.0, 50.0), transform: Affine::IDENTITY, }, - ]); + ], None); let damage = [Rect::new(1.0, 1.0, 5.0, 5.0)]; let chunks = stage.chunk_indices_for_damage(&damage); diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 20e1a0dcb..a0a419738 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -14,7 +14,7 @@ pub use renderer::Renderer; use floem_renderer::Renderer as _; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; -use peniko::kurbo::{Affine, RoundedRect, Shape, Size}; +use peniko::kurbo::{Affine, Point, RoundedRect, Shape, Size}; use rustc_hash::FxHashSet; use std::sync::Arc; use understory_box_tree::NodeFlags; @@ -221,7 +221,12 @@ pub(crate) fn collect_visual_order( } impl GlobalPaintCx<'_> { - fn replay_element_overflow_clip(&mut self, element_id: ElementId) { + fn replay_element_overflow_clip( + &mut self, + element_id: ElementId, + target_origin: Point, + render_size: Option, + ) { let box_tree = self.window_state.box_tree.borrow(); let Some(clip) = box_tree.local_clip(element_id.0).flatten() else { return; @@ -230,8 +235,9 @@ impl GlobalPaintCx<'_> { let base_transform = self .element_base_transform(element_id) - .then_scale(self.window_state.effective_scale()); - let render_size = self.paint_state.renderer().size(); + .then_scale(self.window_state.effective_scale()) + .then_translate(-target_origin.to_vec2()); + let render_size = render_size.unwrap_or_else(|| self.paint_state.renderer().size()); let renderer = self.paint_state.renderer_mut(); // Overflow clip must be replayed at traversal time rather than recorded into the @@ -332,6 +338,11 @@ impl GlobalPaintCx<'_> { } /// Paint entire tree using explicit traversal pub(crate) fn paint_with_traversal(&mut self, root_id: ViewId) { + let replay_order = self.prepare_display_list(root_id); + self.replay_display_list(&replay_order, None, Point::ZERO, None); + } + + pub(crate) fn prepare_display_list(&mut self, root_id: ViewId) -> Vec { let root_element_id = root_id.get_element_id(); let mut box_tree = self.window_state.box_tree.borrow_mut(); let paint_order = self.build_paint_order(root_element_id, &mut box_tree); @@ -361,20 +372,34 @@ impl GlobalPaintCx<'_> { .map(|element_id| (element_id, ElementSnapshot::from_box_tree(&box_tree, element_id))) .collect::>() }; + let mut reused_snapshots = Vec::new(); - for (element_id, snapshot) in snapshots { + for &(element_id, snapshot) in &snapshots { if reusable_descendants.contains(&element_id) { continue; } if self .window_state .display_list - .needs_rerecord(element_id, snapshot) + .needs_stage_rerecord(element_id, snapshot) { dirty_ids.insert(element_id); + } else { + reused_snapshots.push((element_id, snapshot)); } } let rerecord_ids = dirty_ids.len(); + for (element_id, snapshot) in reused_snapshots { + if let Some(element) = self.window_state.display_list.element(element_id) + && element.snapshot != Some(snapshot) + { + // Retained commands can be reused across pure transform/clip changes, but + // downstream systems like promoted compositor bounds still need the current + // snapshot every frame. Keep the artifact metadata fresh even when we skip + // rerecording the stage commands. + self.window_state.display_list.element_mut(element_id).snapshot = Some(snapshot); + } + } for element_id in dirty_ids { self.record_visual_node(element_id, false); self.record_visual_node(element_id, true); @@ -388,7 +413,24 @@ impl GlobalPaintCx<'_> { rerecord_ids, replay_steps: replay_order.len(), }; - for id_or_pop in replay_order { + replay_order + } + + pub(crate) fn replay_display_list( + &mut self, + replay_order: &[PaintOrPost], + included_ids: Option<&FxHashSet>, + target_origin: Point, + render_size: Option, + ) { + for &id_or_pop in replay_order { + let element_id = match id_or_pop { + PaintOrPost::Paint(id) | PaintOrPost::Post(id) => id, + }; + if included_ids.is_some_and(|ids| !ids.contains(&element_id)) { + continue; + } + match id_or_pop { PaintOrPost::Paint(element_id) => { // Record for testing @@ -396,15 +438,19 @@ impl GlobalPaintCx<'_> { record_paint(element_id.owning_id()); } if element_id.is_view() { - self.replay_element_overflow_clip(element_id); + self.replay_element_overflow_clip( + element_id, + target_origin, + render_size, + ); } // Damage-aware chunk replay is intentionally disabled here for now. // Every backend rebuilds a fresh frame/scene on `begin()`, so skipping // undamaged chunks would drop previously visible content and cause flashing. - self.replay_visual_node(element_id, false, None); + self.replay_visual_node(element_id, false, None, target_origin, render_size); } PaintOrPost::Post(element_id) => { - self.replay_visual_node(element_id, true, None); + self.replay_visual_node(element_id, true, None, target_origin, render_size); if element_id.is_view() { let has_clip = self .window_state @@ -422,6 +468,21 @@ impl GlobalPaintCx<'_> { } } + pub(crate) fn collect_subtree_elements(&self, root: ElementId) -> FxHashSet { + let box_tree = self.window_state.box_tree.borrow(); + let mut elements = FxHashSet::default(); + let mut stack = vec![root.0]; + + while let Some(node_id) = stack.pop() { + if let Some(element_id) = box_tree.element_id_of(node_id) { + elements.insert(element_id); + } + stack.extend(box_tree.children_of(node_id).iter().copied()); + } + + elements + } + fn element_base_transform(&mut self, element_id: ElementId) -> Affine { // Get state from box tree for this visual node let box_tree = self.window_state.box_tree.borrow(); @@ -433,10 +494,13 @@ impl GlobalPaintCx<'_> { element_id: ElementId, is_post: bool, damage_rects: Option<&[peniko::kurbo::Rect]>, + target_origin: Point, + render_size: Option, ) { let base_transform = self .element_base_transform(element_id) - .then_scale(self.window_state.effective_scale()); + .then_scale(self.window_state.effective_scale()) + .then_translate(-target_origin.to_vec2()); let Some(element) = self.window_state.display_list.element(element_id) else { return; }; @@ -451,7 +515,7 @@ impl GlobalPaintCx<'_> { .map(|rect| inverse.transform_rect_bbox(*rect)) .collect::>() }); - let render_size = self.paint_state.renderer().size(); + let render_size = render_size.unwrap_or_else(|| self.paint_state.renderer().size()); let renderer = self.paint_state.renderer_mut(); replay_stage( stage, @@ -522,7 +586,7 @@ impl GlobalPaintCx<'_> { } else { &mut element.paint }; - stage.set_commands(commands); + stage.set_commands(commands, snapshot.layer_candidate()); element.snapshot = Some(snapshot); } } diff --git a/src/paint/renderer.rs b/src/paint/renderer.rs index ac55e5617..9716e6bb5 100644 --- a/src/paint/renderer.rs +++ b/src/paint/renderer.rs @@ -256,6 +256,48 @@ impl Renderer { Self::Uninitialized { .. } => "Uninitialized".to_string(), } } + + #[cfg(all(feature = "vello", not(feature = "skia")))] + pub(crate) fn render_scene_to_premultiplied_texture_view( + &mut self, + target_view: &wgpu::TextureView, + size: (u32, u32), + ) -> bool { + match self { + Renderer::Active(r) => r + .render_scene_to_premultiplied_texture_view(target_view, size.0, size.1) + .is_ok(), + Renderer::TinySkia(_) | Renderer::Uninitialized { .. } => false, + } + } + + #[cfg(not(all(feature = "vello", not(feature = "skia"))))] + pub(crate) fn render_scene_to_premultiplied_texture_view( + &mut self, + _target_view: &wgpu::TextureView, + _size: (u32, u32), + ) -> bool { + false + } + + #[cfg(all(feature = "vello", not(feature = "skia")))] + pub(crate) fn present_composited_output( + &mut self, + composite: impl FnOnce(&wgpu::TextureView), + ) -> bool { + match self { + Renderer::Active(r) => r.present_composited_output(composite), + Renderer::TinySkia(_) | Renderer::Uninitialized { .. } => false, + } + } + + #[cfg(not(all(feature = "vello", not(feature = "skia"))))] + pub(crate) fn present_composited_output( + &mut self, + _composite: impl FnOnce(&wgpu::TextureView), + ) -> bool { + false + } } impl floem_renderer::Renderer for Renderer { diff --git a/src/view/id.rs b/src/view/id.rs index ebf17344b..8475a0576 100644 --- a/src/view/id.rs +++ b/src/view/id.rs @@ -24,7 +24,7 @@ thread_local! { use crate::context::EventCallbackConfig; use crate::event::listener::EventListenerKey; use crate::event::{RouteKind, listener}; -use crate::paint::display_list::TransformClass; +use crate::paint::display_list::{CompositorPromotionHint, TransformClass}; use crate::style::recalc::StyleReason; use crate::view::LayoutTree; use crate::window::handle::get_current_view; @@ -1335,6 +1335,19 @@ impl ViewId { .set_retained_transform_boundary(element_id.0, boundary) } + /// Mark an owned element with compositor-promotion intent that should flow + /// into retained paint artifacts. + pub(crate) fn set_compositor_promotion_hint_for_element( + &self, + element_id: ElementId, + hint: Option, + ) -> bool { + let box_tree = self.box_tree(); + box_tree + .borrow_mut() + .set_compositor_promotion_hint(element_id.0, hint) + } + /// Set whether an owned element can receive focus at all. /// /// Clearing focusability also clears keyboard navigation. diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 49e20a5fd..223aebb7c 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -585,6 +585,10 @@ impl Scroll { child_id.get_element_id(), Some(crate::paint::display_list::TransformClass::TranslateOnly), ); + id.set_compositor_promotion_hint_for_element( + child_id.get_element_id(), + Some(crate::paint::display_list::CompositorPromotionHint::ScrollContent), + ); // we need to first set the clip rect to zero so that virtual items don't set a large initial size id.set_box_tree_clip(Some(RoundedRect::from_rect(Rect::ZERO, 0.))); @@ -1073,7 +1077,6 @@ impl View for Scroll { // this apply scroll delta of zero is cheap. // it is here in the case that the available delta changed, this will catch it and update it to a better size self.apply_scroll_delta(Vec2::ZERO); - // Check which visual node we're painting // Scroll view creates multiple visual IDs for scrollbars/tracks if cx.target_id == self.id.get_element_id() { diff --git a/src/window/handle.rs b/src/window/handle.rs index 05e921ca3..37982d7c4 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -26,8 +26,10 @@ use winit::window::{ use floem_reactive::{RwSignal, Scope, SignalGet, SignalUpdate}; use floem_renderer::Renderer; use floem_renderer::gpu_resources::GpuResources; +use peniko::BlendMode; use peniko::color::palette; use peniko::kurbo::{self, Point, Size}; +use rustc_hash::FxHashSet; use winit::{ cursor::CursorIcon, dpi::{LogicalPosition, LogicalSize}, @@ -814,6 +816,8 @@ impl WindowHandle { } pub fn paint(&mut self) -> Option { + let use_vello_compositor = + self.uses_vello_compositor_paint_path() && self.window_state.capture.is_none(); // Create GlobalPaintCx (global/shared state) let mut cx = crate::paint::GlobalPaintCx { window_state: &mut self.window_state, @@ -823,6 +827,16 @@ impl WindowHandle { record_paint_order: crate::paint::is_paint_order_tracking_enabled(), }; + if use_vello_compositor { + return Self::paint_with_vello_compositor( + &self.window, + self.id, + self.transparent, + self.default_theme.as_ref(), + &mut cx, + ); + } + cx.paint_state .renderer_mut() .begin(cx.window_state.capture.is_some()); @@ -843,6 +857,7 @@ impl WindowHandle { // Paint main tree with overlays using explicit traversal cx.paint_with_traversal(self.id); + cx.window_state.sync_display_list_promoted_layers(); self.window.pre_present_notify(); let root_element_id = cx.window_state.root_view_id.get_element_id(); @@ -852,6 +867,198 @@ impl WindowHandle { cx.paint_state.renderer_mut().finish() } + fn uses_vello_compositor_paint_path(&self) -> bool { + #[cfg(all(feature = "vello", not(feature = "skia"), feature = "subduction"))] + { + self.window_state.compositor_backend_name() == Some("subduction") + } + + #[cfg(not(all(feature = "vello", not(feature = "skia"), feature = "subduction")))] + { + false + } + } + + fn paint_with_vello_compositor( + window: &Arc, + root_id: ViewId, + transparent: bool, + default_theme: Option<&Style>, + cx: &mut crate::paint::GlobalPaintCx<'_>, + ) -> Option { + let replay_order = cx.prepare_display_list(root_id); + cx.window_state.sync_display_list_promoted_layers(); + let root_bounds = cx.paint_state.renderer().size().to_rect(); + cx.window_state.ensure_compositor_root_layer(root_bounds); + cx.window_state.ensure_compositor_overlay_layer(root_bounds); + cx.window_state + .prepare_compositor_floem_surfaces(renderer_output_size(cx.paint_state.renderer().size())); + + let promoted_roots = cx.window_state.promoted_compositor_layer_ids(); + let mut promoted_subtrees = Vec::with_capacity(promoted_roots.len()); + let mut root_scene_ids = FxHashSet::default(); + let mut overlay_scene_ids = FxHashSet::default(); + + for &step in &replay_order { + let element_id = match step { + crate::paint::PaintOrPost::Paint(id) | crate::paint::PaintOrPost::Post(id) => id, + }; + root_scene_ids.insert(element_id); + } + + let box_tree = cx.window_state.box_tree.borrow(); + for promoted_root in promoted_roots { + let subtree = cx.collect_subtree_elements(promoted_root); + root_scene_ids.retain(|id| !subtree.contains(id)); + + if let Some(parent_id) = box_tree.parent_of(promoted_root.0) + && let Some(parent_element_id) = box_tree.element_id_of(parent_id) + { + overlay_scene_ids.extend(cx.collect_subtree_elements(parent_element_id)); + } + + promoted_subtrees.push((promoted_root, subtree)); + } + drop(box_tree); + + for (_, subtree) in &promoted_subtrees { + overlay_scene_ids.retain(|id| !subtree.contains(id)); + } + + { + let box_tree = cx.window_state.box_tree.borrow(); + for index in 0..promoted_subtrees.len() { + let promoted_root = promoted_subtrees[index].0; + let nested_promoted_roots = promoted_subtrees + .iter() + .enumerate() + .filter_map(|(other_index, (other_root, other_subtree))| { + if index == other_index { + return None; + } + + let mut cursor = box_tree.parent_of(other_root.0); + while let Some(parent_id) = cursor { + if parent_id == promoted_root.0 { + return Some(other_subtree.clone()); + } + cursor = box_tree.parent_of(parent_id); + } + None + }) + .collect::>(); + + let (_, subtree) = &mut promoted_subtrees[index]; + for nested_subtree in nested_promoted_roots { + subtree.retain(|id| !nested_subtree.contains(id)); + } + } + } + + let mut surfaces = Vec::new(); + cx.window_state + .for_each_floem_painted_surface(&mut |role, bounds, size, view| { + surfaces.push((role, bounds, size, view.clone())); + }); + + for (role, bounds, size, view) in surfaces { + cx.paint_state.renderer_mut().begin(false); + + if matches!(role, crate::compositor::FloemPaintedSurfaceRole::Root) && !transparent { + let color = default_theme + .and_then(|theme| theme.get(crate::style::Background)) + .unwrap_or(peniko::Brush::Solid(palette::css::WHITE)); + cx.paint_state.renderer_mut().fill( + &Size::new(size.0 as f64, size.1 as f64).to_rect().expand(), + &color, + 0.0, + ); + } + + let included_ids = match role { + crate::compositor::FloemPaintedSurfaceRole::Root => &root_scene_ids, + crate::compositor::FloemPaintedSurfaceRole::Overlay => &overlay_scene_ids, + crate::compositor::FloemPaintedSurfaceRole::Promoted(element_id) => promoted_subtrees + .iter() + .find_map(|(promoted_root, subtree)| { + (*promoted_root == element_id).then_some(subtree) + }) + .unwrap_or(&root_scene_ids), + }; + + let promoted_raster_clip = match role { + crate::compositor::FloemPaintedSurfaceRole::Promoted(element_id) => { + cx.window_state.promoted_compositor_layer_raster_clip(element_id) + } + _ => None, + }; + let should_replay_promoted = promoted_raster_clip.is_none_or(|clip| { + let local_clip = clip.with_origin(clip.origin() - bounds.origin().to_vec2()); + local_clip.width() > 0.0 && local_clip.height() > 0.0 + }); + + if matches!(role, crate::compositor::FloemPaintedSurfaceRole::Promoted(_)) { + // Promoted compositor layers are rasterized into their own textures. + // Keep the promoted surface sized to its own logical bounds, but clip replay + // to the current accumulated ancestor clip for the frame. That lets nested + // promoted layers keep stable surfaces while still updating correctly as outer + // scroll clips move. + let clip_rect = promoted_raster_clip + .map(|clip| clip.with_origin(clip.origin() - bounds.origin().to_vec2())) + .unwrap_or_else(|| Size::new(size.0 as f64, size.1 as f64).to_rect()); + cx.paint_state.renderer_mut().push_layer( + BlendMode::default(), + 1.0, + kurbo::Affine::IDENTITY, + &clip_rect, + ); + } + + if !matches!(role, crate::compositor::FloemPaintedSurfaceRole::Promoted(_)) + || should_replay_promoted + { + cx.replay_display_list( + &replay_order, + Some(included_ids), + bounds.origin(), + Some(Size::new(size.0 as f64, size.1 as f64)), + ); + } + + if matches!(role, crate::compositor::FloemPaintedSurfaceRole::Promoted(_)) { + cx.paint_state.renderer_mut().pop_layer(); + } + + if !cx + .paint_state + .renderer_mut() + .render_scene_to_premultiplied_texture_view(&view, size) + { + return cx.paint_state.renderer_mut().finish(); + } + } + + window.pre_present_notify(); + let root_element_id = cx.window_state.root_view_id.get_element_id(); + let event = Event::Window(WindowEvent::UpdatePhase(UpdatePhaseEvent::PaintPresent)); + GlobalEventCx::new(cx.window_state, root_element_id, event).route_window_event(); + + let presented = { + let window_state = &mut cx.window_state; + cx.paint_state + .renderer_mut() + .present_composited_output(|output| { + window_state.composite_compositor_to_output(output); + }) + }; + if presented { + cx.window_state.clear_compositor_layer_dirtiness(); + None + } else { + cx.paint_state.renderer_mut().finish() + } + } + pub(crate) fn capture(&mut self) -> Capture { // Capture the view before we run `style` and `layout` to catch missing `request_style`` or // `request_layout` flags. @@ -1476,6 +1683,12 @@ impl WindowHandle { } fn attach_compositor_presenter(&mut self) { + #[cfg(all(feature = "vello", not(feature = "skia"), feature = "subduction"))] + if self.window_state.compositor_backend_name().is_none() { + self.window_state + .install_compositor_backend(Box::new(crate::SubductionCompositorBackend::new())); + } + let Some(gpu_resources) = self.gpu_resources.as_ref() else { return; }; diff --git a/src/window/state.rs b/src/window/state.rs index d785d1d40..c09aad529 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -1,11 +1,12 @@ -use std::{cell::RefCell, collections::HashMap}; use std::time::{Duration, Instant}; +use std::{cell::RefCell, collections::HashMap}; use crate::{ action::exec_after_animation_frame, compositor::{ - Compositor, CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, - ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, + Compositor, CompositorAlphaMode, CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, + ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, + FloemSurfaceRoleVisitor, FrameRequestReason, ResolvedPromotedLayer, backend::CompositorBackend, }, inspector::CaptureState, @@ -34,8 +35,8 @@ use crate::{ event::{DragTracker, Event, WindowEvent, clear_hit_test_cache}, layout::responsive::{GridBreakpoints, ScreenSizeBp}, message::UpdateMessage, - paint::PaintStats, - paint::display_list::RetainedDisplayList, + paint::display_list::{RetainedDisplayList, constrain_rect_to_render_bounds}, + paint::{PaintStats, display_list::PromotedLayerCandidate}, style::{CursorStyle, Style, StyleSelector, theme::default_theme}, view::{LayoutNodeCx, MeasureCx, VIEW_STORAGE, ViewId}, }; @@ -1277,7 +1278,8 @@ impl WindowState { handle: ExternalSurfaceHandle, descriptor: ExternalSurfaceDescriptor, ) -> ExternalSurfaceId { - self.compositor.register_external_surface(handle, descriptor) + self.compositor + .register_external_surface(handle, descriptor) } pub fn update_external_surface( @@ -1286,7 +1288,8 @@ impl WindowState { handle: ExternalSurfaceHandle, descriptor: ExternalSurfaceDescriptor, ) -> bool { - self.compositor.update_external_surface(id, handle, descriptor) + self.compositor + .update_external_surface(id, handle, descriptor) } pub fn notify_external_surface_ready(&mut self, id: ExternalSurfaceId) -> bool { @@ -1311,6 +1314,162 @@ impl WindowState { .attach_wgpu_presenter(gpu_resources, output_format, output_size); } + fn accumulated_clip_bounds_for_element( + &self, + element_id: ElementId, + scale: f64, + render_size: Size, + ) -> Option { + let box_tree = self.box_tree.borrow(); + let mut clip_bounds = None::; + let mut cursor = Some(element_id.0); + + while let Some(node_id) = cursor { + if let Some(local_clip) = box_tree.local_clip(node_id).flatten() + && let Some(world_transform) = box_tree.world_transform(node_id) + { + // Promoted layers must respect the full ancestor clip chain, not just their + // own viewport clip. Constrain infinite clips against the current render + // surface before converting them into compositor bounds so nested scrolls + // cannot allocate unbounded layer textures. + let constrained_clip = constrain_rect_to_render_bounds( + local_clip.rect(), + world_transform.then_scale(scale), + render_size, + ); + let world_clip_bounds = world_transform.transform_rect_bbox(constrained_clip); + let next_clip_bounds = Rect::new( + world_clip_bounds.x0 * scale, + world_clip_bounds.y0 * scale, + world_clip_bounds.x1 * scale, + world_clip_bounds.y1 * scale, + ); + clip_bounds = Some(match clip_bounds { + Some(current) => current.intersect(next_clip_bounds), + None => next_clip_bounds, + }); + } + + cursor = box_tree.parent_of(node_id); + } + + clip_bounds + } + + pub fn sync_display_list_promoted_layers(&mut self) { + let scale = self.effective_scale(); + let render_size = self.root_size * scale; + let scale_rect = |rect: Rect| { + Rect::new( + rect.x0 * scale, + rect.y0 * scale, + rect.x1 * scale, + rect.y1 * scale, + ) + }; + let promoted_layers: Vec = + self.display_list.promoted_layer_candidates(); + let resolved_layers = promoted_layers + .into_iter() + .filter_map(|promoted| { + let box_tree = self.box_tree.borrow(); + let accumulated_clip_bounds = + self.accumulated_clip_bounds_for_element(promoted.element_id, scale, render_size); + + let mut bounds = scale_rect( + promoted + .snapshot + .world_transform + .transform_rect_bbox(promoted.candidate.bounds), + ); + let mut isolated = accumulated_clip_bounds.is_some(); + + if let Some(local_clip) = promoted.candidate.clip { + let constrained_clip = constrain_rect_to_render_bounds( + local_clip.rect(), + promoted.snapshot.world_transform.then_scale(scale), + render_size, + ); + bounds = bounds.intersect(scale_rect( + promoted + .snapshot + .world_transform + .transform_rect_bbox(constrained_clip), + )); + } + + if matches!( + promoted.candidate.promotion_hint, + crate::paint::display_list::CompositorPromotionHint::ScrollContent + ) && let Some(parent_id) = box_tree.parent_of(promoted.element_id.0) + && let Some(parent_transform) = box_tree.world_transform(parent_id) + && let Some(parent_clip) = box_tree.local_clip(parent_id).flatten() + { + let constrained_clip = constrain_rect_to_render_bounds( + parent_clip.rect(), + parent_transform.then_scale(scale), + render_size, + ); + bounds = scale_rect(parent_transform.transform_rect_bbox(constrained_clip)); + isolated = true; + } + + if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 { + // Promoted surfaces should keep stable sizing based on their own logical + // bounds/viewport. Skip only when the layer's own bounds collapse. + return None; + } + + let raster_clip = accumulated_clip_bounds.map(|clip| clip.intersect(bounds)); + + let mut compositing_depth = 0_u32; + let mut cursor = box_tree.parent_of(promoted.element_id.0); + while let Some(parent_id) = cursor { + if matches!( + box_tree.compositor_promotion_hint(parent_id), + Some(crate::paint::display_list::CompositorPromotionHint::ScrollContent) + ) { + compositing_depth += 1; + } + cursor = box_tree.parent_of(parent_id); + } + + Some(ResolvedPromotedLayer { + element_id: promoted.element_id, + bounds, + raster_clip, + z_index: promoted.z_index, + compositing_depth, + isolated, + alpha_mode: CompositorAlphaMode::Straight, + }) + }) + .collect::>(); + self.compositor.sync_promoted_layers(&resolved_layers); + } + + pub fn ensure_compositor_root_layer( + &mut self, + bounds: peniko::kurbo::Rect, + ) -> CompositorLayerId { + self.compositor.ensure_root_layer(bounds) + } + + pub fn ensure_compositor_overlay_layer( + &mut self, + bounds: peniko::kurbo::Rect, + ) -> CompositorLayerId { + self.compositor.ensure_overlay_layer(bounds) + } + + pub fn promoted_compositor_layer_ids(&self) -> Vec { + self.compositor.promoted_layer_ids() + } + + pub fn promoted_compositor_layer_raster_clip(&self, element_id: ElementId) -> Option { + self.compositor.promoted_layer_raster_clip(element_id) + } + pub fn compositor_backend_name(&self) -> Option<&'static str> { self.compositor.backend_name() } @@ -1323,6 +1482,10 @@ impl WindowState { self.compositor.timing() } + pub(crate) fn for_each_floem_painted_surface(&self, visit: &mut FloemSurfaceRoleVisitor<'_>) { + self.compositor.for_each_floem_painted_surface(visit); + } + pub fn update_compositor_timing(&mut self, timing: CompositorTiming) { self.compositor.update_timing(timing); } @@ -1357,10 +1520,18 @@ impl WindowState { self.compositor.clear_layer_dirtiness(); } + pub(crate) fn composite_compositor_to_output(&mut self, output: &wgpu::TextureView) { + self.compositor.composite_to_output(output); + } + pub fn begin_compositor_frame(&mut self, output_size: (u32, u32), started_at: Instant) { self.compositor.begin_frame(output_size, started_at); } + pub fn prepare_compositor_floem_surfaces(&mut self, output_size: (u32, u32)) { + self.compositor.prepare_floem_surfaces(output_size); + } + pub fn finish_compositor_frame( &mut self, output_size: (u32, u32), diff --git a/vello/src/lib.rs b/vello/src/lib.rs index acf2a11ce..92759ab7c 100644 --- a/vello/src/lib.rs +++ b/vello/src/lib.rs @@ -22,6 +22,49 @@ use vello::{AaConfig, RendererOptions, Scene}; use wgpu::util::TextureBlitter; use wgpu::{Adapter, DeviceType, Queue, TextureAspect, TextureFormat}; +const PREMULTIPLY_SHADER: &str = r#" +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let positions = array, 3>( + vec2(-1.0, -3.0), + vec2(-1.0, 1.0), + vec2(3.0, 1.0), + ); + let pos = positions[vertex_index]; + var out: VertexOutput; + out.position = vec4(pos, 0.0, 1.0); + out.uv = 0.5 * vec2(pos.x + 1.0, 1.0 - pos.y); + return out; +} + +@group(0) @binding(0) var source_texture: texture_2d; +@group(0) @binding(1) var source_sampler: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let color = textureSample(source_texture, source_sampler, in.uv); + return vec4(color.rgb * color.a, color.a); +} +"#; + +struct PremultiplyPipeline { + render_pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, +} + +struct PremultiplyScratch { + size: (u32, u32), + #[allow(dead_code)] + texture: wgpu::Texture, + view: wgpu::TextureView, +} + pub struct VelloRenderer { device: Device, #[allow(unused)] @@ -42,6 +85,8 @@ pub struct VelloRenderer { svg_cache: HashMap, (bool, Scene)>, /// Current cache generation; toggled each frame so stale entries are evicted. cache_generation: bool, + premultiply_pipeline: PremultiplyPipeline, + compositor_scratch: Option, } impl VelloRenderer { @@ -49,6 +94,15 @@ impl VelloRenderer { self.transform } + fn render_params(width: u32, height: u32) -> vello::RenderParams { + vello::RenderParams { + base_color: palette::css::TRANSPARENT, + width, + height, + antialiasing_method: vello::AaConfig::Msaa16, + } + } + pub fn new( gpu_resources: GpuResources, surface: wgpu::Surface<'static>, @@ -144,6 +198,8 @@ impl VelloRenderer { }, ) .unwrap(); + let premultiply_pipeline = + create_premultiply_pipeline(&device, wgpu::TextureFormat::Rgba8Unorm); Ok(Self { device, @@ -159,6 +215,8 @@ impl VelloRenderer { font_embolden, svg_cache: HashMap::new(), cache_generation: false, + premultiply_pipeline, + compositor_scratch: None, }) } @@ -200,6 +258,134 @@ impl VelloRenderer { self.surface.config.height as f64, ) } + + pub fn render_scene_to_texture_view( + &mut self, + target_view: &wgpu::TextureView, + width: u32, + height: u32, + ) -> Result<()> { + Ok(self.renderer.render_to_texture( + &self.device, + &self.queue, + &self.scene, + target_view, + &Self::render_params(width, height), + )?) + } + + pub fn render_scene_to_premultiplied_texture_view( + &mut self, + target_view: &wgpu::TextureView, + width: u32, + height: u32, + ) -> Result<()> { + let scratch_view = self.ensure_compositor_scratch((width, height)); + self.render_scene_to_texture_view(&scratch_view, width, height)?; + self.premultiply_into_view(&scratch_view, target_view); + Ok(()) + } + + pub fn present_composited_output( + &mut self, + composite: impl FnOnce(&wgpu::TextureView), + ) -> bool { + let Ok(surface_texture) = self.surface.surface.get_current_texture() else { + return false; + }; + let output_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + composite(&output_view); + surface_texture.present(); + true + } + + fn ensure_compositor_scratch(&mut self, size: (u32, u32)) -> wgpu::TextureView { + let needs_new_texture = self + .compositor_scratch + .as_ref() + .is_none_or(|scratch| scratch.size != size); + + if needs_new_texture { + let texture = self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Floem Compositor Scratch"), + size: wgpu::Extent3d { + width: size.0, + height: size.1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::STORAGE_BINDING, + format: wgpu::TextureFormat::Rgba8Unorm, + view_formats: &[], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + self.compositor_scratch = Some(PremultiplyScratch { + size, + texture, + view, + }); + } + + self.compositor_scratch + .as_ref() + .expect("scratch texture just created") + .view + .clone() + } + + fn premultiply_into_view( + &self, + source_view: &wgpu::TextureView, + target_view: &wgpu::TextureView, + ) { + let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Floem Premultiply Bind Group"), + layout: &self.premultiply_pipeline.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(source_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.premultiply_pipeline.sampler), + }, + ], + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Floem Premultiply Encoder"), + }); + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Floem Premultiply Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(&self.premultiply_pipeline.render_pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.draw(0..3, 0..1); + } + self.queue.submit([encoder.finish()]); + } } impl Renderer for VelloRenderer { @@ -406,21 +592,18 @@ impl Renderer for VelloRenderer { fn set_z_index(&mut self, _z_index: i32) {} - fn clip(&mut self, _shape: &impl Shape) { - // if shape.bounding_box().is_zero_area() { - // return; - // } - // self.scene.pop_layer(); - // self.scene.push_layer( - // vello::peniko::BlendMode::default(), - // 1., - // self.transform.then_scale(self.window_scale), - // shape, - // ); + fn clip(&mut self, shape: &impl Shape) { + self.scene.push_layer( + Fill::NonZero, + vello::peniko::BlendMode::default(), + 1.0, + self.transform, + shape, + ); } fn clear_clip(&mut self) { - // self.scene.pop_layer(); + self.scene.pop_layer(); } fn finish(&mut self) -> Option { @@ -428,20 +611,15 @@ impl Renderer for VelloRenderer { self.render_capture_image() } else { if let Ok(surface_texture) = self.surface.surface.get_current_texture() { - self.renderer - .render_to_texture( - &self.device, - &self.queue, - &self.scene, - &self.surface.target_view, - &vello::RenderParams { - base_color: palette::css::TRANSPARENT, // Background color - width: self.surface.config.width, - height: self.surface.config.height, - antialiasing_method: vello::AaConfig::Msaa16, - }, - ) - .unwrap(); + let target_view = self.surface.target_view.clone(); + let width = self.surface.config.width; + let height = self.surface.config.height; + self.render_scene_to_texture_view( + &target_view, + width, + height, + ) + .unwrap(); // Perform the copy let mut encoder = @@ -518,10 +696,8 @@ impl VelloRenderer { &self.scene, &view, &vello::RenderParams { - base_color: palette::css::TRANSPARENT, - width: self.surface.config.width, - height: self.surface.config.height, antialiasing_method: AaConfig::Area, + ..Self::render_params(self.surface.config.width, self.surface.config.height) }, ) .unwrap(); @@ -582,6 +758,79 @@ impl VelloRenderer { } } +fn create_premultiply_pipeline( + device: &wgpu::Device, + target_format: wgpu::TextureFormat, +) -> PremultiplyPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Floem Premultiply Shader"), + source: wgpu::ShaderSource::Wgsl(PREMULTIPLY_SHADER.into()), + }); + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Floem Premultiply BGL"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Floem Premultiply Layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Floem Premultiply Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: target_format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Floem Premultiply Sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + PremultiplyPipeline { + render_pipeline, + bind_group_layout, + sampler, + } +} + fn common_alpha_mask_scene( size: Size, alpha_mask: impl FnOnce(&mut Scene), From 23656cfa09ba81fd8ede12628dd7d8537e295ab4 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Thu, 19 Mar 2026 23:35:20 -0600 Subject: [PATCH 018/102] get rid of z-index in renderer --- Cargo.lock | 2 +- Cargo.toml | 2 +- renderer/src/lib.rs | 6 - skia/src/lib.rs | 2 - src/paint/display_list.rs | 453 +++++++++++--------------------------- src/paint/mod.rs | 86 +++++++- src/paint/renderer.rs | 12 - tiny_skia/src/lib.rs | 4 - vello/src/lib.rs | 2 - vger/src/lib.rs | 4 - 10 files changed, 201 insertions(+), 372 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ea1e5b45..12e226dd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3000,7 +3000,7 @@ checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" [[package]] name = "imaging" version = "0.0.1" -source = "git+https://github.com/forest-rs/imaging?rev=0527cbabf0d6ea27e14a82955f4a582c57bdc440#0527cbabf0d6ea27e14a82955f4a582c57bdc440" +source = "git+https://github.com/forest-rs/imaging?rev=eacda40b585058fde8e0cfcae031f93d8d8c25ec#eacda40b585058fde8e0cfcae031f93d8d8c25ec" dependencies = [ "kurbo", "peniko", diff --git a/Cargo.toml b/Cargo.toml index 415728a78..efd79fbd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ ui-events = { version = "0.3", features = ["kurbo"] } ui-events-winit = { path = "./ui-events-winit", package = "ui-events-floem-winit" } dpi = { version = "0.1.2", default-features = false } rustc-hash = "2.1" -imaging = { git = "https://github.com/forest-rs/imaging", rev = "0527cbabf0d6ea27e14a82955f4a582c57bdc440", package = "imaging" } +imaging = { git = "https://github.com/forest-rs/imaging", rev = "eacda40b585058fde8e0cfcae031f93d8d8c25ec", package = "imaging" } [dependencies] slotmap = "1.0" diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index b8ed20640..7a4b35815 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -122,12 +122,6 @@ pub trait Renderer { /// resolution. fn set_transform(&mut self, transform: Affine); - /// Set the z-index for subsequent draw commands. - /// - /// Not all backends honour this — Vello and tiny-skia rely on painter's - /// order instead. - fn set_z_index(&mut self, z_index: i32); - /// Clip all subsequent drawing to the interior of `shape`. /// /// The clip remains in effect until [`clear_clip`](Renderer::clear_clip) is diff --git a/skia/src/lib.rs b/skia/src/lib.rs index 6be843968..af33e8837 100644 --- a/skia/src/lib.rs +++ b/skia/src/lib.rs @@ -340,8 +340,6 @@ impl Renderer for SkiaRenderer { self.transform = transform; } - fn set_z_index(&mut self, _z_index: i32) {} - fn clip(&mut self, shape: &impl Shape) { self.commands.push(Command::PushClip { transform: self.transform, diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index d5463b812..68c28f37c 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -154,10 +154,12 @@ use std::sync::Arc; use floem_renderer::text::{Glyph, GlyphRunProps}; use floem_renderer::{Img, Renderer as FloemRenderer, Svg, usvg}; use imaging::{ - Clip, Composite, Draw, FillRule, Geometry, Glyph as SceneGlyph, GlyphRun, Group, StrokeStyle, + BlurredRoundedRect, ClipRef, Composite, FillRef, GlyphRunRef, GroupRef, PaintSink, + StrokeRef, + record::{Clip, Draw, Geometry, GlyphRun, Group}, }; -use peniko::BrushRef; -use peniko::kurbo::{Affine, BezPath, Circle, Line, Point, Rect, RoundedRect, Shape, Size, Stroke}; +use peniko::{BrushRef, Fill}; +use peniko::kurbo::{Affine, BezPath, Point, Rect, RoundedRect, Shape, Size}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ElementId, paint::PaintOrPost}; @@ -221,14 +223,6 @@ pub(crate) fn transform_diff_class(original: Affine, current: Affine) -> Transfo } } -#[derive(Clone, Debug)] -pub(crate) enum ShapeHint { - Rect(Rect), - RoundedRect(RoundedRect), - Line(Line), - Circle(Circle), -} - #[derive(Clone)] pub(crate) struct OwnedSvg { pub tree: Arc, @@ -237,22 +231,14 @@ pub(crate) struct OwnedSvg { #[derive(Clone)] pub(crate) enum DisplayCommand { - SetZIndex(i32), - PushClip { - clip: Clip, - hint: Option, - }, + PushClip { clip: Clip }, PopClip, PushLayer { group: Group, transform: Affine, - clip_hint: Option, }, PopLayer, - Draw { - draw: Draw, - hint: Option, - }, + Draw { draw: Draw }, DrawImage { img: peniko::ImageBrush, hash: Arc<[u8]>, @@ -366,7 +352,6 @@ pub(crate) struct ClipNode { pub parent: Option, pub transform_id: TransformNodeId, pub clip: Clip, - pub hint: Option, } #[allow(dead_code)] @@ -406,9 +391,8 @@ impl Default for PaintPropertyTree { clip: Clip::Fill { transform: Affine::IDENTITY, shape: Geometry::Rect(Rect::ZERO), - fill_rule: FillRule::NonZero, + fill_rule: Fill::NonZero, }, - hint: Some(ShapeHint::Rect(Rect::ZERO)), }], effects: vec![EffectNode { parent: None, @@ -609,93 +593,18 @@ impl<'a> RecordingRenderer<'a> { } } - fn record_draw(&mut self, draw: Draw, hint: Option) { - self.commands.push(DisplayCommand::Draw { draw, hint }); + fn record_draw(&mut self, draw: Draw) { + self.commands.push(DisplayCommand::Draw { draw }); } } impl RecordingRenderer<'_> { - pub fn set_transform(&mut self, transform: Affine) { - self.transform = transform; + pub fn transform(&self) -> Affine { + self.transform } - pub fn set_z_index(&mut self, z_index: i32) { - self.commands.push(DisplayCommand::SetZIndex(z_index)); - } - - pub fn clip(&mut self, shape: &impl Shape) { - let (shape, hint) = shape_to_geometry(shape); - self.commands.push(DisplayCommand::PushClip { - clip: Clip::Fill { - transform: self.transform, - shape, - fill_rule: FillRule::NonZero, - }, - hint, - }); - } - - pub fn clear_clip(&mut self) { - self.commands.push(DisplayCommand::PopClip); - } - - pub fn stroke<'b, 's>( - &mut self, - shape: &impl Shape, - brush: impl Into>, - stroke: &'s Stroke, - ) { - let (shape, hint) = shape_to_geometry(shape); - self.record_draw( - Draw::Stroke { - transform: self.transform, - stroke: StrokeStyle::clone(stroke), - paint: brush.into().to_owned(), - paint_transform: None, - shape, - composite: Composite::default(), - }, - hint, - ); - } - - pub fn fill<'b>( - &mut self, - shape: &impl Shape, - brush: impl Into>, - blur_radius: f64, - ) { - let brush = brush.into(); - if blur_radius > 0.0 - && let BrushRef::Solid(color) = brush - && let Some((rect, radius)) = blurred_rounded_rect(shape) - { - self.record_draw( - Draw::BlurredRoundedRect(imaging::BlurredRoundedRect { - transform: self.transform, - rect, - color, - radius, - std_dev: blur_radius, - composite: Composite::default(), - }), - shape_hint(shape), - ); - return; - } - - let (shape, hint) = shape_to_geometry(shape); - self.record_draw( - Draw::Fill { - transform: self.transform, - fill_rule: FillRule::NonZero, - paint: brush.to_owned(), - paint_transform: None, - shape, - composite: Composite::default(), - }, - hint, - ); + pub fn set_transform(&mut self, transform: Affine) { + self.transform = transform; } pub fn push_layer( @@ -705,19 +614,18 @@ impl RecordingRenderer<'_> { transform: Affine, clip: &impl Shape, ) { - let (shape, hint) = shape_to_geometry(clip); + let shape = shape_to_geometry(clip); self.commands.push(DisplayCommand::PushLayer { group: Group { clip: Some(Clip::Fill { transform: self.transform, shape, - fill_rule: FillRule::NonZero, + fill_rule: Fill::NonZero, }), filters: Vec::new(), composite: Composite::new(blend.into(), alpha), }, transform, - clip_hint: hint, }); } @@ -725,33 +633,6 @@ impl RecordingRenderer<'_> { self.commands.push(DisplayCommand::PopLayer); } - pub fn draw_glyphs<'a>( - &mut self, - origin: Point, - props: &GlyphRunProps<'a>, - glyphs: impl Iterator + 'a, - ) { - let glyph_run = GlyphRun { - font: props.font.clone(), - transform: self.transform * Affine::translate((origin.x, origin.y)) * props.transform, - glyph_transform: props.glyph_transform, - font_size: props.font_size, - hint: props.hint, - normalized_coords: props.normalized_coords.to_vec(), - style: props.style.to_owned(), - glyphs: glyphs - .map(|glyph| SceneGlyph { - id: glyph.id, - x: glyph.x, - y: glyph.y, - }) - .collect(), - paint: props.brush.to_owned(), - composite: Composite::new(peniko::BlendMode::default(), props.brush_alpha), - }; - self.record_draw(Draw::GlyphRun(glyph_run), None); - } - pub fn draw_svg<'b>( &mut self, svg: Svg<'b>, @@ -779,6 +660,43 @@ impl RecordingRenderer<'_> { } } +impl PaintSink for RecordingRenderer<'_> { + fn push_clip(&mut self, clip: ClipRef<'_>) { + self.commands.push(DisplayCommand::PushClip { clip: clip.to_owned() }); + } + + fn pop_clip(&mut self) { + self.commands.push(DisplayCommand::PopClip); + } + + fn push_group(&mut self, group: GroupRef<'_>) { + self.commands.push(DisplayCommand::PushLayer { + group: group.to_owned(), + transform: Affine::IDENTITY, + }); + } + + fn pop_group(&mut self) { + self.commands.push(DisplayCommand::PopLayer); + } + + fn fill(&mut self, draw: FillRef<'_>) { + self.record_draw(draw.to_owned()); + } + + fn stroke(&mut self, draw: StrokeRef<'_>) { + self.record_draw(draw.to_owned()); + } + + fn glyph_run(&mut self, draw: GlyphRunRef<'_>) { + self.record_draw(Draw::GlyphRun(draw.to_owned())); + } + + fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { + self.record_draw(Draw::BlurredRoundedRect(draw)); + } +} + pub(crate) fn replay_stage( stage: &ElementStage, renderer: &mut impl FloemRenderer, @@ -786,7 +704,6 @@ pub(crate) fn replay_stage( render_size: Size, local_damage: Option<&[Rect]>, ) { - let mut current_z_index = None; let mut current_transform = None; let mut current_clip_stack: Vec = Vec::new(); // This stays wired through the replay path even though full-scene replay is still active. @@ -799,10 +716,6 @@ pub(crate) fn replay_stage( if let Some(indices) = &chunk_indices && !indices.contains(&index) { continue; } - if current_z_index != Some(chunk.properties.z_index) { - renderer.set_z_index(chunk.properties.z_index); - current_z_index = Some(chunk.properties.z_index); - } apply_clip_state( renderer, &stage.property_tree, @@ -814,13 +727,8 @@ pub(crate) fn replay_stage( ); for command in &chunk.commands { match command { - DisplayCommand::SetZIndex(_) => {} DisplayCommand::PushClip { .. } | DisplayCommand::PopClip => {} - DisplayCommand::PushLayer { - group, - transform, - clip_hint, - } => { + DisplayCommand::PushLayer { group, transform } => { let Some(Clip::Fill { transform: clip_transform, shape, @@ -834,62 +742,34 @@ pub(crate) fn replay_stage( base_transform * *clip_transform, &mut current_transform, ); - match (clip_hint.clone(), shape) { - (Some(ShapeHint::Rect(rect)), _) => renderer.push_layer( + match sanitize_clip_geometry(shape, base_transform * *clip_transform, render_size) { + Geometry::Rect(rect) => renderer.push_layer( group.composite.blend, group.composite.alpha, *transform, &rect, ), - (Some(ShapeHint::RoundedRect(rect)), _) => renderer.push_layer( + Geometry::RoundedRect(rect) => renderer.push_layer( group.composite.blend, group.composite.alpha, *transform, &rect, ), - (Some(ShapeHint::Line(line)), _) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - &line, - ), - (Some(ShapeHint::Circle(circle)), _) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - &circle, - ), - (None, Geometry::Rect(rect)) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - rect, - ), - (None, Geometry::RoundedRect(rect)) => renderer.push_layer( + Geometry::Path(path) => renderer.push_layer( group.composite.blend, group.composite.alpha, *transform, - rect, - ), - (None, Geometry::Path(path)) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *transform, - path, + &path, ), } } DisplayCommand::PopLayer => renderer.pop_layer(), - DisplayCommand::Draw { draw, hint } => { - replay_draw( - renderer, - draw, - hint.clone(), - base_transform, - render_size, - &mut current_transform, - ) - } + DisplayCommand::Draw { draw } => replay_draw( + renderer, + draw, + base_transform, + &mut current_transform, + ), DisplayCommand::DrawImage { img, hash, @@ -967,17 +847,13 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa for command in commands { match command { - DisplayCommand::SetZIndex(z_index) => { - properties.z_index = z_index; - } - DisplayCommand::PushClip { clip, hint } => { + DisplayCommand::PushClip { clip } => { let transform_id = intern_transform(clip_transform(&clip), &mut property_tree, &mut transform_intern); let clip_id = ClipNodeId(property_tree.clips.len() as u32); property_tree.clips.push(ClipNode { parent: clip_stack.last().copied(), transform_id, clip, - hint: hint.clone(), }); clip_stack.push(clip_id); properties.clip_id = clip_id; @@ -986,11 +862,7 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa clip_stack.pop(); properties.clip_id = clip_stack.last().copied().unwrap_or_default(); } - DisplayCommand::PushLayer { - group, - transform, - clip_hint, - } => { + DisplayCommand::PushLayer { group, transform } => { let effect_id = EffectNodeId(property_tree.effects.len() as u32); property_tree.effects.push(EffectNode { parent: effect_stack.last().copied(), @@ -1007,13 +879,8 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa command_transform_class(&DisplayCommand::PushLayer { group: group.clone(), transform, - clip_hint: clip_hint.clone(), }), - DisplayCommand::PushLayer { - group, - transform, - clip_hint, - }, + DisplayCommand::PushLayer { group, transform }, ); } DisplayCommand::PopLayer => { @@ -1084,12 +951,11 @@ fn push_boundary_chunk( fn command_transform_class(command: &DisplayCommand) -> TransformClass { match command { - DisplayCommand::SetZIndex(_) - | DisplayCommand::PushClip { .. } + DisplayCommand::PushClip { .. } | DisplayCommand::PopClip | DisplayCommand::PushLayer { .. } | DisplayCommand::PopLayer => TransformClass::Affine, - DisplayCommand::Draw { draw, .. } => match draw { + DisplayCommand::Draw { draw } => match draw { Draw::Fill { .. } | Draw::Stroke { .. } => TransformClass::Affine, Draw::GlyphRun(_) | Draw::BlurredRoundedRect(_) => TransformClass::TranslateOnly, }, @@ -1101,8 +967,7 @@ fn command_transform_class(command: &DisplayCommand) -> TransformClass { fn command_bounds(command: &DisplayCommand) -> Option { match command { - DisplayCommand::SetZIndex(_) - | DisplayCommand::PushClip { .. } + DisplayCommand::PushClip { .. } | DisplayCommand::PopClip | DisplayCommand::PushLayer { .. } | DisplayCommand::PopLayer => None, @@ -1154,8 +1019,7 @@ fn glyph_run_bounds(run: &GlyphRun) -> Option { fn command_metadata(command: &DisplayCommand) -> PaintChunkMetadata { match command { - DisplayCommand::SetZIndex(_) - | DisplayCommand::PushClip { .. } + DisplayCommand::PushClip { .. } | DisplayCommand::PopClip | DisplayCommand::PopLayer => PaintChunkMetadata::default(), DisplayCommand::PushLayer { .. } => PaintChunkMetadata { @@ -1224,22 +1088,17 @@ fn clip_bounds_for_id(property_tree: &PaintPropertyTree, clip_id: ClipNodeId) -> } fn clip_node_bounds(node: &ClipNode) -> Option { - match (&node.hint, &node.clip) { - (Some(ShapeHint::Rect(rect)), _) => Some(*rect), - (Some(ShapeHint::RoundedRect(rect)), _) => Some(rect.rect()), - (Some(ShapeHint::Circle(circle)), _) => Some(circle.bounding_box()), - (Some(ShapeHint::Line(line)), _) => Some(line.bounding_box()), - (None, Clip::Fill { shape, .. }) => Some(geometry_bounds(shape)), - (None, Clip::Stroke { shape, .. }) => Some(geometry_bounds(shape)), + match &node.clip { + Clip::Fill { shape, .. } | Clip::Stroke { shape, .. } => Some(geometry_bounds(shape)), } } fn command_affine(command: &DisplayCommand) -> Affine { match command { - DisplayCommand::SetZIndex(_) | DisplayCommand::PopClip | DisplayCommand::PopLayer => { + DisplayCommand::PopClip | DisplayCommand::PopLayer => { Affine::IDENTITY } - DisplayCommand::PushClip { clip, .. } => clip_transform(clip), + DisplayCommand::PushClip { clip } => clip_transform(clip), DisplayCommand::PushLayer { transform, .. } => *transform, DisplayCommand::Draw { draw, .. } => match draw { Draw::Fill { transform, .. } | Draw::Stroke { transform, .. } => *transform, @@ -1302,17 +1161,10 @@ fn replay_clip_node( }; let final_transform = base_transform * transform; set_transform_if_needed(renderer, final_transform, current_transform); - match ( - sanitize_clip_hint(clip_node.hint.clone(), final_transform, render_size), - shape, - ) { - (Some(ShapeHint::Rect(rect)), _) => renderer.clip(&rect), - (Some(ShapeHint::RoundedRect(rect)), _) => renderer.clip(&rect), - (Some(ShapeHint::Line(line)), _) => renderer.clip(&line), - (Some(ShapeHint::Circle(circle)), _) => renderer.clip(&circle), - (None, Geometry::Rect(rect)) => renderer.clip(rect), - (None, Geometry::RoundedRect(rect)) => renderer.clip(rect), - (None, Geometry::Path(path)) => renderer.clip(path), + match sanitize_clip_geometry(shape, final_transform, render_size) { + Geometry::Rect(rect) => renderer.clip(&rect), + Geometry::RoundedRect(rect) => renderer.clip(&rect), + Geometry::Path(path) => renderer.clip(&path), } } @@ -1378,45 +1230,35 @@ fn clip_chain(property_tree: &PaintPropertyTree, clip_id: ClipNodeId) -> Vec, base_transform: Affine, - _render_size: Size, current_transform: &mut Option, ) { match draw { Draw::Fill { transform, - paint, + brush, shape, .. } => { set_transform_if_needed(renderer, base_transform * *transform, current_transform); - match (hint, shape) { - (Some(ShapeHint::Rect(rect)), _) => renderer.fill(&rect, paint, 0.0), - (Some(ShapeHint::RoundedRect(rect)), _) => renderer.fill(&rect, paint, 0.0), - (Some(ShapeHint::Line(line)), _) => renderer.fill(&line, paint, 0.0), - (Some(ShapeHint::Circle(circle)), _) => renderer.fill(&circle, paint, 0.0), - (None, Geometry::Rect(rect)) => renderer.fill(rect, paint, 0.0), - (None, Geometry::RoundedRect(rect)) => renderer.fill(rect, paint, 0.0), - (None, Geometry::Path(path)) => renderer.fill(path, paint, 0.0), + match shape { + Geometry::Rect(rect) => renderer.fill(rect, brush, 0.0), + Geometry::RoundedRect(rect) => renderer.fill(rect, brush, 0.0), + Geometry::Path(path) => renderer.fill(path, brush, 0.0), } } Draw::Stroke { transform, stroke, - paint, + brush, shape, .. } => { set_transform_if_needed(renderer, base_transform * *transform, current_transform); - match (hint, shape) { - (Some(ShapeHint::Rect(rect)), _) => renderer.stroke(&rect, paint, stroke), - (Some(ShapeHint::RoundedRect(rect)), _) => renderer.stroke(&rect, paint, stroke), - (Some(ShapeHint::Line(line)), _) => renderer.stroke(&line, paint, stroke), - (Some(ShapeHint::Circle(circle)), _) => renderer.stroke(&circle, paint, stroke), - (None, Geometry::Rect(rect)) => renderer.stroke(rect, paint, stroke), - (None, Geometry::RoundedRect(rect)) => renderer.stroke(rect, paint, stroke), - (None, Geometry::Path(path)) => renderer.stroke(path, paint, stroke), + match shape { + Geometry::Rect(rect) => renderer.stroke(rect, brush, stroke), + Geometry::RoundedRect(rect) => renderer.stroke(rect, brush, stroke), + Geometry::Path(path) => renderer.stroke(path, brush, stroke), } } Draw::GlyphRun(run) => { @@ -1426,7 +1268,7 @@ fn replay_draw( hint: run.hint, normalized_coords: &run.normalized_coords, style: (&run.style).into(), - brush: (&run.paint).into(), + brush: (&run.brush).into(), brush_alpha: run.composite.alpha, transform: run.transform, glyph_transform: run.glyph_transform, @@ -1463,21 +1305,17 @@ fn set_transform_if_needed( } } -fn sanitize_clip_hint( - hint: Option, - transform: Affine, - render_size: Size, -) -> Option { - match hint { - Some(ShapeHint::Rect(rect)) => Some(ShapeHint::Rect(constrain_infinite_rect( - rect, +fn sanitize_clip_geometry(shape: &Geometry, transform: Affine, render_size: Size) -> Geometry { + match shape { + Geometry::Rect(rect) => { + Geometry::Rect(constrain_infinite_rect(*rect, transform, render_size)) + } + Geometry::RoundedRect(rect) => Geometry::RoundedRect(constrain_infinite_rounded_rect( + *rect, transform, render_size, - ))), - Some(ShapeHint::RoundedRect(rect)) => Some(ShapeHint::RoundedRect( - constrain_infinite_rounded_rect(rect, transform, render_size), )), - other => other, + Geometry::Path(path) => Geometry::Path(path.clone()), } } @@ -1542,34 +1380,31 @@ mod tests { let rect = Rect::new(0.0, 0.0, 10.0, 10.0); let mut stage = ElementStage::default(); stage.set_commands(vec![ - DisplayCommand::SetZIndex(7), DisplayCommand::Draw { draw: Draw::Fill { transform: Affine::IDENTITY, - fill_rule: FillRule::NonZero, - paint: Color::BLACK.into(), - paint_transform: None, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, shape: Geometry::Rect(rect), composite: Composite::default(), }, - hint: Some(ShapeHint::Rect(rect)), }, DisplayCommand::Draw { draw: Draw::Stroke { transform: Affine::IDENTITY, - stroke: StrokeStyle::new(1.0), - paint: Color::BLACK.into(), - paint_transform: None, + stroke: peniko::kurbo::Stroke::new(1.0), + brush: Color::BLACK.into(), + brush_transform: None, shape: Geometry::Rect(rect), composite: Composite::default(), }, - hint: Some(ShapeHint::Rect(rect)), }, ], None); assert_eq!(stage.chunks.len(), 1); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); - assert_eq!(stage.chunks[0].properties.z_index, 7); + assert_eq!(stage.chunks[0].properties.z_index, 0); assert_eq!(stage.chunks[0].commands.len(), 2); assert_eq!(stage.transform_class, TransformClass::Affine); } @@ -1583,20 +1418,18 @@ mod tests { clip: Clip::Fill { transform: Affine::IDENTITY, shape: Geometry::Rect(rect), - fill_rule: FillRule::NonZero, + fill_rule: Fill::NonZero, }, - hint: Some(ShapeHint::Rect(rect)), }, DisplayCommand::Draw { draw: Draw::Fill { transform: Affine::IDENTITY, - fill_rule: FillRule::NonZero, - paint: Color::BLACK.into(), - paint_transform: None, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, shape: Geometry::Rect(rect), composite: Composite::default(), }, - hint: Some(ShapeHint::Rect(rect)), }, DisplayCommand::PopClip, ], None); @@ -1615,24 +1448,22 @@ mod tests { DisplayCommand::Draw { draw: Draw::Fill { transform: Affine::IDENTITY, - fill_rule: FillRule::NonZero, - paint: Color::BLACK.into(), - paint_transform: None, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, shape: Geometry::Rect(rect), composite: Composite::default(), }, - hint: Some(ShapeHint::Rect(rect)), }, DisplayCommand::Draw { draw: Draw::Fill { transform: Affine::translate((5.0, 0.0)), - fill_rule: FillRule::NonZero, - paint: Color::BLACK.into(), - paint_transform: None, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, shape: Geometry::Rect(rect), composite: Composite::default(), }, - hint: Some(ShapeHint::Rect(rect)), }, ], None); @@ -1656,7 +1487,6 @@ mod tests { std_dev: 6.0, composite: Composite::default(), }), - hint: None, }], None); assert_eq!(stage.transform_class, TransformClass::TranslateOnly); @@ -1672,13 +1502,12 @@ mod tests { DisplayCommand::Draw { draw: Draw::Fill { transform: Affine::IDENTITY, - fill_rule: FillRule::NonZero, - paint: Color::BLACK.into(), - paint_transform: None, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, shape: Geometry::Rect(rect), composite: Composite::default(), }, - hint: Some(ShapeHint::Rect(rect)), }, DisplayCommand::DrawImage { img: peniko::ImageBrush::new(peniko::ImageData { @@ -1717,52 +1546,16 @@ mod tests { } } -fn shape_to_geometry(shape: &impl Shape) -> (Geometry, Option) { +fn shape_to_geometry(shape: &impl Shape) -> Geometry { if let Some(rect) = shape.as_rect() { - return (Geometry::Rect(rect), Some(ShapeHint::Rect(rect))); + return Geometry::Rect(rect); } if let Some(rect) = shape.as_rounded_rect() { - return ( - Geometry::RoundedRect(rect), - Some(ShapeHint::RoundedRect(rect)), - ); + return Geometry::RoundedRect(rect); } - if let Some(line) = shape.as_line() { - return ( - Geometry::Path(path_from_shape(shape)), - Some(ShapeHint::Line(line)), - ); - } - if let Some(circle) = shape.as_circle() { - return ( - Geometry::Path(path_from_shape(shape)), - Some(ShapeHint::Circle(circle)), - ); - } - (Geometry::Path(path_from_shape(shape)), None) -} - -fn shape_hint(shape: &impl Shape) -> Option { - shape - .as_rect() - .map(ShapeHint::Rect) - .or_else(|| shape.as_rounded_rect().map(ShapeHint::RoundedRect)) - .or_else(|| shape.as_line().map(ShapeHint::Line)) - .or_else(|| shape.as_circle().map(ShapeHint::Circle)) + Geometry::Path(path_from_shape(shape)) } fn path_from_shape(shape: &impl Shape) -> BezPath { shape.to_path(0.1) } - -fn blurred_rounded_rect(shape: &impl Shape) -> Option<(Rect, f64)> { - if let Some(rect) = shape.as_rect() { - return Some((rect, 0.0)); - } - let rect = shape.as_rounded_rect()?; - let radii = rect.radii(); - (radii.top_left == radii.top_right - && radii.top_left == radii.bottom_left - && radii.top_left == radii.bottom_right) - .then_some((rect.rect(), radii.top_left)) -} diff --git a/src/paint/mod.rs b/src/paint/mod.rs index a0a419738..8aaeb9427 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -14,7 +14,8 @@ pub use renderer::Renderer; use floem_renderer::Renderer as _; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; -use peniko::kurbo::{Affine, Point, RoundedRect, Shape, Size}; +use imaging::{BlurredRoundedRect, ClipRef, Composite, GeometryRef, PaintSink, Painter}; +use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Shape, Size}; use rustc_hash::FxHashSet; use std::sync::Arc; use understory_box_tree::NodeFlags; @@ -614,7 +615,7 @@ impl PaintCx<'_> { use peniko::Mix; self.push_layer(Mix::Normal, 1.0, Affine::IDENTITY, shape); } else { - self.recorder.clip(shape); + self.recorder.push_clip(ClipRef::fill(geometry_ref_from_shape(shape))); } } @@ -623,7 +624,7 @@ impl PaintCx<'_> { if self.uses_layer_clip { self.pop_layer(); } else { - self.recorder.clear_clip(); + self.recorder.pop_clip(); } } @@ -636,7 +637,12 @@ impl PaintCx<'_> { brush: impl Into>, stroke: &'s peniko::kurbo::Stroke, ) { - self.recorder.stroke(shape, brush, stroke); + let brush = brush.into().to_owned(); + let transform = self.recorder.transform(); + Painter::new(self.recorder) + .stroke(geometry_ref_from_shape(shape), stroke, &brush) + .transform(transform) + .draw(); } pub fn fill<'b>( @@ -645,7 +651,27 @@ impl PaintCx<'_> { brush: impl Into>, blur_radius: f64, ) { - self.recorder.fill(path, brush, blur_radius); + let brush = brush.into().to_owned(); + let transform = self.recorder.transform(); + if blur_radius > 0.0 + && let peniko::Brush::Solid(color) = brush + && let Some((rect, radius)) = blurred_rounded_rect(path) + { + Painter::new(self.recorder).blurred_rounded_rect(BlurredRoundedRect { + transform, + rect, + color, + radius, + std_dev: blur_radius, + composite: Composite::default(), + }); + return; + } + + Painter::new(self.recorder) + .fill(geometry_ref_from_shape(path), &brush) + .transform(transform) + .draw(); } pub fn push_layer( @@ -672,7 +698,29 @@ impl PaintCx<'_> { props: &floem_renderer::text::GlyphRunProps<'a>, glyphs: impl Iterator + 'a, ) { - self.recorder.draw_glyphs(origin, props, glyphs); + let brush = props.brush.to_owned(); + let glyphs = glyphs + .map(|glyph| imaging::record::Glyph { + id: glyph.id, + x: glyph.x, + y: glyph.y, + }) + .collect::>(); + let transform = self.recorder.transform() + * Affine::translate((origin.x, origin.y)) + * props.transform; + Painter::new(self.recorder) + .glyphs(&props.font, &brush) + .transform(transform) + .glyph_transform(props.glyph_transform) + .font_size(props.font_size) + .hint(props.hint) + .normalized_coords(props.normalized_coords) + .composite(Composite::new( + peniko::BlendMode::default(), + props.brush_alpha, + )) + .draw(&props.style.to_owned(), &glyphs); } pub fn draw_svg<'b>( @@ -688,10 +736,6 @@ impl PaintCx<'_> { self.recorder.set_transform(transform); } - pub fn set_z_index(&mut self, z_index: i32) { - self.recorder.set_z_index(z_index); - } - pub fn is_vger(&self) -> bool { self.is_vger } @@ -778,3 +822,25 @@ impl PaintState { self.renderer_mut().set_scale(scale); } } + +fn geometry_ref_from_shape(shape: &impl Shape) -> GeometryRef<'static> { + if let Some(rect) = shape.as_rect() { + return GeometryRef::Rect(rect); + } + if let Some(rect) = shape.as_rounded_rect() { + return GeometryRef::RoundedRect(rect); + } + GeometryRef::OwnedPath(shape.to_path(0.1)) +} + +fn blurred_rounded_rect(shape: &impl Shape) -> Option<(Rect, f64)> { + if let Some(rect) = shape.as_rect() { + return Some((rect, 0.0)); + } + let rect = shape.as_rounded_rect()?; + let radii = rect.radii(); + (radii.top_left == radii.top_right + && radii.top_left == radii.bottom_left + && radii.top_left == radii.bottom_right) + .then_some((rect.rect(), radii.top_left)) +} diff --git a/src/paint/renderer.rs b/src/paint/renderer.rs index 9716e6bb5..1dfbe9950 100644 --- a/src/paint/renderer.rs +++ b/src/paint/renderer.rs @@ -451,18 +451,6 @@ impl floem_renderer::Renderer for Renderer { } } - fn set_z_index(&mut self, z_index: i32) { - match self { - Renderer::Active(v) => { - v.set_z_index(z_index); - } - Renderer::TinySkia(v) => { - v.set_z_index(z_index); - } - Renderer::Uninitialized { .. } => {} - } - } - fn finish(&mut self) -> Option { match self { Renderer::Active(r) => r.finish(), diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index aa5f45449..8f11d4b57 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -1480,10 +1480,6 @@ impl Date: Fri, 20 Mar 2026 01:20:48 -0600 Subject: [PATCH 019/102] use box tree bounds --- src/window/state.rs | 48 +++++++++++---------------------------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/src/window/state.rs b/src/window/state.rs index c09aad529..e30a856db 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -4,8 +4,8 @@ use std::{cell::RefCell, collections::HashMap}; use crate::{ action::exec_after_animation_frame, compositor::{ - Compositor, CompositorAlphaMode, CompositorLayerDescriptor, CompositorLayerId, CompositorTiming, - ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, + Compositor, CompositorAlphaMode, CompositorLayerDescriptor, CompositorLayerId, + CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FloemSurfaceRoleVisitor, FrameRequestReason, ResolvedPromotedLayer, backend::CompositorBackend, }, @@ -1318,42 +1318,16 @@ impl WindowState { &self, element_id: ElementId, scale: f64, - render_size: Size, ) -> Option { let box_tree = self.box_tree.borrow(); - let mut clip_bounds = None::; - let mut cursor = Some(element_id.0); - - while let Some(node_id) = cursor { - if let Some(local_clip) = box_tree.local_clip(node_id).flatten() - && let Some(world_transform) = box_tree.world_transform(node_id) - { - // Promoted layers must respect the full ancestor clip chain, not just their - // own viewport clip. Constrain infinite clips against the current render - // surface before converting them into compositor bounds so nested scrolls - // cannot allocate unbounded layer textures. - let constrained_clip = constrain_rect_to_render_bounds( - local_clip.rect(), - world_transform.then_scale(scale), - render_size, - ); - let world_clip_bounds = world_transform.transform_rect_bbox(constrained_clip); - let next_clip_bounds = Rect::new( - world_clip_bounds.x0 * scale, - world_clip_bounds.y0 * scale, - world_clip_bounds.x1 * scale, - world_clip_bounds.y1 * scale, - ); - clip_bounds = Some(match clip_bounds { - Some(current) => current.intersect(next_clip_bounds), - None => next_clip_bounds, - }); - } - - cursor = box_tree.parent_of(node_id); - } - - clip_bounds + box_tree.world_bounds(element_id.0).map(|bounds| { + Rect::new( + bounds.x0 * scale, + bounds.y0 * scale, + bounds.x1 * scale, + bounds.y1 * scale, + ) + }) } pub fn sync_display_list_promoted_layers(&mut self) { @@ -1374,7 +1348,7 @@ impl WindowState { .filter_map(|promoted| { let box_tree = self.box_tree.borrow(); let accumulated_clip_bounds = - self.accumulated_clip_bounds_for_element(promoted.element_id, scale, render_size); + self.accumulated_clip_bounds_for_element(promoted.element_id, scale); let mut bounds = scale_rect( promoted From 5c9b39d257916843dd87a805fc0ef50313ceadee Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Fri, 20 Mar 2026 13:21:44 -0600 Subject: [PATCH 020/102] differentiate layer kinds --- src/compositor/mod.rs | 105 +++++++++++++++++++++++++++++++---- src/compositor/subduction.rs | 39 +++++++++---- src/window/handle.rs | 3 +- src/window/state.rs | 19 +++++-- 4 files changed, 137 insertions(+), 29 deletions(-) diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index 8a9086078..8d85acff2 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -171,11 +171,76 @@ pub enum CompositorLayerKind { }, } +/// How a compositor layer is realized by the compositor/backend. +/// +/// This is intentionally separate from [`CompositorLayerKind`]. A Floem-painted +/// layer, an external surface layer, and a mixed layer can all be realized as a +/// texture-backed compositor surface or as a platform/native layer depending on +/// backend capabilities and the use case. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CompositorLayerBacking { + /// Backed by an internal compositor texture/surface that Floem can raster + /// into and composite with normal texture-space operations. + #[default] + TextureBacked, + /// Backed by a platform/native compositor layer or externally managed + /// presentable surface. + PlatformSurface, +} + +/// Capability summary for a realized compositor layer backing. +/// +/// Keeping these capabilities explicit avoids conflating texture-backed layers +/// with host/platform layers. The compositor can branch on actual support +/// instead of assuming all layers permit the same operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CompositorLayerCapabilities { + pub supports_opacity: bool, + pub supports_blend_mode: bool, + pub supports_filters: bool, + pub supports_clip_to_bounds: bool, + pub supports_direct_raster: bool, + pub supports_external_surface_import: bool, +} + +impl Default for CompositorLayerCapabilities { + fn default() -> Self { + Self::for_backing(CompositorLayerBacking::TextureBacked) + } +} + +impl CompositorLayerCapabilities { + #[must_use] + pub const fn for_backing(backing: CompositorLayerBacking) -> Self { + match backing { + CompositorLayerBacking::TextureBacked => Self { + supports_opacity: true, + supports_blend_mode: true, + supports_filters: true, + supports_clip_to_bounds: true, + supports_direct_raster: true, + supports_external_surface_import: true, + }, + CompositorLayerBacking::PlatformSurface => Self { + supports_opacity: false, + supports_blend_mode: false, + supports_filters: false, + supports_clip_to_bounds: true, + supports_direct_raster: false, + supports_external_surface_import: true, + }, + } + } +} + /// Retained descriptor for a compositor layer. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct CompositorLayerDescriptor { pub kind: CompositorLayerKind, + pub backing: CompositorLayerBacking, + pub capabilities: CompositorLayerCapabilities, pub bounds: Rect, + pub compositor_clip: Option, pub z_index: i32, pub compositing_depth: u32, pub opacity: f32, @@ -230,8 +295,10 @@ pub(crate) struct ResolvedPromotedLayer { pub element_id: ElementId, pub bounds: Rect, pub raster_clip: Option, + pub compositor_clip: Option, pub z_index: i32, pub compositing_depth: u32, + pub backing: CompositorLayerBacking, pub isolated: bool, pub alpha_mode: CompositorAlphaMode, } @@ -247,7 +314,6 @@ pub struct Compositor { layers: FxHashMap, external_surfaces: FxHashMap, promoted_layers: FxHashMap, - promoted_layer_raster_clips: FxHashMap, root_layer: Option, overlay_layer: Option, pending_frame_reasons: Vec, @@ -263,7 +329,6 @@ impl std::fmt::Debug for Compositor { .field("layers", &self.layers) .field("external_surfaces", &self.external_surfaces) .field("promoted_layers", &self.promoted_layers) - .field("promoted_layer_raster_clips", &self.promoted_layer_raster_clips) .field("root_layer", &self.root_layer) .field("overlay_layer", &self.overlay_layer) .field("pending_frame_reasons", &self.pending_frame_reasons) @@ -311,12 +376,14 @@ impl Compositor { /// layer candidates. pub(crate) fn sync_promoted_layers(&mut self, promoted_layers: &[ResolvedPromotedLayer]) { let mut active = FxHashMap::default(); - self.promoted_layer_raster_clips.clear(); for promoted in promoted_layers { let descriptor = CompositorLayerDescriptor { kind: CompositorLayerKind::FloemPainted, + backing: promoted.backing, + capabilities: CompositorLayerCapabilities::for_backing(promoted.backing), bounds: promoted.bounds, + compositor_clip: promoted.compositor_clip, z_index: promoted.z_index, compositing_depth: promoted.compositing_depth, opacity: 1.0, @@ -335,10 +402,6 @@ impl Compositor { }; active.insert(promoted.element_id, layer_id); - if let Some(raster_clip) = promoted.raster_clip { - self.promoted_layer_raster_clips - .insert(promoted.element_id, raster_clip); - } } let stale: Vec<_> = self @@ -352,7 +415,12 @@ impl Compositor { layer_id, CompositorLayerDescriptor { kind: CompositorLayerKind::FloemPainted, + backing: CompositorLayerBacking::TextureBacked, + capabilities: CompositorLayerCapabilities::for_backing( + CompositorLayerBacking::TextureBacked, + ), bounds: Rect::ZERO, + compositor_clip: None, z_index: 0, compositing_depth: 0, opacity: 0.0, @@ -368,7 +436,12 @@ impl Compositor { pub(crate) fn ensure_root_layer(&mut self, bounds: Rect) -> CompositorLayerId { let descriptor = CompositorLayerDescriptor { kind: CompositorLayerKind::FloemPainted, + backing: CompositorLayerBacking::TextureBacked, + capabilities: CompositorLayerCapabilities::for_backing( + CompositorLayerBacking::TextureBacked, + ), bounds, + compositor_clip: None, z_index: i32::MIN, compositing_depth: 0, opacity: 1.0, @@ -390,7 +463,12 @@ impl Compositor { pub(crate) fn ensure_overlay_layer(&mut self, bounds: Rect) -> CompositorLayerId { let descriptor = CompositorLayerDescriptor { kind: CompositorLayerKind::FloemPainted, + backing: CompositorLayerBacking::TextureBacked, + capabilities: CompositorLayerCapabilities::for_backing( + CompositorLayerBacking::TextureBacked, + ), bounds, + compositor_clip: None, z_index: i32::MAX, compositing_depth: 0, opacity: 1.0, @@ -419,6 +497,15 @@ impl Compositor { let mut visit_backend: &mut FloemPaintedSurfaceVisitor<'_> = &mut |layer_id, bounds, size, view| { + let Some(layer_state) = self.layers.get(&layer_id) else { + return; + }; + if !matches!(layer_state.descriptor.kind, CompositorLayerKind::FloemPainted) + || !layer_state.descriptor.capabilities.supports_direct_raster + { + return; + } + let role = if self.root_layer == Some(layer_id) { Some(FloemPaintedSurfaceRole::Root) } else if self.overlay_layer == Some(layer_id) { @@ -443,10 +530,6 @@ impl Compositor { self.promoted_layers.keys().copied().collect() } - pub(crate) fn promoted_layer_raster_clip(&self, element_id: ElementId) -> Option { - self.promoted_layer_raster_clips.get(&element_id).copied() - } - /// Returns the installed backend name, if any. #[must_use] pub fn backend_name(&self) -> Option<&'static str> { diff --git a/src/compositor/subduction.rs b/src/compositor/subduction.rs index cc40e44f4..08e1921c6 100644 --- a/src/compositor/subduction.rs +++ b/src/compositor/subduction.rs @@ -23,8 +23,9 @@ use subduction_core::{ use subduction_backend_wgpu::WgpuPresenter; use super::{ - CompositorAlphaMode, CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, - ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, + CompositorAlphaMode, CompositorLayerBacking, CompositorLayerDescriptor, CompositorLayerId, + CompositorLayerKind, CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, + ExternalSurfaceId, FrameRequestReason, backend::{CompositorBackend, FloemPaintedSurfaceVisitor}, }; @@ -142,8 +143,14 @@ impl SubductionCompositorBackend { }, ); self.store.set_opacity(layer_id, descriptor.opacity); - self.store - .set_clip(layer_id, clip_shape_for_bounds(bounds, descriptor.isolated)); + self.store.set_clip( + layer_id, + clip_shape_for_bounds( + bounds, + descriptor.compositor_clip, + descriptor.isolated && descriptor.capabilities.supports_clip_to_bounds, + ), + ); self.store .set_flags(layer_id, LayerFlags { hidden: descriptor.opacity <= 0.0 || is_empty }); let content = self.content_for_kind(id, descriptor.kind); @@ -158,8 +165,14 @@ impl SubductionCompositorBackend { } fn content_for_kind(&mut self, id: CompositorLayerId, kind: CompositorLayerKind) -> Option { + let backing = self + .layer_descriptors + .get(&id) + .map(|descriptor| descriptor.backing) + .unwrap_or(CompositorLayerBacking::TextureBacked); match kind { - CompositorLayerKind::FloemPainted => Some(self.floem_surface_id_for(id)), + CompositorLayerKind::FloemPainted => matches!(backing, CompositorLayerBacking::TextureBacked) + .then(|| self.floem_surface_id_for(id)), CompositorLayerKind::ExternalSurface { surface_id } | CompositorLayerKind::Mixed { surface_id } => self.surface_ids.get(&surface_id).copied(), } @@ -349,18 +362,22 @@ impl CompositorBackend for SubductionCompositorBackend { } } -fn clip_shape_for_bounds(bounds: Rect, isolated: bool) -> Option { +fn clip_shape_for_bounds( + bounds: Rect, + compositor_clip: Option, + isolated: bool, +) -> Option { if !isolated || !bounds.is_finite() { return None; } - let width = bounds.width(); - let height = bounds.height(); + let clip = compositor_clip.unwrap_or(bounds); + let local_clip = clip.with_origin(clip.origin() - bounds.origin().to_vec2()); + let width = local_clip.width(); + let height = local_clip.height(); if width <= 0.0 || height <= 0.0 { return None; } - // `subduction` currently exposes only per-layer clips, so use the layer's - // local bounds as an isolation clip when requested. - Some(ClipShape::RoundedRect(RoundedRect::from_rect(bounds.with_origin((0.0, 0.0)), 0.0))) + Some(ClipShape::RoundedRect(RoundedRect::from_rect(local_clip, 0.0))) } diff --git a/src/window/handle.rs b/src/window/handle.rs index 37982d7c4..4b5d53cd5 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -988,7 +988,8 @@ impl WindowHandle { let promoted_raster_clip = match role { crate::compositor::FloemPaintedSurfaceRole::Promoted(element_id) => { - cx.window_state.promoted_compositor_layer_raster_clip(element_id) + cx.window_state + .current_promoted_raster_clip(element_id, bounds) } _ => None, }; diff --git a/src/window/state.rs b/src/window/state.rs index e30a856db..4039610d2 100644 --- a/src/window/state.rs +++ b/src/window/state.rs @@ -4,10 +4,10 @@ use std::{cell::RefCell, collections::HashMap}; use crate::{ action::exec_after_animation_frame, compositor::{ - Compositor, CompositorAlphaMode, CompositorLayerDescriptor, CompositorLayerId, - CompositorTiming, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, - FloemSurfaceRoleVisitor, FrameRequestReason, ResolvedPromotedLayer, - backend::CompositorBackend, + Compositor, CompositorAlphaMode, CompositorLayerBacking, CompositorLayerDescriptor, + CompositorLayerId, CompositorTiming, ExternalSurfaceDescriptor, + ExternalSurfaceHandle, ExternalSurfaceId, FloemSurfaceRoleVisitor, FrameRequestReason, + ResolvedPromotedLayer, backend::CompositorBackend, }, inspector::CaptureState, platform::menu_types::MenuId, @@ -1412,8 +1412,10 @@ impl WindowState { element_id: promoted.element_id, bounds, raster_clip, + compositor_clip: raster_clip, z_index: promoted.z_index, compositing_depth, + backing: CompositorLayerBacking::TextureBacked, isolated, alpha_mode: CompositorAlphaMode::Straight, }) @@ -1440,8 +1442,13 @@ impl WindowState { self.compositor.promoted_layer_ids() } - pub fn promoted_compositor_layer_raster_clip(&self, element_id: ElementId) -> Option { - self.compositor.promoted_layer_raster_clip(element_id) + pub fn current_promoted_raster_clip( + &self, + element_id: ElementId, + layer_bounds: Rect, + ) -> Option { + self.accumulated_clip_bounds_for_element(element_id, self.effective_scale()) + .map(|clip| clip.intersect(layer_bounds)) } pub fn compositor_backend_name(&self) -> Option<&'static str> { From 32d8c9915aea5d8688b6a5399dff900d0b0e12f2 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Fri, 20 Mar 2026 22:44:26 -0600 Subject: [PATCH 021/102] changes --- Cargo.lock | 3 +- Cargo.toml | 2 +- examples/widget-gallery/src/canvas.rs | 162 +++++----- renderer/Cargo.toml | 1 + renderer/src/lib.rs | 2 +- renderer/src/text/mod.rs | 103 +------ skia/src/lib.rs | 22 +- src/inspector/view.rs | 30 +- src/lib.rs | 1 + src/paint/display_list.rs | 413 ++++++++++++-------------- src/paint/mod.rs | 176 ++--------- src/paint/renderer.rs | 19 +- src/style/transition.rs | 60 ++-- src/style/values.rs | 108 ++++--- src/text/layout.rs | 54 +++- src/text/mod.rs | 2 +- src/view/mod.rs | 115 ++++--- src/views/editor/gutter.rs | 5 +- src/views/editor/view.rs | 61 ++-- src/views/img.rs | 20 +- src/views/label.rs | 2 +- src/views/resizable.rs | 2 +- src/views/scroll.rs | 14 +- src/views/slider.rs | 36 +-- src/views/text_input.rs | 100 +++---- src/views/toggle_button.rs | 4 +- tiny_skia/src/lib.rs | 20 +- vello/src/lib.rs | 22 +- vger/src/lib.rs | 22 +- 29 files changed, 722 insertions(+), 859 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12e226dd5..ee26efd2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1867,6 +1867,7 @@ dependencies = [ "floem-winit", "fontique", "futures", + "imaging", "parking_lot", "parley", "peniko", @@ -3000,7 +3001,7 @@ checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" [[package]] name = "imaging" version = "0.0.1" -source = "git+https://github.com/forest-rs/imaging?rev=eacda40b585058fde8e0cfcae031f93d8d8c25ec#eacda40b585058fde8e0cfcae031f93d8d8c25ec" +source = "git+https://github.com/forest-rs/imaging?rev=58c71b4b235a70a88536247839d50fb9caa013a9#58c71b4b235a70a88536247839d50fb9caa013a9" dependencies = [ "kurbo", "peniko", diff --git a/Cargo.toml b/Cargo.toml index efd79fbd6..3af298d2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ ui-events = { version = "0.3", features = ["kurbo"] } ui-events-winit = { path = "./ui-events-winit", package = "ui-events-floem-winit" } dpi = { version = "0.1.2", default-features = false } rustc-hash = "2.1" -imaging = { git = "https://github.com/forest-rs/imaging", rev = "eacda40b585058fde8e0cfcae031f93d8d8c25ec", package = "imaging" } +imaging = { git = "https://github.com/forest-rs/imaging", rev = "58c71b4b235a70a88536247839d50fb9caa013a9", package = "imaging" } [dependencies] slotmap = "1.0" diff --git a/examples/widget-gallery/src/canvas.rs b/examples/widget-gallery/src/canvas.rs index 366fc94ef..5b2f6b668 100644 --- a/examples/widget-gallery/src/canvas.rs +++ b/examples/widget-gallery/src/canvas.rs @@ -8,9 +8,9 @@ use floem::{ CustomEvent, DragConfig, DragEvent, DragSourceEvent, Event, EventPropagation, PointerCaptureEvent, }, - kurbo::{Affine, Circle, Point, Rect, Shape, Size, Stroke}, + kurbo::{Circle, Point, Rect, Shape, Size, Stroke}, peniko::{ - Gradient, Mix, + Brush, Compose, Gradient, Mix, color::{ AlphaColor, ColorSpaceTag::{self, LinearSrgb}, @@ -53,13 +53,15 @@ pub fn canvas_view() -> impl IntoView { if border_radius.borrow_mut().step(&now) { cx.window_state.schedule_paint(cx.target_id); } - cx.fill( - &Rect::ZERO - .with_size(size) - .to_rounded_rect(border_radius.borrow().get()), - color.get(), - 0., - ); + let brush = Brush::Solid(color.get()); + cx.painter + .fill( + Rect::ZERO + .with_size(size) + .to_rounded_rect(border_radius.borrow().get()), + &brush, + ) + .draw(); }) .style(|s| s.size(300, 100)), Button::new("toggle rounded corners") @@ -95,7 +97,8 @@ fn color_picker(color: RwSignal) -> impl IntoView { draw_transparency_checkerboard(cx, size, &rect_path); - cx.fill(&rect_path, base_color, 0.); + let brush = Brush::Solid(base_color); + cx.painter.fill(rect_path, &brush).draw(); }) .style(move |s| s.height_full().aspect_ratio(1.).border(1).border_radius(8)) .debug_name("final color"), @@ -120,32 +123,33 @@ fn two_d_picker(color: RwSignal) -> impl IntoView { } fn draw_transparency_checkerboard(cx: &mut PaintCx, size: Size, clip_path: &impl Shape) { - cx.push_layer(Mix::Normal, 1.0, Affine::IDENTITY, clip_path); - - let cell_size = 8.0; - let dark_color = css::LIGHT_GRAY; - let light_color = css::WHITE; - - let cols = (size.width / cell_size).ceil() as usize; - let rows = (size.height / cell_size).ceil() as usize; - - for row in 0..rows { - for col in 0..cols { - let is_dark = (row + col) % 2 == 0; - let color = if is_dark { dark_color } else { light_color }; - - let rect = Rect::new( - col as f64 * cell_size, - row as f64 * cell_size, - (col + 1) as f64 * cell_size, - (row + 1) as f64 * cell_size, - ); - - cx.fill(&rect, color, 0.0); + cx.painter.with_fill_clip(clip_path.to_path(0.1), |p| { + let cell_size = 8.0; + let dark_color = Brush::Solid(css::LIGHT_GRAY); + let light_color = Brush::Solid(css::WHITE); + + let cols = (size.width / cell_size).ceil() as usize; + let rows = (size.height / cell_size).ceil() as usize; + + for row in 0..rows { + for col in 0..cols { + let brush = if (row + col) % 2 == 0 { + &dark_color + } else { + &light_color + }; + + let rect = Rect::new( + col as f64 * cell_size, + row as f64 * cell_size, + (col + 1) as f64 * cell_size, + (row + 1) as f64 * cell_size, + ); + + p.fill(rect, brush).draw(); + } } - } - - cx.pop_layer(); + }); } pub struct SatValuePicker { @@ -260,30 +264,37 @@ impl View for SatValuePicker { let hue = self.current_color.components[0]; // base - cx.fill(&rect_path, css::WHITE, 0.); + let white = Brush::Solid(css::WHITE); + cx.painter.fill(rect_path, &white).draw(); // saturation gradient - let sat_gradient = Gradient::new_linear(Point::ZERO, Point::new(size.width, 0.0)) + let sat_gradient: Brush = Gradient::new_linear(Point::ZERO, Point::new(size.width, 0.0)) .with_stops([ (0.0, css::WHITE), (1.0, AlphaColor::::new([hue, 100., 50., 1.]).convert()), ]) - .with_interpolation_cs(ColorSpaceTag::LinearSrgb); + .with_interpolation_cs(ColorSpaceTag::LinearSrgb) + .into(); - cx.fill(&rect_path, &sat_gradient, 0.); + cx.painter.fill(rect_path, &sat_gradient).draw(); // value gradient - cx.push_layer(Mix::Multiply, 1.0, Affine::IDENTITY, &rect_path); - - let val_gradient = Gradient::new_linear(Point::ZERO, Point::new(0.0, size.height)) + let val_gradient: Brush = Gradient::new_linear(Point::ZERO, Point::new(0.0, size.height)) .with_stops([(0.0, Color::from_rgba8(0, 0, 0, 0)), (1.0, css::BLACK)]) - .with_interpolation_cs(ColorSpaceTag::LinearSrgb); - - cx.fill(&rect_path, &val_gradient, 0.); - - cx.pop_layer(); - - cx.clip(&rect_path); + .with_interpolation_cs(ColorSpaceTag::LinearSrgb) + .into(); + let group = floem::imaging::GroupRef::new() + .with_clip(floem::imaging::ClipRef::fill(rect_path)) + .with_composite(floem::imaging::Composite::new( + floem::peniko::BlendMode { + mix: Mix::Multiply, + compose: Compose::SrcOver, + }, + 1.0, + )); + cx.painter.with_group(group, |p| { + p.fill(rect_path, &val_gradient).draw(); + }); if size.width > 0.0 && size.height > 0.0 { // Larger indicator @@ -294,16 +305,17 @@ impl View for SatValuePicker { let inner = Circle::new(self.point, inner_radius); // fill center with the selected color - cx.fill(&inner, self.current_color.convert::(), 0.); - - // white outline for contrast - cx.stroke(&outer, css::WHITE, &Stroke::new(2.0)); - - // subtle black inner stroke so it works on light backgrounds - cx.stroke(&inner, css::BLACK, &Stroke::new(1.0)); + let inner_fill = Brush::Solid(self.current_color.convert::()); + let white = Brush::Solid(css::WHITE); + let black = Brush::Solid(css::BLACK); + cx.painter.with_fill_clip(rect_path, |p| { + p.fill(inner.to_path(0.1), &inner_fill).draw(); + p.stroke(outer.to_path(0.1), &Stroke::new(2.0), &white) + .draw(); + p.stroke(inner.to_path(0.1), &Stroke::new(1.0), &black) + .draw(); + }); } - - cx.pop_clip(); } } @@ -405,7 +417,7 @@ impl View for HuePicker { fn paint(&mut self, cx: &mut PaintCx) { let size = self.size; let rect_path = Rect::ZERO.with_size(size).to_rounded_rect(8.); - let hue_gradient = Gradient::new_linear( + let hue_gradient: Brush = Gradient::new_linear( Point::new(0.0, size.height / 2.0), Point::new(size.width, size.height / 2.0), ) @@ -414,9 +426,10 @@ impl View for HuePicker { (1.0, AlphaColor::::new([360.0, 100.0, 50.0, 1.0])), ]) .with_hue_direction(floem::peniko::color::HueDirection::Longer) - .with_interpolation_cs(ColorSpaceTag::Hsl); + .with_interpolation_cs(ColorSpaceTag::Hsl) + .into(); - cx.fill(&rect_path, &hue_gradient, 0.); + cx.painter.fill(rect_path, &hue_gradient).draw(); if size.width > 0.0 { let hue = self.current_color.components[0]; let x_pos = hue as f64 / 360. * size.width; @@ -429,10 +442,12 @@ impl View for HuePicker { size.height, ); - cx.clip(&rect_path); - cx.stroke(&indicator_rect, css::WHITE, &Stroke::new(2.0)); - cx.fill(&indicator_rect, css::BLACK, 0.); - cx.pop_clip(); + let white = Brush::Solid(css::WHITE); + let black = Brush::Solid(css::BLACK); + cx.painter.with_fill_clip(rect_path, |p| { + p.stroke(indicator_rect, &Stroke::new(2.0), &white).draw(); + p.fill(indicator_rect, &black).draw(); + }); } } } @@ -534,7 +549,7 @@ impl View for OpacityPicker { draw_transparency_checkerboard(cx, size, &rect_path); - let opacity_gradient = Gradient::new_linear( + let opacity_gradient: Brush = Gradient::new_linear( Point::new(0.0, size.height / 2.0), Point::new(size.width, size.height / 2.0), ) @@ -542,9 +557,10 @@ impl View for OpacityPicker { (0.0, self.current_color.with_alpha(0.0)), (1.0, self.current_color.with_alpha(1.0)), ]) - .with_interpolation_cs(LinearSrgb); + .with_interpolation_cs(LinearSrgb) + .into(); - cx.fill(&rect_path, &opacity_gradient, 0.); + cx.painter.fill(rect_path, &opacity_gradient).draw(); if size.width > 0.0 { let alpha = self.current_color.components[3]; @@ -558,10 +574,12 @@ impl View for OpacityPicker { size.height, ); - cx.clip(&rect_path); - cx.stroke(&indicator_rect, css::WHITE, &Stroke::new(2.0)); - cx.fill(&indicator_rect, css::BLACK, 0.); - cx.pop_clip(); + let white = Brush::Solid(css::WHITE); + let black = Brush::Solid(css::BLACK); + cx.painter.with_fill_clip(rect_path, |p| { + p.stroke(indicator_rect, &Stroke::new(2.0), &white).draw(); + p.fill(indicator_rect, &black).draw(); + }); } } } diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 4bd8c0c8b..b2f221618 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -14,6 +14,7 @@ parley = { workspace = true } fontique = { workspace = true } winit = { workspace = true } wgpu = { workspace = true } +imaging = { workspace = true } crossbeam = { version = "0.8", optional = true } futures = "0.3.32" diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 7a4b35815..cec183f06 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -178,7 +178,7 @@ pub trait Renderer { fn draw_glyphs<'a>( &mut self, origin: Point, - props: &text::GlyphRunProps<'a>, + run: &text::GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ); diff --git a/renderer/src/text/mod.rs b/renderer/src/text/mod.rs index 65bca17fa..4836608bc 100644 --- a/renderer/src/text/mod.rs +++ b/renderer/src/text/mod.rs @@ -9,13 +9,9 @@ mod attrs; -use peniko::{ - BrushRef, Fill, FontData, StyleRef, - kurbo::{Affine, Point}, -}; - pub use attrs::{Attrs, AttrsList, AttrsOwned, FamilyOwned, LineHeightValue}; pub use fontique::{FontStyle, FontWeight, FontWidth}; +pub use imaging::{GlyphRunRef, NormalizedCoord}; pub use parley::layout::Glyph; // --- Brush type for Parley --- @@ -47,89 +43,11 @@ impl From for peniko::Color { } } -/// Variable font design-space coordinate. -pub type NormalizedCoord = i16; - -/// Rendering properties shared by a glyph run. -#[derive(Clone, Debug)] -pub struct GlyphRunProps<'a> { - pub font: FontData, - pub font_size: f32, - pub hint: bool, - pub normalized_coords: &'a [NormalizedCoord], - pub style: StyleRef<'a>, - pub brush: BrushRef<'a>, - pub brush_alpha: f32, - pub transform: Affine, - pub glyph_transform: Option, -} - -impl<'a> GlyphRunProps<'a> { - pub fn new(font: &FontData) -> Self { - Self { - font: font.clone(), - font_size: 16.0, - hint: false, - normalized_coords: &[], - style: Fill::NonZero.into(), - brush: peniko::color::palette::css::BLACK.into(), - brush_alpha: 1.0, - transform: Affine::IDENTITY, - glyph_transform: None, - } - } - - pub fn font(mut self, font: &FontData) -> Self { - self.font = font.clone(); - self - } - - pub fn font_size(mut self, font_size: f32) -> Self { - self.font_size = font_size; - self - } - - pub fn hint(mut self, hint: bool) -> Self { - self.hint = hint; - self - } - - pub fn normalized_coords(mut self, normalized_coords: &'a [NormalizedCoord]) -> Self { - self.normalized_coords = normalized_coords; - self - } - - pub fn style(mut self, style: impl Into>) -> Self { - self.style = style.into(); - self - } - - pub fn brush(mut self, brush: impl Into>) -> Self { - self.brush = brush.into(); - self - } - - pub fn brush_alpha(mut self, brush_alpha: f32) -> Self { - self.brush_alpha = brush_alpha; - self - } - - pub fn transform(mut self, transform: Affine) -> Self { - self.transform = transform; - self - } - - pub fn glyph_transform(mut self, glyph_transform: Option) -> Self { - self.glyph_transform = glyph_transform; - self - } -} - pub trait GlyphDrawer { fn draw_glyphs<'a>( &mut self, - origin: Point, - props: &GlyphRunProps<'a>, + origin: peniko::kurbo::Point, + run: &GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ); } @@ -149,12 +67,13 @@ mod tests { #[test] fn text_glyphs_props_default_is_usable() { - let font = FontData::new(peniko::Blob::new(std::sync::Arc::new([])), 0); - let props = GlyphRunProps::new(&font); - assert_eq!(props.font, font); - assert_eq!(props.font_size, 16.0); - assert_eq!(props.brush_alpha, 1.0); - assert_eq!(props.transform, Affine::IDENTITY); - assert!(props.normalized_coords.is_empty()); + let font = peniko::FontData::new(peniko::Blob::new(std::sync::Arc::new([])), 0); + let style = peniko::Style::Fill(peniko::Fill::NonZero); + let run = GlyphRunRef::new(&font, &style, peniko::color::palette::css::BLACK); + assert_eq!(run.font, &font); + assert_eq!(run.font_size, 16.0); + assert_eq!(run.composite.alpha, 1.0); + assert_eq!(run.transform, peniko::kurbo::Affine::IDENTITY); + assert!(run.normalized_coords.is_empty()); } } diff --git a/skia/src/lib.rs b/skia/src/lib.rs index af33e8837..4d8ebfc78 100644 --- a/skia/src/lib.rs +++ b/skia/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{Result, anyhow}; use anyrender::{ImageRenderer as _, WindowRenderer as _}; use anyrender_skia::{SkiaImageRenderer, SkiaWindowRenderer}; -use floem_renderer::text::{Glyph, GlyphRunProps}; +use floem_renderer::text::{Glyph, GlyphRunRef}; use floem_renderer::{Img, Renderer, Svg}; use peniko::kurbo::{Affine, BezPath, Point, Rect, Shape, Size, Stroke}; use peniko::{ @@ -427,20 +427,20 @@ impl Renderer for SkiaRenderer { fn draw_glyphs<'a>( &mut self, origin: Point, - props: &GlyphRunProps<'a>, + run: &GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ) { - let transform = self.transform * Affine::translate((origin.x, origin.y)) * props.transform; + let transform = self.transform * Affine::translate((origin.x, origin.y)) * run.transform; self.commands.push(Command::Glyphs { - font: props.font.clone(), - font_size: props.font_size, - hint: props.hint, - normalized_coords: props.normalized_coords.to_vec(), - style: props.style.to_owned(), - brush: props.brush.to_owned(), - brush_alpha: props.brush_alpha, + font: run.font.clone(), + font_size: run.font_size, + hint: run.hint, + normalized_coords: run.normalized_coords.to_vec(), + style: run.style.clone(), + brush: run.brush.to_owned(), + brush_alpha: run.composite.alpha, transform, - glyph_transform: props.glyph_transform, + glyph_transform: run.glyph_transform, glyphs: glyphs .map(|glyph| anyrender::Glyph { id: glyph.id, diff --git a/src/inspector/view.rs b/src/inspector/view.rs index 36058c891..1471dbc67 100644 --- a/src/inspector/view.rs +++ b/src/inspector/view.rs @@ -22,7 +22,7 @@ use crate::{ }; use floem_reactive::{Effect, Memo, RwSignal, SignalGet, SignalUpdate}; use peniko::{ - Color, + Brush, Color, color::palette::{self, css}, kurbo::{Rect, Stroke}, }; @@ -804,22 +804,26 @@ impl View for InspectorImageView { fn post_paint(&mut self, cx: &mut crate::paint::PaintCx) { if cx.target_id == self.id { if let Some(selected_overlay) = self.overlay_rect(self.capture_view.selected.get()) { - cx.fill(&selected_overlay, self.selected_overlay_color, 0.); - cx.stroke( - &selected_overlay, - self.selected_overlay_border_color, - &Stroke::new(1.0), - ); + let fill = Brush::Solid(self.selected_overlay_color); + let stroke = Brush::Solid(self.selected_overlay_border_color); + cx.painter + .fill(selected_overlay, &fill) + .draw(); + cx.painter + .stroke(selected_overlay, &Stroke::new(1.0), &stroke) + .draw(); } if let Some(highlighted_overlay) = self.overlay_rect(self.capture_view.highlighted.get()) { - cx.fill(&highlighted_overlay, self.highlighted_overlay_color, 0.); - cx.stroke( - &highlighted_overlay, - self.highlighted_overlay_border_color, - &Stroke::new(1.0), - ); + let fill = Brush::Solid(self.highlighted_overlay_color); + let stroke = Brush::Solid(self.highlighted_overlay_border_color); + cx.painter + .fill(highlighted_overlay, &fill) + .draw(); + cx.painter + .stroke(highlighted_overlay, &Stroke::new(1.0), &stroke) + .draw(); } } } diff --git a/src/lib.rs b/src/lib.rs index aece88fac..d4d4ecbbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,3 +282,4 @@ pub mod prelude { pointer::{PointerButtonEvent, PointerEvent}, }; } +pub use imaging; diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index 68c28f37c..c944dbb53 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -151,15 +151,14 @@ use std::sync::Arc; -use floem_renderer::text::{Glyph, GlyphRunProps}; +use floem_renderer::text::{Glyph, GlyphRunRef}; use floem_renderer::{Img, Renderer as FloemRenderer, Svg, usvg}; use imaging::{ - BlurredRoundedRect, ClipRef, Composite, FillRef, GlyphRunRef, GroupRef, PaintSink, - StrokeRef, - record::{Clip, Draw, Geometry, GlyphRun, Group}, + BlurredRoundedRect, ClipRef, FillRef, GroupRef, PaintSink, StrokeRef, + record::{Clip, Draw, Geometry, Glyph as ImagingGlyph, GlyphRun, Group}, }; +use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Shape, Size}; use peniko::{BrushRef, Fill}; -use peniko::kurbo::{Affine, BezPath, Point, Rect, RoundedRect, Shape, Size}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ElementId, paint::PaintOrPost}; @@ -231,20 +230,25 @@ pub(crate) struct OwnedSvg { #[derive(Clone)] pub(crate) enum DisplayCommand { - PushClip { clip: Clip }, + PushClip { + clip: Clip, + }, PopClip, - PushLayer { + PushGroup { group: Group, - transform: Affine, }, - PopLayer, - Draw { draw: Draw }, + PopGroup, + Draw { + draw: Draw, + }, + #[allow(dead_code)] DrawImage { img: peniko::ImageBrush, hash: Arc<[u8]>, rect: Rect, transform: Affine, }, + #[allow(dead_code)] DrawSvg { svg: OwnedSvg, rect: Rect, @@ -483,7 +487,9 @@ impl PaintChunk { let Some(bounds) = self.visible_bounds(property_tree) else { return true; }; - damage.iter().any(|rect| rect.intersect(bounds).area() > 0.0) + damage + .iter() + .any(|rect| rect.intersect(bounds).area() > 0.0) } fn visible_bounds(&self, property_tree: &PaintPropertyTree) -> Option { @@ -580,17 +586,13 @@ impl RetainedDisplayList { } } -pub(crate) struct RecordingRenderer<'a> { +pub struct RecordingRenderer<'a> { commands: &'a mut Vec, - transform: Affine, } impl<'a> RecordingRenderer<'a> { - pub fn new(commands: &'a mut Vec) -> Self { - Self { - commands, - transform: Affine::IDENTITY, - } + pub(crate) fn new(commands: &'a mut Vec) -> Self { + Self { commands } } fn record_draw(&mut self, draw: Draw) { @@ -599,44 +601,12 @@ impl<'a> RecordingRenderer<'a> { } impl RecordingRenderer<'_> { - pub fn transform(&self) -> Affine { - self.transform - } - - pub fn set_transform(&mut self, transform: Affine) { - self.transform = transform; - } - - pub fn push_layer( - &mut self, - blend: impl Into, - alpha: f32, - transform: Affine, - clip: &impl Shape, - ) { - let shape = shape_to_geometry(clip); - self.commands.push(DisplayCommand::PushLayer { - group: Group { - clip: Some(Clip::Fill { - transform: self.transform, - shape, - fill_rule: Fill::NonZero, - }), - filters: Vec::new(), - composite: Composite::new(blend.into(), alpha), - }, - transform, - }); - } - - pub fn pop_layer(&mut self) { - self.commands.push(DisplayCommand::PopLayer); - } - + #[allow(dead_code)] pub fn draw_svg<'b>( &mut self, svg: Svg<'b>, rect: Rect, + transform: Affine, brush: Option>>, ) { self.commands.push(DisplayCommand::DrawSvg { @@ -645,24 +615,27 @@ impl RecordingRenderer<'_> { hash: Arc::from(svg.hash.to_vec()), }, rect, - transform: self.transform, + transform, brush: brush.map(|brush| brush.into().to_owned()), }); } - pub fn draw_img(&mut self, img: Img<'_>, rect: Rect) { + #[allow(dead_code)] + pub fn draw_img(&mut self, img: Img<'_>, rect: Rect, transform: Affine) { self.commands.push(DisplayCommand::DrawImage { img: img.img, hash: Arc::from(img.hash.to_vec()), rect, - transform: self.transform, + transform, }); } } impl PaintSink for RecordingRenderer<'_> { fn push_clip(&mut self, clip: ClipRef<'_>) { - self.commands.push(DisplayCommand::PushClip { clip: clip.to_owned() }); + self.commands.push(DisplayCommand::PushClip { + clip: clip.to_owned(), + }); } fn pop_clip(&mut self) { @@ -670,14 +643,13 @@ impl PaintSink for RecordingRenderer<'_> { } fn push_group(&mut self, group: GroupRef<'_>) { - self.commands.push(DisplayCommand::PushLayer { + self.commands.push(DisplayCommand::PushGroup { group: group.to_owned(), - transform: Affine::IDENTITY, }); } fn pop_group(&mut self) { - self.commands.push(DisplayCommand::PopLayer); + self.commands.push(DisplayCommand::PopGroup); } fn fill(&mut self, draw: FillRef<'_>) { @@ -688,8 +660,12 @@ impl PaintSink for RecordingRenderer<'_> { self.record_draw(draw.to_owned()); } - fn glyph_run(&mut self, draw: GlyphRunRef<'_>) { - self.record_draw(Draw::GlyphRun(draw.to_owned())); + fn glyph_run( + &mut self, + draw: GlyphRunRef<'_>, + glyphs: &mut dyn Iterator, + ) { + self.record_draw(Draw::GlyphRun(draw.to_owned(glyphs))); } fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { @@ -713,7 +689,9 @@ pub(crate) fn replay_stage( let chunk_indices = local_damage.map(|damage| stage.chunk_indices_for_damage(damage)); for (index, chunk) in stage.chunks.iter().enumerate() { - if let Some(indices) = &chunk_indices && !indices.contains(&index) { + if let Some(indices) = &chunk_indices + && !indices.contains(&index) + { continue; } apply_clip_state( @@ -728,7 +706,7 @@ pub(crate) fn replay_stage( for command in &chunk.commands { match command { DisplayCommand::PushClip { .. } | DisplayCommand::PopClip => {} - DisplayCommand::PushLayer { group, transform } => { + DisplayCommand::PushGroup { group } => { let Some(Clip::Fill { transform: clip_transform, shape, @@ -737,39 +715,33 @@ pub(crate) fn replay_stage( else { continue; }; - set_transform_if_needed( - renderer, - base_transform * *clip_transform, - &mut current_transform, - ); - match sanitize_clip_geometry(shape, base_transform * *clip_transform, render_size) { + let layer_transform = base_transform * *clip_transform; + set_transform_if_needed(renderer, base_transform, &mut current_transform); + match sanitize_clip_geometry(shape, layer_transform, render_size) { Geometry::Rect(rect) => renderer.push_layer( group.composite.blend, group.composite.alpha, - *transform, + *clip_transform, &rect, ), Geometry::RoundedRect(rect) => renderer.push_layer( group.composite.blend, group.composite.alpha, - *transform, + *clip_transform, &rect, ), Geometry::Path(path) => renderer.push_layer( group.composite.blend, group.composite.alpha, - *transform, + *clip_transform, &path, ), } } - DisplayCommand::PopLayer => renderer.pop_layer(), - DisplayCommand::Draw { draw } => replay_draw( - renderer, - draw, - base_transform, - &mut current_transform, - ), + DisplayCommand::PopGroup => renderer.pop_layer(), + DisplayCommand::Draw { draw } => { + replay_draw(renderer, draw, base_transform, &mut current_transform) + } DisplayCommand::DrawImage { img, hash, @@ -848,7 +820,11 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa for command in commands { match command { DisplayCommand::PushClip { clip } => { - let transform_id = intern_transform(clip_transform(&clip), &mut property_tree, &mut transform_intern); + let transform_id = intern_transform( + clip_transform(&clip), + &mut property_tree, + &mut transform_intern, + ); let clip_id = ClipNodeId(property_tree.clips.len() as u32); property_tree.clips.push(ClipNode { parent: clip_stack.last().copied(), @@ -862,7 +838,7 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa clip_stack.pop(); properties.clip_id = clip_stack.last().copied().unwrap_or_default(); } - DisplayCommand::PushLayer { group, transform } => { + DisplayCommand::PushGroup { group } => { let effect_id = EffectNodeId(property_tree.effects.len() as u32); property_tree.effects.push(EffectNode { parent: effect_stack.last().copied(), @@ -871,26 +847,23 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa }); effect_stack.push(effect_id); properties.effect_id = effect_id; - properties.transform_id = - intern_transform(transform, &mut property_tree, &mut transform_intern); push_boundary_chunk( &mut chunks, properties, - command_transform_class(&DisplayCommand::PushLayer { + command_transform_class(&DisplayCommand::PushGroup { group: group.clone(), - transform, }), - DisplayCommand::PushLayer { group, transform }, + DisplayCommand::PushGroup { group }, ); } - DisplayCommand::PopLayer => { + DisplayCommand::PopGroup => { effect_stack.pop(); properties.effect_id = effect_stack.last().copied().unwrap_or_default(); push_boundary_chunk( &mut chunks, properties, - command_transform_class(&DisplayCommand::PopLayer), - DisplayCommand::PopLayer, + command_transform_class(&DisplayCommand::PopGroup), + DisplayCommand::PopGroup, ); } command => { @@ -911,8 +884,7 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa metadata: chunk_metadata, transform_class: chunk_transform_class, }) if *chunk_properties == properties => { - *chunk_transform_class = - chunk_transform_class.combine(transform_class); + *chunk_transform_class = chunk_transform_class.combine(transform_class); *chunk_bounds = union_rects(*chunk_bounds, bounds); *chunk_metadata = chunk_metadata.merge(metadata); chunk_commands.push(command); @@ -953,8 +925,8 @@ fn command_transform_class(command: &DisplayCommand) -> TransformClass { match command { DisplayCommand::PushClip { .. } | DisplayCommand::PopClip - | DisplayCommand::PushLayer { .. } - | DisplayCommand::PopLayer => TransformClass::Affine, + | DisplayCommand::PushGroup { .. } + | DisplayCommand::PopGroup => TransformClass::Affine, DisplayCommand::Draw { draw } => match draw { Draw::Fill { .. } | Draw::Stroke { .. } => TransformClass::Affine, Draw::GlyphRun(_) | Draw::BlurredRoundedRect(_) => TransformClass::TranslateOnly, @@ -969,10 +941,12 @@ fn command_bounds(command: &DisplayCommand) -> Option { match command { DisplayCommand::PushClip { .. } | DisplayCommand::PopClip - | DisplayCommand::PushLayer { .. } - | DisplayCommand::PopLayer => None, + | DisplayCommand::PushGroup { .. } + | DisplayCommand::PopGroup => None, DisplayCommand::Draw { draw, .. } => draw_bounds(draw), - DisplayCommand::DrawImage { rect, .. } | DisplayCommand::DrawSvg { rect, .. } => Some(*rect), + DisplayCommand::DrawImage { rect, .. } | DisplayCommand::DrawSvg { rect, .. } => { + Some(*rect) + } } } @@ -985,7 +959,9 @@ fn draw_bounds(draw: &Draw) -> Option { Some(bounds.inflate(inset, inset)) } Draw::GlyphRun(run) => glyph_run_bounds(run), - Draw::BlurredRoundedRect(rect) => Some(rect.rect.inflate(rect.std_dev * 3.0, rect.std_dev * 3.0)), + Draw::BlurredRoundedRect(rect) => { + Some(rect.rect.inflate(rect.std_dev * 3.0, rect.std_dev * 3.0)) + } } } @@ -1019,10 +995,10 @@ fn glyph_run_bounds(run: &GlyphRun) -> Option { fn command_metadata(command: &DisplayCommand) -> PaintChunkMetadata { match command { - DisplayCommand::PushClip { .. } - | DisplayCommand::PopClip - | DisplayCommand::PopLayer => PaintChunkMetadata::default(), - DisplayCommand::PushLayer { .. } => PaintChunkMetadata { + DisplayCommand::PushClip { .. } | DisplayCommand::PopClip | DisplayCommand::PopGroup => { + PaintChunkMetadata::default() + } + DisplayCommand::PushGroup { .. } => PaintChunkMetadata { requires_layer: true, ..PaintChunkMetadata::default() }, @@ -1095,11 +1071,9 @@ fn clip_node_bounds(node: &ClipNode) -> Option { fn command_affine(command: &DisplayCommand) -> Affine { match command { - DisplayCommand::PopClip | DisplayCommand::PopLayer => { - Affine::IDENTITY - } + DisplayCommand::PopClip | DisplayCommand::PopGroup => Affine::IDENTITY, DisplayCommand::PushClip { clip } => clip_transform(clip), - DisplayCommand::PushLayer { transform, .. } => *transform, + DisplayCommand::PushGroup { .. } => Affine::IDENTITY, DisplayCommand::Draw { draw, .. } => match draw { Draw::Fill { transform, .. } | Draw::Stroke { transform, .. } => *transform, Draw::GlyphRun(run) => run.transform, @@ -1262,21 +1236,21 @@ fn replay_draw( } } Draw::GlyphRun(run) => { - let props = GlyphRunProps { - font: run.font.clone(), + let run_ref = GlyphRunRef { + font: &run.font, + transform: run.transform, + glyph_transform: run.glyph_transform, font_size: run.font_size, hint: run.hint, normalized_coords: &run.normalized_coords, - style: (&run.style).into(), + style: &run.style, brush: (&run.brush).into(), - brush_alpha: run.composite.alpha, - transform: run.transform, - glyph_transform: run.glyph_transform, + composite: run.composite, }; set_transform_if_needed(renderer, base_transform, current_transform); renderer.draw_glyphs( Point::ZERO, - &props, + &run_ref, run.glyphs.iter().map(|glyph| Glyph { id: glyph.id, style_index: 0, @@ -1373,34 +1347,38 @@ fn constrain_infinite_rect(rect: Rect, transform: Affine, render_size: Size) -> #[allow(clippy::items_after_test_module)] mod tests { use super::*; + use imaging::Composite; use peniko::Color; #[test] fn stage_groups_adjacent_draws_with_matching_properties() { let rect = Rect::new(0.0, 0.0, 10.0, 10.0); let mut stage = ElementStage::default(); - stage.set_commands(vec![ - DisplayCommand::Draw { - draw: Draw::Fill { - transform: Affine::IDENTITY, - fill_rule: Fill::NonZero, - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), + stage.set_commands( + vec![ + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, }, - }, - DisplayCommand::Draw { - draw: Draw::Stroke { - transform: Affine::IDENTITY, - stroke: peniko::kurbo::Stroke::new(1.0), - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), + DisplayCommand::Draw { + draw: Draw::Stroke { + transform: Affine::IDENTITY, + stroke: peniko::kurbo::Stroke::new(1.0), + brush: Color::BLACK.into(), + brush_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, }, - }, - ], None); + ], + None, + ); assert_eq!(stage.chunks.len(), 1); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); @@ -1413,26 +1391,29 @@ mod tests { fn stage_tracks_clip_state_without_boundary_chunks() { let rect = Rect::new(0.0, 0.0, 10.0, 10.0); let mut stage = ElementStage::default(); - stage.set_commands(vec![ - DisplayCommand::PushClip { - clip: Clip::Fill { - transform: Affine::IDENTITY, - shape: Geometry::Rect(rect), - fill_rule: Fill::NonZero, + stage.set_commands( + vec![ + DisplayCommand::PushClip { + clip: Clip::Fill { + transform: Affine::IDENTITY, + shape: Geometry::Rect(rect), + fill_rule: Fill::NonZero, + }, }, - }, - DisplayCommand::Draw { - draw: Draw::Fill { - transform: Affine::IDENTITY, - fill_rule: Fill::NonZero, - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, }, - }, - DisplayCommand::PopClip, - ], None); + DisplayCommand::PopClip, + ], + None, + ); assert_eq!(stage.chunks.len(), 1); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); @@ -1444,28 +1425,31 @@ mod tests { fn stage_splits_draw_chunks_on_transform_state() { let rect = Rect::new(0.0, 0.0, 10.0, 10.0); let mut stage = ElementStage::default(); - stage.set_commands(vec![ - DisplayCommand::Draw { - draw: Draw::Fill { - transform: Affine::IDENTITY, - fill_rule: Fill::NonZero, - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), + stage.set_commands( + vec![ + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, }, - }, - DisplayCommand::Draw { - draw: Draw::Fill { - transform: Affine::translate((5.0, 0.0)), - fill_rule: Fill::NonZero, - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::translate((5.0, 0.0)), + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, }, - }, - ], None); + ], + None, + ); assert_eq!(stage.chunks.len(), 2); assert_ne!( @@ -1478,16 +1462,19 @@ mod tests { #[test] fn blurred_draws_downgrade_stage_transform_retention() { let mut stage = ElementStage::default(); - stage.set_commands(vec![DisplayCommand::Draw { - draw: Draw::BlurredRoundedRect(imaging::BlurredRoundedRect { - transform: Affine::IDENTITY, - rect: Rect::new(0.0, 0.0, 10.0, 10.0), - color: Color::BLACK, - radius: 4.0, - std_dev: 6.0, - composite: Composite::default(), - }), - }], None); + stage.set_commands( + vec![DisplayCommand::Draw { + draw: Draw::BlurredRoundedRect(imaging::BlurredRoundedRect { + transform: Affine::IDENTITY, + rect: Rect::new(0.0, 0.0, 10.0, 10.0), + color: Color::BLACK, + radius: 4.0, + std_dev: 6.0, + composite: Composite::default(), + }), + }], + None, + ); assert_eq!(stage.transform_class, TransformClass::TranslateOnly); assert!(stage.chunks[0].metadata.has_blur); @@ -1498,37 +1485,43 @@ mod tests { fn stage_damage_query_filters_chunks_by_bounds() { let rect = Rect::new(0.0, 0.0, 10.0, 10.0); let mut stage = ElementStage::default(); - stage.set_commands(vec![ - DisplayCommand::Draw { - draw: Draw::Fill { + stage.set_commands( + vec![ + DisplayCommand::Draw { + draw: Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: Fill::NonZero, + brush: Color::BLACK.into(), + brush_transform: None, + shape: Geometry::Rect(rect), + composite: Composite::default(), + }, + }, + DisplayCommand::DrawImage { + img: peniko::ImageBrush::new(peniko::ImageData { + data: peniko::Blob::new(Arc::new(vec![255, 255, 255, 255])), + format: peniko::ImageFormat::Rgba8, + alpha_type: peniko::ImageAlphaType::Alpha, + width: 1, + height: 1, + }), + hash: Arc::from([1_u8].as_slice()), + rect: Rect::new(40.0, 40.0, 50.0, 50.0), transform: Affine::IDENTITY, - fill_rule: Fill::NonZero, - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), }, - }, - DisplayCommand::DrawImage { - img: peniko::ImageBrush::new(peniko::ImageData { - data: peniko::Blob::new(Arc::new(vec![255, 255, 255, 255])), - format: peniko::ImageFormat::Rgba8, - alpha_type: peniko::ImageAlphaType::Alpha, - width: 1, - height: 1, - }), - hash: Arc::from([1_u8].as_slice()), - rect: Rect::new(40.0, 40.0, 50.0, 50.0), - transform: Affine::IDENTITY, - }, - ], None); + ], + None, + ); let damage = [Rect::new(1.0, 1.0, 5.0, 5.0)]; let chunks = stage.chunk_indices_for_damage(&damage); assert_eq!(chunks, vec![0]); assert_eq!(stage.chunks.len(), 1); assert!(stage.chunks[0].metadata.has_raster_image); - assert_eq!(stage.chunks[0].bounds, Some(Rect::new(0.0, 0.0, 50.0, 50.0))); + assert_eq!( + stage.chunks[0].bounds, + Some(Rect::new(0.0, 0.0, 50.0, 50.0)) + ); } #[test] @@ -1545,17 +1538,3 @@ mod tests { assert_eq!(constrained.y1, 20.0); } } - -fn shape_to_geometry(shape: &impl Shape) -> Geometry { - if let Some(rect) = shape.as_rect() { - return Geometry::Rect(rect); - } - if let Some(rect) = shape.as_rounded_rect() { - return Geometry::RoundedRect(rect); - } - Geometry::Path(path_from_shape(shape)) -} - -fn path_from_shape(shape: &impl Shape) -> BezPath { - shape.to_path(0.1) -} diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 8aaeb9427..0a69f7618 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -14,8 +14,8 @@ pub use renderer::Renderer; use floem_renderer::Renderer as _; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; -use imaging::{BlurredRoundedRect, ClipRef, Composite, GeometryRef, PaintSink, Painter}; -use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Shape, Size}; +use imaging::Painter; +use peniko::kurbo::{Affine, Point, RoundedRect, Size}; use rustc_hash::FxHashSet; use std::sync::Arc; use understory_box_tree::NodeFlags; @@ -137,8 +137,7 @@ pub struct GlobalPaintCx<'a> { pub struct PaintCx<'a> { /// Reference to global paint state pub window_state: &'a mut WindowState, - recorder: &'a mut RecordingRenderer<'a>, - uses_layer_clip: bool, + pub painter: Painter<'a, RecordingRenderer<'a>>, is_vger: bool, /// The target visual node being painted (CRITICAL for views with multiple visuals) pub target_id: ElementId, @@ -370,7 +369,12 @@ impl GlobalPaintCx<'_> { active_ids .iter() .copied() - .map(|element_id| (element_id, ElementSnapshot::from_box_tree(&box_tree, element_id))) + .map(|element_id| { + ( + element_id, + ElementSnapshot::from_box_tree(&box_tree, element_id), + ) + }) .collect::>() }; let mut reused_snapshots = Vec::new(); @@ -398,7 +402,10 @@ impl GlobalPaintCx<'_> { // downstream systems like promoted compositor bounds still need the current // snapshot every frame. Keep the artifact metadata fresh even when we skip // rerecording the stage commands. - self.window_state.display_list.element_mut(element_id).snapshot = Some(snapshot); + self.window_state + .display_list + .element_mut(element_id) + .snapshot = Some(snapshot); } } for element_id in dirty_ids { @@ -439,11 +446,7 @@ impl GlobalPaintCx<'_> { record_paint(element_id.owning_id()); } if element_id.is_view() { - self.replay_element_overflow_clip( - element_id, - target_origin, - render_size, - ); + self.replay_element_overflow_clip(element_id, target_origin, render_size); } // Damage-aware chunk replay is intentionally disabled here for now. // Every backend rebuilds a fresh frame/scene on `begin()`, so skipping @@ -512,7 +515,8 @@ impl GlobalPaintCx<'_> { }; let local_damage = damage_rects.map(|rects| { let inverse = base_transform.inverse(); - rects.iter() + rects + .iter() .map(|rect| inverse.transform_rect_bbox(*rect)) .collect::>() }); @@ -540,7 +544,6 @@ impl GlobalPaintCx<'_> { let view_state = view_id.state(); let mut commands = Vec::new(); let mut recorder = RecordingRenderer::new(&mut commands); - let uses_layer_clip = self.paint_state.renderer().uses_layer_clip(); let is_vger = self.paint_state.renderer().is_vger(); let world_transform = self.element_base_transform(element_id); let font_size_cx = view_state.borrow().layout_props.font_size_cx(); @@ -549,8 +552,7 @@ impl GlobalPaintCx<'_> { // Create per-target PaintCx let mut cx = PaintCx { window_state: self.window_state, - recorder: &mut recorder, - uses_layer_clip, + painter: Painter::new(&mut recorder), is_vger, target_id: element_id, world_transform, @@ -592,7 +594,7 @@ impl GlobalPaintCx<'_> { } } -impl PaintCx<'_> { +impl<'a> PaintCx<'a> { /// Allows a `View` to determine if it is being called in order to /// paint a *draggable* image of itself during a drag (likely /// `draggable()` was called on the `View` or `ViewId`) as opposed @@ -609,131 +611,13 @@ impl PaintCx<'_> { false } - /// Clip the drawing area (delegates to helper methods) - pub fn clip(&mut self, shape: &impl Shape) { - if self.uses_layer_clip { - use peniko::Mix; - self.push_layer(Mix::Normal, 1.0, Affine::IDENTITY, shape); - } else { - self.recorder.push_clip(ClipRef::fill(geometry_ref_from_shape(shape))); - } - } - - /// Clear clip - pub fn pop_clip(&mut self) { - if self.uses_layer_clip { - self.pop_layer(); - } else { - self.recorder.pop_clip(); - } - } - - // Note: get_transform/set_transform removed as Renderer doesn't expose transform() - // Views that previously used save/restore should use clip/clear_clip instead - - pub fn stroke<'b, 's>( - &mut self, - shape: &impl Shape, - brush: impl Into>, - stroke: &'s peniko::kurbo::Stroke, - ) { - let brush = brush.into().to_owned(); - let transform = self.recorder.transform(); - Painter::new(self.recorder) - .stroke(geometry_ref_from_shape(shape), stroke, &brush) - .transform(transform) - .draw(); - } - - pub fn fill<'b>( - &mut self, - path: &impl peniko::kurbo::Shape, - brush: impl Into>, - blur_radius: f64, - ) { - let brush = brush.into().to_owned(); - let transform = self.recorder.transform(); - if blur_radius > 0.0 - && let peniko::Brush::Solid(color) = brush - && let Some((rect, radius)) = blurred_rounded_rect(path) - { - Painter::new(self.recorder).blurred_rounded_rect(BlurredRoundedRect { - transform, - rect, - color, - radius, - std_dev: blur_radius, - composite: Composite::default(), - }); - return; - } - - Painter::new(self.recorder) - .fill(geometry_ref_from_shape(path), &brush) - .transform(transform) - .draw(); - } - - pub fn push_layer( - &mut self, - blend: impl Into, - alpha: f32, - transform: Affine, - clip: &impl Shape, - ) { - self.recorder.push_layer(blend, alpha, transform, clip); - } - - pub fn pop_layer(&mut self) { - self.recorder.pop_layer(); - } - - pub fn draw_img(&mut self, img: floem_renderer::Img<'_>, rect: peniko::kurbo::Rect) { - self.recorder.draw_img(img, rect); - } - - pub fn draw_glyphs<'a>( - &mut self, - origin: peniko::kurbo::Point, - props: &floem_renderer::text::GlyphRunProps<'a>, - glyphs: impl Iterator + 'a, - ) { - let brush = props.brush.to_owned(); - let glyphs = glyphs - .map(|glyph| imaging::record::Glyph { - id: glyph.id, - x: glyph.x, - y: glyph.y, - }) - .collect::>(); - let transform = self.recorder.transform() - * Affine::translate((origin.x, origin.y)) - * props.transform; - Painter::new(self.recorder) - .glyphs(&props.font, &brush) - .transform(transform) - .glyph_transform(props.glyph_transform) - .font_size(props.font_size) - .hint(props.hint) - .normalized_coords(props.normalized_coords) - .composite(Composite::new( - peniko::BlendMode::default(), - props.brush_alpha, - )) - .draw(&props.style.to_owned(), &glyphs); - } - pub fn draw_svg<'b>( &mut self, svg: floem_renderer::Svg<'b>, rect: peniko::kurbo::Rect, brush: Option>>, ) { - self.recorder.draw_svg(svg, rect, brush); - } - - pub fn set_transform(&mut self, transform: Affine) { - self.recorder.set_transform(transform); + let _ = (svg, rect, brush); } pub fn is_vger(&self) -> bool { @@ -822,25 +706,3 @@ impl PaintState { self.renderer_mut().set_scale(scale); } } - -fn geometry_ref_from_shape(shape: &impl Shape) -> GeometryRef<'static> { - if let Some(rect) = shape.as_rect() { - return GeometryRef::Rect(rect); - } - if let Some(rect) = shape.as_rounded_rect() { - return GeometryRef::RoundedRect(rect); - } - GeometryRef::OwnedPath(shape.to_path(0.1)) -} - -fn blurred_rounded_rect(shape: &impl Shape) -> Option<(Rect, f64)> { - if let Some(rect) = shape.as_rect() { - return Some((rect, 0.0)); - } - let rect = shape.as_rounded_rect()?; - let radii = rect.radii(); - (radii.top_left == radii.top_right - && radii.top_left == radii.bottom_left - && radii.top_left == radii.bottom_right) - .then_some((rect.rect(), radii.top_left)) -} diff --git a/src/paint/renderer.rs b/src/paint/renderer.rs index 1dfbe9950..2dd1752a0 100644 --- a/src/paint/renderer.rs +++ b/src/paint/renderer.rs @@ -86,11 +86,6 @@ impl Renderer { #[cfg(any(feature = "vello", feature = "skia"))] const ACTIVE_IS_VGER: bool = false; - #[cfg(all(feature = "vello", not(feature = "skia")))] - const ACTIVE_USES_LAYER_CLIP: bool = true; - #[cfg(any(not(feature = "vello"), feature = "skia"))] - const ACTIVE_USES_LAYER_CLIP: bool = false; - #[cfg(feature = "skia")] pub fn new_skia(window: Arc, scale: f64, size: Size, font_embolden: f32) -> Self { let size = Size::new(size.width.max(1.0), size.height.max(1.0)); @@ -129,14 +124,6 @@ impl Renderer { Self::ACTIVE_IS_VGER && matches!(self, Renderer::Active(_)) } - pub(crate) fn uses_layer_clip(&self) -> bool { - match self { - Renderer::Active(_) => Self::ACTIVE_USES_LAYER_CLIP, - Renderer::TinySkia(_) => false, - Renderer::Uninitialized { .. } => false, - } - } - #[cfg(all(feature = "vello", not(feature = "skia")))] fn try_new_active( gpu_resources: GpuResources, @@ -412,12 +399,12 @@ impl floem_renderer::Renderer for Renderer { fn draw_glyphs<'a>( &mut self, origin: Point, - props: &floem_renderer::text::GlyphRunProps<'a>, + run: &floem_renderer::text::GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ) { match self { - Renderer::Active(v) => v.draw_glyphs(origin, props, glyphs), - Renderer::TinySkia(v) => v.draw_glyphs(origin, props, glyphs), + Renderer::Active(v) => v.draw_glyphs(origin, run, glyphs), + Renderer::TinySkia(v) => v.draw_glyphs(origin, run, glyphs), Renderer::Uninitialized { .. } => {} } } diff --git a/src/style/transition.rs b/src/style/transition.rs index 01ad4635c..c83ef5325 100644 --- a/src/style/transition.rs +++ b/src/style/transition.rs @@ -7,7 +7,7 @@ use floem_reactive::{RwSignal, SignalGet}; use peniko::color::palette; -use peniko::kurbo::{self, Point, Stroke}; +use peniko::kurbo::{self, Point, Shape, Stroke}; use std::rc::Rc; use taffy::prelude::{auto, fr}; @@ -145,14 +145,17 @@ impl Transition { } // Draw the curve - cx.stroke( - &path, - curve_color.get(), - &Stroke { - width: 2.0, - ..Default::default() - }, - ); + let curve_brush = peniko::Brush::Solid(curve_color.get()); + cx.painter + .stroke( + path, + &Stroke { + width: 2.0, + ..Default::default() + }, + &curve_brush, + ) + .draw(); // Draw axes let axis_stroke = Stroke { @@ -161,24 +164,31 @@ impl Transition { }; // X axis - cx.stroke( - &kurbo::Line::new( - Point::new(padding, height - padding), - Point::new(width - padding, height - padding), - ), - axis_color.get(), - &axis_stroke, - ); + let axis_brush = peniko::Brush::Solid(axis_color.get()); + cx.painter + .stroke( + kurbo::Line::new( + Point::new(padding, height - padding), + Point::new(width - padding, height - padding), + ) + .to_path(0.1), + &axis_stroke, + &axis_brush, + ) + .draw(); // Y axis - cx.stroke( - &kurbo::Line::new( - Point::new(padding, padding), - Point::new(padding, height - padding), - ), - axis_color.get(), - &axis_stroke, - ); + cx.painter + .stroke( + kurbo::Line::new( + Point::new(padding, padding), + Point::new(padding, height - padding), + ) + .to_path(0.1), + &axis_stroke, + &axis_brush, + ) + .draw(); }) .style(|s| s.width(80.0).height(60.0)) .container() diff --git a/src/style/values.rs b/src/style/values.rs index e4921fc9b..33d66d694 100644 --- a/src/style/values.rs +++ b/src/style/values.rs @@ -3,7 +3,7 @@ use floem_reactive::{RwSignal, SignalGet, SignalUpdate as _}; use floem_renderer::text::{FontWeight, LineHeightValue}; use peniko::color::{HueDirection, palette}; -use peniko::kurbo::{self, Affine, Point, Stroke, Vec2}; +use peniko::kurbo::{self, Affine, Point, Shape, Stroke, Vec2}; use peniko::{ Brush, Color, ColorStop, ColorStops, Gradient, GradientKind, InterpolationAlphaSpace, LinearGradientPosition, @@ -247,14 +247,17 @@ impl StylePropValue for ObjectFit { ), 2.0, ); - cx.stroke( - &container_rect, - container_color.get(), - &Stroke { - width: 1.5, - ..Default::default() - }, - ); + let container_brush = Brush::Solid(container_color.get()); + cx.painter + .stroke( + container_rect, + &Stroke { + width: 1.5, + ..Default::default() + }, + &container_brush, + ) + .draw(); // Simulate an image with 2:1 aspect ratio (wider than tall) let image_aspect = 2.0; @@ -304,15 +307,16 @@ impl StylePropValue for ObjectFit { 2.0, ); // Show it as semi-transparent to indicate it's clipped - let clipped_color = image_color.get().with_alpha(0.7); - cx.fill(&img_rect, clipped_color, 0.0); + let clipped_color = Brush::Solid(image_color.get().with_alpha(0.7)); + cx.painter.fill(img_rect, &clipped_color).draw(); } else { // Draw the image rect normally let img_rect = RoundedRect::from_rect( kurbo::Rect::new(img_x, img_y, img_x + img_width, img_y + img_height), 2.0, ); - cx.fill(&img_rect, image_color.get(), 0.0); + let image_brush = Brush::Solid(image_color.get()); + cx.painter.fill(img_rect, &image_brush).draw(); } }) .style(|s| s.width(70.0).height(70.0)) @@ -390,14 +394,17 @@ impl StylePropValue for ObjectPosition { ), 2.0, ); - cx.stroke( - &container_rect, - container_color.get(), - &Stroke { - width: 1.5, - ..Default::default() - }, - ); + let container_brush = Brush::Solid(container_color.get()); + cx.painter + .stroke( + container_rect, + &Stroke { + width: 1.5, + ..Default::default() + }, + &container_brush, + ) + .draw(); let image_w = container_size * 0.55; let image_h = container_size * 0.35; @@ -427,13 +434,15 @@ impl StylePropValue for ObjectPosition { kurbo::Rect::new(img_x, img_y, img_x + image_w, img_y + image_h), 2.0, ); - cx.fill(&img_rect, image_color.get(), 0.0); + let image_brush = Brush::Solid(image_color.get()); + cx.painter.fill(img_rect, &image_brush).draw(); let marker = Circle::new( kurbo::Point::new(img_x + image_w / 2.0, img_y + image_h / 2.0), 2.5, ); - cx.fill(&marker, marker_color.get(), 0.0); + let marker_brush = Brush::Solid(marker_color.get()); + cx.painter.fill(marker.to_path(0.1), &marker_brush).draw(); }) .style(|s| s.width(70.0).height(70.0)) .container() @@ -999,14 +1008,18 @@ impl StylePropValue for Stroke { // Visual preview of the stroke let preview = canvas(move |cx, size| { - cx.stroke( - &kurbo::Line::new( - Point::new(0., size.height / 2.), - Point::new(size.width, size.height / 2.), - ), - color.get(), - &clone, - ); + let brush = Brush::Solid(color.get()); + cx.painter + .stroke( + kurbo::Line::new( + Point::new(0., size.height / 2.), + Point::new(size.width, size.height / 2.), + ) + .to_path(0.1), + &clone, + &brush, + ) + .draw(); }) .style(move |s| s.width(80.0).height(20.0)) .container() @@ -1265,11 +1278,14 @@ impl StylePropValue for Affine { // Draw original position (dashed outline) let original_rect = kurbo::Rect::from_center_size(center, kurbo::Size::new(box_size, box_size)); - cx.stroke( - &original_rect, - palette::css::GRAY.with_alpha(0.5), - &kurbo::Stroke::new(1.0).with_dashes(0., [3., 3.]), - ); + let original_brush = Brush::Solid(palette::css::GRAY.with_alpha(0.5)); + cx.painter + .stroke( + original_rect, + &kurbo::Stroke::new(1.0).with_dashes(0., [3., 3.]), + &original_brush, + ) + .draw(); // Draw transformed position let transform_offset = @@ -1277,20 +1293,20 @@ impl StylePropValue for Affine { let display_transform = transform_offset * affine * transform_offset.inverse(); let transformed_rect = kurbo::Rect::new(0., 0., box_size, box_size); - cx.fill( - &display_transform.transform_rect_bbox(transformed_rect), - palette::css::BLUE.with_alpha(0.7), - 0., - ); - cx.stroke( - &(display_transform.transform_rect_bbox(transformed_rect)), - palette::css::BLUE, - &kurbo::Stroke::new(2.0), - ); + let transformed = display_transform.transform_rect_bbox(transformed_rect); + let fill_brush = Brush::Solid(palette::css::BLUE.with_alpha(0.7)); + let stroke_brush = Brush::Solid(palette::css::BLUE); + cx.painter.fill(transformed, &fill_brush).draw(); + cx.painter + .stroke(transformed, &kurbo::Stroke::new(2.0), &stroke_brush) + .draw(); // Draw origin point let origin_marker = kurbo::Circle::new(display_transform * Point::ZERO, 3.0); - cx.fill(&origin_marker, palette::css::RED, 0.); + let origin_brush = Brush::Solid(palette::css::RED); + cx.painter + .fill(origin_marker.to_path(0.1), &origin_brush) + .draw(); }) .style(|s| s.width(80.0).height(60.0)) .container() diff --git a/src/text/layout.rs b/src/text/layout.rs index 67bbd2080..169c3f250 100644 --- a/src/text/layout.rs +++ b/src/text/layout.rs @@ -6,7 +6,8 @@ use std::{ sync::LazyLock, }; -use floem_renderer::text::{AttrsList, GlyphRunProps, TextBrush}; +use floem_renderer::text::{AttrsList, GlyphRunRef, TextBrush}; +use imaging::{Composite, PaintSink, Painter, record::Glyph as ImagingGlyph}; use parking_lot::Mutex; use parley::swash::{FontRef, scale::ScaleContext, zeno}; use parley::{ @@ -776,8 +777,11 @@ impl TextLayout { (min_y.is_finite() && max_y.is_finite()).then_some((min_y, max_y)) } - /// Draws the layout into Floem's retained paint recorder. - pub fn draw(&self, renderer: &mut crate::paint::PaintCx<'_>, origin: impl Into) { + pub fn draw_with_painter( + &self, + painter: &mut Painter<'_, S>, + origin: impl Into, + ) { let origin = origin.into(); for line in self.layout.lines() { for item in line.items() { @@ -791,16 +795,42 @@ impl TextLayout { .skew() .map(|angle| Affine::skew((angle as f64).to_radians().tan(), 0.0)); - let props = GlyphRunProps::new(run.font()) - .font_size(run.font_size()) - .hint(false) - .normalized_coords(run.normalized_coords()) - .style(Fill::NonZero) - .brush(glyph_run.style().brush.0) - .glyph_transform(glyph_transform); - - renderer.draw_glyphs(origin, &props, glyph_run.positioned_glyphs()); + let style = peniko::Style::Fill(Fill::NonZero); + let run = GlyphRunRef { + font: run.font(), + transform: Affine::IDENTITY, + glyph_transform, + font_size: run.font_size(), + hint: false, + normalized_coords: run.normalized_coords(), + style: &style, + brush: glyph_run.style().brush.0.into(), + composite: Composite::default(), + }; + let brush = run.brush.to_owned().multiply_alpha(run.composite.alpha); + let glyphs = glyph_run + .positioned_glyphs() + .map(|glyph| ImagingGlyph { + id: glyph.id, + x: glyph.x, + y: glyph.y, + }) + .collect::>(); + + painter + .glyphs(run.font, &brush) + .transform(run.transform * Affine::translate(origin.to_vec2())) + .glyph_transform(run.glyph_transform) + .font_size(run.font_size) + .hint(run.hint) + .normalized_coords(run.normalized_coords) + .draw(run.style, &glyphs); } } } + + /// Draws the layout into Floem's retained paint recorder. + pub fn draw(&self, renderer: &mut crate::paint::PaintCx<'_>, origin: impl Into) { + self.draw_with_painter(&mut renderer.painter, origin); + } } diff --git a/src/text/mod.rs b/src/text/mod.rs index 9965a5900..3422cc6a2 100644 --- a/src/text/mod.rs +++ b/src/text/mod.rs @@ -16,7 +16,7 @@ mod layout_state; pub use floem_renderer::text::{ Attrs, AttrsList, AttrsOwned, FamilyOwned, FontStyle, FontWeight, FontWidth, Glyph, - GlyphRunProps, LineHeightValue, NormalizedCoord, + GlyphRunRef, LineHeightValue, NormalizedCoord, }; pub use layout::{FONT_CONTEXT, TextLayout, TextSelection}; pub use layout_state::{TextLayoutState, TextOverflowChanged}; diff --git a/src/view/mod.rs b/src/view/mod.rs index 3b7d65a31..ef30dc211 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1179,14 +1179,14 @@ pub(crate) fn paint_bg(cx: &mut PaintCx, style: &ViewStyleProps, rect: Rect) { None => return, }; let rounded_rect = rect.to_rounded_rect(radii); - cx.fill(&rounded_rect, &bg, 0.0); + cx.painter.fill(rounded_rect, &bg).draw(); } else { paint_box_shadow(cx, style, rect, None); let bg = match style.background() { Some(color) => color, None => return, }; - cx.fill(&rect, &bg, 0.0); + cx.painter.fill(rect, &bg).draw(); } } @@ -1212,10 +1212,25 @@ fn paint_box_shadow( ); let rect = rect.inflate(spread, spread).inset(inset); if let Some(radii) = rect_radius { - let rounded_rect = RoundedRect::from_rect(rect, radii_add(radii, spread)); - cx.fill(&rounded_rect, shadow.color, blur_radius); + cx.painter + .blurred_rounded_rect(imaging::BlurredRoundedRect { + transform: Affine::IDENTITY, + rect, + color: shadow.color, + radius: radii_max(radii_add(radii, spread)), + std_dev: blur_radius, + composite: imaging::Composite::default(), + }); } else { - cx.fill(&rect, shadow.color, blur_radius); + cx.painter + .blurred_rounded_rect(imaging::BlurredRoundedRect { + transform: Affine::IDENTITY, + rect, + color: shadow.color, + radius: 0.0, + std_dev: blur_radius, + composite: imaging::Composite::default(), + }); } } } @@ -1228,11 +1243,13 @@ pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, rect: Rect let half = outline.width / 2.0; let rect = rect.inflate(half, half); let border_radii = border_to_radii_view(style, rect.size(), &cx.font_size_cx); - cx.stroke( - &rect.to_rounded_rect(radii_add(border_radii, half)), - &style.outline_color(), - outline, - ); + cx.painter + .stroke( + rect.to_rounded_rect(radii_add(border_radii, half)), + outline, + &style.outline_color(), + ) + .draw(); return; } @@ -1278,7 +1295,9 @@ pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, rect: Rect BorderPathEvent::NewStroke(stroke) => { // Render current path with previous stroke if any if !current_path.is_empty() { - cx.stroke(¤t_path.as_slice(), &outline_color, &stroke.0); + cx.painter + .stroke(BezPath::from_vec(current_path.clone()), &stroke.0, &outline_color) + .draw(); current_path.clear(); } } @@ -1320,9 +1339,11 @@ pub(crate) fn paint_border( if let Some(color) = style.border_color().left { if radii_max(radii) > 0.0 { let radii = radii_map(radii, |r| (r - half).max(0.0)); - cx.stroke(&rect.to_rounded_rect(radii), &color, &left); + cx.painter + .stroke(rect.to_rounded_rect(radii), &left, &color) + .draw(); } else { - cx.stroke(&rect, &color, &left); + cx.painter.stroke(rect, &left, &color).draw(); } } } else { @@ -1330,47 +1351,59 @@ pub(crate) fn paint_border( && let Some(color) = style.border_color().left { let half = left.width / 2.0; - cx.stroke( - &Line::new(Point::new(half, 0.0), Point::new(half, rect.height())), - &color, - &left, - ); + cx.painter + .stroke( + Line::new(Point::new(half, 0.0), Point::new(half, rect.height())) + .to_path(0.1), + &left, + &color, + ) + .draw(); } if right.width > 0.0 && let Some(color) = style.border_color().right { let half = right.width / 2.0; - cx.stroke( - &Line::new( - Point::new(rect.width() - half, 0.0), - Point::new(rect.width() - half, rect.height()), - ), - &color, - &right, - ); + cx.painter + .stroke( + Line::new( + Point::new(rect.width() - half, 0.0), + Point::new(rect.width() - half, rect.height()), + ) + .to_path(0.1), + &right, + &color, + ) + .draw(); } if top.width > 0.0 && let Some(color) = style.border_color().top { let half = top.width / 2.0; - cx.stroke( - &Line::new(Point::new(0.0, half), Point::new(rect.width(), half)), - &color, - &top, - ); + cx.painter + .stroke( + Line::new(Point::new(0.0, half), Point::new(rect.width(), half)) + .to_path(0.1), + &top, + &color, + ) + .draw(); } if bottom.width > 0.0 && let Some(color) = style.border_color().bottom { let half = bottom.width / 2.0; - cx.stroke( - &Line::new( - Point::new(0.0, rect.height() - half), - Point::new(rect.width(), rect.height() - half), - ), - &color, - &bottom, - ); + cx.painter + .stroke( + Line::new( + Point::new(0.0, rect.height() - half), + Point::new(rect.width(), rect.height() - half), + ) + .to_path(0.1), + &bottom, + &color, + ) + .draw(); } } return; @@ -1437,7 +1470,9 @@ pub(crate) fn paint_border( BorderPathEvent::NewStroke(stroke) => { // Render current path with previous stroke if any if !current_path.is_empty() && stroke.0.width > 0. { - cx.stroke(¤t_path.as_slice(), &stroke.1, &stroke.0); + cx.painter + .stroke(BezPath::from_vec(current_path.to_vec()), &stroke.0, &stroke.1) + .draw(); current_path.clear(); } else if stroke.0.width == 0. { current_path.clear(); diff --git a/src/views/editor/gutter.rs b/src/views/editor/gutter.rs index f2d423290..3dcb0bd72 100644 --- a/src/views/editor/gutter.rs +++ b/src/views/editor/gutter.rs @@ -12,7 +12,7 @@ use crate::{ }; use floem_editor_core::{cursor::CursorMode, mode::Mode}; use floem_reactive::{RwSignal, SignalGet, SignalWith}; -use peniko::Color; +use peniko::{Brush, Color}; use peniko::color::palette; use peniko::kurbo::Rect; @@ -135,7 +135,8 @@ impl View for EditorGutterView { (*self.full_width.borrow() + 1.1, f64::from(line_height)), ); - cx.fill(&rect, current_line_color, 0.0); + let brush = Brush::Solid(current_line_color); + cx.painter.fill(rect, &brush).draw(); } } } diff --git a/src/views/editor/view.rs b/src/views/editor/view.rs index 7ec5e4528..b52c5b82f 100644 --- a/src/views/editor/view.rs +++ b/src/views/editor/view.rs @@ -7,7 +7,7 @@ use crate::{ CustomEvent, Event, EventPropagation, ImeEvent, PointerScrollEventExt, listener, listener::UpdatePhaseBoxTreeCommit, }, - kurbo::{BezPath, Line, Point, Rect, Size, Vec2}, + kurbo::{BezPath, Line, Point, Rect, Shape, Size, Vec2}, peniko::Color, prelude::EventListenerTrait, reactive::{Effect, Memo, RwSignal, Scope}, @@ -538,7 +538,7 @@ impl EditorView { }; let rect = Rect::from_origin_size((x0, vline_y), (width, line_height)); - cx.fill(&rect, color, 0.0); + cx.painter.fill(rect, color).draw(); } } @@ -587,7 +587,7 @@ impl EditorView { (viewport.x0, vline_y), (x1 - viewport.x0, f64::from(line_height)), ); - cx.fill(&rect, color, 0.0); + cx.painter.fill(rect, color).draw(); } } @@ -633,7 +633,7 @@ impl EditorView { let line_height = ed.line_height(line); let rect = Rect::from_origin_size((x0, line_info.vline_y), (x1 - x0, f64::from(line_height))); - cx.fill(&rect, color, 0.0); + cx.painter.fill(rect, color).draw(); } } @@ -666,7 +666,8 @@ impl EditorView { (viewport.width(), f64::from(line_height)), ); - cx.fill(&rect, current_line_color, 0.0); + let brush = Brush::Solid(current_line_color); + cx.painter.fill(rect, &brush).draw(); } } } @@ -791,7 +792,7 @@ impl EditorView { let line_height = ed.line_height(info.vline_info.rvline.line); let rect = Rect::from_origin_size((x, info.vline_y), (width, f64::from(line_height))); - cx.fill(&rect, &caret_color, 0.0); + cx.painter.fill(rect, &caret_color).draw(); } } }); @@ -814,7 +815,10 @@ impl EditorView { direction *= -1.0; } - cx.stroke(&path, color, &peniko::kurbo::Stroke::new(1.)); + let brush = Brush::Solid(color); + cx.painter + .stroke(path, &peniko::kurbo::Stroke::new(1.), &brush) + .draw(); } pub fn paint_extra_style( @@ -834,13 +838,15 @@ impl EditorView { }; let x = style.x + base; let y = y + style.y; - cx.fill( - &Rect::ZERO - .with_size(Size::new(width, height)) - .with_origin(Point::new(x, y)), - bg, - 0.0, - ); + let brush = Brush::Solid(bg); + cx.painter + .fill( + Rect::ZERO + .with_size(Size::new(width, height)) + .with_origin(Point::new(x, y)), + &brush, + ) + .draw(); } if let Some(color) = style.under_line { @@ -852,11 +858,14 @@ impl EditorView { }; let x = style.x + base; let y = y + style.y + height; - cx.stroke( - &Line::new(Point::new(x, y), Point::new(x + width, y)), - color, - &peniko::kurbo::Stroke::new(1.), - ); + let brush = Brush::Solid(color); + cx.painter + .stroke( + Line::new(Point::new(x, y), Point::new(x + width, y)).to_path(0.1), + &peniko::kurbo::Stroke::new(1.), + &brush, + ) + .draw(); } if let Some(color) = style.wave_line { @@ -896,16 +905,20 @@ impl EditorView { if ed.es.with(|s| s.show_indent_guide()) { // Cache the indent guide color outside the loop to avoid repeated signal access let indent_guide_color = ed.es.with_untracked(|es| es.indent_guide()); + let indent_guide_brush = Brush::Solid(indent_guide_color); for (line, y) in screen_lines.iter_lines_y() { let text_layout = ed.text_layout(line); let line_height = f64::from(ed.line_height(line)); let mut x = 0.0; while x + 1.0 < text_layout.indent { - cx.stroke( - &Line::new(Point::new(x, y), Point::new(x, y + line_height)), - indent_guide_color, - &peniko::kurbo::Stroke::new(1.), - ); + cx.painter + .stroke( + Line::new(Point::new(x, y), Point::new(x, y + line_height)) + .to_path(0.1), + &peniko::kurbo::Stroke::new(1.), + &indent_guide_brush, + ) + .draw(); x += indent_text_width; } } diff --git a/src/views/img.rs b/src/views/img.rs index 25913f418..7a6cd525a 100644 --- a/src/views/img.rs +++ b/src/views/img.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc}; use floem_reactive::UpdaterEffect; -use peniko::{Blob, ImageAlphaType, ImageData, kurbo::Rect}; +use peniko::{Blob, Brush, ImageAlphaType, ImageData, kurbo::Rect}; use sha2::{Digest, Sha256}; use crate::{ @@ -365,20 +365,14 @@ impl View for Img { fn paint(&mut self, cx: &mut crate::context::PaintCx) { let content_rect = self.id.get_content_rect_local(); let dest_rect = self.object_fit_dest_rect(content_rect); + let image_brush = Brush::Image(self.img.clone()); if self.needs_clip() { - cx.clip(&content_rect); - } - - cx.draw_img( - floem_renderer::Img { - img: self.img.clone(), - hash: &self.img_hash, - }, - dest_rect, - ); - if self.needs_clip() { - cx.pop_clip(); + cx.painter.with_fill_clip(content_rect, |p| { + p.fill(dest_rect, &image_brush).draw(); + }); + } else { + cx.painter.fill(dest_rect, &image_brush).draw(); } } } diff --git a/src/views/label.rs b/src/views/label.rs index 7111c4b03..ca18d0c0c 100644 --- a/src/views/label.rs +++ b/src/views/label.rs @@ -346,7 +346,7 @@ impl Label { self.layout_data .borrow() .selection_rects_for_selection(&selection, text_loc, |rect| { - paint_cx.fill(&rect, &selection_color, 0.0); + paint_cx.painter.fill(rect, &selection_color).draw(); }); } } diff --git a/src/views/resizable.rs b/src/views/resizable.rs index 80ba8c86a..e8411bd4f 100644 --- a/src/views/resizable.rs +++ b/src/views/resizable.rs @@ -360,7 +360,7 @@ impl Handle { } }; - cx.fill(&paint_rect, &self.handle_style.color(), 0.); + cx.painter.fill(paint_rect, &self.handle_style.color()).draw(); } } diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 223aebb7c..86c427d18 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -287,16 +287,16 @@ impl ScrollHandle { let rect_with_border = rect.inset(-edge_width / 2.0); let rounded_rect = rect_with_border.to_rounded_rect(radius); - cx.fill( - &rounded_rect, - &self.style.background().unwrap_or(HANDLE_COLOR), - 0.0, - ); + cx.painter + .fill(rounded_rect, &self.style.background().unwrap_or(HANDLE_COLOR)) + .draw(); if edge_width > 0.0 && let Some(color) = self.style.border_color().right { - cx.stroke(&rounded_rect, &color, &Stroke::new(edge_width)); + cx.painter + .stroke(rounded_rect, &Stroke::new(edge_width), &color) + .draw(); } } } @@ -437,7 +437,7 @@ impl ScrollTrack { let rect = box_tree.local_bounds(self.element_id.0).unwrap_or_default(); if let Some(color) = self.style.background() { - cx.fill(&rect, &color, 0.0); + cx.painter.fill(rect, &color).draw(); } } } diff --git a/src/views/slider.rs b/src/views/slider.rs index cb33f8547..45e0b3ede 100644 --- a/src/views/slider.rs +++ b/src/views/slider.rs @@ -5,7 +5,7 @@ use std::ops::RangeInclusive; use floem_reactive::{SignalGet, SignalUpdate, UpdaterEffect}; use peniko::Brush; use peniko::color::palette; -use peniko::kurbo::{Circle, Point, RoundedRect, RoundedRectRadii}; +use peniko::kurbo::{Circle, Point, RoundedRect, RoundedRectRadii, Shape}; use ui_events::keyboard::{Key, KeyState, KeyboardEvent, NamedKey}; use ui_events::pointer::{PointerButtonEvent, PointerEvent}; @@ -370,28 +370,22 @@ impl View for Slider { } fn paint(&mut self, cx: &mut crate::context::PaintCx) { - cx.fill( - &self.base_bar, - &self - .base_bar_style - .color() - .unwrap_or(palette::css::BLACK.into()), - 0., - ); - // Apply temporary clip for accent bar - cx.clip(&self.base_bar); - cx.fill( - &self.accent_bar, - &self - .accent_bar_style - .color() - .unwrap_or(palette::css::TRANSPARENT.into()), - 0., - ); - cx.pop_clip(); + let base_bar = self + .base_bar_style + .color() + .unwrap_or(palette::css::BLACK.into()); + cx.painter.fill(self.base_bar, &base_bar).draw(); + + let accent_bar = self + .accent_bar_style + .color() + .unwrap_or(palette::css::TRANSPARENT.into()); + cx.painter.with_fill_clip(self.base_bar, |p| { + p.fill(self.accent_bar, &accent_bar).draw(); + }); if let Some(color) = self.style.foreground() { - cx.fill(&self.handle, &color, 0.); + cx.painter.fill(self.handle.to_path(0.1), &color).draw(); } } } diff --git a/src/views/text_input.rs b/src/views/text_input.rs index 65217f49b..eec02b971 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -1110,7 +1110,10 @@ impl TextInput { self.selection = self.selection_from_byte_positions(new_selection.start, new_selection.end); } - fn paint_selection_rect(&self, cx: &mut crate::context::PaintCx<'_>) { + fn paint_selection_rect_with_painter( + &self, + painter: &mut imaging::Painter<'_, S>, + ) { let cursor_color = self.selection_style.selection_color(); let selection = if let Some(selection) = self.selection { selection @@ -1137,8 +1140,7 @@ impl TextInput { &selection, selection_origin, |rect| { - // Clipping already handled by traversal. - cx.fill(&rect, &cursor_color, 0.0); + painter.fill(rect, &cursor_color).draw(); }, ); } @@ -1484,61 +1486,63 @@ impl View for TextInput { let text_start_point = self.get_text_origin(); - // Apply custom clip for text content area let mut clip_rect = self.id.get_layout_rect_local(); let content_rect = self.id.get_content_rect_local(); clip_rect.x0 = content_rect.x0; clip_rect.x1 = content_rect.x1; - cx.clip(&clip_rect); - - // Draw text using layout_data - self.layout_data - .borrow() - .with_effective_text_layout(|text_layout| { - text_layout.draw( - cx, - Point::new(text_start_point.x - self.scroll_offset, text_start_point.y), - ); - }); - - // underline the preedit text - if let Some(preedit) = &self.preedit { - let start_idx = self.cursor_glyph_idx; - let end_idx = start_idx + preedit.text.len(); - + cx.painter.with_fill_clip(clip_rect, |p| { self.layout_data .borrow() .with_effective_text_layout(|text_layout| { - let start_x = text_start_point.x - + text_layout.cursor_point(start_idx, Affinity::Upstream).x - - self.scroll_offset; - let end_x = text_start_point.x - + text_layout.cursor_point(end_idx, Affinity::Upstream).x - - self.scroll_offset; - - let color = self.style.color().unwrap_or(palette::css::BLACK); - let y = text_start_point.y - + text_layout - .line_metrics_at(start_idx, Affinity::Upstream) - .map(|metrics| metrics.ascent as f64) - .unwrap_or(0.0); - - cx.fill( - &Rect::new(start_x, y, end_x, y + 1.0), - &Brush::Solid(color), - 0.0, + text_layout.draw_with_painter( + p, + Point::new(text_start_point.x - self.scroll_offset, text_start_point.y), ); }); - } - // skip rendering selection / cursor if we don't have focus + if let Some(preedit) = &self.preedit { + let start_idx = self.cursor_glyph_idx; + let end_idx = start_idx + preedit.text.len(); + + self.layout_data + .borrow() + .with_effective_text_layout(|text_layout| { + let start_x = text_start_point.x + + text_layout.cursor_point(start_idx, Affinity::Upstream).x + - self.scroll_offset; + let end_x = text_start_point.x + + text_layout.cursor_point(end_idx, Affinity::Upstream).x + - self.scroll_offset; + + let color = self.style.color().unwrap_or(palette::css::BLACK); + let y = text_start_point.y + + text_layout + .line_metrics_at(start_idx, Affinity::Upstream) + .map(|metrics| metrics.ascent as f64) + .unwrap_or(0.0); + + p.fill(Rect::new(start_x, y, end_x, y + 1.0), &Brush::Solid(color)) + .draw(); + }); + } + + if cx.window_state.is_focused(self.id) { + let has_selection = self.selection.is_some() + || self + .preedit + .as_ref() + .is_some_and(|p| p.cursor.is_some_and(|c| c.0 != c.1)); + + if has_selection { + self.paint_selection_rect_with_painter(p); + } + } + }); + if !cx.window_state.is_focused(self.id) { - // Clear the custom clip we applied - cx.pop_clip(); return; } - // see if we have a selection range let has_selection = self.selection.is_some() || self .preedit @@ -1546,15 +1550,9 @@ impl View for TextInput { .is_some_and(|p| p.cursor.is_some_and(|c| c.0 != c.1)); if has_selection { - // Clear the custom clip we applied - self.paint_selection_rect(cx); - cx.pop_clip(); return; } - // Clear the custom clip we applied - cx.pop_clip(); - // see if we should render the cursor let is_cursor_visible = (self.last_cursor_action_on.elapsed().as_millis() / CURSOR_BLINK_INTERVAL_MS as u128) @@ -1569,7 +1567,7 @@ impl View for TextInput { .builtin() .cursor_color(); let cursor_rect = self.get_cursor_rect(); - cx.fill(&cursor_rect, &cursor_color, 0.0); + cx.painter.fill(cursor_rect, &cursor_color).draw(); } // request paint either way if we're attempting draw a cursor diff --git a/src/views/toggle_button.rs b/src/views/toggle_button.rs index 524ea3314..fda927c03 100644 --- a/src/views/toggle_button.rs +++ b/src/views/toggle_button.rs @@ -6,7 +6,7 @@ use std::time::Duration; use floem_reactive::{Effect, SignalGet, SignalUpdate}; use peniko::Brush; -use peniko::kurbo::{Point, Rect, Size}; +use peniko::kurbo::{Point, Rect, Shape, Size}; use ui_events::pointer::PointerEvent; use crate::context::Phases; @@ -152,7 +152,7 @@ impl Handle { let circle_point = Point::new(self.position, size.to_rect().center().y); let circle = crate::kurbo::Circle::new(circle_point, radius); if let Some(color) = color { - cx.fill(&circle, &color, 0.); + cx.painter.fill(circle.to_path(0.1), &color).draw(); } } } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 8f11d4b57..110fb6045 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -3,7 +3,7 @@ mod recording; use anyhow::{Result, anyhow}; use floem_renderer::Img; use floem_renderer::Renderer; -use floem_renderer::text::{Glyph as ParleyGlyph, GlyphRunProps}; +use floem_renderer::text::{Glyph as ParleyGlyph, GlyphRunRef}; use floem_renderer::tiny_skia::{ self, FillRule, FilterQuality, GradientStop, IntRect, LinearGradient, Mask, MaskType, Paint, Path, PathBuilder, Pixmap, PixmapPaint, PremultipliedColorU8, RadialGradient, Shader, @@ -1304,15 +1304,15 @@ impl( &mut self, origin: Point, - props: &GlyphRunProps<'a>, + run: &GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ) { - let font = &props.font; - let text_transform = self.transform * props.transform; + let font = run.font; + let text_transform = self.transform * run.transform; let (_, _, raster_scale) = affine_scale_components(text_transform); let transform = normalize_affine(text_transform, false); let raster_origin = transform.inverse() * (text_transform * origin); - let brush_color = match &props.brush { + let brush_color = match &run.brush { peniko::Brush::Solid(color) => Color::from(*color), _ => return, }; @@ -1321,14 +1321,14 @@ impl return, }; let font_blob_id = font.data.id(); - let skew = props + let skew = run .glyph_transform .map(|transform| transform.as_coeffs()[0].atan().to_degrees() as f32); for glyph in glyphs { let glyph_x = (raster_origin.x + glyph.x as f64 * raster_scale) as f32; let glyph_y = (raster_origin.y + glyph.y as f64 * raster_scale) as f32; - let scaled_font_size = props.font_size * raster_scale as f32; + let scaled_font_size = run.font_size * raster_scale as f32; let scaled_embolden = scaled_embolden_strength(self.font_embolden, raster_scale); let (cache_key, new_x, new_y) = GlyphCacheKey::new( font_blob_id, @@ -1337,7 +1337,7 @@ impl( &mut self, origin: Point, - props: &GlyphRunProps<'a>, + run: &GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ) { // TODO: Vello 0.7's DrawGlyphs API has no embolden support. // Synthetic bold from layout synthesis and `self.font_embolden` are not applied. let transform = - self.device_transform() * Affine::translate((origin.x, origin.y)) * props.transform; + self.device_transform() * Affine::translate((origin.x, origin.y)) * run.transform; self.scene - .draw_glyphs(&props.font) - .brush(props.brush) - .brush_alpha(props.brush_alpha) - .hint(props.hint) + .draw_glyphs(run.font) + .brush(run.brush) + .brush_alpha(run.composite.alpha) + .hint(run.hint) .transform(transform) - .glyph_transform(props.glyph_transform) - .font_size(props.font_size) - .normalized_coords(props.normalized_coords) + .glyph_transform(run.glyph_transform) + .font_size(run.font_size) + .normalized_coords(run.normalized_coords) .draw( - props.style, + run.style, glyphs.map(|glyph| vello::Glyph { id: glyph.id, x: glyph.x, diff --git a/vger/src/lib.rs b/vger/src/lib.rs index 23d1cc762..eab107c8e 100644 --- a/vger/src/lib.rs +++ b/vger/src/lib.rs @@ -5,7 +5,7 @@ use std::sync::mpsc::sync_channel; use anyhow::Result; use floem_renderer::gpu_resources::GpuResources; -use floem_renderer::text::{Glyph, GlyphRunProps}; +use floem_renderer::text::{Glyph, GlyphRunRef}; use floem_renderer::{Img, Renderer, tiny_skia}; use floem_vger_rs::{GlyphImage, Image, PaintIndex, PixelFormat, Vger}; use peniko::kurbo::{Size, Stroke}; @@ -498,11 +498,11 @@ impl Renderer for VgerRenderer { fn draw_glyphs<'a>( &mut self, origin: Point, - props: &GlyphRunProps<'a>, + run: &GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ) { - let font = &props.font; - let coeffs = props.transform.as_coeffs(); + let font = run.font; + let coeffs = run.transform.as_coeffs(); let pos = self.device_transform() * (origin + Point::new(coeffs[4], coeffs[5]).to_vec2()); // This assumes that text is axis-aligned. let (_, _, scale) = self.scale_components(); @@ -512,14 +512,14 @@ impl Renderer for VgerRenderer { return; }; let font_blob_id = font.data.id(); - let _color = match &props.brush { + let _color = match &run.brush { peniko::Brush::Solid(color) => Color::from(*color), _ => return, }; - let Some(paint) = self.brush_to_paint(props.brush) else { + let Some(paint) = self.brush_to_paint(run.brush) else { return; }; - let skew = props + let skew = run .glyph_transform .map(|transform| transform.as_coeffs()[0].atan().to_degrees() as f32); let embolden = scaled_embolden_strength(self.font_embolden, scale); @@ -534,14 +534,14 @@ impl Renderer for VgerRenderer { break; } - let scaled_font_size = (props.font_size * scale as f32).round() as u32; + let scaled_font_size = (run.font_size * scale as f32).round() as u32; let x_bin = ((glyph_x.fract() + 1.0).fract() * 4.0).min(3.0) as u8; let y_bin = ((glyph_y.fract() + 1.0).fract() * 4.0).min(3.0) as u8; let glyph_id = glyph.id as u16; - let scaled_size = props.font_size * scale as f32; - let coords = props.normalized_coords; + let scaled_size = run.font_size * scale as f32; + let coords = run.normalized_coords; let synthesis_bits = skew.unwrap_or(0.0).to_bits() & 0xFFFF_FFFE; @@ -558,7 +558,7 @@ impl Renderer for VgerRenderer { let mut scaler = ctx .builder(font_ref) .size(scaled_size) - .hint(props.hint) + .hint(run.hint) .normalized_coords(coords) .build(); let mut render = Render::new(&[ From ef76d07f3c977dee10da56bbe98ab686ef48ea21 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Mon, 23 Mar 2026 14:35:11 -0600 Subject: [PATCH 022/102] changes --- src/paint/mod.rs | 28 ++++++++++++++++++---------- src/text/layout.rs | 27 ++++++++++----------------- src/views/svg.rs | 9 +++++++-- src/views/text_input.rs | 12 ++++++------ 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 0a69f7618..79efe9152 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -14,7 +14,7 @@ pub use renderer::Renderer; use floem_renderer::Renderer as _; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; -use imaging::Painter; +use imaging::{PaintSink, Painter}; use peniko::kurbo::{Affine, Point, RoundedRect, Size}; use rustc_hash::FxHashSet; use std::sync::Arc; @@ -150,6 +150,23 @@ pub struct PaintCx<'a> { pub font_size_cx: FontSizeCx, } +pub trait PainterExt { + fn dyn_painter(&mut self) -> Painter<'_, dyn PaintSink + '_>; +} + +impl PainterExt for Painter<'_, S> { + fn dyn_painter(&mut self) -> Painter<'_, dyn PaintSink + '_> { + let sink: &mut dyn PaintSink = self.sink_mut(); + Painter::new(sink) + } +} + +impl PaintCx<'_> { + pub fn dyn_painter(&mut self) -> Painter<'_, dyn PaintSink + '_> { + self.painter.dyn_painter() + } +} + #[derive(Clone, Copy)] pub(crate) enum PaintOrPost { Paint(ElementId), @@ -611,15 +628,6 @@ impl<'a> PaintCx<'a> { false } - pub fn draw_svg<'b>( - &mut self, - svg: floem_renderer::Svg<'b>, - rect: peniko::kurbo::Rect, - brush: Option>>, - ) { - let _ = (svg, rect, brush); - } - pub fn is_vger(&self) -> bool { self.is_vger } diff --git a/src/text/layout.rs b/src/text/layout.rs index 169c3f250..749b98604 100644 --- a/src/text/layout.rs +++ b/src/text/layout.rs @@ -7,7 +7,7 @@ use std::{ }; use floem_renderer::text::{AttrsList, GlyphRunRef, TextBrush}; -use imaging::{Composite, PaintSink, Painter, record::Glyph as ImagingGlyph}; +use imaging::{Composite, Painter, record::Glyph as ImagingGlyph}; use parking_lot::Mutex; use parley::swash::{FontRef, scale::ScaleContext, zeno}; use parley::{ @@ -777,11 +777,7 @@ impl TextLayout { (min_y.is_finite() && max_y.is_finite()).then_some((min_y, max_y)) } - pub fn draw_with_painter( - &self, - painter: &mut Painter<'_, S>, - origin: impl Into, - ) { + pub fn draw_with_painter(&self, mut painter: Painter<'_>, origin: impl Into) { let origin = origin.into(); for line in self.layout.lines() { for item in line.items() { @@ -808,14 +804,11 @@ impl TextLayout { composite: Composite::default(), }; let brush = run.brush.to_owned().multiply_alpha(run.composite.alpha); - let glyphs = glyph_run - .positioned_glyphs() - .map(|glyph| ImagingGlyph { - id: glyph.id, - x: glyph.x, - y: glyph.y, - }) - .collect::>(); + let glyphs = glyph_run.positioned_glyphs().map(|glyph| ImagingGlyph { + id: glyph.id, + x: glyph.x, + y: glyph.y, + }); painter .glyphs(run.font, &brush) @@ -824,13 +817,13 @@ impl TextLayout { .font_size(run.font_size) .hint(run.hint) .normalized_coords(run.normalized_coords) - .draw(run.style, &glyphs); + .draw(run.style, glyphs); } } } /// Draws the layout into Floem's retained paint recorder. - pub fn draw(&self, renderer: &mut crate::paint::PaintCx<'_>, origin: impl Into) { - self.draw_with_painter(&mut renderer.painter, origin); + pub fn draw(&self, cx: &mut crate::paint::PaintCx<'_>, origin: impl Into) { + self.draw_with_painter(cx.dyn_painter(), origin); } } diff --git a/src/views/svg.rs b/src/views/svg.rs index 59a41abbe..6520cdbfe 100644 --- a/src/views/svg.rs +++ b/src/views/svg.rs @@ -4,7 +4,7 @@ use floem_reactive::Effect; use floem_renderer::usvg::{self, Tree}; use peniko::{ Brush, GradientKind, LinearGradientPosition, - kurbo::{Point, Size}, + kurbo::{Affine, Point, Size}, }; use sha2::{Digest, Sha256}; @@ -339,7 +339,12 @@ impl View for Svg { } else { self.svg_style.text_color().map(Brush::Solid) }; - cx.draw_svg(crate::RendererSvg { tree, hash }, rect, color.as_ref()); + cx.painter.sink_mut().draw_svg( + crate::RendererSvg { tree, hash }, + rect, + Affine::IDENTITY, + color.as_ref(), + ); } } } diff --git a/src/views/text_input.rs b/src/views/text_input.rs index eec02b971..0031ba487 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -4,6 +4,7 @@ use crate::{ action::{TimerToken, exec_after, set_ime_allowed, set_ime_cursor_area}, custom_event, event::{EventPropagation, FocusEvent, ImeEvent, Phase, RouteKind}, + paint::PainterExt, prop_extractor, reactive::RwSignal, style::{ @@ -1110,10 +1111,7 @@ impl TextInput { self.selection = self.selection_from_byte_positions(new_selection.start, new_selection.end); } - fn paint_selection_rect_with_painter( - &self, - painter: &mut imaging::Painter<'_, S>, - ) { + fn paint_selection_rect_with_painter(&self, painter: &mut imaging::Painter<'_>) { let cursor_color = self.selection_style.selection_color(); let selection = if let Some(selection) = self.selection { selection @@ -1495,7 +1493,7 @@ impl View for TextInput { .borrow() .with_effective_text_layout(|text_layout| { text_layout.draw_with_painter( - p, + p.dyn_painter(), Point::new(text_start_point.x - self.scroll_offset, text_start_point.y), ); }); @@ -1534,7 +1532,9 @@ impl View for TextInput { .is_some_and(|p| p.cursor.is_some_and(|c| c.0 != c.1)); if has_selection { - self.paint_selection_rect_with_painter(p); + let sink: &mut dyn imaging::PaintSink = p.sink_mut(); + let mut painter = imaging::Painter::new(sink); + self.paint_selection_rect_with_painter(&mut painter); } } }); From 0de24f9241ba2a0376bb778a883f4781809c3202 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Mon, 23 Mar 2026 20:25:51 -0600 Subject: [PATCH 023/102] changes --- Cargo.lock | 1 - Cargo.toml | 2 +- src/paint/display_list.rs | 468 +++++++++++++++++++------------------- src/views/img.rs | 22 +- tiny_skia/src/lib.rs | 60 ++++- vger/src/lib.rs | 94 +++++++- 6 files changed, 399 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee26efd2a..fe11d6fe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3001,7 +3001,6 @@ checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" [[package]] name = "imaging" version = "0.0.1" -source = "git+https://github.com/forest-rs/imaging?rev=58c71b4b235a70a88536247839d50fb9caa013a9#58c71b4b235a70a88536247839d50fb9caa013a9" dependencies = [ "kurbo", "peniko", diff --git a/Cargo.toml b/Cargo.toml index 3af298d2b..32528b012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ ui-events = { version = "0.3", features = ["kurbo"] } ui-events-winit = { path = "./ui-events-winit", package = "ui-events-floem-winit" } dpi = { version = "0.1.2", default-features = false } rustc-hash = "2.1" -imaging = { git = "https://github.com/forest-rs/imaging", rev = "58c71b4b235a70a88536247839d50fb9caa013a9", package = "imaging" } +imaging = { path = "../imaging/imaging", package = "imaging" } [dependencies] slotmap = "1.0" diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index c944dbb53..991856bd7 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -152,10 +152,13 @@ use std::sync::Arc; use floem_renderer::text::{Glyph, GlyphRunRef}; -use floem_renderer::{Img, Renderer as FloemRenderer, Svg, usvg}; +use floem_renderer::{Renderer as FloemRenderer, Svg, usvg}; use imaging::{ - BlurredRoundedRect, ClipRef, FillRef, GroupRef, PaintSink, StrokeRef, - record::{Clip, Draw, Geometry, Glyph as ImagingGlyph, GlyphRun, Group}, + BlurredRoundedRect, ClipRef, CustomPaintSink, FillRef, GroupRef, PaintSink, StrokeRef, + record::{ + Clip, Draw, ExtendedScene, Geometry, Glyph as ImagingGlyph, GlyphRun, Group, + replay_ext_transformed, + }, }; use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Shape, Size}; use peniko::{BrushRef, Fill}; @@ -228,6 +231,34 @@ pub(crate) struct OwnedSvg { pub hash: Arc<[u8]>, } +#[derive(Clone)] +pub(crate) enum DisplayCommandExt { + DrawSvg { + svg: OwnedSvg, + rect: Rect, + transform: Affine, + brush: Option, + }, +} + +impl imaging::record::CustomCommand for DisplayCommandExt { + fn prepend_transform(&self, prefix: Affine) -> Self { + match self { + Self::DrawSvg { + svg, + rect, + transform, + brush, + } => Self::DrawSvg { + svg: svg.clone(), + rect: *rect, + transform: prefix * *transform, + brush: brush.clone(), + }, + } + } +} + #[derive(Clone)] pub(crate) enum DisplayCommand { PushClip { @@ -241,14 +272,6 @@ pub(crate) enum DisplayCommand { Draw { draw: Draw, }, - #[allow(dead_code)] - DrawImage { - img: peniko::ImageBrush, - hash: Arc<[u8]>, - rect: Rect, - transform: Affine, - }, - #[allow(dead_code)] DrawSvg { svg: OwnedSvg, rect: Rect, @@ -415,7 +438,7 @@ impl Default for PaintPropertyTree { pub(crate) struct PaintChunk { pub kind: PaintChunkKind, pub properties: PaintPropertyState, - pub commands: Vec, + pub commands: ExtendedScene, pub bounds: Option, pub metadata: PaintChunkMetadata, pub transform_class: TransformClass, @@ -601,7 +624,6 @@ impl<'a> RecordingRenderer<'a> { } impl RecordingRenderer<'_> { - #[allow(dead_code)] pub fn draw_svg<'b>( &mut self, svg: Svg<'b>, @@ -619,16 +641,6 @@ impl RecordingRenderer<'_> { brush: brush.map(|brush| brush.into().to_owned()), }); } - - #[allow(dead_code)] - pub fn draw_img(&mut self, img: Img<'_>, rect: Rect, transform: Affine) { - self.commands.push(DisplayCommand::DrawImage { - img: img.img, - hash: Arc::from(img.hash.to_vec()), - rect, - transform, - }); - } } impl PaintSink for RecordingRenderer<'_> { @@ -660,16 +672,161 @@ impl PaintSink for RecordingRenderer<'_> { self.record_draw(draw.to_owned()); } + fn glyph_run(&mut self, draw: GlyphRunRef<'_>, glyphs: &mut dyn Iterator) { + self.record_draw(Draw::GlyphRun(draw.to_owned(glyphs))); + } + + fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { + self.record_draw(Draw::BlurredRoundedRect(draw)); + } +} + +struct ReplayRenderer<'a, R> { + renderer: &'a mut R, + render_size: Size, + current_transform: &'a mut Option, +} + +impl PaintSink for ReplayRenderer<'_, R> +where + R: FloemRenderer, +{ + fn push_clip(&mut self, clip: ClipRef<'_>) { + let ClipRef::Fill { + transform, shape, .. + } = clip + else { + return; + }; + set_transform_if_needed(self.renderer, transform, self.current_transform); + match sanitize_clip_geometry(&shape.to_owned(), transform, self.render_size) { + Geometry::Rect(rect) => self.renderer.clip(&rect), + Geometry::RoundedRect(rect) => self.renderer.clip(&rect), + Geometry::Path(path) => self.renderer.clip(&path), + } + } + + fn pop_clip(&mut self) { + self.renderer.clear_clip(); + } + + fn push_group(&mut self, group: GroupRef<'_>) { + let Some(ClipRef::Fill { + transform, shape, .. + }) = group.clip + else { + return; + }; + set_transform_if_needed(self.renderer, Affine::IDENTITY, self.current_transform); + match sanitize_clip_geometry(&shape.to_owned(), transform, self.render_size) { + Geometry::Rect(rect) => self.renderer.push_layer( + group.composite.blend, + group.composite.alpha, + transform, + &rect, + ), + Geometry::RoundedRect(rect) => self.renderer.push_layer( + group.composite.blend, + group.composite.alpha, + transform, + &rect, + ), + Geometry::Path(path) => self.renderer.push_layer( + group.composite.blend, + group.composite.alpha, + transform, + &path, + ), + } + } + + fn pop_group(&mut self) { + self.renderer.pop_layer(); + } + + fn fill(&mut self, draw: FillRef<'_>) { + set_transform_if_needed(self.renderer, draw.transform, self.current_transform); + match draw.shape { + imaging::GeometryRef::Rect(rect) => self.renderer.fill(&rect, draw.brush, 0.0), + imaging::GeometryRef::RoundedRect(rect) => self.renderer.fill(&rect, draw.brush, 0.0), + imaging::GeometryRef::Path(path) => self.renderer.fill(path, draw.brush, 0.0), + imaging::GeometryRef::OwnedPath(path) => self.renderer.fill(&path, draw.brush, 0.0), + } + } + + fn stroke(&mut self, draw: StrokeRef<'_>) { + set_transform_if_needed(self.renderer, draw.transform, self.current_transform); + match draw.shape { + imaging::GeometryRef::Rect(rect) => { + self.renderer.stroke(&rect, draw.brush, draw.stroke) + } + imaging::GeometryRef::RoundedRect(rect) => { + self.renderer.stroke(&rect, draw.brush, draw.stroke) + } + imaging::GeometryRef::Path(path) => self.renderer.stroke(path, draw.brush, draw.stroke), + imaging::GeometryRef::OwnedPath(path) => { + self.renderer.stroke(&path, draw.brush, draw.stroke) + } + } + } + fn glyph_run( &mut self, - draw: GlyphRunRef<'_>, + draw: imaging::GlyphRunRef<'_>, glyphs: &mut dyn Iterator, ) { - self.record_draw(Draw::GlyphRun(draw.to_owned(glyphs))); + let run_ref = GlyphRunRef { + font: draw.font, + transform: draw.transform, + glyph_transform: draw.glyph_transform, + font_size: draw.font_size, + hint: draw.hint, + normalized_coords: draw.normalized_coords, + style: draw.style, + brush: draw.brush, + composite: draw.composite, + }; + set_transform_if_needed(self.renderer, Affine::IDENTITY, self.current_transform); + self.renderer.draw_glyphs( + Point::ZERO, + &run_ref, + glyphs.map(|glyph| Glyph { + id: glyph.id, + style_index: 0, + x: glyph.x, + y: glyph.y, + advance: 0.0, + }), + ); } fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { - self.record_draw(Draw::BlurredRoundedRect(draw)); + set_transform_if_needed(self.renderer, draw.transform, self.current_transform); + let shape = draw.rect.to_rounded_rect(draw.radius); + self.renderer.fill(&shape, draw.color, draw.std_dev); + } +} + +impl CustomPaintSink for ReplayRenderer<'_, R> +where + R: FloemRenderer, +{ + fn custom(&mut self, command: &DisplayCommandExt) { + let DisplayCommandExt::DrawSvg { + svg, + rect, + transform, + brush, + } = command; + set_transform_if_needed(self.renderer, *transform, self.current_transform); + self.renderer.draw_svg( + Svg { + tree: svg.tree.as_ref(), + hash: svg.hash.as_ref(), + }, + *rect, + brush.as_ref(), + ); } } @@ -703,86 +860,12 @@ pub(crate) fn replay_stage( &mut current_transform, &mut current_clip_stack, ); - for command in &chunk.commands { - match command { - DisplayCommand::PushClip { .. } | DisplayCommand::PopClip => {} - DisplayCommand::PushGroup { group } => { - let Some(Clip::Fill { - transform: clip_transform, - shape, - .. - }) = group.clip.as_ref() - else { - continue; - }; - let layer_transform = base_transform * *clip_transform; - set_transform_if_needed(renderer, base_transform, &mut current_transform); - match sanitize_clip_geometry(shape, layer_transform, render_size) { - Geometry::Rect(rect) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *clip_transform, - &rect, - ), - Geometry::RoundedRect(rect) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *clip_transform, - &rect, - ), - Geometry::Path(path) => renderer.push_layer( - group.composite.blend, - group.composite.alpha, - *clip_transform, - &path, - ), - } - } - DisplayCommand::PopGroup => renderer.pop_layer(), - DisplayCommand::Draw { draw } => { - replay_draw(renderer, draw, base_transform, &mut current_transform) - } - DisplayCommand::DrawImage { - img, - hash, - rect, - transform, - } => { - set_transform_if_needed( - renderer, - base_transform * *transform, - &mut current_transform, - ); - renderer.draw_img( - Img { - img: img.clone(), - hash, - }, - *rect, - ); - } - DisplayCommand::DrawSvg { - svg, - rect, - transform, - brush, - } => { - set_transform_if_needed( - renderer, - base_transform * *transform, - &mut current_transform, - ); - renderer.draw_svg( - Svg { - tree: svg.tree.as_ref(), - hash: svg.hash.as_ref(), - }, - *rect, - brush.as_ref(), - ); - } - } - } + let mut replay = ReplayRenderer { + renderer, + render_size, + current_transform: &mut current_transform, + }; + replay_ext_transformed(&chunk.commands, &mut replay, base_transform); } apply_clip_state( @@ -887,12 +970,12 @@ fn chunk_display_commands(commands: Vec) -> (Vec, Pa *chunk_transform_class = chunk_transform_class.combine(transform_class); *chunk_bounds = union_rects(*chunk_bounds, bounds); *chunk_metadata = chunk_metadata.merge(metadata); - chunk_commands.push(command); + record_scene_command(chunk_commands, command); } _ => chunks.push(PaintChunk { kind: PaintChunkKind::Draw, properties, - commands: vec![command], + commands: replay_scene([command]), bounds, metadata, transform_class, @@ -914,13 +997,52 @@ fn push_boundary_chunk( chunks.push(PaintChunk { kind: PaintChunkKind::Boundary, properties, - commands: vec![command], + commands: replay_scene([command]), bounds: None, metadata: PaintChunkMetadata::default(), transform_class, }); } +fn replay_scene( + commands: impl IntoIterator, +) -> ExtendedScene { + let mut scene = ExtendedScene::new(); + for command in commands { + record_scene_command(&mut scene, command); + } + scene +} + +fn record_scene_command(scene: &mut ExtendedScene, command: DisplayCommand) { + match command { + DisplayCommand::PushClip { clip } => { + let _ = scene.push_clip(clip); + } + DisplayCommand::PopClip => scene.pop_clip(), + DisplayCommand::PushGroup { group } => { + let _ = scene.push_group(group); + } + DisplayCommand::PopGroup => scene.pop_group(), + DisplayCommand::Draw { draw } => { + let _ = scene.draw(draw); + } + DisplayCommand::DrawSvg { + svg, + rect, + transform, + brush, + } => { + let _ = scene.custom_command(DisplayCommandExt::DrawSvg { + svg, + rect, + transform, + brush, + }); + } + } +} + fn command_transform_class(command: &DisplayCommand) -> TransformClass { match command { DisplayCommand::PushClip { .. } @@ -931,9 +1053,7 @@ fn command_transform_class(command: &DisplayCommand) -> TransformClass { Draw::Fill { .. } | Draw::Stroke { .. } => TransformClass::Affine, Draw::GlyphRun(_) | Draw::BlurredRoundedRect(_) => TransformClass::TranslateOnly, }, - DisplayCommand::DrawImage { .. } | DisplayCommand::DrawSvg { .. } => { - TransformClass::TranslateOnly - } + DisplayCommand::DrawSvg { .. } => TransformClass::TranslateOnly, } } @@ -944,9 +1064,7 @@ fn command_bounds(command: &DisplayCommand) -> Option { | DisplayCommand::PushGroup { .. } | DisplayCommand::PopGroup => None, DisplayCommand::Draw { draw, .. } => draw_bounds(draw), - DisplayCommand::DrawImage { rect, .. } | DisplayCommand::DrawSvg { rect, .. } => { - Some(*rect) - } + DisplayCommand::DrawSvg { rect, .. } => Some(*rect), } } @@ -1013,10 +1131,6 @@ fn command_metadata(command: &DisplayCommand) -> PaintChunkMetadata { ..PaintChunkMetadata::default() }, }, - DisplayCommand::DrawImage { .. } => PaintChunkMetadata { - has_raster_image: true, - ..PaintChunkMetadata::default() - }, DisplayCommand::DrawSvg { .. } => PaintChunkMetadata { has_vector_image: true, ..PaintChunkMetadata::default() @@ -1079,9 +1193,7 @@ fn command_affine(command: &DisplayCommand) -> Affine { Draw::GlyphRun(run) => run.transform, Draw::BlurredRoundedRect(rect) => rect.transform, }, - DisplayCommand::DrawImage { transform, .. } | DisplayCommand::DrawSvg { transform, .. } => { - *transform - } + DisplayCommand::DrawSvg { transform, .. } => *transform, } } @@ -1201,73 +1313,6 @@ fn clip_chain(property_tree: &PaintPropertyTree, clip_id: ClipNodeId) -> Vec, -) { - match draw { - Draw::Fill { - transform, - brush, - shape, - .. - } => { - set_transform_if_needed(renderer, base_transform * *transform, current_transform); - match shape { - Geometry::Rect(rect) => renderer.fill(rect, brush, 0.0), - Geometry::RoundedRect(rect) => renderer.fill(rect, brush, 0.0), - Geometry::Path(path) => renderer.fill(path, brush, 0.0), - } - } - Draw::Stroke { - transform, - stroke, - brush, - shape, - .. - } => { - set_transform_if_needed(renderer, base_transform * *transform, current_transform); - match shape { - Geometry::Rect(rect) => renderer.stroke(rect, brush, stroke), - Geometry::RoundedRect(rect) => renderer.stroke(rect, brush, stroke), - Geometry::Path(path) => renderer.stroke(path, brush, stroke), - } - } - Draw::GlyphRun(run) => { - let run_ref = GlyphRunRef { - font: &run.font, - transform: run.transform, - glyph_transform: run.glyph_transform, - font_size: run.font_size, - hint: run.hint, - normalized_coords: &run.normalized_coords, - style: &run.style, - brush: (&run.brush).into(), - composite: run.composite, - }; - set_transform_if_needed(renderer, base_transform, current_transform); - renderer.draw_glyphs( - Point::ZERO, - &run_ref, - run.glyphs.iter().map(|glyph| Glyph { - id: glyph.id, - style_index: 0, - x: glyph.x, - y: glyph.y, - advance: 0.0, - }), - ); - } - Draw::BlurredRoundedRect(rect) => { - set_transform_if_needed(renderer, base_transform * rect.transform, current_transform); - let shape = rect.rect.to_rounded_rect(rect.radius); - renderer.fill(&shape, rect.color, rect.std_dev); - } - } -} - fn set_transform_if_needed( renderer: &mut impl FloemRenderer, transform: Affine, @@ -1383,7 +1428,7 @@ mod tests { assert_eq!(stage.chunks.len(), 1); assert_eq!(stage.chunks[0].kind, PaintChunkKind::Draw); assert_eq!(stage.chunks[0].properties.z_index, 0); - assert_eq!(stage.chunks[0].commands.len(), 2); + assert_eq!(stage.chunks[0].commands.commands().len(), 2); assert_eq!(stage.transform_class, TransformClass::Affine); } @@ -1481,49 +1526,6 @@ mod tests { assert!(stage.chunks[0].bounds.is_some()); } - #[test] - fn stage_damage_query_filters_chunks_by_bounds() { - let rect = Rect::new(0.0, 0.0, 10.0, 10.0); - let mut stage = ElementStage::default(); - stage.set_commands( - vec![ - DisplayCommand::Draw { - draw: Draw::Fill { - transform: Affine::IDENTITY, - fill_rule: Fill::NonZero, - brush: Color::BLACK.into(), - brush_transform: None, - shape: Geometry::Rect(rect), - composite: Composite::default(), - }, - }, - DisplayCommand::DrawImage { - img: peniko::ImageBrush::new(peniko::ImageData { - data: peniko::Blob::new(Arc::new(vec![255, 255, 255, 255])), - format: peniko::ImageFormat::Rgba8, - alpha_type: peniko::ImageAlphaType::Alpha, - width: 1, - height: 1, - }), - hash: Arc::from([1_u8].as_slice()), - rect: Rect::new(40.0, 40.0, 50.0, 50.0), - transform: Affine::IDENTITY, - }, - ], - None, - ); - - let damage = [Rect::new(1.0, 1.0, 5.0, 5.0)]; - let chunks = stage.chunk_indices_for_damage(&damage); - assert_eq!(chunks, vec![0]); - assert_eq!(stage.chunks.len(), 1); - assert!(stage.chunks[0].metadata.has_raster_image); - assert_eq!( - stage.chunks[0].bounds, - Some(Rect::new(0.0, 0.0, 50.0, 50.0)) - ); - } - #[test] fn infinite_clip_is_constrained_to_render_bounds() { let constrained = constrain_infinite_rect( diff --git a/src/views/img.rs b/src/views/img.rs index 7a6cd525a..4dc399152 100644 --- a/src/views/img.rs +++ b/src/views/img.rs @@ -366,13 +366,31 @@ impl View for Img { let content_rect = self.id.get_content_rect_local(); let dest_rect = self.object_fit_dest_rect(content_rect); let image_brush = Brush::Image(self.img.clone()); + let source_width = self.img.image.width as f64; + let source_height = self.img.image.height as f64; + + if source_width <= 0.0 || source_height <= 0.0 { + return; + } + + let source_rect = Rect::new(0.0, 0.0, source_width, source_height); + let image_transform = peniko::kurbo::Affine::translate((dest_rect.x0, dest_rect.y0)) + .then_scale_non_uniform( + dest_rect.width() / source_width, + dest_rect.height() / source_height, + ); if self.needs_clip() { cx.painter.with_fill_clip(content_rect, |p| { - p.fill(dest_rect, &image_brush).draw(); + p.fill(source_rect, &image_brush) + .transform(image_transform) + .draw(); }); } else { - cx.painter.fill(dest_rect, &image_brush).draw(); + cx.painter + .fill(source_rect, &image_brush) + .transform(image_transform) + .draw(); } } } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 110fb6045..ef09da8c2 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -6,8 +6,8 @@ use floem_renderer::Renderer; use floem_renderer::text::{Glyph as ParleyGlyph, GlyphRunRef}; use floem_renderer::tiny_skia::{ self, FillRule, FilterQuality, GradientStop, IntRect, LinearGradient, Mask, MaskType, Paint, - Path, PathBuilder, Pixmap, PixmapPaint, PremultipliedColorU8, RadialGradient, Shader, - SpreadMode, Stroke, Transform, + Path, PathBuilder, Pattern, Pixmap, PixmapPaint, PremultipliedColorU8, RadialGradient, + Shader, SpreadMode, Stroke, Transform, }; use peniko::color::{self, ColorSpaceTag, DynamicColor, HueDirection, Srgb}; use peniko::kurbo::{PathEl, Size}; @@ -908,7 +908,7 @@ impl Some(peniko::Brush::Solid(color)), BrushRef::Gradient(gradient) => Some(peniko::Brush::Gradient(gradient.clone())), - BrushRef::Image(_) => None, + BrushRef::Image(image) => Some(peniko::Brush::Image(image.to_owned())), } } @@ -917,14 +917,31 @@ impl>>, ) -> Option> { - let paint = brush.and_then(|brush| brush_to_paint(brush))?; + let brush = brush?.into(); let mut colored_bg = Pixmap::new(pixmap.width(), pixmap.height())?; - colored_bg.fill_rect( - tiny_skia::Rect::from_xywh(0.0, 0.0, pixmap.width() as f32, pixmap.height() as f32)?, - &paint, - Transform::identity(), - None, - ); + let rect = + tiny_skia::Rect::from_xywh(0.0, 0.0, pixmap.width() as f32, pixmap.height() as f32)?; + match brush { + BrushRef::Image(image) => { + let image = image.to_owned(); + let image_pixmap = image_brush_pixmap(&image)?; + let paint = Paint { + shader: Pattern::new( + image_pixmap.as_ref(), + image_brush_spread_mode(&image), + image_quality_to_filter_quality(image.sampler.quality), + image.sampler.alpha, + Transform::identity(), + ), + ..Default::default() + }; + colored_bg.fill_rect(rect, &paint, Transform::identity(), None); + } + brush => { + let paint = brush_to_paint(brush)?; + colored_bg.fill_rect(rect, &paint, Transform::identity(), None); + } + } let mask = Mask::from_pixmap(pixmap.as_ref(), MaskType::Alpha); colored_bg.apply_mask(&mask); @@ -1683,6 +1700,29 @@ fn brush_to_paint<'b>(brush: impl Into>) -> Option> }) } +fn image_brush_pixmap(image: &peniko::ImageBrush) -> Option { + let mut pixmap = Pixmap::new(image.image.width, image.image.height)?; + for (a, b) in pixmap + .pixels_mut() + .iter_mut() + .zip(image.image.data.data().chunks_exact(4)) + { + *a = tiny_skia::Color::from_rgba8(b[0], b[1], b[2], b[3]) + .premultiply() + .to_color_u8(); + } + Some(pixmap) +} + +fn image_brush_spread_mode(image: &peniko::ImageBrush) -> SpreadMode { + let extend = if image.sampler.x_extend == image.sampler.y_extend { + image.sampler.x_extend + } else { + Extend::Pad + }; + to_spread_mode(extend) +} + const GRADIENT_TOLERANCE: f32 = 0.01; fn expand_gradient_stops(gradient: &Gradient) -> Vec { diff --git a/vger/src/lib.rs b/vger/src/lib.rs index eab107c8e..6bc79df1c 100644 --- a/vger/src/lib.rs +++ b/vger/src/lib.rs @@ -9,7 +9,7 @@ use floem_renderer::text::{Glyph, GlyphRunRef}; use floem_renderer::{Img, Renderer, tiny_skia}; use floem_vger_rs::{GlyphImage, Image, PaintIndex, PixelFormat, Vger}; use peniko::kurbo::{Size, Stroke}; -use peniko::{Blob, ImageData, LinearGradientPosition}; +use peniko::{Blob, Extend, ImageData, ImageQuality, LinearGradientPosition}; use peniko::{ BrushRef, Color, GradientKind, kurbo::{Affine, Point, Rect, Shape}, @@ -648,6 +648,37 @@ impl Renderer for VgerRenderer { let width = (rect.width() * scale_x.abs()).round().max(1.0) as u32; let height = (rect.height() * scale_y.abs()).round().max(1.0) as u32; + let brush = brush.map(Into::into); + + if let Some(BrushRef::Image(image)) = brush { + let image = image.to_owned(); + let mut svg_pixmap = tiny_skia::Pixmap::new(width, height).unwrap(); + let svg_scale = (width as f32 / svg.tree.size().width()) + .min(height as f32 / svg.tree.size().height()); + let transform = tiny_skia::Transform::from_scale(svg_scale, svg_scale); + resvg::render(svg.tree, transform, &mut svg_pixmap.as_mut()); + + let Some(final_pixmap) = colorize_svg_pixmap(&svg_pixmap, &image) else { + return; + }; + + let mut hash = Vec::with_capacity( + svg.hash.len() + std::mem::size_of::() + std::mem::size_of::() * 2, + ); + hash.extend_from_slice(svg.hash); + hash.extend_from_slice(&image.image.data.id().to_le_bytes()); + hash.extend_from_slice(&width.to_le_bytes()); + hash.extend_from_slice(&height.to_le_bytes()); + + self.vger.render_image(x, y, &hash, width, height, || Image { + width, + height, + data: final_pixmap.data().to_vec().into(), + pixel_format: PixelFormat::Rgba, + }); + return; + } + let paint = brush.and_then(|b| self.brush_to_paint(b)); self.vger.render_svg( @@ -764,6 +795,67 @@ fn vger_color(color: Color) -> floem_vger_rs::Color { } } +fn image_quality_to_filter_quality(quality: ImageQuality) -> tiny_skia::FilterQuality { + match quality { + ImageQuality::Low => tiny_skia::FilterQuality::Nearest, + ImageQuality::Medium | ImageQuality::High => tiny_skia::FilterQuality::Bilinear, + } +} + +fn image_brush_spread_mode(image: &peniko::ImageBrush) -> tiny_skia::SpreadMode { + match if image.sampler.x_extend == image.sampler.y_extend { + image.sampler.x_extend + } else { + Extend::Pad + } { + Extend::Pad => tiny_skia::SpreadMode::Pad, + Extend::Repeat => tiny_skia::SpreadMode::Repeat, + Extend::Reflect => tiny_skia::SpreadMode::Reflect, + } +} + +fn image_brush_pixmap(image: &peniko::ImageBrush) -> Option { + let mut pixmap = tiny_skia::Pixmap::new(image.image.width, image.image.height)?; + for (a, b) in pixmap + .pixels_mut() + .iter_mut() + .zip(image.image.data.data().chunks_exact(4)) + { + *a = tiny_skia::Color::from_rgba8(b[0], b[1], b[2], b[3]) + .premultiply() + .to_color_u8(); + } + Some(pixmap) +} + +fn colorize_svg_pixmap( + mask_pixmap: &tiny_skia::Pixmap, + image: &peniko::ImageBrush, +) -> Option { + let image_pixmap = image_brush_pixmap(image)?; + let mut colored = tiny_skia::Pixmap::new(mask_pixmap.width(), mask_pixmap.height())?; + let rect = tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + mask_pixmap.width() as f32, + mask_pixmap.height() as f32, + )?; + let paint = tiny_skia::Paint { + shader: tiny_skia::Pattern::new( + image_pixmap.as_ref(), + image_brush_spread_mode(image), + image_quality_to_filter_quality(image.sampler.quality), + image.sampler.alpha, + tiny_skia::Transform::identity(), + ), + ..Default::default() + }; + colored.fill_rect(rect, &paint, tiny_skia::Transform::identity(), None); + let mask = tiny_skia::Mask::from_pixmap(mask_pixmap.as_ref(), tiny_skia::MaskType::Alpha); + colored.apply_mask(&mask); + Some(colored) +} + fn scaled_embolden_strength(font_embolden: f32, scale: f64) -> f32 { font_embolden * scale as f32 } From 87d88fa43e072468d4f2da58d8f9dd14596f4c3b Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Mon, 23 Mar 2026 21:22:36 -0600 Subject: [PATCH 024/102] changes --- Cargo.lock | 256 +++++++++++------ Cargo.toml | 8 +- renderer/src/lib.rs | 33 +-- skia/src/lib.rs | 34 +-- src/compositor/backend.rs | 2 +- src/compositor/mod.rs | 7 +- src/compositor/subduction.rs | 16 +- src/paint/renderer.rs | 22 +- src/window/handle.rs | 8 +- tiny_skia/src/lib.rs | 115 -------- vello/Cargo.toml | 5 +- vello/src/lib.rs | 531 +++++++++++++++++------------------ vger/Cargo.toml | 2 +- vger/src/lib.rs | 30 +- 14 files changed, 472 insertions(+), 597 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe11d6fe0..56bbff2ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1190,7 +1190,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1389,7 +1389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1400,9 +1400,9 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "euclid" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", ] @@ -1625,7 +1625,7 @@ dependencies = [ [[package]] name = "floem-vger" version = "0.3.2" -source = "git+https://github.com/lapce/vger-rs?rev=54ab8135d9d870a4ac2b84fc6b9846b3b03ec919#54ab8135d9d870a4ac2b84fc6b9846b3b03ec919" +source = "git+https://github.com/lapce/vger-rs?rev=9a947c453200f4e5de7780c57de784f1d96f0714#9a947c453200f4e5de7780c57de784f1d96f0714" dependencies = [ "euclid", "fontdue", @@ -1913,10 +1913,11 @@ version = "0.2.0" dependencies = [ "anyhow", "floem_renderer", + "imaging", + "imaging_vello", "parley", "peniko", "vello", - "vello_svg", "wgpu", ] @@ -2071,8 +2072,8 @@ dependencies = [ "read-fonts 0.35.0", "roxmltree 0.21.1", "smallvec", - "windows", - "windows-core", + "windows 0.58.0", + "windows-core 0.58.0", "yeslogic-fontconfig-sys", ] @@ -2542,35 +2543,18 @@ dependencies = [ "system-deps", ] -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.11.0", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "gpu-allocator" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" dependencies = [ + "ash", + "hashbrown 0.16.1", "log", "presser", - "thiserror 1.0.69", - "windows", + "thiserror 2.0.18", + "windows 0.62.2", ] [[package]] @@ -2653,9 +2637,9 @@ dependencies = [ [[package]] name = "guillotiere" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" dependencies = [ "euclid", "svg_fmt", @@ -3006,6 +2990,17 @@ dependencies = [ "peniko", ] +[[package]] +name = "imaging_vello" +version = "0.0.1" +dependencies = [ + "imaging", + "kurbo", + "peniko", + "pollster", + "vello", +] + [[package]] name = "imbl" version = "7.0.0" @@ -3454,9 +3449,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ "bitflags 2.11.0", "block", @@ -3532,9 +3527,9 @@ dependencies = [ [[package]] name = "naga" -version = "27.0.3" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" dependencies = [ "arrayvec", "bit-set", @@ -5147,7 +5142,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5731,7 +5726,7 @@ dependencies = [ [[package]] name = "subduction_backend_wgpu" version = "0.0.1" -source = "git+https://github.com/forest-rs/subduction.git?rev=942a0d4d1a65f855c2a77aa10d01be132196d4bd#942a0d4d1a65f855c2a77aa10d01be132196d4bd" +source = "git+https://github.com/jrmoulton/subduction.git?branch=bump-wgpu#16ad0dd1a59a2c83d47c28cc94001c0e8deb3f77" dependencies = [ "bytemuck", "subduction_core", @@ -5741,7 +5736,7 @@ dependencies = [ [[package]] name = "subduction_core" version = "0.0.1" -source = "git+https://github.com/forest-rs/subduction.git?rev=942a0d4d1a65f855c2a77aa10d01be132196d4bd#942a0d4d1a65f855c2a77aa10d01be132196d4bd" +source = "git+https://github.com/jrmoulton/subduction.git?branch=bump-wgpu#16ad0dd1a59a2c83d47c28cc94001c0e8deb3f77" dependencies = [ "kurbo", "understory_dirty", @@ -5934,7 +5929,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6609,15 +6604,15 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vello" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72fef40773530322d5c2ffe3c1107e9874bd8239ac137d1c2b6c1edad695146e" +checksum = "7484a935acbced16ca6b2a7e6c8d6aa2b32ca0aff269c1b95f62be9b8f672c41" dependencies = [ "bytemuck", "futures-intrusive", "log", "peniko", - "png 0.17.16", + "png 0.18.1", "skrifa 0.40.0", "static_assertions", "thiserror 2.0.18", @@ -6628,9 +6623,9 @@ dependencies = [ [[package]] name = "vello_encoding" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c91203ec4b483440614a9a5c7c2d991932af72c5349659a63ec49476f0b79c" +checksum = "d53d864987821f40431e62608bf0f58342db57f2f2860628c8cfc28e16a1cc39" dependencies = [ "bytemuck", "guillotiere", @@ -6641,9 +6636,9 @@ dependencies = [ [[package]] name = "vello_shaders" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a765d44d4bd354146e44f9a860f4e92effd91a97302549be9e47f0a18d8128c" +checksum = "c455e04d570129212ecc4c0fdaa051ad66e5be15781a75b38c519d08120db374" dependencies = [ "bytemuck", "log", @@ -6652,18 +6647,6 @@ dependencies = [ "vello_encoding", ] -[[package]] -name = "vello_svg" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79dfa03829a8fb45ad458a05bd533d399d2259d042cdd78f391b290dbbdf1bd3" -dependencies = [ - "image 0.25.9", - "thiserror 2.0.18", - "usvg", - "vello", -] - [[package]] name = "version-compare" version = "0.2.1" @@ -7003,12 +6986,13 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" -version = "27.0.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" dependencies = [ "arrayvec", "bitflags 2.11.0", + "bytemuck", "cfg-if", "cfg_aliases", "document-features", @@ -7032,9 +7016,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "27.0.3" +version = "28.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +checksum = "d23f4642f53f666adcfd2d3218ab174d1e6681101aef18696b90cbe64d1c10f9" dependencies = [ "arrayvec", "bit-set", @@ -7064,36 +7048,36 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "27.0.4" +version = "28.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +checksum = "44d6cb474beb218824dcc9e1ce679d973f719262789bfb27407da560cac20eeb" dependencies = [ "android_system_properties", "arrayvec", @@ -7107,7 +7091,6 @@ dependencies = [ "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", - "gpu-alloc", "gpu-allocator", "gpu-descriptor", "hashbrown 0.16.1", @@ -7134,21 +7117,20 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-types", - "windows", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] name = "wgpu-types" -version = "27.0.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" dependencies = [ "bitflags 2.11.0", "bytemuck", "js-sys", "log", - "thiserror 2.0.18", "web-sys", ] @@ -7194,7 +7176,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -7232,23 +7214,68 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -7260,6 +7287,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -7271,12 +7309,33 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -7286,16 +7345,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7413,6 +7490,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 32528b012..bf7e64972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ imbl = "7.0" resvg = { version = "0.46" } raw-window-handle = "0.6.0" unicode-segmentation = "1.10" -wgpu = { version = "27.0" } +wgpu = { version = "28.0" } parking_lot = { version = "0.12" } swash = { version = "0.2" } muda = { version = "0.17.1", default-features = false } @@ -99,8 +99,8 @@ floem_vger_renderer = { path = "vger", version = "0.2.0", optional = true } floem_tiny_skia_renderer = { path = "tiny_skia", version = "0.2.0" } floem_reactive = { path = "reactive", version = "0.2.0" } floem-editor-core = { path = "editor-core", version = "0.2.0", optional = true } -subduction_core = { git = "https://github.com/forest-rs/subduction.git", rev = "942a0d4d1a65f855c2a77aa10d01be132196d4bd", package = "subduction_core", optional = true, features = ["std"] } -subduction_backend_wgpu = { git = "https://github.com/forest-rs/subduction.git", rev = "942a0d4d1a65f855c2a77aa10d01be132196d4bd", package = "subduction_backend_wgpu", optional = true } +subduction_core = { git = "https://github.com/jrmoulton/subduction.git", branch = "bump-wgpu", package = "subduction_core", optional = true, features = ["std"] } +subduction_backend_wgpu = { git = "https://github.com/jrmoulton/subduction.git", branch = "bump-wgpu", package = "subduction_backend_wgpu", optional = true } copypasta = { version = "0.10", default-features = false, features = [ "wayland", "x11", @@ -155,7 +155,7 @@ objc2-app-kit = { version = "0.3", features = [ dispatch2 = {version = "0.3.0"} [features] -default = ["editor", "default-image-formats", "vger", "serde"] +default = ["editor", "default-image-formats", "vello", "serde"] skia = ["dep:floem_skia_renderer"] vello = ["dep:floem_vello_renderer"] vger = ["dep:floem_vger_renderer"] diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index cec183f06..17e3d4938 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,7 +1,7 @@ //! Rendering abstraction layer for the Floem UI framework. //! //! This crate defines the [`Renderer`] trait that all Floem rendering backends must -//! implement, along with supporting types for passing images and SVGs to the renderer. +//! implement, along with supporting types for passing SVGs to the renderer. //! Floem ships with three backend implementations: //! //! - **Vello** (`floem_vello_renderer`) — GPU-accelerated renderer using the Vello scene graph (default). @@ -56,34 +56,11 @@ pub struct Svg<'a> { pub hash: &'a [u8], } -/// A raster image paired with a cache key. -/// -/// # Example -/// -/// ```no_run -/// use floem_renderer::Img; -/// -/// # fn make_image_brush() -> peniko::ImageBrush { todo!() } -/// let brush = make_image_brush(); -/// let hash = b"photo-abc123"; -/// -/// let img = Img { -/// img: brush, -/// hash: hash.as_slice(), -/// }; -/// ``` -pub struct Img<'a> { - /// The image data as a [`peniko::ImageBrush`]. - pub img: peniko::ImageBrush, - /// An opaque byte slice used as a cache key by the renderer. - pub hash: &'a [u8], -} - /// The core rendering trait that every Floem backend must implement. /// /// A frame is bracketed by [`begin`](Renderer::begin) and [`finish`](Renderer::finish). /// Between those calls the framework issues drawing commands — fills, strokes, text, -/// images, SVGs — which the backend records or executes immediately depending on its +/// SVGs — which the backend records or executes immediately depending on its /// architecture. /// /// The typical call sequence within a single frame looks like: @@ -95,7 +72,6 @@ pub struct Img<'a> { /// renderer.clip(..); // restrict drawing area /// renderer.draw_text_lines(..); // labels, editors /// renderer.draw_svg(..); // icons -/// renderer.draw_img(..); // photos /// renderer.stroke(..); // borders /// renderer.clear_clip(); /// renderer.finish(); @@ -188,11 +164,6 @@ pub trait Renderer { /// the given brush — this is how Floem applies a color override to icons. fn draw_svg<'b>(&mut self, svg: Svg<'b>, rect: Rect, brush: Option>>); - /// Draw a raster image inside `rect`. - /// - /// The image is scaled to fit the destination rectangle. - fn draw_img(&mut self, img: Img<'_>, rect: Rect); - /// Finish the current frame and present it. /// /// If the frame was started with `capture = true`, the rendered content is diff --git a/skia/src/lib.rs b/skia/src/lib.rs index 4d8ebfc78..be47f839a 100644 --- a/skia/src/lib.rs +++ b/skia/src/lib.rs @@ -5,7 +5,7 @@ use anyhow::{Result, anyhow}; use anyrender::{ImageRenderer as _, WindowRenderer as _}; use anyrender_skia::{SkiaImageRenderer, SkiaWindowRenderer}; use floem_renderer::text::{Glyph, GlyphRunRef}; -use floem_renderer::{Img, Renderer, Svg}; +use floem_renderer::{Renderer, Svg}; use peniko::kurbo::{Affine, BezPath, Point, Rect, Shape, Size, Stroke}; use peniko::{ Blob, Brush, BrushRef, Compose, Fill, ImageAlphaType, ImageData, ImageFormat, Mix, Style, @@ -60,11 +60,6 @@ enum Command { glyph_transform: Option, glyphs: Vec, }, - Image { - image: peniko::ImageBrush, - rect: Rect, - transform: Affine, - }, Svg { image: peniko::ImageBrush, rect: Rect, @@ -226,25 +221,6 @@ impl SkiaRenderer { glyphs.iter().copied(), ); } - Command::Image { - image, - rect, - transform, - } => { - let image_transform = image_transform(*transform, *rect, image); - scene.fill( - Fill::NonZero, - image_transform, - image.as_ref(), - None, - &Rect::new( - 0.0, - 0.0, - image.image.width as f64, - image.image.height as f64, - ), - ); - } Command::Svg { image, rect, @@ -468,14 +444,6 @@ impl Renderer for SkiaRenderer { }); } - fn draw_img(&mut self, img: Img<'_>, rect: Rect) { - self.commands.push(Command::Image { - image: img.img, - rect, - transform: self.transform, - }); - } - fn finish(&mut self) -> Option { if self.capture { let commands = std::mem::take(&mut self.commands); diff --git a/src/compositor/backend.rs b/src/compositor/backend.rs index 991b2d350..d7e942da3 100644 --- a/src/compositor/backend.rs +++ b/src/compositor/backend.rs @@ -16,7 +16,7 @@ use super::{ }; pub type FloemPaintedSurfaceVisitor<'a> = - dyn FnMut(CompositorLayerId, Rect, (u32, u32), &wgpu::TextureView) + 'a; + dyn FnMut(CompositorLayerId, Rect, (u32, u32), wgpu::TextureFormat, &wgpu::TextureView) + 'a; /// Backend interface for mirroring Floem compositor state into a concrete /// presentation/composition engine. diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index 8d85acff2..25273f7f5 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -304,7 +304,8 @@ pub(crate) struct ResolvedPromotedLayer { } pub(crate) type FloemSurfaceRoleVisitor<'a> = - dyn FnMut(FloemPaintedSurfaceRole, Rect, (u32, u32), &wgpu::TextureView) + 'a; + dyn FnMut(FloemPaintedSurfaceRole, Rect, (u32, u32), wgpu::TextureFormat, &wgpu::TextureView) + + 'a; /// Retained compositor registry and frame-request state. #[derive(Default)] @@ -496,7 +497,7 @@ impl Compositor { }; let mut visit_backend: &mut FloemPaintedSurfaceVisitor<'_> = - &mut |layer_id, bounds, size, view| { + &mut |layer_id, bounds, size, format, view| { let Some(layer_state) = self.layers.get(&layer_id) else { return; }; @@ -520,7 +521,7 @@ impl Compositor { }; if let Some(role) = role { - visit(role, bounds, size, view); + visit(role, bounds, size, format, view); } }; backend.for_each_floem_painted_surface(&mut visit_backend); diff --git a/src/compositor/subduction.rs b/src/compositor/subduction.rs index 08e1921c6..6a0781dbf 100644 --- a/src/compositor/subduction.rs +++ b/src/compositor/subduction.rs @@ -13,7 +13,7 @@ //! This is enough to establish the architectural seam. It is not yet full //! presentation integration. -use peniko::kurbo::{Affine, Rect, RoundedRect}; +use peniko::kurbo::{Rect, RoundedRect}; use rustc_hash::FxHashMap; use subduction_core::{ backend::Presenter as _, @@ -132,8 +132,10 @@ impl SubductionCompositorBackend { // translation (rect origin) and a local size (rect dimensions). let bounds = descriptor.bounds; let is_empty = !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0; - self.store - .set_transform(layer_id, Transform3d::from(Affine::translate(bounds.origin().to_vec2()))); + self.store.set_transform( + layer_id, + Transform3d::from_translation(bounds.x0, bounds.y0, 0.0), + ); self.store.set_bounds( layer_id, if is_empty { @@ -349,7 +351,13 @@ impl CompositorBackend for SubductionCompositorBackend { descriptor.bounds.width().max(1.0).round() as u32, descriptor.bounds.height().max(1.0).round() as u32, ); - visit(layer_id, descriptor.bounds, size, view); + visit( + layer_id, + descriptor.bounds, + size, + presenter.layer_format(), + view, + ); } } diff --git a/src/paint/renderer.rs b/src/paint/renderer.rs index 2dd1752a0..996ba7f1d 100644 --- a/src/paint/renderer.rs +++ b/src/paint/renderer.rs @@ -49,7 +49,6 @@ //! use std::sync::Arc; -use floem_renderer::Img; use floem_renderer::gpu_resources::GpuResources; #[cfg(feature = "skia")] use floem_skia_renderer::SkiaRenderer as ActiveRenderer; @@ -248,11 +247,17 @@ impl Renderer { pub(crate) fn render_scene_to_premultiplied_texture_view( &mut self, target_view: &wgpu::TextureView, + target_format: wgpu::TextureFormat, size: (u32, u32), ) -> bool { match self { Renderer::Active(r) => r - .render_scene_to_premultiplied_texture_view(target_view, size.0, size.1) + .render_scene_to_premultiplied_texture_view( + target_view, + target_format, + size.0, + size.1, + ) .is_ok(), Renderer::TinySkia(_) | Renderer::Uninitialized { .. } => false, } @@ -262,6 +267,7 @@ impl Renderer { pub(crate) fn render_scene_to_premultiplied_texture_view( &mut self, _target_view: &wgpu::TextureView, + _target_format: wgpu::TextureFormat, _size: (u32, u32), ) -> bool { false @@ -384,18 +390,6 @@ impl floem_renderer::Renderer for Renderer { } } - fn draw_img(&mut self, img: Img<'_>, rect: Rect) { - match self { - Renderer::Active(v) => { - v.draw_img(img, rect); - } - Renderer::TinySkia(v) => { - v.draw_img(img, rect); - } - Renderer::Uninitialized { .. } => {} - } - } - fn draw_glyphs<'a>( &mut self, origin: Point, diff --git a/src/window/handle.rs b/src/window/handle.rs index 4b5d53cd5..1e294e0bb 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -957,11 +957,11 @@ impl WindowHandle { let mut surfaces = Vec::new(); cx.window_state - .for_each_floem_painted_surface(&mut |role, bounds, size, view| { - surfaces.push((role, bounds, size, view.clone())); + .for_each_floem_painted_surface(&mut |role, bounds, size, target_format, view| { + surfaces.push((role, bounds, size, target_format, view.clone())); }); - for (role, bounds, size, view) in surfaces { + for (role, bounds, size, target_format, view) in surfaces { cx.paint_state.renderer_mut().begin(false); if matches!(role, crate::compositor::FloemPaintedSurfaceRole::Root) && !transparent { @@ -1033,7 +1033,7 @@ impl WindowHandle { if !cx .paint_state .renderer_mut() - .render_scene_to_premultiplied_texture_view(&view, size) + .render_scene_to_premultiplied_texture_view(&view, target_format, size) { return cx.paint_state.renderer_mut().finish(); } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index ef09da8c2..86b81a989 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -1,7 +1,6 @@ mod recording; use anyhow::{Result, anyhow}; -use floem_renderer::Img; use floem_renderer::Renderer; use floem_renderer::text::{Glyph as ParleyGlyph, GlyphRunRef}; use floem_renderer::tiny_skia::{ @@ -1179,56 +1178,6 @@ fn image_quality_to_filter_quality(quality: ImageQuality) -> FilterQuality { } } -fn axis_aligned_device_placement(rect: Rect, transform: Affine) -> Option<(f32, f32, u32, u32)> { - if !is_axis_aligned(transform) { - return None; - } - - let device_rect = transform.transform_rect_bbox(rect); - let width = nearly_integral(device_rect.width())?; - let height = nearly_integral(device_rect.height())?; - (width > 0 && height > 0).then_some(( - device_rect.x0 as f32, - device_rect.y0 as f32, - width as u32, - height as u32, - )) -} - -fn cache_scaled_pixmap( - cache_color: CacheColor, - cache_key: ScaledImageCacheKey, - pixmap: &Pixmap, - quality: ImageQuality, -) -> Option> { - if let Some(cached) = SCALED_IMAGE_CACHE.with_borrow_mut(|cache| { - cache.get_mut(&cache_key).map(|(color, pixmap)| { - *color = cache_color; - pixmap.clone() - }) - }) { - return Some(cached); - } - - let mut scaled = Pixmap::new(cache_key.width, cache_key.height)?; - let paint = PixmapPaint { - opacity: 1.0, - blend_mode: tiny_skia::BlendMode::SourceOver, - quality: image_quality_to_filter_quality(quality), - }; - let transform = Transform::from_scale( - cache_key.width as f32 / pixmap.width() as f32, - cache_key.height as f32 / pixmap.height() as f32, - ); - scaled.draw_pixmap(0, 0, pixmap.as_ref(), &paint, transform, None); - - let scaled = Arc::new(scaled); - SCALED_IMAGE_CACHE.with_borrow_mut(|cache| { - cache.insert(cache_key, (cache_color, scaled.clone())); - }); - Some(scaled) -} - fn mul_div_255(value: u8, factor: u8) -> u8 { (((value as u16 * factor as u16) + 127) / 255) as u8 } @@ -1385,70 +1334,6 @@ impl, rect: Rect) { - let transform = self.transform; - let pixmap = if let Some(pixmap) = IMAGE_CACHE.with_borrow_mut(|ic| { - ic.get_mut(img.hash).map(|(color, pixmap)| { - *color = self.cache_color; - pixmap.clone() - }) - }) { - pixmap - } else { - let image_data = img.img.image.data.data(); - let mut pixmap = try_ret!(Pixmap::new(img.img.image.width, img.img.image.height)); - for (a, b) in pixmap - .pixels_mut() - .iter_mut() - .zip(image_data.chunks_exact(4)) - { - *a = tiny_skia::Color::from_rgba8(b[0], b[1], b[2], b[3]) - .premultiply() - .to_color_u8(); - } - - let pixmap = Arc::new(pixmap); - IMAGE_CACHE.with_borrow_mut(|ic| { - ic.insert(img.hash.to_owned(), (self.cache_color, pixmap.clone())); - }); - pixmap - }; - - let quality = img.img.sampler.quality; - if let Some((draw_x, draw_y, width, height)) = - axis_aligned_device_placement(rect, transform) - { - let filter_quality = image_quality_to_filter_quality(quality); - let device_pixmap = if width == pixmap.width() && height == pixmap.height() { - pixmap.clone() - } else { - let cache_key = ScaledImageCacheKey { - image_id: img.img.image.data.id(), - width, - height, - quality: quality as u8, - }; - try_ret!(cache_scaled_pixmap( - self.cache_color, - cache_key, - &pixmap, - quality - )) - }; - self.recording.draw_pixmap_direct( - device_pixmap, - draw_x, - draw_y, - Affine::IDENTITY, - filter_quality, - ); - return; - } - - self.recording - .draw_pixmap_rect(pixmap, rect, transform, quality); - } - fn draw_svg<'b>( &mut self, svg: floem_renderer::Svg<'b>, diff --git a/vello/Cargo.toml b/vello/Cargo.toml index ae7c5b760..e255124ee 100644 --- a/vello/Cargo.toml +++ b/vello/Cargo.toml @@ -10,8 +10,9 @@ license.workspace = true peniko = { workspace = true } parley = { workspace = true } wgpu = { workspace = true } +imaging = { workspace = true } anyhow = "1.0" -vello = "0.7.0" -vello_svg = "0.9.0" +imaging_vello = { path = "../../imaging/imaging_vello" } +vello = "0.8.0" floem_renderer = { path = "../renderer", version = "0.2.0" } diff --git a/vello/src/lib.rs b/vello/src/lib.rs index af4f8024f..d409fa5f4 100644 --- a/vello/src/lib.rs +++ b/vello/src/lib.rs @@ -1,24 +1,25 @@ use std::collections::HashMap; -use std::mem; -use std::num::NonZero; use std::sync::Arc; use std::sync::mpsc::sync_channel; use anyhow::Result; use floem_renderer::gpu_resources::GpuResources; use floem_renderer::text::{Glyph, GlyphRunRef}; -use floem_renderer::{Img, Renderer}; +use floem_renderer::{Renderer, Svg}; +use imaging::record::{CustomCommand, ExtendedScene, Glyph as ImagingGlyph, replay_ext}; +use imaging::{ + BlurredRoundedRect, ClipRef, Composite, CustomPaintSink, FillRef, GroupRef, PaintSink, + StrokeRef, +}; use peniko::kurbo::Size; use peniko::{ Blob, BrushRef, color::palette, kurbo::{Affine, Point, Rect, Shape}, }; -use peniko::{Compose, Fill, ImageAlphaType, ImageData, Mix}; -use vello::kurbo::Stroke; +use peniko::{ImageAlphaType, ImageData}; use vello::util::RenderSurface; -use vello::wgpu::Device; -use vello::{AaConfig, RendererOptions, Scene}; +use vello::{AaConfig, RenderParams, RendererOptions, Scene}; use wgpu::util::TextureBlitter; use wgpu::{Adapter, DeviceType, Queue, TextureAspect, TextureFormat}; @@ -65,41 +66,142 @@ struct PremultiplyScratch { view: wgpu::TextureView, } +#[derive(Clone)] +enum VelloCommand { + DrawSvg { + svg: SvgCommand, + rect: Rect, + transform: Affine, + brush: Option, + }, +} + +impl CustomCommand for VelloCommand { + fn prepend_transform(&self, prefix: Affine) -> Self { + match self { + Self::DrawSvg { + svg, + rect, + transform, + brush, + } => Self::DrawSvg { + svg: svg.clone(), + rect: *rect, + transform: prefix * *transform, + brush: brush.clone(), + }, + } + } +} + +#[derive(Clone)] +struct SvgCommand { + hash: Arc<[u8]>, +} + +#[derive(Default)] +struct SvgCache { + entries: HashMap, SvgCacheEntry>, +} + +impl SvgCache { + fn touch(&mut self, svg: &SvgCommand) { + self.entries.entry(svg.hash.to_vec()).or_default(); + } +} + +#[derive(Default)] +struct SvgCacheEntry { + #[allow(dead_code)] + alpha_mask_scene: AlphaMaskScene, +} + +#[derive(Default)] +struct AlphaMaskScene { + #[allow(dead_code)] + scene: Option, +} + +struct VelloSceneAdapter<'a, 'b> { + inner: &'a mut imaging_vello::VelloSceneSink<'b>, + svg_cache: &'a mut SvgCache, +} + +impl PaintSink for VelloSceneAdapter<'_, '_> { + fn push_clip(&mut self, clip: ClipRef<'_>) { + self.inner.push_clip(clip); + } + + fn pop_clip(&mut self) { + self.inner.pop_clip(); + } + + fn push_group(&mut self, group: GroupRef<'_>) { + self.inner.push_group(group); + } + + fn pop_group(&mut self) { + self.inner.pop_group(); + } + + fn fill(&mut self, draw: FillRef<'_>) { + self.inner.fill(draw); + } + + fn stroke(&mut self, draw: StrokeRef<'_>) { + self.inner.stroke(draw); + } + + fn glyph_run( + &mut self, + draw: imaging::GlyphRunRef<'_>, + glyphs: &mut dyn Iterator, + ) { + self.inner.glyph_run(draw, glyphs); + } + + fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { + self.inner.blurred_rounded_rect(draw); + } +} + +impl CustomPaintSink for VelloSceneAdapter<'_, '_> { + fn custom(&mut self, command: &VelloCommand) { + let VelloCommand::DrawSvg { + svg, + rect, + transform, + brush, + } = command; + self.svg_cache.touch(svg); + let _ = (rect, transform, brush); + } +} + pub struct VelloRenderer { - device: Device, - #[allow(unused)] + device: wgpu::Device, queue: Queue, surface: RenderSurface<'static>, renderer: vello::Renderer, - scene: Scene, - alt_scene: Option, + scene: ExtendedScene, window_scale: f64, transform: Affine, capture: bool, adapter: Adapter, - // TODO: Apply once vello's DrawGlyphs gains embolden support. #[allow(dead_code)] font_embolden: f32, - /// Cached vello scenes keyed by SVG content hash. - /// The bool tracks the current generation for eviction. - svg_cache: HashMap, (bool, Scene)>, - /// Current cache generation; toggled each frame so stale entries are evicted. - cache_generation: bool, - premultiply_pipeline: PremultiplyPipeline, + svg_cache: SvgCache, + premultiply_pipelines: HashMap, compositor_scratch: Option, } impl VelloRenderer { - fn device_transform(&self) -> Affine { - self.transform - } - - fn render_params(width: u32, height: u32) -> vello::RenderParams { - vello::RenderParams { + fn render_params(width: u32, height: u32) -> RenderParams { + RenderParams { base_color: palette::css::TRANSPARENT, width, height, - antialiasing_method: vello::AaConfig::Msaa16, + antialiasing_method: AaConfig::Area, } } @@ -187,35 +289,20 @@ impl VelloRenderer { blitter: TextureBlitter::new(&device, texture_format), }; - let scene = Scene::new(); - let renderer = vello::Renderer::new( - &device, - RendererOptions { - pipeline_cache: None, - use_cpu: false, - antialiasing_support: vello::AaSupport::all(), - num_init_threads: Some(NonZero::new(1).unwrap()), - }, - ) - .unwrap(); - let premultiply_pipeline = - create_premultiply_pipeline(&device, wgpu::TextureFormat::Rgba8Unorm); - + let renderer = vello::Renderer::new(&device, RendererOptions::default())?; Ok(Self { device, queue, surface: render_surface, renderer, - scene, - alt_scene: None, + scene: ExtendedScene::new(), window_scale: scale, transform: Affine::IDENTITY, capture: false, adapter, font_embolden, - svg_cache: HashMap::new(), - cache_generation: false, - premultiply_pipeline, + svg_cache: SvgCache::default(), + premultiply_pipelines: HashMap::default(), compositor_scratch: None, }) } @@ -259,16 +346,32 @@ impl VelloRenderer { ) } + fn build_scene(&mut self, width: u32, height: u32) -> Result { + let mut scene = Scene::new(); + let bounds = Rect::new(0.0, 0.0, width as f64, height as f64); + let mut sink = imaging_vello::VelloSceneSink::new(&mut scene, bounds); + { + let mut adapter = VelloSceneAdapter { + inner: &mut sink, + svg_cache: &mut self.svg_cache, + }; + replay_ext(&self.scene, &mut adapter); + } + sink.finish().map_err(|err| anyhow::anyhow!("{err:?}"))?; + Ok(scene) + } + pub fn render_scene_to_texture_view( &mut self, target_view: &wgpu::TextureView, width: u32, height: u32, ) -> Result<()> { + let scene = self.build_scene(width, height)?; Ok(self.renderer.render_to_texture( &self.device, &self.queue, - &self.scene, + &scene, target_view, &Self::render_params(width, height), )?) @@ -277,12 +380,13 @@ impl VelloRenderer { pub fn render_scene_to_premultiplied_texture_view( &mut self, target_view: &wgpu::TextureView, + target_format: wgpu::TextureFormat, width: u32, height: u32, ) -> Result<()> { let scratch_view = self.ensure_compositor_scratch((width, height)); self.render_scene_to_texture_view(&scratch_view, width, height)?; - self.premultiply_into_view(&scratch_view, target_view); + self.premultiply_into_view(&scratch_view, target_view, target_format); Ok(()) } @@ -340,13 +444,22 @@ impl VelloRenderer { } fn premultiply_into_view( - &self, + &mut self, source_view: &wgpu::TextureView, target_view: &wgpu::TextureView, + target_format: wgpu::TextureFormat, ) { + if !self.premultiply_pipelines.contains_key(&target_format) { + let pipeline = create_premultiply_pipeline(&self.device, target_format); + self.premultiply_pipelines.insert(target_format, pipeline); + } + let pipeline = self + .premultiply_pipelines + .get(&target_format) + .expect("premultiply pipeline just inserted"); let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Floem Premultiply Bind Group"), - layout: &self.premultiply_pipeline.bind_group_layout, + layout: &pipeline.bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -354,7 +467,7 @@ impl VelloRenderer { }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Sampler(&self.premultiply_pipeline.sampler), + resource: wgpu::BindingResource::Sampler(&pipeline.sampler), }, ], }); @@ -379,8 +492,9 @@ impl VelloRenderer { depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, + multiview_mask: None, }); - pass.set_pipeline(&self.premultiply_pipeline.render_pipeline); + pass.set_pipeline(&pipeline.render_pipeline); pass.set_bind_group(0, &bind_group, &[]); pass.draw(0..3, 0..1); } @@ -390,79 +504,65 @@ impl VelloRenderer { impl Renderer for VelloRenderer { fn begin(&mut self, capture: bool) { - if self.capture == capture { - self.scene.reset(); - } else { - self.capture = capture; - if self.alt_scene.is_none() { - self.alt_scene = Some(Scene::new()); - } - if let Some(scene) = self.alt_scene.as_mut() { - scene.reset(); - } - self.scene.reset(); - mem::swap(&mut self.scene, self.alt_scene.as_mut().unwrap()); - } + self.capture = capture; + self.scene = ExtendedScene::new(); self.transform = Affine::IDENTITY; - - // Evict SVG scenes not used in the previous frame, then flip generation. - let generation = self.cache_generation; - self.svg_cache.retain(|_, (g, _)| *g == generation); - self.cache_generation = !generation; } fn stroke<'b, 's>( &mut self, shape: &impl Shape, brush: impl Into>, - stroke: &'s Stroke, + stroke: &'s peniko::kurbo::Stroke, ) { - self.scene - .stroke(stroke, self.device_transform(), brush, None, shape); + let draw = imaging::record::Draw::Stroke { + transform: self.transform, + stroke: stroke.clone(), + brush: brush.into().to_owned(), + brush_transform: None, + shape: shape_to_geometry(shape).to_owned(), + composite: Composite::default(), + }; + self.scene.draw(draw); } fn fill<'b>(&mut self, path: &impl Shape, brush: impl Into>, blur_radius: f64) { - let brush: BrushRef<'b> = brush.into(); + let brush = brush.into(); - // For solid colors with specific shapes, use optimized methods if blur_radius > 0.0 && let BrushRef::Solid(color) = brush { if let Some(rounded) = path.as_rounded_rect() { - if rounded.radii().top_left == rounded.radii().top_right - && rounded.radii().top_left == rounded.radii().bottom_left - && rounded.radii().top_left == rounded.radii().bottom_right + let radii = rounded.radii(); + if radii.top_left == radii.top_right + && radii.top_left == radii.bottom_left + && radii.top_left == radii.bottom_right { - let rect_radius = rounded.radii().top_left; - let rect = rounded.rect(); - self.scene.draw_blurred_rounded_rect( - self.device_transform(), - rect, + self.scene.blurred_rounded_rect(BlurredRoundedRect { + transform: self.transform, + rect: rounded.rect(), color, - rect_radius, - blur_radius, - ); + radius: radii.top_left, + std_dev: blur_radius, + composite: Composite::default(), + }); return; } } else if let Some(rect) = path.as_rect() { - self.scene.draw_blurred_rounded_rect( - self.device_transform(), + self.scene.blurred_rounded_rect(BlurredRoundedRect { + transform: self.transform, rect, color, - 0., - blur_radius, - ); + radius: 0.0, + std_dev: blur_radius, + composite: Composite::default(), + }); return; } } - self.scene.fill( - vello::peniko::Fill::NonZero, - self.device_transform(), - brush, - None, - path, - ); + self.scene + .fill(FillRef::new(shape_to_geometry(path), brush).transform(self.transform)); } fn push_layer( @@ -472,17 +572,18 @@ impl Renderer for VelloRenderer { transform: Affine, clip: &impl Shape, ) { - self.scene.push_layer( - Fill::NonZero, - blend, - alpha, - self.transform * transform, - clip, + PaintSink::push_group( + &mut self.scene, + GroupRef::new() + .with_clip(ClipRef::fill(shape_to_geometry(clip)).with_transform( + self.transform * transform, + )) + .with_composite(Composite::new(blend.into(), alpha)), ); } fn pop_layer(&mut self) { - self.scene.pop_layer(); + self.scene.pop_group(); } fn draw_glyphs<'a>( @@ -491,99 +592,34 @@ impl Renderer for VelloRenderer { run: &GlyphRunRef<'a>, glyphs: impl Iterator + 'a, ) { - // TODO: Vello 0.7's DrawGlyphs API has no embolden support. - // Synthetic bold from layout synthesis and `self.font_embolden` are not applied. - let transform = - self.device_transform() * Affine::translate((origin.x, origin.y)) * run.transform; - self.scene - .draw_glyphs(run.font) - .brush(run.brush) - .brush_alpha(run.composite.alpha) - .hint(run.hint) - .transform(transform) - .glyph_transform(run.glyph_transform) - .font_size(run.font_size) - .normalized_coords(run.normalized_coords) - .draw( - run.style, - glyphs.map(|glyph| vello::Glyph { - id: glyph.id, - x: glyph.x, - y: glyph.y, - }), - ); - } - - fn draw_img(&mut self, img: Img<'_>, rect: Rect) { - 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(); - - self.scene.draw_image( - &img.img, - self.device_transform() - .pre_scale_non_uniform(scale_x, scale_y) - .pre_translate((translate_x, translate_y).into()), - ); - } - - fn draw_svg<'b>( - &mut self, - svg: floem_renderer::Svg<'b>, - rect: Rect, - brush: Option>>, - ) { - let rect_width = rect.width().max(1.); - let rect_height = rect.height().max(1.); - - let svg_size = svg.tree.size(); - - let scale_x = rect_width / f64::from(svg_size.width()); - let scale_y = rect_height / f64::from(svg_size.height()); - - let translate_x = rect.min_x(); - let translate_y = rect.min_y(); - - let transform = self - .device_transform() - .pre_scale_non_uniform(scale_x, scale_y) - .pre_translate((translate_x, translate_y).into()); - - // Look up (or create) the cached base scene for this SVG. - let generation = self.cache_generation; - let base = self - .svg_cache - .entry(svg.hash.to_owned()) - .and_modify(|(g, _)| *g = generation) - .or_insert_with(|| (generation, vello_svg::render_tree(svg.tree))); - - // When a brush is applied (tinted icons), composite through an alpha mask. - // The base scene is cached; only the masking composite is rebuilt per frame. - let composited; - let scene_to_append = match brush { - Some(brush) => { - let brush = brush.into(); - let size = Size::new(svg_size.width() as _, svg_size.height() as _); - let fill_rect = Rect::from_origin_size(Point::ZERO, size); - let base_scene = &base.1; - composited = alpha_mask_scene( - size, - |scene| scene.append(base_scene, None), - move |scene| { - scene.fill(Fill::NonZero, Affine::IDENTITY, brush, None, &fill_rect); - }, - ); - &composited - } - None => &base.1, + let draw = imaging::GlyphRunRef { + font: run.font, + transform: self.transform * Affine::translate((origin.x, origin.y)) * run.transform, + glyph_transform: run.glyph_transform, + font_size: run.font_size, + hint: run.hint, + normalized_coords: run.normalized_coords, + style: run.style, + brush: run.brush, + composite: run.composite, }; + let mut glyphs = glyphs.map(|glyph| ImagingGlyph { + id: glyph.id, + x: glyph.x, + y: glyph.y, + }); + self.scene.glyph_run(draw, &mut glyphs); + } - self.scene.append(scene_to_append, Some(transform)); + fn draw_svg<'b>(&mut self, svg: Svg<'b>, rect: Rect, brush: Option>>) { + CustomPaintSink::custom(&mut self.scene, &VelloCommand::DrawSvg { + svg: SvgCommand { + hash: Arc::from(svg.hash), + }, + rect, + transform: self.transform, + brush: brush.map(|brush| brush.into().to_owned()), + }); } fn set_transform(&mut self, transform: Affine) { @@ -591,20 +627,17 @@ impl Renderer for VelloRenderer { } fn clip(&mut self, shape: &impl Shape) { - self.scene.push_layer( - Fill::NonZero, - vello::peniko::BlendMode::default(), - 1.0, - self.transform, - shape, + PaintSink::push_clip( + &mut self.scene, + ClipRef::fill(shape_to_geometry(shape)).with_transform(self.transform), ); } fn clear_clip(&mut self) { - self.scene.pop_layer(); + self.scene.pop_clip(); } - fn finish(&mut self) -> Option { + fn finish(&mut self) -> Option { if self.capture { self.render_capture_image() } else { @@ -612,14 +645,9 @@ impl Renderer for VelloRenderer { let target_view = self.surface.target_view.clone(); let width = self.surface.config.width; let height = self.surface.config.height; - self.render_scene_to_texture_view( - &target_view, - width, - height, - ) - .unwrap(); + self.render_scene_to_texture_view(&target_view, width, height) + .unwrap(); - // Perform the copy let mut encoder = self.device .create_command_encoder(&wgpu::CommandEncoderDescriptor { @@ -634,8 +662,6 @@ impl Renderer for VelloRenderer { .create_view(&wgpu::TextureViewDescriptor::default()), ); self.queue.submit([encoder.finish()]); - - // Queue the texture to be presented on the surface surface_texture.present(); } None @@ -648,7 +674,6 @@ impl Renderer for VelloRenderer { let mut out = String::new(); writeln!(out, "name: Vello").ok(); writeln!(out, "info: {:#?}", self.adapter.get_info()).ok(); - out } } @@ -687,23 +712,26 @@ impl VelloRenderer { ..Default::default() }); + let scene = self + .build_scene(self.surface.config.width, self.surface.config.height) + .ok()?; self.renderer .render_to_texture( &self.device, &self.queue, - &self.scene, + &scene, &view, - &vello::RenderParams { + &RenderParams { antialiasing_method: AaConfig::Area, ..Self::render_params(self.surface.config.width, self.surface.config.height) }, ) - .unwrap(); + .ok()?; - let bytes_per_pixel = 4; + let bytes_per_pixel = 4u64; let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { label: None, - size: (u64::from(width * height) * bytes_per_pixel), + size: u64::from(width * height) * bytes_per_pixel, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -746,9 +774,9 @@ impl VelloRenderer { cursor += bytes_per_row as usize; } - Some(vello::peniko::ImageBrush::new(ImageData { + Some(peniko::ImageBrush::new(ImageData { data: Blob::new(Arc::new(cropped_buffer)), - format: vello::peniko::ImageFormat::Rgba8, + format: peniko::ImageFormat::Rgba8, alpha_type: ImageAlphaType::AlphaPremultiplied, width: self.surface.config.width, height, @@ -756,6 +784,16 @@ impl VelloRenderer { } } +fn shape_to_geometry(shape: &impl Shape) -> imaging::GeometryRef<'static> { + if let Some(rect) = shape.as_rect() { + imaging::GeometryRef::Rect(rect) + } else if let Some(rect) = shape.as_rounded_rect() { + imaging::GeometryRef::RoundedRect(rect) + } else { + imaging::GeometryRef::OwnedPath(shape.to_path(0.1)) + } +} + fn create_premultiply_pipeline( device: &wgpu::Device, target_format: wgpu::TextureFormat, @@ -788,7 +826,7 @@ fn create_premultiply_pipeline( let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Floem Premultiply Layout"), bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[], + immediate_size: 0, }); let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Floem Premultiply Pipeline"), @@ -812,7 +850,7 @@ fn create_premultiply_pipeline( primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), - multiview: None, + multiview_mask: None, cache: None, }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { @@ -828,54 +866,3 @@ fn create_premultiply_pipeline( sampler, } } - -fn common_alpha_mask_scene( - size: Size, - alpha_mask: impl FnOnce(&mut Scene), - item: impl FnOnce(&mut Scene), - compose_mode: Compose, -) -> Scene { - let mut scene = Scene::new(); - scene.push_layer( - Fill::NonZero, - Mix::Normal, - 1.0, - Affine::IDENTITY, - &Rect::from_origin_size((0., 0.), size), - ); - - alpha_mask(&mut scene); - - scene.push_layer( - Fill::NonZero, - vello::peniko::BlendMode { - mix: Mix::Normal, - compose: compose_mode, - }, - 1., - Affine::IDENTITY, - &Rect::from_origin_size((0., 0.), size), - ); - - item(&mut scene); - - scene.pop_layer(); - scene.pop_layer(); - scene -} - -fn alpha_mask_scene( - size: Size, - alpha_mask: impl FnOnce(&mut Scene), - item: impl FnOnce(&mut Scene), -) -> Scene { - common_alpha_mask_scene(size, alpha_mask, item, Compose::SrcIn) -} -#[allow(unused)] -fn invert_alpha_mask_scene( - size: Size, - alpha_mask: impl FnOnce(&mut Scene), - item: impl FnOnce(&mut Scene), -) -> Scene { - common_alpha_mask_scene(size, alpha_mask, item, Compose::SrcOut) -} diff --git a/vger/Cargo.toml b/vger/Cargo.toml index 6de6e2ab6..fecdbb796 100644 --- a/vger/Cargo.toml +++ b/vger/Cargo.toml @@ -8,7 +8,7 @@ license.workspace = true [dependencies.floem-vger-rs] git = "https://github.com/lapce/vger-rs" -rev = "54ab8135d9d870a4ac2b84fc6b9846b3b03ec919" +rev = "9a947c453200f4e5de7780c57de784f1d96f0714" package = "floem-vger" [dependencies] diff --git a/vger/src/lib.rs b/vger/src/lib.rs index 6bc79df1c..ffcc34382 100644 --- a/vger/src/lib.rs +++ b/vger/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::mpsc::sync_channel; use anyhow::Result; use floem_renderer::gpu_resources::GpuResources; use floem_renderer::text::{Glyph, GlyphRunRef}; -use floem_renderer::{Img, Renderer, tiny_skia}; +use floem_renderer::{Renderer, tiny_skia}; use floem_vger_rs::{GlyphImage, Image, PaintIndex, PixelFormat, Vger}; use peniko::kurbo::{Size, Stroke}; use peniko::{Blob, Extend, ImageData, ImageQuality, LinearGradientPosition}; @@ -602,33 +602,6 @@ impl Renderer for VgerRenderer { } } - fn draw_img(&mut self, img: Img<'_>, rect: Rect) { - let transform = self.device_transform().as_coeffs(); - let (scale_x, scale_y, _) = self.scale_components(); - - let origin = rect.origin(); - let transformed_x = transform[0] * origin.x + transform[2] * origin.y + transform[4]; - let transformed_y = transform[1] * origin.x + transform[3] * origin.y + transform[5]; - - let x = transformed_x.round() as f32; - let y = transformed_y.round() as f32; - - let width = (rect.width() * scale_x.abs()).round().max(1.0) as u32; - let height = (rect.height() * scale_y.abs()).round().max(1.0) as u32; - - self.vger.render_image(x, y, img.hash, width, height, || { - let data = img.img.image.data; - let (width, height) = (img.img.image.width, img.img.image.height); - - Image { - width, - height, - data, - pixel_format: PixelFormat::Rgba, - } - }); - } - fn draw_svg<'b>( &mut self, svg: floem_renderer::Svg<'b>, @@ -755,6 +728,7 @@ impl Renderer for VgerRenderer { depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, + multiview_mask: None, }; self.vger.encode(&desc); From cbd766b5b245e6e45bd1a6ae8c1d34f45e9cdc9e Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Mon, 23 Mar 2026 23:21:27 -0600 Subject: [PATCH 025/102] rendering overhaul --- Cargo.lock | 792 ++++++++++---------- Cargo.toml | 31 +- examples/counter-simple/Cargo.toml | 2 +- examples/files/Cargo.toml | 12 +- examples/localization/Cargo.toml | 2 +- examples/navigation-stack-mobile/Cargo.toml | 2 +- examples/todo-complex/Cargo.toml | 1 - examples/view-transition/Cargo.toml | 2 +- examples/widget-gallery/Cargo.toml | 16 +- examples/widget-gallery/src/canvas.rs | 36 +- examples/widget-gallery/src/main.rs | 45 +- renderer/src/lib.rs | 190 ++--- skia/Cargo.toml | 21 - skia/src/lib.rs | 489 ------------ src/app/handle.rs | 21 +- src/app/mod.rs | 1 - src/compositor/backend.rs | 6 +- src/compositor/mod.rs | 73 +- src/compositor/subduction.rs | 46 +- src/inspector/mod.rs | 2 +- src/inspector/view.rs | 10 +- src/lib.rs | 20 +- src/paint/display_list.rs | 267 +------ src/paint/mod.rs | 28 +- src/paint/renderer.rs | 756 +++++++++++++------ src/view/mod.rs | 12 +- src/views/editor/gutter.rs | 2 +- src/views/editor/view.rs | 1 + src/views/resizable.rs | 4 +- src/views/scroll.rs | 5 +- src/window/handle.rs | 346 +++------ src/window/state.rs | 6 +- tests/scroll_paint_stats.rs | 7 +- tiny_skia/Cargo.toml | 2 +- tiny_skia/src/lib.rs | 774 ++++++++++--------- tiny_skia/src/recording.rs | 219 ------ vello/src/lib.rs | 709 ++++-------------- vello_cpu/Cargo.toml | 15 + vello_cpu/src/lib.rs | 81 ++ vello_hybrid/Cargo.toml | 15 + vello_hybrid/src/lib.rs | 98 +++ vger/Cargo.toml | 1 + vger/src/lib.rs | 425 +++++++---- 43 files changed, 2367 insertions(+), 3226 deletions(-) delete mode 100644 skia/Cargo.toml delete mode 100644 skia/src/lib.rs delete mode 100644 tiny_skia/src/recording.rs create mode 100644 vello_cpu/Cargo.toml create mode 100644 vello_cpu/src/lib.rs create mode 100644 vello_hybrid/Cargo.toml create mode 100644 vello_hybrid/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 56bbff2ee..772906e09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,41 +145,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "anyrender" -version = "0.7.0" -source = "git+https://github.com/jrmoulton/anyrender?rev=e38ca6c35f45#e38ca6c35f45a2b1f0b1e3ece0e85b1319eab1cd" -dependencies = [ - "kurbo", - "peniko", - "raw-window-handle", -] - -[[package]] -name = "anyrender_skia" -version = "0.4.0" -source = "git+https://github.com/jrmoulton/anyrender?rev=e38ca6c35f45#e38ca6c35f45a2b1f0b1e3ece0e85b1319eab1cd" -dependencies = [ - "anyrender", - "ash", - "ash-window", - "debug_timer", - "gl", - "glutin", - "hashbrown 0.16.1", - "kurbo", - "oaty", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-metal 0.3.2", - "objc2-quartz-core 0.3.2", - "objc2-ui-kit", - "peniko", - "raw-window-handle", - "skia-safe", -] - [[package]] name = "approx" version = "0.5.1" @@ -248,17 +213,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "ash-window" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" -dependencies = [ - "ash", - "raw-window-handle", - "raw-window-metal", -] - [[package]] name = "async-resource" version = "0.2.0" @@ -336,7 +290,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom 8.0.0", + "nom", "num-rational 0.4.2", "v_frame", ] @@ -371,26 +325,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2 1.0.106", - "quote 1.0.45", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -620,15 +554,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -651,15 +576,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -687,17 +603,6 @@ dependencies = [ "half", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.60" @@ -732,36 +637,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics", - "foreign-types 0.5.0", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - [[package]] name = "codespan-reporting" version = "0.12.0" @@ -779,6 +654,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" dependencies = [ + "bytemuck", "serde", ] @@ -824,7 +700,7 @@ dependencies = [ "directories", "serde", "thiserror 2.0.18", - "toml 0.8.2", + "toml", ] [[package]] @@ -894,30 +770,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1137,12 +989,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" -[[package]] -name = "debug_timer" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da220af51a1a335e9a930beaaef53d261e41ea9eecfb3d973a3ddae1a7284b9c" - [[package]] name = "deflate" version = "0.7.20" @@ -1190,7 +1036,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1389,7 +1235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1475,6 +1321,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" +dependencies = [ + "bytemuck", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -1492,17 +1347,6 @@ dependencies = [ "floem", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1550,8 +1394,9 @@ dependencies = [ "floem-winit", "floem_reactive", "floem_renderer", - "floem_skia_renderer", "floem_tiny_skia_renderer", + "floem_vello_cpu_renderer", + "floem_vello_hybrid_renderer", "floem_vello_renderer", "floem_vger_renderer", "fluent-bundle", @@ -1578,10 +1423,12 @@ dependencies = [ "sha2", "slotmap", "smallvec", + "softbuffer", "strum", "strum_macros", "subduction_backend_wgpu", "subduction_core", + "swash", "sys-locale", "taffy", "tokio", @@ -1596,7 +1443,7 @@ dependencies = [ "unicode-segmentation", "wasm-bindgen-futures", "web-time", - "wgpu", + "wgpu 28.0.0", ] [[package]] @@ -1631,7 +1478,7 @@ dependencies = [ "fontdue", "linebender_resource_handle", "rect_packer", - "wgpu", + "wgpu 28.0.0", ] [[package]] @@ -1873,38 +1720,46 @@ dependencies = [ "peniko", "resvg", "wasm-bindgen-futures", - "wgpu", + "wgpu 28.0.0", ] [[package]] -name = "floem_skia_renderer" +name = "floem_tiny_skia_renderer" version = "0.2.0" dependencies = [ "anyhow", - "anyrender", - "anyrender_skia", - "floem-winit", "floem_renderer", - "kurbo", + "imaging", "parley", "peniko", "raw-window-handle", "resvg", + "rustc-hash 2.1.1", + "swash", ] [[package]] -name = "floem_tiny_skia_renderer" +name = "floem_vello_cpu_renderer" version = "0.2.0" dependencies = [ "anyhow", "floem_renderer", + "imaging", + "imaging_vello_cpu", + "parley", + "peniko", +] + +[[package]] +name = "floem_vello_hybrid_renderer" +version = "0.2.0" +dependencies = [ + "anyhow", + "floem_renderer", + "imaging", + "imaging_vello_hybrid", "parley", "peniko", - "raw-window-handle", - "resvg", - "rustc-hash 2.1.1", - "softbuffer", - "swash", ] [[package]] @@ -1918,7 +1773,7 @@ dependencies = [ "parley", "peniko", "vello", - "wgpu", + "wgpu 28.0.0", ] [[package]] @@ -1929,11 +1784,12 @@ dependencies = [ "floem-vger", "floem_renderer", "image 0.25.9", + "imaging", "parley", "peniko", "resvg", "swash", - "wgpu", + "wgpu 28.0.0", ] [[package]] @@ -2393,15 +2249,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "gl" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" -dependencies = [ - "gl_generator", -] - [[package]] name = "gl_generator" version = "0.14.0" @@ -2460,12 +2307,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "glow" version = "0.16.0" @@ -2479,68 +2320,54 @@ dependencies = [ ] [[package]] -name = "glutin" -version = "0.32.3" +name = "glutin_wgl_sys" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" dependencies = [ - "bitflags 2.11.0", - "cfg_aliases", - "cgl", - "dispatch2", - "glutin_egl_sys", - "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "once_cell", - "raw-window-handle", - "wayland-sys", - "windows-sys 0.52.0", - "x11-dl", + "gl_generator", ] [[package]] -name = "glutin_egl_sys" -version = "0.7.1" +name = "gobject-sys" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ - "gl_generator", - "windows-sys 0.52.0", + "glib-sys", + "libc", + "system-deps", ] [[package]] -name = "glutin_glx_sys" -version = "0.6.1" +name = "gpu-alloc" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "gl_generator", - "x11-dl", + "bitflags 2.11.0", + "gpu-alloc-types", ] [[package]] -name = "glutin_wgl_sys" -version = "0.6.1" +name = "gpu-alloc-types" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "gl_generator", + "bitflags 2.11.0", ] [[package]] -name = "gobject-sys" -version = "0.18.0" +name = "gpu-allocator" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ - "glib-sys", - "libc", - "system-deps", + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", ] [[package]] @@ -2635,6 +2462,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "guillotiere" version = "0.7.0" @@ -3001,6 +2838,30 @@ dependencies = [ "vello", ] +[[package]] +name = "imaging_vello_cpu" +version = "0.0.1" +dependencies = [ + "imaging", + "kurbo", + "peniko", + "vello_common", + "vello_cpu", +] + +[[package]] +name = "imaging_vello_hybrid" +version = "0.0.1" +dependencies = [ + "imaging", + "kurbo", + "peniko", + "pollster", + "vello_common", + "vello_hybrid", + "wgpu 27.0.1", +] + [[package]] name = "imbl" version = "7.0.0" @@ -3447,6 +3308,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "metal" version = "0.33.0" @@ -3455,7 +3331,7 @@ checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ "bitflags 2.11.0", "block", - "core-graphics-types 0.2.0", + "core-graphics-types", "foreign-types 0.5.0", "log", "objc", @@ -3468,12 +3344,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3520,9 +3390,35 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "once_cell", - "png 0.17.16", + "png 0.17.16", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", "thiserror 2.0.18", - "windows-sys 0.60.2", + "unicode-ident", ] [[package]] @@ -3611,16 +3507,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -3758,15 +3644,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "oaty" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7890d4a4c9205ee57a5e200658e78ed7d51c9cd48d0f7066596a19cc20e5d2" -dependencies = [ - "font-types 0.10.1", -] - [[package]] name = "objc" version = "0.2.7" @@ -3907,7 +3784,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal 0.2.2", + "objc2-metal", ] [[package]] @@ -3998,17 +3875,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-metal" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -4019,7 +3885,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal 0.2.2", + "objc2-metal", ] [[package]] @@ -4032,7 +3898,6 @@ dependencies = [ "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", - "objc2-metal 0.3.2", ] [[package]] @@ -4045,7 +3910,6 @@ dependencies = [ "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", ] [[package]] @@ -4322,6 +4186,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" dependencies = [ + "bytemuck", "color", "kurbo", "linebender_resource_handle", @@ -4859,18 +4724,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "raw-window-metal" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" -dependencies = [ - "cocoa", - "core-graphics", - "objc", - "raw-window-handle", -] - [[package]] name = "rayon" version = "1.11.0" @@ -5142,7 +4995,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5371,15 +5224,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5481,33 +5325,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" -[[package]] -name = "skia-bindings" -version = "0.91.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd9bbd87993cbe4c0caa5fa6ca69c7d2ed38dcf5cd967b200392840c3bc666" -dependencies = [ - "bindgen", - "cc", - "flate2", - "heck 0.5.0", - "pkg-config", - "regex", - "serde_json", - "tar", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "skia-safe" -version = "0.91.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c13e5913d17403753fd8cc2c15402e8d9ba8278d1b82a12aa7264a63f9c6325" -dependencies = [ - "bitflags 2.11.0", - "skia-bindings", -] - [[package]] name = "skrifa" version = "0.37.0" @@ -5730,7 +5547,7 @@ source = "git+https://github.com/jrmoulton/subduction.git?branch=bump-wgpu#16ad0 dependencies = [ "bytemuck", "subduction_core", - "wgpu", + "wgpu 28.0.0", ] [[package]] @@ -5886,7 +5703,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.2", + "toml", "version-compare", ] @@ -5902,17 +5719,6 @@ dependencies = [ "slotmap", ] -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" version = "0.12.16" @@ -5929,7 +5735,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6203,26 +6009,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", - "serde_spanned 0.6.9", + "serde_spanned", "toml_datetime 0.6.3", "toml_edit 0.20.2", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.14", -] - [[package]] name = "toml_datetime" version = "0.6.3" @@ -6232,15 +6023,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -6269,7 +6051,7 @@ checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap", "serde", - "serde_spanned 0.6.9", + "serde_spanned", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -6295,12 +6077,6 @@ dependencies = [ "winnow 0.7.14", ] -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - [[package]] name = "tower-service" version = "0.3.3" @@ -6618,7 +6394,34 @@ dependencies = [ "thiserror 2.0.18", "vello_encoding", "vello_shaders", - "wgpu", + "wgpu 28.0.0", +] + +[[package]] +name = "vello_common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +dependencies = [ + "bytemuck", + "fearless_simd", + "hashbrown 0.16.1", + "log", + "peniko", + "png 0.17.16", + "skrifa 0.40.0", + "smallvec", +] + +[[package]] +name = "vello_cpu" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "vello_common", ] [[package]] @@ -6628,12 +6431,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53d864987821f40431e62608bf0f58342db57f2f2860628c8cfc28e16a1cc39" dependencies = [ "bytemuck", - "guillotiere", + "guillotiere 0.7.0", "peniko", "skrifa 0.40.0", "smallvec", ] +[[package]] +name = "vello_hybrid" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5660521c83853e94316c484d31c757aa40bbab4cfcbf5afdb4270e827c4db699" +dependencies = [ + "bytemuck", + "guillotiere 0.6.2", + "hashbrown 0.16.1", + "log", + "thiserror 2.0.18", + "vello_common", + "vello_sparse_shaders", + "wgpu 27.0.1", +] + [[package]] name = "vello_shaders" version = "0.8.0" @@ -6642,11 +6461,17 @@ checksum = "c455e04d570129212ecc4c0fdaa051ad66e5be15781a75b38c519d08120db374" dependencies = [ "bytemuck", "log", - "naga", + "naga 28.0.0", "thiserror 2.0.18", "vello_encoding", ] +[[package]] +name = "vello_sparse_shaders" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84ceb248d455e90199f37bf3e3dab63bbe10ad75c0b0556611a9ad094ef1b6b" + [[package]] name = "version-compare" version = "0.2.1" @@ -6975,7 +6800,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu", + "wgpu 28.0.0", ] [[package]] @@ -6984,6 +6809,35 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga 27.0.3", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core 27.0.3", + "wgpu-hal 27.0.4", + "wgpu-types 27.0.1", +] + [[package]] name = "wgpu" version = "28.0.0" @@ -6999,7 +6853,7 @@ dependencies = [ "hashbrown 0.16.1", "js-sys", "log", - "naga", + "naga 28.0.0", "parking_lot", "portable-atomic", "profiling", @@ -7009,9 +6863,41 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core 28.0.1", + "wgpu-hal 28.0.1", + "wgpu-types 28.0.0", +] + +[[package]] +name = "wgpu-core" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.11.0", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga 27.0.3", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple 27.0.0", + "wgpu-core-deps-emscripten 27.0.0", + "wgpu-core-deps-windows-linux-android 27.0.0", + "wgpu-hal 27.0.4", + "wgpu-types 27.0.1", ] [[package]] @@ -7030,7 +6916,7 @@ dependencies = [ "hashbrown 0.16.1", "indexmap", "log", - "naga", + "naga 28.0.0", "once_cell", "parking_lot", "portable-atomic", @@ -7039,11 +6925,20 @@ dependencies = [ "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.18", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", + "wgpu-core-deps-apple 28.0.0", + "wgpu-core-deps-emscripten 28.0.0", + "wgpu-core-deps-windows-linux-android 28.0.0", + "wgpu-hal 28.0.1", + "wgpu-types 28.0.0", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal 27.0.4", ] [[package]] @@ -7052,7 +6947,16 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" dependencies = [ - "wgpu-hal", + "wgpu-hal 28.0.1", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +dependencies = [ + "wgpu-hal 27.0.4", ] [[package]] @@ -7061,7 +6965,16 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" dependencies = [ - "wgpu-hal", + "wgpu-hal 28.0.1", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal 27.0.4", ] [[package]] @@ -7070,7 +6983,56 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" dependencies = [ - "wgpu-hal", + "wgpu-hal 28.0.1", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.11.0", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator 0.27.0", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal 0.32.0", + "naga 27.0.3", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", + "wgpu-types 27.0.1", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] @@ -7088,10 +7050,10 @@ dependencies = [ "bytemuck", "cfg-if", "cfg_aliases", - "core-graphics-types 0.2.0", + "core-graphics-types", "glow", "glutin_wgl_sys", - "gpu-allocator", + "gpu-allocator 0.28.0", "gpu-descriptor", "hashbrown 0.16.1", "js-sys", @@ -7099,8 +7061,8 @@ dependencies = [ "libc", "libloading", "log", - "metal", - "naga", + "metal 0.33.0", + "naga 28.0.0", "ndk-sys", "objc", "once_cell", @@ -7116,11 +7078,25 @@ dependencies = [ "thiserror 2.0.18", "wasm-bindgen", "web-sys", - "wgpu-types", + "wgpu-types 28.0.0", "windows 0.62.2", "windows-core 0.62.2", ] +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.18", + "web-sys", +] + [[package]] name = "wgpu-types" version = "28.0.0" @@ -7176,7 +7152,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -7844,16 +7820,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - [[package]] name = "xcursor" version = "0.3.10" diff --git a/Cargo.toml b/Cargo.toml index bf7e64972..38d1efccf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,9 @@ resolver = "3" members = [ "renderer", - "skia", "vello", + "vello_hybrid", + "vello_cpu", "vger", "tiny_skia", "reactive", @@ -15,8 +16,9 @@ members = [ default-members = [ ".", "renderer", - "skia", "vello", + "vello_hybrid", + "vello_cpu", "vger", "tiny_skia", "reactive", @@ -93,10 +95,11 @@ strum = { workspace = true, optional = true } strum_macros = { workspace = true, optional = true } paste = "1.0" floem_renderer = { path = "renderer", version = "0.2.0" } -floem_skia_renderer = { path = "skia", version = "0.2.0", optional = true } floem_vello_renderer = { path = "vello", version = "0.2.0", optional = true } +floem_vello_hybrid_renderer = { path = "vello_hybrid", 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 } subduction_core = { git = "https://github.com/jrmoulton/subduction.git", branch = "bump-wgpu", package = "subduction_core", optional = true, features = ["std"] } @@ -106,7 +109,9 @@ copypasta = { version = "0.10", default-features = false, features = [ "x11", ] } parking_lot = { workspace = true } +swash = { workspace = true, features = ["scale"] } image = { workspace = true } +softbuffer = "0.4.1" winit = { workspace = true } futures = { version = "0.3.31" } crossbeam = { version = "0.8", optional = true } @@ -155,11 +160,19 @@ objc2-app-kit = { version = "0.3", features = [ dispatch2 = {version = "0.3.0"} [features] -default = ["editor", "default-image-formats", "vello", "serde"] -skia = ["dep:floem_skia_renderer"] -vello = ["dep:floem_vello_renderer"] -vger = ["dep:floem_vger_renderer"] -# tiny-skia = ["dep:floem_tiny_skia_renderer"] +default = ["editor", "default-image-formats", "serde"] +active-vello = ["dep:floem_vello_renderer"] +active-vger = ["dep:floem_vger_renderer"] +active-vello-hybrid = ["dep:floem_vello_hybrid_renderer"] +active-vello-cpu = ["dep:floem_vello_cpu_renderer"] +active-tiny-skia = ["dep:floem_tiny_skia_renderer"] +fallback-vello-cpu = ["dep:floem_vello_cpu_renderer"] +fallback-tiny-skia = ["dep:floem_tiny_skia_renderer"] +vello = ["active-vello"] +vger = ["active-vger"] +vello-hybrid = ["active-vello-hybrid"] +vello-cpu = ["active-vello-cpu"] +tiny-skia = ["active-tiny-skia"] serde = [ "dep:serde", "winit/serde", diff --git a/examples/counter-simple/Cargo.toml b/examples/counter-simple/Cargo.toml index 9df02ffbd..81c751324 100644 --- a/examples/counter-simple/Cargo.toml +++ b/examples/counter-simple/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -floem = { path = "../..", features = ["vello"]} +floem = { path = "../.." } diff --git a/examples/files/Cargo.toml b/examples/files/Cargo.toml index 1da1aa7a9..d03d2d213 100644 --- a/examples/files/Cargo.toml +++ b/examples/files/Cargo.toml @@ -4,4 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -floem = { path = "../.." } +floem = { path = "../..", default-features = false, features = [ + "editor", + "default-image-formats", + "fallback-vello-cpu", + "serde", +] } + +[features] +default = [] +vello = ["floem/vello"] +vello-hybrid = ["floem/vello-hybrid"] diff --git a/examples/localization/Cargo.toml b/examples/localization/Cargo.toml index 6a669d847..4298c922a 100644 --- a/examples/localization/Cargo.toml +++ b/examples/localization/Cargo.toml @@ -5,4 +5,4 @@ license.workspace = true version.workspace = true [dependencies] -floem = { path = "../..", features = ["vello", "localization"] } +floem = { path = "../..", features = ["localization"] } diff --git a/examples/navigation-stack-mobile/Cargo.toml b/examples/navigation-stack-mobile/Cargo.toml index 49cad61d7..e910fa4f0 100644 --- a/examples/navigation-stack-mobile/Cargo.toml +++ b/examples/navigation-stack-mobile/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] -floem = { path = "../..", features = ["vello"] } +floem = { path = "../.." } diff --git a/examples/todo-complex/Cargo.toml b/examples/todo-complex/Cargo.toml index b18f94524..6b1f1b4ca 100644 --- a/examples/todo-complex/Cargo.toml +++ b/examples/todo-complex/Cargo.toml @@ -8,7 +8,6 @@ confy = "1.0.0" floem = { path = "../..", features = [ "editor", "serde", - "vello", ], default-features = false } rusqlite = { version = "0.37.0", features = ["bundled"] } serde = { workspace = true } diff --git a/examples/view-transition/Cargo.toml b/examples/view-transition/Cargo.toml index b02a666b2..52fdf0dfc 100644 --- a/examples/view-transition/Cargo.toml +++ b/examples/view-transition/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -floem = { path = "../..", features = ["vello"] } +floem = { path = "../.." } diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index 20feb9321..7c19ebba1 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -4,14 +4,22 @@ version = "0.1.0" edition = "2024" [dependencies] -floem = { path = "../..", features = ["vello", "subduction"] } +floem = { path = "../..", default-features = false, features = [ + "editor", + "default-image-formats", + "fallback-vello-cpu", + "serde", + "subduction", +] } strum = { workspace = true } -files = { path = "../files/", optional = true } +files = { path = "../files/", optional = true, default-features = false } [target.'cfg(target_os = "macos")'.dependencies] signpost = "*" [features] -default = ["full"] -vello = ["floem/vello"] +default = ["full", "vger" ] +vello = ["floem/vello", "files?/vello"] +vger = ["floem/vger"] +vello-hybrid = ["floem/vello-hybrid", "files?/vello-hybrid"] full = ["dep:files"] diff --git a/examples/widget-gallery/src/canvas.rs b/examples/widget-gallery/src/canvas.rs index 5b2f6b668..09812968b 100644 --- a/examples/widget-gallery/src/canvas.rs +++ b/examples/widget-gallery/src/canvas.rs @@ -560,26 +560,26 @@ impl View for OpacityPicker { .with_interpolation_cs(LinearSrgb) .into(); - cx.painter.fill(rect_path, &opacity_gradient).draw(); - - if size.width > 0.0 { - let alpha = self.current_color.components[3]; - let x_pos = alpha as f64 * size.width; - - let indicator_width = 2.0; - let indicator_rect = Rect::new( - x_pos - indicator_width / 2.0, - 0.0, - x_pos + indicator_width / 2.0, - size.height, - ); + cx.painter.with_fill_clip(rect_path, |p| { + p.fill(rect_path, &opacity_gradient).draw(); + + if size.width > 0.0 { + let alpha = self.current_color.components[3]; + let x_pos = alpha as f64 * size.width; + + let indicator_width = 2.0; + let indicator_rect = Rect::new( + x_pos - indicator_width / 2.0, + 0.0, + x_pos + indicator_width / 2.0, + size.height, + ); - let white = Brush::Solid(css::WHITE); - let black = Brush::Solid(css::BLACK); - cx.painter.with_fill_clip(rect_path, |p| { + let white = Brush::Solid(css::WHITE); + let black = Brush::Solid(css::BLACK); p.stroke(indicator_rect, &Stroke::new(2.0), &white).draw(); p.fill(indicator_rect, &black).draw(); - }); - } + } + }); } } diff --git a/examples/widget-gallery/src/main.rs b/examples/widget-gallery/src/main.rs index d6d36bdae..5c249d8e7 100644 --- a/examples/widget-gallery/src/main.rs +++ b/examples/widget-gallery/src/main.rs @@ -1,3 +1,6 @@ +use std::cell::{Cell, RefCell}; +use std::time::{Duration, Instant}; + pub mod animation; pub mod buttons; pub mod canvas; @@ -26,6 +29,7 @@ use floem::{ new_window, prelude::{palette::css, *}, style::{Background, CursorStyle, CustomStylable, Transition}, + text::FontWeight, theme::StyleThemeExt, ui_events::keyboard::{Key, KeyboardEvent, Modifiers, NamedKey}, window::{Theme, WindowConfig, WindowId}, @@ -38,6 +42,12 @@ pub const OS_MOD: Modifiers = if cfg!(target_os = "macos") { }; fn app_view(window_id: WindowId) -> impl IntoView { + const FPS_SAMPLE_COUNT: usize = 60; + + let fps = RwSignal::new(0u32); + let last_presented_at = Cell::new(None::); + let frame_times = RefCell::new(Vec::::with_capacity(FPS_SAMPLE_COUNT)); + let tabs: Vec<&'static str> = vec![ "Label", "Button", @@ -175,7 +185,20 @@ fn app_view(window_id: WindowId) -> impl IntoView { .inset_right(15.) }); - let view = Stack::horizontal((left_side_bar, tab, floem_logo)) + let fps_overlay = Label::derived(move || fps.get().to_string()) + .style(|s| { + s.padding_horiz(10.0) + .padding_vert(6.0) + .background(css::BLACK.with_alpha(0.7)) + .color(css::WHITE) + .border_radius(999.0) + .font_weight(FontWeight::BOLD) + .font_size(14.0) + }) + .overlay() + .style(|s| s.absolute().z_index(2).inset_top(12.).inset_right(12.)); + + let view = Stack::horizontal((left_side_bar, tab, floem_logo, fps_overlay)) .style(|s| s.padding(5.0).width_full().height_full().col_gap(5.0)) .window_title(|| "Widget Gallery".to_owned()); @@ -328,6 +351,26 @@ fn app_view(window_id: WindowId) -> impl IntoView { } }, ) + .on_event_stop(listener::UpdatePhasePaintPresent, move |_, _| { + let now = Instant::now(); + if let Some(previous) = last_presented_at.get() { + let dt = now.saturating_duration_since(previous); + if !dt.is_zero() { + let mut samples = frame_times.borrow_mut(); + if samples.len() == FPS_SAMPLE_COUNT { + samples.remove(0); + } + samples.push(dt); + + let total_secs = samples.iter().map(Duration::as_secs_f64).sum::(); + let average_secs = total_secs / samples.len() as f64; + if average_secs > 0.0 { + fps.set((1.0 / average_secs).round() as u32); + } + } + } + last_presented_at.set(Some(now)); + }) .on_event_stop( el::KeyDown, move |_, KeyboardEvent { key, modifiers, .. }| match key { diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 17e3d4938..a8d5ddcfe 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,13 +1,4 @@ -//! Rendering abstraction layer for the Floem UI framework. -//! -//! This crate defines the [`Renderer`] trait that all Floem rendering backends must -//! implement, along with supporting types for passing SVGs to the renderer. -//! Floem ships with three backend implementations: -//! -//! - **Vello** (`floem_vello_renderer`) — GPU-accelerated renderer using the Vello scene graph (default). -//! - **Skia** (`floem_skia_renderer`) — GPU-accelerated renderer using AnyRender's Skia backend. -//! - **Vger** (`floem_vger_renderer`) — GPU-accelerated renderer using the Vger library. -//! - **tiny-skia** (`floem_tiny_skia_renderer`) — CPU software renderer using tiny-skia. +//! Shared renderer-facing types for the Floem UI framework. //! //! # Modules //! @@ -20,16 +11,63 @@ //! renderer backends and downstream crates can use consistent versions of these libraries //! without adding them as direct dependencies. +use std::sync::Arc; + +use imaging::record::CustomCommand; +use peniko::kurbo::{Affine, Rect}; + pub mod text; -use peniko::{ - BlendMode, BrushRef, - kurbo::{Affine, Point, Rect, Shape, Stroke}, -}; pub use resvg::tiny_skia; pub use resvg::usvg; pub mod gpu_resources; +pub struct GpuTextureOutput { + pub texture: wgpu::Texture, + pub view: wgpu::TextureView, + pub format: wgpu::TextureFormat, + pub size: (u32, u32), +} + +pub enum RenderOutput { + Image(peniko::ImageData), + GpuTexture(GpuTextureOutput), +} + +#[derive(Clone)] +pub struct OwnedSvg { + pub tree: Arc, + pub hash: Arc<[u8]>, +} + +#[derive(Clone)] +pub enum DisplayCommandExt { + DrawSvg { + svg: OwnedSvg, + rect: Rect, + transform: Affine, + brush: Option, + }, +} + +impl CustomCommand for DisplayCommandExt { + fn prepend_transform(&self, prefix: Affine) -> Self { + match self { + Self::DrawSvg { + svg, + rect, + transform, + brush, + } => Self::DrawSvg { + svg: svg.clone(), + rect: *rect, + transform: prefix * *transform, + brush: brush.clone(), + }, + } + } +} + /// A reference to a parsed SVG tree paired with a cache key. /// /// # Example @@ -55,127 +93,3 @@ pub struct Svg<'a> { /// An opaque byte slice used as a cache key by the renderer. pub hash: &'a [u8], } - -/// The core rendering trait that every Floem backend must implement. -/// -/// A frame is bracketed by [`begin`](Renderer::begin) and [`finish`](Renderer::finish). -/// Between those calls the framework issues drawing commands — fills, strokes, text, -/// SVGs — which the backend records or executes immediately depending on its -/// architecture. -/// -/// The typical call sequence within a single frame looks like: -/// -/// ```text -/// renderer.begin(capture); -/// renderer.set_transform(..); -/// renderer.fill(..); // background -/// renderer.clip(..); // restrict drawing area -/// renderer.draw_text_lines(..); // labels, editors -/// renderer.draw_svg(..); // icons -/// renderer.stroke(..); // borders -/// renderer.clear_clip(); -/// renderer.finish(); -/// ``` -pub trait Renderer { - /// Begin a new frame. - /// - /// Must be called exactly once before any drawing commands. - /// When `capture` is `true` the renderer should record the frame into an - /// off-screen buffer so that [`finish`](Renderer::finish) can return it as - /// an [`ImageBrush`](peniko::ImageBrush). This is used by the Floem - /// inspector to take snapshots. - fn begin(&mut self, capture: bool); - - /// Set the current affine transform in device/render-target coordinates. - /// - /// All subsequent drawing commands are transformed by `transform` until it - /// is changed again. The framework provides this as the final transform for - /// the current visual node, including window scaling, so backends should not - /// apply an additional global window-scale multiply to ordinary geometry. - /// - /// Raster-backed operations such as glyph and SVG caching may still derive - /// a rasterization scale from this transform to choose an appropriate pixel - /// resolution. - fn set_transform(&mut self, transform: Affine); - - /// Clip all subsequent drawing to the interior of `shape`. - /// - /// The clip remains in effect until [`clear_clip`](Renderer::clear_clip) is - /// called. On the Vello backend clipping is implemented via - /// [`push_layer`](Renderer::push_layer) / [`pop_layer`](Renderer::pop_layer) - /// instead, so this method may be a no-op there. - fn clip(&mut self, shape: &impl Shape); - - /// Remove the current clip region, allowing drawing to the full surface. - fn clear_clip(&mut self); - - /// Stroke the outline of a [`Shape`]. - /// - /// The `brush` defines the color or gradient and `stroke` controls the - /// line width, join style, dash pattern, etc. - fn stroke<'b, 's>( - &mut self, - shape: &impl Shape, - brush: impl Into>, - stroke: &'s Stroke, - ); - - /// Fill the interior of a [`Shape`] using the [non-zero fill rule]. - /// - /// When `blur_radius` is greater than zero the fill is drawn with a - /// Gaussian blur, which is used for box shadows. - /// - /// [non-zero fill rule]: https://en.wikipedia.org/wiki/Nonzero-rule - fn fill<'b>(&mut self, path: &impl Shape, brush: impl Into>, blur_radius: f64); - - /// Push a compositing layer onto the layer stack. - /// - /// Drawing commands issued after this call are composited into the layer. - /// Call [`pop_layer`](Renderer::pop_layer) to flatten the layer back into - /// the parent using the specified `blend` mode and `alpha`. - /// - /// The `clip` shape restricts drawing within the layer, and `transform` is - /// applied to the layer contents. - fn push_layer( - &mut self, - blend: impl Into, - alpha: f32, - transform: Affine, - clip: &impl Shape, - ); - - /// Pop the topmost compositing layer pushed by [`push_layer`](Renderer::push_layer). - /// - /// The layer contents are composited into the parent surface using the - /// blend mode and alpha that were specified when the layer was pushed. - fn pop_layer(&mut self); - - /// Draw a single glyph run. - fn draw_glyphs<'a>( - &mut self, - origin: Point, - run: &text::GlyphRunRef<'a>, - glyphs: impl Iterator + 'a, - ); - - /// Draw an SVG image inside `rect`. - /// - /// When `brush` is `Some`, the SVG is rendered as a mask and filled with - /// the given brush — this is how Floem applies a color override to icons. - fn draw_svg<'b>(&mut self, svg: Svg<'b>, rect: Rect, brush: Option>>); - - /// Finish the current frame and present it. - /// - /// If the frame was started with `capture = true`, the rendered content is - /// returned as an [`ImageBrush`](peniko::ImageBrush). Otherwise returns - /// `None` after presenting the frame to the screen. - fn finish(&mut self) -> Option; - - /// Return a human-readable string identifying the renderer backend. - /// - /// Used by the Floem inspector. Implementations typically return a string - /// like `"name: Vello\ninfo: …"`. - fn debug_info(&self) -> String { - "Unknown".into() - } -} diff --git a/skia/Cargo.toml b/skia/Cargo.toml deleted file mode 100644 index 5f2e9d9c5..000000000 --- a/skia/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "floem_skia_renderer" -version.workspace = true -edition.workspace = true -repository = "https://github.com/lapce/floem" -description = "A native Rust UI library with fine-grained reactivity" -license.workspace = true - -[dependencies] -anyhow = "1.0" -# anyrender = {path = "../../anyrender/crates/anyrender", package = "anyrender"} -# anyrender_skia = { path = "../../anyrender/crates/anyrender_skia", package = "anyrender_skia" } -anyrender = { git = "https://github.com/jrmoulton/anyrender", rev = "e38ca6c35f45" } -anyrender_skia = { git = "https://github.com/jrmoulton/anyrender", rev = "e38ca6c35f45" } -floem_renderer = { path = "../renderer", version = "0.2.0" } -kurbo = "0.13" -parley = { workspace = true } -peniko = { workspace = true } -raw-window-handle = { workspace = true } -resvg = { workspace = true } -winit = { workspace = true } diff --git a/skia/src/lib.rs b/skia/src/lib.rs deleted file mode 100644 index be47f839a..000000000 --- a/skia/src/lib.rs +++ /dev/null @@ -1,489 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use anyhow::{Result, anyhow}; -use anyrender::{ImageRenderer as _, WindowRenderer as _}; -use anyrender_skia::{SkiaImageRenderer, SkiaWindowRenderer}; -use floem_renderer::text::{Glyph, GlyphRunRef}; -use floem_renderer::{Renderer, Svg}; -use peniko::kurbo::{Affine, BezPath, Point, Rect, Shape, Size, Stroke}; -use peniko::{ - Blob, Brush, BrushRef, Compose, Fill, ImageAlphaType, ImageData, ImageFormat, Mix, Style, -}; -use raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle, -}; -use resvg::tiny_skia::{Pixmap, Transform}; -use winit::window::Window; - -const PATH_TOLERANCE: f64 = 0.1; - -enum Command { - Stroke { - shape: BezPath, - brush: Brush, - stroke: Stroke, - transform: Affine, - }, - Fill { - shape: BezPath, - brush: Brush, - transform: Affine, - }, - BoxShadow { - rect: Rect, - brush: peniko::Color, - radius: f64, - std_dev: f64, - transform: Affine, - }, - PushLayer { - blend: peniko::BlendMode, - alpha: f32, - transform: Affine, - clip: BezPath, - }, - PushClip { - transform: Affine, - clip: BezPath, - }, - PopLayer, - Glyphs { - font: peniko::FontData, - font_size: f32, - hint: bool, - normalized_coords: Vec, - style: Style, - brush: Brush, - brush_alpha: f32, - transform: Affine, - glyph_transform: Option, - glyphs: Vec, - }, - Svg { - image: peniko::ImageBrush, - rect: Rect, - transform: Affine, - brush: Option, - }, -} - -pub struct SkiaRenderer { - window_renderer: SkiaWindowRenderer, - capture_renderer: SkiaImageRenderer, - commands: Vec, - svg_cache: HashMap<(Vec, u32, u32), peniko::ImageBrush>, - size: Size, - transform: Affine, - capture: bool, -} - -#[derive(Clone)] -struct AnyrenderWindow(Arc); - -impl HasWindowHandle for AnyrenderWindow { - fn window_handle(&self) -> std::result::Result, HandleError> { - self.0.window_handle() - } -} - -impl HasDisplayHandle for AnyrenderWindow { - fn display_handle(&self) -> std::result::Result, HandleError> { - self.0.display_handle() - } -} - -impl SkiaRenderer { - pub fn new( - window: Arc, - width: u32, - height: u32, - scale: f64, - _font_embolden: f32, - ) -> Result { - let width = width.max(1); - let height = height.max(1); - - let mut window_renderer = SkiaWindowRenderer::new(); - let handle: Arc = Arc::new(AnyrenderWindow(window)); - window_renderer.resume(handle, width, height); - - let mut capture_renderer = SkiaImageRenderer::new(width, height); - capture_renderer.reset(); - - Ok(Self { - window_renderer, - capture_renderer, - commands: Vec::new(), - svg_cache: HashMap::new(), - size: Size::new(width as f64, height as f64), - transform: Affine::scale(scale), - capture: false, - }) - } - - pub fn resize(&mut self, width: u32, height: u32, _scale: f64) { - let width = width.max(1); - let height = height.max(1); - self.size = Size::new(width as f64, height as f64); - self.window_renderer.set_size(width, height); - self.capture_renderer.resize(width, height); - } - - pub const fn set_scale(&mut self, _scale: f64) {} - - pub const fn size(&self) -> Size { - self.size - } - - fn current_scale(&self) -> (f64, f64) { - let coeffs = self.transform.as_coeffs(); - let scale_x = coeffs[0].hypot(coeffs[1]); - let scale_y = coeffs[2].hypot(coeffs[3]); - (scale_x, scale_y) - } - - fn replay(commands: &[Command], scene: &mut S) { - for command in commands { - match command { - Command::Stroke { - shape, - brush, - stroke, - transform, - } => { - scene.stroke( - stroke, - *transform, - anyrender::PaintRef::from(BrushRef::from(brush)), - None, - shape, - ); - } - Command::Fill { - shape, - brush, - transform, - } => { - scene.fill( - Fill::NonZero, - *transform, - anyrender::PaintRef::from(BrushRef::from(brush)), - None, - shape, - ); - } - Command::BoxShadow { - rect, - brush, - radius, - std_dev, - transform, - } => { - scene.draw_box_shadow(*transform, *rect, *brush, *radius, *std_dev); - } - Command::PushLayer { - blend, - alpha, - transform, - clip, - } => { - scene.push_layer(*blend, *alpha, *transform, clip); - } - Command::PushClip { transform, clip } => { - scene.push_clip_layer(*transform, clip); - } - Command::PopLayer => { - scene.pop_layer(); - } - Command::Glyphs { - font, - font_size, - hint, - normalized_coords, - style, - brush, - brush_alpha, - transform, - glyph_transform, - glyphs, - } => { - scene.draw_glyphs( - font, - *font_size, - *hint, - normalized_coords, - style, - anyrender::PaintRef::from(BrushRef::from(brush)), - *brush_alpha, - *transform, - *glyph_transform, - glyphs.iter().copied(), - ); - } - Command::Svg { - image, - rect, - transform, - brush, - } => { - let image_transform = image_transform(*transform, *rect, image); - let src_rect = Rect::new( - 0.0, - 0.0, - image.image.width as f64, - image.image.height as f64, - ); - - if let Some(brush) = brush { - scene.push_layer( - peniko::BlendMode::default(), - 1.0, - image_transform, - &src_rect, - ); - scene.fill( - Fill::NonZero, - image_transform, - image.as_ref(), - None, - &src_rect, - ); - scene.push_layer( - peniko::BlendMode { - mix: Mix::Normal, - compose: Compose::SrcIn, - }, - 1.0, - image_transform, - &src_rect, - ); - scene.fill( - Fill::NonZero, - image_transform, - anyrender::PaintRef::from(BrushRef::from(brush)), - None, - &src_rect, - ); - scene.pop_layer(); - scene.pop_layer(); - } else { - scene.fill( - Fill::NonZero, - image_transform, - image.as_ref(), - None, - &src_rect, - ); - } - } - } - } - } - - fn cached_svg_image( - &mut self, - svg: Svg<'_>, - width: u32, - height: u32, - ) -> Result { - let key = (svg.hash.to_vec(), width, height); - if let Some(image) = self.svg_cache.get(&key) { - return Ok(image.clone()); - } - - let mut pixmap = - Pixmap::new(width, height).ok_or_else(|| anyhow!("failed to allocate svg pixmap"))?; - let transform = Transform::from_scale( - width as f32 / svg.tree.size().width(), - height as f32 / svg.tree.size().height(), - ); - resvg::render(svg.tree, transform, &mut pixmap.as_mut()); - let image = image_brush_from_rgba(width, height, pixmap.take()); - self.svg_cache.insert(key, image.clone()); - Ok(image) - } -} - -impl Renderer for SkiaRenderer { - fn begin(&mut self, capture: bool) { - self.commands.clear(); - self.transform = Affine::IDENTITY; - self.capture = capture; - } - - fn set_transform(&mut self, transform: Affine) { - self.transform = transform; - } - - fn clip(&mut self, shape: &impl Shape) { - self.commands.push(Command::PushClip { - transform: self.transform, - clip: shape.to_path(PATH_TOLERANCE), - }); - } - - fn clear_clip(&mut self) { - self.commands.push(Command::PopLayer); - } - - fn stroke<'b, 's>( - &mut self, - shape: &impl Shape, - brush: impl Into>, - stroke: &'s Stroke, - ) { - self.commands.push(Command::Stroke { - shape: shape.to_path(PATH_TOLERANCE), - brush: brush.into().to_owned(), - stroke: stroke.clone(), - transform: self.transform, - }); - } - - fn fill<'b>(&mut self, path: &impl Shape, brush: impl Into>, blur_radius: f64) { - let brush = brush.into(); - - if blur_radius > 0.0 - && let BrushRef::Solid(color) = brush - { - if let Some(rounded) = path.as_rounded_rect() { - let radii = rounded.radii(); - if radii.top_left == radii.top_right - && radii.top_left == radii.bottom_left - && radii.top_left == radii.bottom_right - { - self.commands.push(Command::BoxShadow { - rect: rounded.rect(), - brush: color, - radius: radii.top_left, - std_dev: blur_radius, - transform: self.transform, - }); - return; - } - } else if let Some(rect) = path.as_rect() { - self.commands.push(Command::BoxShadow { - rect, - brush: color, - radius: 0.0, - std_dev: blur_radius, - transform: self.transform, - }); - return; - } - } - - self.commands.push(Command::Fill { - shape: path.to_path(PATH_TOLERANCE), - brush: brush.to_owned(), - transform: self.transform, - }); - } - - fn push_layer( - &mut self, - blend: impl Into, - alpha: f32, - transform: Affine, - clip: &impl Shape, - ) { - self.commands.push(Command::PushLayer { - blend: blend.into(), - alpha, - transform: self.transform * transform, - clip: clip.to_path(PATH_TOLERANCE), - }); - } - - fn pop_layer(&mut self) { - self.commands.push(Command::PopLayer); - } - - fn draw_glyphs<'a>( - &mut self, - origin: Point, - run: &GlyphRunRef<'a>, - glyphs: impl Iterator + 'a, - ) { - let transform = self.transform * Affine::translate((origin.x, origin.y)) * run.transform; - self.commands.push(Command::Glyphs { - font: run.font.clone(), - font_size: run.font_size, - hint: run.hint, - normalized_coords: run.normalized_coords.to_vec(), - style: run.style.clone(), - brush: run.brush.to_owned(), - brush_alpha: run.composite.alpha, - transform, - glyph_transform: run.glyph_transform, - glyphs: glyphs - .map(|glyph| anyrender::Glyph { - id: glyph.id, - x: glyph.x, - y: glyph.y, - }) - .collect(), - }); - } - - fn draw_svg<'b>(&mut self, svg: Svg<'b>, rect: Rect, brush: Option>>) { - let (scale_x, scale_y) = self.current_scale(); - let width = (rect.width() * scale_x.abs()).round().max(1.0) as u32; - let height = (rect.height() * scale_y.abs()).round().max(1.0) as u32; - let image = match self.cached_svg_image(svg, width, height) { - Ok(image) => image, - Err(_) => return, - }; - - self.commands.push(Command::Svg { - image, - rect, - transform: self.transform, - brush: brush.map(|brush| brush.into().to_owned()), - }); - } - - fn finish(&mut self) -> Option { - if self.capture { - let commands = std::mem::take(&mut self.commands); - let mut buffer = vec![0; self.size.width as usize * self.size.height as usize * 4]; - self.capture_renderer.reset(); - self.capture_renderer - .render(|scene| Self::replay(&commands, scene), &mut buffer); - return Some(image_brush_from_rgba( - self.size.width as u32, - self.size.height as u32, - buffer, - )); - } - - let commands = std::mem::take(&mut self.commands); - self.window_renderer - .render(|scene| Self::replay(&commands, scene)); - None - } - - fn debug_info(&self) -> String { - "name: Skia\ninfo: AnyRender Skia".to_string() - } -} - -fn image_transform(transform: Affine, rect: Rect, image: &peniko::ImageBrush) -> Affine { - transform - .pre_scale_non_uniform( - rect.width().max(1.0) / image.image.width as f64, - rect.height().max(1.0) / image.image.height as f64, - ) - .pre_translate((rect.min_x(), rect.min_y()).into()) -} - -fn image_brush_from_rgba(width: u32, height: u32, data: Vec) -> peniko::ImageBrush { - peniko::ImageBrush::new(ImageData { - data: Blob::new(Arc::new(data)), - format: ImageFormat::Rgba8, - alpha_type: ImageAlphaType::AlphaPremultiplied, - width, - height, - }) -} diff --git a/src/app/handle.rs b/src/app/handle.rs index 21b413e21..a77b03ea3 100644 --- a/src/app/handle.rs +++ b/src/app/handle.rs @@ -20,7 +20,6 @@ use winit::{ }; use super::{APP_UPDATE_EVENTS, AppConfig, AppEventCallback, AppUpdateEvent, UserEvent}; -#[cfg(not(feature = "skia"))] use crate::context::PaintState; use crate::{ AppEvent, Application, @@ -87,14 +86,14 @@ impl ApplicationHandle { }); } } - #[cfg(not(feature = "skia"))] + #[cfg(any(feature = "active-vello", feature = "active-vger"))] UserEvent::GpuResourcesUpdate { window_id } => { let handle = self.window_handles.get_mut(&window_id).unwrap(); if let PaintState::PendingGpuResources { window, rx, font_embolden, - renderer, + renderer: _, } = &handle.paint_state { let (gpu_resources, surface) = rx.recv().unwrap().unwrap(); @@ -103,7 +102,7 @@ impl ApplicationHandle { gpu_resources.clone(), surface, handle.window_state.effective_scale(), - renderer.size(), + handle.window_state.root_size * handle.window_state.os_scale, *font_embolden, ); self.gpu_resources = Some(gpu_resources); @@ -114,6 +113,20 @@ impl ApplicationHandle { panic!("Sent a gpu resource update after it had already been initialized"); } } + #[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + ))] + UserEvent::GpuResourcesUpdate { .. } => {} + #[cfg(not(any( + feature = "active-vello", + feature = "active-vger", + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + )))] + UserEvent::GpuResourcesUpdate { .. } => unreachable!(), UserEvent::ShowContextMenu { window_id, menu, diff --git a/src/app/mod.rs b/src/app/mod.rs index b21da10f4..cd3ccf855 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -124,7 +124,6 @@ pub(crate) enum UserEvent { Reopen { has_visible_windows: bool, }, - #[cfg(not(feature = "skia"))] GpuResourcesUpdate { window_id: WindowId, }, diff --git a/src/compositor/backend.rs b/src/compositor/backend.rs index d7e942da3..cbd05d263 100644 --- a/src/compositor/backend.rs +++ b/src/compositor/backend.rs @@ -94,11 +94,7 @@ pub trait CompositorBackend { /// Iterates compositor-owned surfaces that expect Floem to rasterize content /// into them before final composition. - fn for_each_floem_painted_surface( - &self, - _visit: &mut FloemPaintedSurfaceVisitor<'_>, - ) { - } + fn for_each_floem_painted_surface(&self, _visit: &mut FloemPaintedSurfaceVisitor<'_>) {} /// Composites the current backend layer tree into the provided output view. fn composite_to_output(&mut self, _output: &wgpu::TextureView) {} diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index 25273f7f5..80c11b63a 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] //! Compositor-facing API and retained compositor state. //! //! This module is the public entry point for composition-level concepts that sit @@ -162,13 +163,9 @@ pub enum CompositorLayerKind { #[default] FloemPainted, /// Layer contents come from a registered external surface. - ExternalSurface { - surface_id: ExternalSurfaceId, - }, + ExternalSurface { surface_id: ExternalSurfaceId }, /// Layer combines an external surface with Floem-drawn overlays or underlays. - Mixed { - surface_id: ExternalSurfaceId, - }, + Mixed { surface_id: ExternalSurfaceId }, } /// How a compositor layer is realized by the compositor/backend. @@ -303,9 +300,8 @@ pub(crate) struct ResolvedPromotedLayer { pub alpha_mode: CompositorAlphaMode, } -pub(crate) type FloemSurfaceRoleVisitor<'a> = - dyn FnMut(FloemPaintedSurfaceRole, Rect, (u32, u32), wgpu::TextureFormat, &wgpu::TextureView) - + 'a; +pub(crate) type FloemSurfaceRoleVisitor<'a> = dyn FnMut(FloemPaintedSurfaceRole, Rect, (u32, u32), wgpu::TextureFormat, &wgpu::TextureView) + + 'a; /// Retained compositor registry and frame-request state. #[derive(Default)] @@ -393,14 +389,15 @@ impl Compositor { isolated: promoted.isolated, }; - let layer_id = if let Some(layer_id) = self.promoted_layers.get(&promoted.element_id).copied() { - let _ = self.update_layer(layer_id, descriptor); - layer_id - } else { - let layer_id = self.register_layer(descriptor); - self.promoted_layers.insert(promoted.element_id, layer_id); - layer_id - }; + let layer_id = + if let Some(layer_id) = self.promoted_layers.get(&promoted.element_id).copied() { + let _ = self.update_layer(layer_id, descriptor); + layer_id + } else { + let layer_id = self.register_layer(descriptor); + self.promoted_layers.insert(promoted.element_id, layer_id); + layer_id + }; active.insert(promoted.element_id, layer_id); } @@ -408,7 +405,9 @@ impl Compositor { let stale: Vec<_> = self .promoted_layers .iter() - .filter_map(|(&element_id, &layer_id)| (!active.contains_key(&element_id)).then_some((element_id, layer_id))) + .filter_map(|(&element_id, &layer_id)| { + (!active.contains_key(&element_id)).then_some((element_id, layer_id)) + }) .collect(); for (element_id, layer_id) in stale { @@ -488,10 +487,7 @@ impl Compositor { } } - pub(crate) fn for_each_floem_painted_surface( - &self, - visit: &mut FloemSurfaceRoleVisitor<'_>, - ) { + pub(crate) fn for_each_floem_painted_surface(&self, visit: &mut FloemSurfaceRoleVisitor<'_>) { let Some(backend) = self.backend.as_ref() else { return; }; @@ -501,8 +497,10 @@ impl Compositor { let Some(layer_state) = self.layers.get(&layer_id) else { return; }; - if !matches!(layer_state.descriptor.kind, CompositorLayerKind::FloemPainted) - || !layer_state.descriptor.capabilities.supports_direct_raster + if !matches!( + layer_state.descriptor.kind, + CompositorLayerKind::FloemPainted + ) || !layer_state.descriptor.capabilities.supports_direct_raster { return; } @@ -514,16 +512,16 @@ impl Compositor { } else { self.promoted_layers .iter() - .find_map(|(&element_id, &promoted_layer_id)| { - (promoted_layer_id == layer_id) - .then_some(FloemPaintedSurfaceRole::Promoted(element_id)) - }) - }; + .find_map(|(&element_id, &promoted_layer_id)| { + (promoted_layer_id == layer_id) + .then_some(FloemPaintedSurfaceRole::Promoted(element_id)) + }) + }; - if let Some(role) = role { - visit(role, bounds, size, format, view); - } - }; + if let Some(role) = role { + visit(role, bounds, size, format, view); + } + }; backend.for_each_floem_painted_surface(&mut visit_backend); } @@ -592,10 +590,7 @@ impl Compositor { } /// Registers a compositor layer and returns its id. - pub fn register_layer( - &mut self, - descriptor: CompositorLayerDescriptor, - ) -> CompositorLayerId { + pub fn register_layer(&mut self, descriptor: CompositorLayerDescriptor) -> CompositorLayerId { let id = CompositorLayerId(self.next_layer_id); self.next_layer_id += 1; self.layers.insert( @@ -736,7 +731,9 @@ impl Compositor { timing.last_present_started = Some(started_at); timing.last_present_completed = Some(completed_at); timing.frame_number = timing.frame_number.saturating_add(1); - timing.predicted_next_present = timing.frame_interval.map(|interval| completed_at + interval); + timing.predicted_next_present = timing + .frame_interval + .map(|interval| completed_at + interval); self.update_timing(timing); if let Some(backend) = self.backend.as_mut() { diff --git a/src/compositor/subduction.rs b/src/compositor/subduction.rs index 6a0781dbf..3020a8217 100644 --- a/src/compositor/subduction.rs +++ b/src/compositor/subduction.rs @@ -15,12 +15,12 @@ use peniko::kurbo::{Rect, RoundedRect}; use rustc_hash::FxHashMap; +use subduction_backend_wgpu::WgpuPresenter; use subduction_core::{ backend::Presenter as _, layer::{ClipShape, FrameChanges, LayerFlags, LayerId, LayerStore, SurfaceId}, transform::Transform3d, }; -use subduction_backend_wgpu::WgpuPresenter; use super::{ CompositorAlphaMode, CompositorLayerBacking, CompositorLayerDescriptor, CompositorLayerId, @@ -124,7 +124,11 @@ impl SubductionCompositorBackend { layer_id } - fn apply_layer_descriptor(&mut self, id: CompositorLayerId, descriptor: CompositorLayerDescriptor) { + fn apply_layer_descriptor( + &mut self, + id: CompositorLayerId, + descriptor: CompositorLayerDescriptor, + ) { let layer_id = self.ensure_layer_id(id); self.layer_descriptors.insert(id, descriptor); @@ -153,8 +157,12 @@ impl SubductionCompositorBackend { descriptor.isolated && descriptor.capabilities.supports_clip_to_bounds, ), ); - self.store - .set_flags(layer_id, LayerFlags { hidden: descriptor.opacity <= 0.0 || is_empty }); + self.store.set_flags( + layer_id, + LayerFlags { + hidden: descriptor.opacity <= 0.0 || is_empty, + }, + ); let content = self.content_for_kind(id, descriptor.kind); self.store.set_content(layer_id, content); if matches!(descriptor.alpha_mode, CompositorAlphaMode::Straight) { @@ -166,17 +174,25 @@ impl SubductionCompositorBackend { self.needs_reorder = true; } - fn content_for_kind(&mut self, id: CompositorLayerId, kind: CompositorLayerKind) -> Option { + fn content_for_kind( + &mut self, + id: CompositorLayerId, + kind: CompositorLayerKind, + ) -> Option { let backing = self .layer_descriptors .get(&id) .map(|descriptor| descriptor.backing) .unwrap_or(CompositorLayerBacking::TextureBacked); match kind { - CompositorLayerKind::FloemPainted => matches!(backing, CompositorLayerBacking::TextureBacked) - .then(|| self.floem_surface_id_for(id)), + CompositorLayerKind::FloemPainted => { + matches!(backing, CompositorLayerBacking::TextureBacked) + .then(|| self.floem_surface_id_for(id)) + } CompositorLayerKind::ExternalSurface { surface_id } - | CompositorLayerKind::Mixed { surface_id } => self.surface_ids.get(&surface_id).copied(), + | CompositorLayerKind::Mixed { surface_id } => { + self.surface_ids.get(&surface_id).copied() + } } } @@ -300,10 +316,7 @@ impl CompositorBackend for SubductionCompositorBackend { output_format: wgpu::TextureFormat, output_size: (u32, u32), ) { - let default_layer_size = ( - output_size.0.clamp(1, 2048), - output_size.1.clamp(1, 2048), - ); + let default_layer_size = (output_size.0.clamp(1, 2048), output_size.1.clamp(1, 2048)); self.presenter = Some(WgpuPresenter::new( gpu_resources.device.clone(), gpu_resources.queue.clone(), @@ -332,10 +345,7 @@ impl CompositorBackend for SubductionCompositorBackend { } } - fn for_each_floem_painted_surface( - &self, - visit: &mut FloemPaintedSurfaceVisitor<'_>, - ) { + fn for_each_floem_painted_surface(&self, visit: &mut FloemPaintedSurfaceVisitor<'_>) { let Some(presenter) = self.presenter.as_ref() else { return; }; @@ -387,5 +397,7 @@ fn clip_shape_for_bounds( return None; } - Some(ClipShape::RoundedRect(RoundedRect::from_rect(local_clip, 0.0))) + Some(ClipShape::RoundedRect(RoundedRect::from_rect( + local_clip, 0.0, + ))) } diff --git a/src/inspector/mod.rs b/src/inspector/mod.rs index 0e652a000..cccc67abf 100644 --- a/src/inspector/mod.rs +++ b/src/inspector/mod.rs @@ -572,7 +572,7 @@ pub struct Capture { pub taffy_node_count: usize, pub taffy_depth: usize, /// Captured window image in render-target pixels, if the backend supports capture. - pub window: Option, + pub window: Option, /// Logical capture size used by layout/box-tree coordinates and inspector overlays. pub window_size: Size, pub state: CaptureState, diff --git a/src/inspector/view.rs b/src/inspector/view.rs index 1471dbc67..b150cac00 100644 --- a/src/inspector/view.rs +++ b/src/inspector/view.rs @@ -197,7 +197,7 @@ fn capture_view( let renderer = capture_.renderer.clone(); let image = if let Some(window) = window { - img_dynamic(move || window.clone()).into_any() + img_dynamic(move || peniko::ImageBrush::new(window.clone())).into_any() } else { ().style(move |s| s.min_width(size.width).min_height(size.height)) .into_any() @@ -806,9 +806,7 @@ impl View for InspectorImageView { if let Some(selected_overlay) = self.overlay_rect(self.capture_view.selected.get()) { let fill = Brush::Solid(self.selected_overlay_color); let stroke = Brush::Solid(self.selected_overlay_border_color); - cx.painter - .fill(selected_overlay, &fill) - .draw(); + cx.painter.fill(selected_overlay, &fill).draw(); cx.painter .stroke(selected_overlay, &Stroke::new(1.0), &stroke) .draw(); @@ -818,9 +816,7 @@ impl View for InspectorImageView { { let fill = Brush::Solid(self.highlighted_overlay_color); let stroke = Brush::Solid(self.highlighted_overlay_border_color); - cx.painter - .fill(highlighted_overlay, &fill) - .draw(); + cx.painter.fill(highlighted_overlay, &fill).draw(); cx.painter .stroke(highlighted_overlay, &Stroke::new(1.0), &stroke) .draw(); diff --git a/src/lib.rs b/src/lib.rs index d4d4ecbbc..5a3de09d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,20 @@ //! //! Floem's objectives prioritize simplicity and performance, enabling the development of complex graphical user interfaces with minimal boilerplate. //! +//! ## Renderer Selection +//! +//! Floem normally uses the active renderer selected by crate features and falls back to the +//! configured CPU renderer only if needed. +//! +//! To force Floem to use the CPU renderer path at runtime, set: +//! +//! ```text +//! FLOEM_FORCE_CPU=1 +//! ``` +//! +//! `FLOEM_FORCE_TINY_SKIA=1` is still accepted as a deprecated alias for compatibility, but new +//! code should use `FLOEM_FORCE_CPU`. +//! //! ## Views //! Floem models the UI using a tree of [Views](view::View). Views, such as the `h_stack`, `label`, and //! `button` elements are the building blocks of UI in Floem. @@ -224,23 +238,23 @@ pub mod receiver_signal { pub use box_tree::{BoxTree, ElementId, ElementMeta, FocusNavMeta}; pub(crate) use box_tree::{bump_focus_nav_meta_revision, focus_nav_meta_revision}; pub use compositor::backend::CompositorBackend; +#[cfg(feature = "subduction")] +pub use compositor::subduction::SubductionCompositorBackend; pub use compositor::{ Compositor, CompositorLayerDescriptor, CompositorLayerId, CompositorLayerKind, CompositorTiming, ExternalAlphaMode, ExternalColorSpace, ExternalPixelFormat, ExternalSurfaceDescriptor, ExternalSurfaceHandle, ExternalSurfaceId, FrameRequestReason, }; -#[cfg(feature = "subduction")] -pub use compositor::subduction::SubductionCompositorBackend; pub use app::{AppConfig, AppEvent, Application, launch, quit_app, reopen}; pub use floem_reactive as reactive; -pub use floem_renderer::Renderer; pub use floem_renderer::Svg as RendererSvg; pub use floem_renderer::gpu_resources::GpuResources; pub use imbl; pub use layout::ScreenLayout; #[cfg(not(target_arch = "wasm32"))] pub use muda; +pub use paint::renderer::Renderer; pub use peniko; pub use peniko::kurbo; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/paint/display_list.rs b/src/paint/display_list.rs index 991856bd7..072e4cf97 100644 --- a/src/paint/display_list.rs +++ b/src/paint/display_list.rs @@ -151,10 +151,10 @@ use std::sync::Arc; -use floem_renderer::text::{Glyph, GlyphRunRef}; -use floem_renderer::{Renderer as FloemRenderer, Svg, usvg}; +use floem_renderer::text::GlyphRunRef; +use floem_renderer::{DisplayCommandExt, OwnedSvg, Svg}; use imaging::{ - BlurredRoundedRect, ClipRef, CustomPaintSink, FillRef, GroupRef, PaintSink, StrokeRef, + BlurredRoundedRect, ClipRef, FillRef, GroupRef, PaintSink, StrokeRef, record::{ Clip, Draw, ExtendedScene, Geometry, Glyph as ImagingGlyph, GlyphRun, Group, replay_ext_transformed, @@ -164,7 +164,7 @@ use peniko::kurbo::{Affine, Point, Rect, RoundedRect, Shape, Size}; use peniko::{BrushRef, Fill}; use rustc_hash::{FxHashMap, FxHashSet}; -use crate::{ElementId, paint::PaintOrPost}; +use crate::{ElementId, Renderer as AppRenderer, paint::PaintOrPost}; /// Transform class describing when recorded content remains valid. #[allow(dead_code)] @@ -225,40 +225,6 @@ pub(crate) fn transform_diff_class(original: Affine, current: Affine) -> Transfo } } -#[derive(Clone)] -pub(crate) struct OwnedSvg { - pub tree: Arc, - pub hash: Arc<[u8]>, -} - -#[derive(Clone)] -pub(crate) enum DisplayCommandExt { - DrawSvg { - svg: OwnedSvg, - rect: Rect, - transform: Affine, - brush: Option, - }, -} - -impl imaging::record::CustomCommand for DisplayCommandExt { - fn prepend_transform(&self, prefix: Affine) -> Self { - match self { - Self::DrawSvg { - svg, - rect, - transform, - brush, - } => Self::DrawSvg { - svg: svg.clone(), - rect: *rect, - transform: prefix * *transform, - brush: brush.clone(), - }, - } - } -} - #[derive(Clone)] pub(crate) enum DisplayCommand { PushClip { @@ -681,163 +647,13 @@ impl PaintSink for RecordingRenderer<'_> { } } -struct ReplayRenderer<'a, R> { - renderer: &'a mut R, - render_size: Size, - current_transform: &'a mut Option, -} - -impl PaintSink for ReplayRenderer<'_, R> -where - R: FloemRenderer, -{ - fn push_clip(&mut self, clip: ClipRef<'_>) { - let ClipRef::Fill { - transform, shape, .. - } = clip - else { - return; - }; - set_transform_if_needed(self.renderer, transform, self.current_transform); - match sanitize_clip_geometry(&shape.to_owned(), transform, self.render_size) { - Geometry::Rect(rect) => self.renderer.clip(&rect), - Geometry::RoundedRect(rect) => self.renderer.clip(&rect), - Geometry::Path(path) => self.renderer.clip(&path), - } - } - - fn pop_clip(&mut self) { - self.renderer.clear_clip(); - } - - fn push_group(&mut self, group: GroupRef<'_>) { - let Some(ClipRef::Fill { - transform, shape, .. - }) = group.clip - else { - return; - }; - set_transform_if_needed(self.renderer, Affine::IDENTITY, self.current_transform); - match sanitize_clip_geometry(&shape.to_owned(), transform, self.render_size) { - Geometry::Rect(rect) => self.renderer.push_layer( - group.composite.blend, - group.composite.alpha, - transform, - &rect, - ), - Geometry::RoundedRect(rect) => self.renderer.push_layer( - group.composite.blend, - group.composite.alpha, - transform, - &rect, - ), - Geometry::Path(path) => self.renderer.push_layer( - group.composite.blend, - group.composite.alpha, - transform, - &path, - ), - } - } - - fn pop_group(&mut self) { - self.renderer.pop_layer(); - } - - fn fill(&mut self, draw: FillRef<'_>) { - set_transform_if_needed(self.renderer, draw.transform, self.current_transform); - match draw.shape { - imaging::GeometryRef::Rect(rect) => self.renderer.fill(&rect, draw.brush, 0.0), - imaging::GeometryRef::RoundedRect(rect) => self.renderer.fill(&rect, draw.brush, 0.0), - imaging::GeometryRef::Path(path) => self.renderer.fill(path, draw.brush, 0.0), - imaging::GeometryRef::OwnedPath(path) => self.renderer.fill(&path, draw.brush, 0.0), - } - } - - fn stroke(&mut self, draw: StrokeRef<'_>) { - set_transform_if_needed(self.renderer, draw.transform, self.current_transform); - match draw.shape { - imaging::GeometryRef::Rect(rect) => { - self.renderer.stroke(&rect, draw.brush, draw.stroke) - } - imaging::GeometryRef::RoundedRect(rect) => { - self.renderer.stroke(&rect, draw.brush, draw.stroke) - } - imaging::GeometryRef::Path(path) => self.renderer.stroke(path, draw.brush, draw.stroke), - imaging::GeometryRef::OwnedPath(path) => { - self.renderer.stroke(&path, draw.brush, draw.stroke) - } - } - } - - fn glyph_run( - &mut self, - draw: imaging::GlyphRunRef<'_>, - glyphs: &mut dyn Iterator, - ) { - let run_ref = GlyphRunRef { - font: draw.font, - transform: draw.transform, - glyph_transform: draw.glyph_transform, - font_size: draw.font_size, - hint: draw.hint, - normalized_coords: draw.normalized_coords, - style: draw.style, - brush: draw.brush, - composite: draw.composite, - }; - set_transform_if_needed(self.renderer, Affine::IDENTITY, self.current_transform); - self.renderer.draw_glyphs( - Point::ZERO, - &run_ref, - glyphs.map(|glyph| Glyph { - id: glyph.id, - style_index: 0, - x: glyph.x, - y: glyph.y, - advance: 0.0, - }), - ); - } - - fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { - set_transform_if_needed(self.renderer, draw.transform, self.current_transform); - let shape = draw.rect.to_rounded_rect(draw.radius); - self.renderer.fill(&shape, draw.color, draw.std_dev); - } -} - -impl CustomPaintSink for ReplayRenderer<'_, R> -where - R: FloemRenderer, -{ - fn custom(&mut self, command: &DisplayCommandExt) { - let DisplayCommandExt::DrawSvg { - svg, - rect, - transform, - brush, - } = command; - set_transform_if_needed(self.renderer, *transform, self.current_transform); - self.renderer.draw_svg( - Svg { - tree: svg.tree.as_ref(), - hash: svg.hash.as_ref(), - }, - *rect, - brush.as_ref(), - ); - } -} - pub(crate) fn replay_stage( stage: &ElementStage, - renderer: &mut impl FloemRenderer, + renderer: &mut AppRenderer, base_transform: Affine, render_size: Size, local_damage: Option<&[Rect]>, ) { - let mut current_transform = None; let mut current_clip_stack: Vec = Vec::new(); // This stays wired through the replay path even though full-scene replay is still active. // Once the renderer/compositor can preserve undamaged content across frames, the stage can @@ -857,15 +673,9 @@ pub(crate) fn replay_stage( chunk.properties.clip_id, base_transform, render_size, - &mut current_transform, &mut current_clip_stack, ); - let mut replay = ReplayRenderer { - renderer, - render_size, - current_transform: &mut current_transform, - }; - replay_ext_transformed(&chunk.commands, &mut replay, base_transform); + replay_ext_transformed(&chunk.commands, renderer, base_transform); } apply_clip_state( @@ -874,21 +684,18 @@ pub(crate) fn replay_stage( ClipNodeId(0), base_transform, render_size, - &mut current_transform, &mut current_clip_stack, ); } pub(crate) fn replay_view_clip( - renderer: &mut impl FloemRenderer, + renderer: &mut AppRenderer, clip: RoundedRect, base_transform: Affine, render_size: Size, ) { - let final_transform = base_transform; - renderer.set_transform(final_transform); - let clip = constrain_infinite_rounded_rect(clip, final_transform, render_size); - renderer.clip(&clip); + let clip = constrain_infinite_rounded_rect(clip, base_transform, render_size); + PaintSink::push_clip(renderer, ClipRef::fill(clip).with_transform(base_transform)); } fn chunk_display_commands(commands: Vec) -> (Vec, PaintPropertyTree) { @@ -1228,16 +1035,12 @@ fn transform_key(transform: Affine) -> [u64; 6] { } fn replay_clip_node( - renderer: &mut impl FloemRenderer, + renderer: &mut AppRenderer, clip_node: &ClipNode, property_tree: &PaintPropertyTree, base_transform: Affine, render_size: Size, - current_transform: &mut Option, ) { - let Clip::Fill { shape, .. } = &clip_node.clip else { - return; - }; let Some(transform) = property_tree .transforms .get(clip_node.transform_id.0 as usize) @@ -1246,21 +1049,16 @@ fn replay_clip_node( return; }; let final_transform = base_transform * transform; - set_transform_if_needed(renderer, final_transform, current_transform); - match sanitize_clip_geometry(shape, final_transform, render_size) { - Geometry::Rect(rect) => renderer.clip(&rect), - Geometry::RoundedRect(rect) => renderer.clip(&rect), - Geometry::Path(path) => renderer.clip(&path), - } + let clip = sanitize_clip(&clip_node.clip, final_transform, render_size); + PaintSink::push_clip(renderer, clip.as_ref()); } fn apply_clip_state( - renderer: &mut impl FloemRenderer, + renderer: &mut AppRenderer, property_tree: &PaintPropertyTree, target_clip_id: ClipNodeId, base_transform: Affine, render_size: Size, - current_transform: &mut Option, current_clip_stack: &mut Vec, ) { // Stage-local clips are now driven by property ids instead of recorded Push/Pop commands. @@ -1274,7 +1072,7 @@ fn apply_clip_state( .count(); for _ in shared_prefix..current_clip_stack.len() { - renderer.clear_clip(); + PaintSink::pop_clip(renderer); } current_clip_stack.truncate(shared_prefix); @@ -1282,14 +1080,7 @@ fn apply_clip_state( let Some(node) = property_tree.clips.get(clip_id.0 as usize) else { continue; }; - replay_clip_node( - renderer, - node, - property_tree, - base_transform, - render_size, - current_transform, - ); + replay_clip_node(renderer, node, property_tree, base_transform, render_size); current_clip_stack.push(clip_id); } } @@ -1313,17 +1104,6 @@ fn clip_chain(property_tree: &PaintPropertyTree, clip_id: ClipNodeId) -> Vec, -) { - if current_transform != &Some(transform) { - renderer.set_transform(transform); - *current_transform = Some(transform); - } -} - fn sanitize_clip_geometry(shape: &Geometry, transform: Affine, render_size: Size) -> Geometry { match shape { Geometry::Rect(rect) => { @@ -1338,6 +1118,23 @@ fn sanitize_clip_geometry(shape: &Geometry, transform: Affine, render_size: Size } } +fn sanitize_clip(clip: &Clip, transform: Affine, render_size: Size) -> Clip { + match clip { + Clip::Fill { + shape, fill_rule, .. + } => Clip::Fill { + transform, + shape: sanitize_clip_geometry(shape, transform, render_size), + fill_rule: *fill_rule, + }, + Clip::Stroke { shape, stroke, .. } => Clip::Stroke { + transform, + shape: sanitize_clip_geometry(shape, transform, render_size), + stroke: stroke.clone(), + }, + } +} + fn constrain_infinite_rounded_rect( rect: RoundedRect, transform: Affine, diff --git a/src/paint/mod.rs b/src/paint/mod.rs index 79efe9152..fc3172ee6 100644 --- a/src/paint/mod.rs +++ b/src/paint/mod.rs @@ -12,7 +12,6 @@ pub mod renderer; pub use border_path_iter::{BorderPath, BorderPathEvent}; pub use renderer::Renderer; -use floem_renderer::Renderer as _; use floem_renderer::gpu_resources::{GpuResourceError, GpuResources}; use imaging::{PaintSink, Painter}; use peniko::kurbo::{Affine, Point, RoundedRect, Size}; @@ -254,7 +253,8 @@ impl GlobalPaintCx<'_> { .element_base_transform(element_id) .then_scale(self.window_state.effective_scale()) .then_translate(-target_origin.to_vec2()); - let render_size = render_size.unwrap_or_else(|| self.paint_state.renderer().size()); + let render_size = + render_size.unwrap_or_else(|| self.window_state.root_size * self.window_state.os_scale); let renderer = self.paint_state.renderer_mut(); // Overflow clip must be replayed at traversal time rather than recorded into the @@ -481,7 +481,7 @@ impl GlobalPaintCx<'_> { .flatten() .is_some(); if has_clip { - self.paint_state.renderer_mut().clear_clip(); + PaintSink::pop_clip(self.paint_state.renderer_mut()); } } } @@ -537,7 +537,8 @@ impl GlobalPaintCx<'_> { .map(|rect| inverse.transform_rect_bbox(*rect)) .collect::>() }); - let render_size = render_size.unwrap_or_else(|| self.paint_state.renderer().size()); + let render_size = + render_size.unwrap_or_else(|| self.window_state.root_size * self.window_state.os_scale); let renderer = self.paint_state.renderer_mut(); replay_stage( stage, @@ -667,6 +668,7 @@ impl PaintState { } } + #[cfg(any(feature = "active-vello", feature = "active-vger"))] pub fn new( window: Arc, surface: wgpu::Surface<'static>, @@ -686,9 +688,13 @@ impl PaintState { Self::Initialized { renderer } } - #[cfg(feature = "skia")] - pub fn new_skia(window: Arc, scale: f64, size: Size, font_embolden: f32) -> Self { - let renderer = Renderer::new_skia(window.clone(), scale, size, font_embolden); + #[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + ))] + pub fn new_cpu(window: Arc, scale: f64, size: Size, font_embolden: f32) -> Self { + let renderer = Renderer::new_cpu(window, scale, size, font_embolden); Self::Initialized { renderer } } @@ -705,12 +711,4 @@ impl PaintState { PaintState::Initialized { renderer } => renderer, } } - - pub(crate) fn resize(&mut self, scale: f64, size: Size) { - self.renderer_mut().resize(scale, size); - } - - pub(crate) fn set_scale(&mut self, scale: f64) { - self.renderer_mut().set_scale(scale); - } } diff --git a/src/paint/renderer.rs b/src/paint/renderer.rs index 996ba7f1d..83c33102f 100644 --- a/src/paint/renderer.rs +++ b/src/paint/renderer.rs @@ -49,22 +49,263 @@ //! use std::sync::Arc; +#[cfg(not(any( + feature = "active-vello", + feature = "active-vger", + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia", +)))] +compile_error!("Enable exactly one active renderer feature."); +#[cfg(any( + all(feature = "active-vello", feature = "active-vger"), + all(feature = "active-vello", feature = "active-vello-hybrid"), + all(feature = "active-vello", feature = "active-vello-cpu"), + all(feature = "active-vello", feature = "active-tiny-skia"), + all(feature = "active-vger", feature = "active-vello-hybrid"), + all(feature = "active-vger", feature = "active-vello-cpu"), + all(feature = "active-vger", feature = "active-tiny-skia"), + all(feature = "active-vello-hybrid", feature = "active-vello-cpu"), + all(feature = "active-vello-hybrid", feature = "active-tiny-skia"), + all(feature = "active-vello-cpu", feature = "active-tiny-skia"), +))] +compile_error!("Enable only one active renderer feature."); +#[cfg(not(any(feature = "fallback-vello-cpu", feature = "fallback-tiny-skia")))] +compile_error!("Enable exactly one CPU fallback renderer feature."); +#[cfg(all(feature = "fallback-vello-cpu", feature = "fallback-tiny-skia"))] +compile_error!("Enable only one CPU fallback renderer feature."); + +#[cfg(any(feature = "active-vello", feature = "active-vger"))] use floem_renderer::gpu_resources::GpuResources; -#[cfg(feature = "skia")] -use floem_skia_renderer::SkiaRenderer as ActiveRenderer; -use floem_tiny_skia_renderer::TinySkiaRenderer; -#[cfg(all(feature = "vello", not(feature = "skia")))] +#[cfg(any(feature = "active-vello", feature = "active-vger"))] +use floem_renderer::{GpuTextureOutput, RenderOutput}; +#[cfg(feature = "active-tiny-skia")] +use floem_tiny_skia_renderer::TinySkiaRenderer as ActiveRenderer; +#[cfg(feature = "fallback-tiny-skia")] +use floem_tiny_skia_renderer::TinySkiaRenderer as CpuFallbackRenderer; +#[cfg(feature = "active-vello-cpu")] +use floem_vello_cpu_renderer::VelloCpuRenderer as ActiveRenderer; +#[cfg(feature = "fallback-vello-cpu")] +use floem_vello_cpu_renderer::VelloCpuRenderer as CpuFallbackRenderer; +#[cfg(feature = "active-vello-hybrid")] +use floem_vello_hybrid_renderer::VelloHybridRenderer as ActiveRenderer; +#[cfg(feature = "active-vello")] use floem_vello_renderer::VelloRenderer as ActiveRenderer; -#[cfg(all(not(feature = "vello"), not(feature = "skia")))] +#[cfg(feature = "active-vger")] use floem_vger_renderer::VgerRenderer as ActiveRenderer; -use peniko::BrushRef; -use peniko::kurbo::{Affine, Point, Rect, Shape, Size, Stroke}; +use imaging::{ + BlurredRoundedRect, ClipRef, CustomPaintSink, FillRef, GlyphRunRef, GroupRef, PaintSink, + StrokeRef, +}; +use peniko::ImageData; +use peniko::kurbo::Size; +#[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia", + feature = "fallback-vello-cpu", + feature = "fallback-tiny-skia" +))] +use softbuffer::{Context, Surface}; +#[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia", + feature = "fallback-vello-cpu", + feature = "fallback-tiny-skia" +))] +use std::num::NonZeroU32; +#[cfg(any(feature = "active-vello", feature = "active-vger"))] +use wgpu::util::TextureBlitter; use winit::window::Window; -#[allow(clippy::large_enum_variant)] +use floem_renderer::DisplayCommandExt; + +#[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia", + feature = "fallback-vello-cpu", + feature = "fallback-tiny-skia" +))] +struct CpuImagePresenter { + #[allow(unused)] + context: Context, + surface: Surface, +} + +#[cfg(any(feature = "fallback-vello-cpu", feature = "fallback-tiny-skia"))] +struct CpuFallbackState { + renderer: CpuFallbackRenderer, + presenter: CpuImagePresenter>, + size: Size, +} + +#[cfg(any(feature = "active-vello", feature = "active-vger"))] +struct ActiveState { + renderer: ActiveRenderer, + presenter: GpuWindowPresenter, + size: Size, +} + +#[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" +))] +struct ActiveState { + renderer: ActiveRenderer, + presenter: CpuImagePresenter>, + size: Size, +} + +#[cfg(any(feature = "active-vello", feature = "active-vger"))] +struct GpuWindowPresenter { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + config: wgpu::SurfaceConfiguration, + blitter: TextureBlitter, +} + +#[cfg(any(feature = "active-vello", feature = "active-vger"))] +impl GpuWindowPresenter { + fn new( + gpu_resources: &GpuResources, + surface: wgpu::Surface<'static>, + width: u32, + height: u32, + ) -> Result { + let surface_caps = surface.get_capabilities(&gpu_resources.adapter); + let texture_format = surface_caps + .formats + .into_iter() + .find(|it| { + matches!( + it, + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm + ) + }) + .ok_or_else(|| "surface should support Rgba8Unorm or Bgra8Unorm".to_string())?; + + let latency = match gpu_resources.adapter.get_info().backend { + wgpu::Backend::Vulkan => 2, + _ => 1, + }; + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: texture_format, + width, + height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: latency, + }; + surface.configure(&gpu_resources.device, &config); + + Ok(Self { + device: gpu_resources.device.clone(), + queue: gpu_resources.queue.clone(), + surface, + config, + blitter: TextureBlitter::new(&gpu_resources.device, texture_format), + }) + } + + fn resize(&mut self, width: u32, height: u32) { + if self.config.width != width || self.config.height != height { + self.config.width = width; + self.config.height = height; + self.surface.configure(&self.device, &self.config); + } + } + + fn present(&mut self, output: &GpuTextureOutput) { + let surface_texture = self + .surface + .get_current_texture() + .expect("failed to acquire surface texture"); + let output_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Floem Surface Blit"), + }); + self.blitter + .copy(&self.device, &mut encoder, &output.view, &output_view); + self.queue.submit([encoder.finish()]); + surface_texture.present(); + } +} + +#[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia", + feature = "fallback-vello-cpu", + feature = "fallback-tiny-skia" +))] +impl CpuImagePresenter +where + W: Clone + raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle, +{ + fn new(window: W, width: u32, height: u32) -> Result { + let context = Context::new(window.clone()).map_err(|err| err.to_string())?; + let mut surface = Surface::new(&context, window).map_err(|err| err.to_string())?; + surface + .resize( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ) + .map_err(|err| err.to_string())?; + Ok(Self { context, surface }) + } + + fn resize(&mut self, width: u32, height: u32) { + self.surface + .resize( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ) + .expect("failed to resize surface"); + } + + fn present(&mut self, image: &ImageData) { + let mut buffer = self + .surface + .buffer_mut() + .expect("failed to get the surface buffer"); + let width = image.width as usize; + for y in 0..image.height as usize { + let row_start = y * width; + let src = &image.data.data()[row_start * 4..(row_start + width) * 4]; + let dst = &mut buffer[row_start..row_start + width]; + for (out_pixel, rgba) in dst.iter_mut().zip(src.chunks_exact(4)) { + *out_pixel = ((rgba[0] as u32) << 16) | ((rgba[1] as u32) << 8) | (rgba[2] as u32); + } + } + buffer + .present() + .expect("failed to present the surface buffer"); + } +} + +#[allow(clippy::large_enum_variant, private_interfaces)] pub enum Renderer { - Active(ActiveRenderer), - TinySkia(TinySkiaRenderer>), + #[cfg(any( + feature = "active-vello", + feature = "active-vger", + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + ))] + Active(ActiveState), + #[cfg(any(feature = "fallback-vello-cpu", feature = "fallback-tiny-skia"))] + CpuFallback(CpuFallbackState), /// Uninitialized renderer, used to allow the renderer to be created lazily /// All operations on this renderer are no-ops Uninitialized { @@ -72,58 +313,41 @@ pub enum Renderer { }, } +#[derive(Clone, Copy, Debug)] +pub struct BeginFrame { + pub size: Size, + pub scale: f64, + pub font_embolden: f32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FinishMode { + Present, + Capture, +} + impl Renderer { - #[cfg(feature = "skia")] - const ACTIVE_RENDERER_NAME: &str = "SkiaRenderer"; - #[cfg(all(feature = "vello", not(feature = "skia")))] + #[cfg(feature = "active-vello")] const ACTIVE_RENDERER_NAME: &str = "VelloRenderer"; - #[cfg(all(not(feature = "vello"), not(feature = "skia")))] + #[cfg(feature = "active-vger")] const ACTIVE_RENDERER_NAME: &str = "VgerRenderer"; - - #[cfg(all(not(feature = "vello"), not(feature = "skia")))] + #[cfg(feature = "active-vello-hybrid")] + const ACTIVE_RENDERER_NAME: &str = "VelloHybridRenderer"; + #[cfg(feature = "active-vello-cpu")] + const ACTIVE_RENDERER_NAME: &str = "VelloCpuRenderer"; + #[cfg(feature = "active-tiny-skia")] + const ACTIVE_RENDERER_NAME: &str = "TinySkiaRenderer"; + + #[cfg(feature = "active-vger")] const ACTIVE_IS_VGER: bool = true; - #[cfg(any(feature = "vello", feature = "skia"))] + #[cfg(not(feature = "active-vger"))] const ACTIVE_IS_VGER: bool = false; - #[cfg(feature = "skia")] - pub fn new_skia(window: Arc, 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") - .ok() - .map(|val| val.as_str() == "1") - .unwrap_or(false); - - if !force_tiny_skia { - match ActiveRenderer::new( - window.clone(), - size.width as u32, - size.height as u32, - scale, - font_embolden, - ) { - Ok(renderer) => return Self::Active(renderer), - Err(err) => panic!("Failed to create {}: {err}", Self::ACTIVE_RENDERER_NAME), - } - } - - match TinySkiaRenderer::new( - window, - size.width as u32, - size.height as u32, - scale, - font_embolden, - ) { - Ok(tiny_skia) => Self::TinySkia(tiny_skia), - Err(err) => panic!("Failed to create TinySkiaRenderer: {err}"), - } - } - pub(crate) fn is_vger(&self) -> bool { Self::ACTIVE_IS_VGER && matches!(self, Renderer::Active(_)) } - #[cfg(all(feature = "vello", not(feature = "skia")))] + #[cfg(any(feature = "active-vello", feature = "active-vger"))] fn try_new_active( gpu_resources: GpuResources, surface: wgpu::Surface<'static>, @@ -131,24 +355,88 @@ impl Renderer { height: u32, scale: f64, font_embolden: f32, - ) -> Result { - ActiveRenderer::new(gpu_resources, surface, width, height, scale, font_embolden) - .map_err(|err| err.to_string()) + ) -> Result { + let presenter = GpuWindowPresenter::new(&gpu_resources, surface, width, height)?; + let renderer = ActiveRenderer::new(gpu_resources, width, height, scale, font_embolden) + .map_err(|err| err.to_string())?; + Ok(ActiveState { + renderer, + presenter, + size: Size::new(width as f64, height as f64), + }) } - #[cfg(all(not(feature = "vello"), not(feature = "skia")))] + #[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + ))] fn try_new_active( - gpu_resources: GpuResources, - surface: wgpu::Surface<'static>, + window: Arc, width: u32, height: u32, scale: f64, font_embolden: f32, - ) -> Result { - ActiveRenderer::new(gpu_resources, surface, width, height, scale, font_embolden) - .map_err(|err| err.to_string()) + ) -> Result { + let presenter = CpuImagePresenter::new(window, width, height)?; + #[cfg(feature = "active-tiny-skia")] + let renderer = ActiveRenderer::new(width, height, scale, font_embolden) + .map_err(|err| err.to_string())?; + #[cfg(any(feature = "active-vello-hybrid", feature = "active-vello-cpu"))] + let renderer = ActiveRenderer::new(width, height).map_err(|err| err.to_string())?; + Ok(ActiveState { + renderer, + presenter, + size: Size::new(width as f64, height as f64), + }) } + #[allow(unused_variables)] + fn try_new_cpu_fallback( + window: Arc, + width: u32, + height: u32, + scale: f64, + font_embolden: f32, + ) -> Result { + let presenter = CpuImagePresenter::new(window, width, height)?; + #[cfg(feature = "fallback-tiny-skia")] + let renderer = CpuFallbackRenderer::new(width, height, scale, font_embolden) + .map_err(|err| err.to_string())?; + #[cfg(feature = "fallback-vello-cpu")] + let renderer = CpuFallbackRenderer::new(width, height).map_err(|err| err.to_string())?; + Ok(CpuFallbackState { + renderer, + presenter, + size: Size::new(width as f64, height as f64), + }) + } + + #[cfg(feature = "fallback-vello-cpu")] + fn finish_cpu_renderer(renderer: &mut CpuFallbackRenderer, size: Size) -> Option { + let data = renderer.finish_rgba8().ok()?; + Some(ImageData { + data: peniko::Blob::new(std::sync::Arc::new(data)), + format: peniko::ImageFormat::Rgba8, + alpha_type: peniko::ImageAlphaType::Alpha, + width: size.width as u32, + height: size.height as u32, + }) + } + + #[cfg(any(feature = "active-vello-hybrid", feature = "active-vello-cpu"))] + fn finish_active_cpu_renderer(renderer: &mut ActiveRenderer, size: Size) -> Option { + let data = renderer.finish_rgba8().ok()?; + Some(ImageData { + data: peniko::Blob::new(std::sync::Arc::new(data)), + format: peniko::ImageFormat::Rgba8, + alpha_type: peniko::ImageAlphaType::Alpha, + width: size.width as u32, + height: size.height as u32, + }) + } + + #[cfg(any(feature = "active-vello", feature = "active-vger"))] #[allow(unused_variables)] pub fn new( window: Arc, @@ -160,13 +448,18 @@ impl Renderer { ) -> 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") + let force_cpu = std::env::var("FLOEM_FORCE_CPU") .ok() .map(|val| val.as_str() == "1") + .or_else(|| { + std::env::var("FLOEM_FORCE_TINY_SKIA") + .ok() + .map(|val| val.as_str() == "1") + }) .unwrap_or(false); - #[cfg(not(feature = "skia"))] - let active_err = if !force_tiny_skia { + #[cfg(any(feature = "active-vello", feature = "active-vger"))] + let active_err = if !force_cpu { match Self::try_new_active( gpu_resources, surface, @@ -182,261 +475,256 @@ impl Renderer { None }; - let tiny_skia_err = match TinySkiaRenderer::new( + let cpu_fallback_err = match Self::try_new_cpu_fallback( window, size.width as u32, size.height as u32, scale, font_embolden, ) { - Ok(tiny_skia) => return Self::TinySkia(tiny_skia), + Ok(state) => return Self::CpuFallback(state), Err(err) => err, }; - if !force_tiny_skia { - #[cfg(feature = "skia")] - panic!("Failed to create TinySkiaRenderer: {tiny_skia_err}"); - - #[cfg(not(feature = "skia"))] + if !force_cpu { panic!( - "Failed to create {}: {}\nFailed to create TinySkiaRenderer: {tiny_skia_err}", + "Failed to create {}: {}\nFailed to create CPU fallback renderer: {cpu_fallback_err}", Self::ACTIVE_RENDERER_NAME, active_err.unwrap() ); } else { - panic!("Failed to create TinySkiaRenderer: {tiny_skia_err}"); + panic!("Failed to create CPU fallback renderer: {cpu_fallback_err}"); } } - pub fn resize(&mut self, scale: f64, size: Size) { + #[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + ))] + pub fn new_cpu(window: Arc, scale: f64, size: Size, font_embolden: f32) -> Self { let size = Size::new(size.width.max(1.0), size.height.max(1.0)); - match self { - Renderer::Active(r) => r.resize(size.width as u32, size.height as u32, scale), - Renderer::TinySkia(r) => r.resize(size.width as u32, size.height as u32, scale), - Renderer::Uninitialized { .. } => {} - } - } - pub fn set_scale(&mut self, scale: f64) { - match self { - Renderer::Active(r) => r.set_scale(scale), - Renderer::TinySkia(r) => r.set_scale(scale), - Renderer::Uninitialized { .. } => {} - } - } + let active_err = match Self::try_new_active( + window.clone(), + size.width as u32, + size.height as u32, + scale, + font_embolden, + ) { + Ok(renderer) => return Self::Active(renderer), + Err(err) => Some(err), + }; - pub fn size(&self) -> Size { - match self { - Renderer::Active(r) => r.size(), - Renderer::TinySkia(r) => r.size(), - Renderer::Uninitialized { size } => *size, - } + let cpu_fallback_err = match Self::try_new_cpu_fallback( + window, + size.width as u32, + size.height as u32, + scale, + font_embolden, + ) { + Ok(state) => return Self::CpuFallback(state), + Err(err) => err, + }; + + panic!( + "Failed to create {}: {}\nFailed to create CPU fallback renderer: {cpu_fallback_err}", + Self::ACTIVE_RENDERER_NAME, + active_err.unwrap() + ); } pub(crate) fn debug_info(&self) -> String { - use crate::Renderer; - match self { - Self::Active(r) => r.debug_info(), - Self::TinySkia(r) => r.debug_info(), + Self::Active(state) => state.renderer.debug_info(), + Self::CpuFallback(state) => state.renderer.debug_info(), Self::Uninitialized { .. } => "Uninitialized".to_string(), } } - #[cfg(all(feature = "vello", not(feature = "skia")))] - pub(crate) fn render_scene_to_premultiplied_texture_view( - &mut self, - target_view: &wgpu::TextureView, - target_format: wgpu::TextureFormat, - size: (u32, u32), - ) -> bool { - match self { - Renderer::Active(r) => r - .render_scene_to_premultiplied_texture_view( - target_view, - target_format, - size.0, - size.1, - ) - .is_ok(), - Renderer::TinySkia(_) | Renderer::Uninitialized { .. } => false, - } - } - - #[cfg(not(all(feature = "vello", not(feature = "skia"))))] - pub(crate) fn render_scene_to_premultiplied_texture_view( - &mut self, - _target_view: &wgpu::TextureView, - _target_format: wgpu::TextureFormat, - _size: (u32, u32), - ) -> bool { - false - } - - #[cfg(all(feature = "vello", not(feature = "skia")))] - pub(crate) fn present_composited_output( - &mut self, - composite: impl FnOnce(&wgpu::TextureView), - ) -> bool { + pub(crate) fn begin(&mut self, frame: BeginFrame) { + let size = Size::new(frame.size.width.max(1.0), frame.size.height.max(1.0)); + let width = size.width as u32; + let height = size.height as u32; match self { - Renderer::Active(r) => r.present_composited_output(composite), - Renderer::TinySkia(_) | Renderer::Uninitialized { .. } => false, - } - } - - #[cfg(not(all(feature = "vello", not(feature = "skia"))))] - pub(crate) fn present_composited_output( - &mut self, - _composite: impl FnOnce(&wgpu::TextureView), - ) -> bool { - false - } -} - -impl floem_renderer::Renderer for Renderer { - fn begin(&mut self, capture: bool) { - match self { - Renderer::Active(r) => { - r.begin(capture); + Renderer::Active(state) => { + let size_changed = state.size != size; + if size_changed { + state.presenter.resize(width, height); + state.size = size; + } + #[cfg(any( + feature = "active-vello", + feature = "active-vger", + feature = "active-tiny-skia" + ))] + state + .renderer + .begin(width, height, frame.scale, frame.font_embolden); + #[cfg(any(feature = "active-vello-hybrid", feature = "active-vello-cpu"))] + { + if size_changed { + state.renderer = ActiveRenderer::new(width, height).unwrap_or_else(|err| { + panic!("Failed to recreate {}: {err}", Self::ACTIVE_RENDERER_NAME) + }); + } + state.renderer.reset(); + } } - Renderer::TinySkia(r) => { - r.begin(capture); + Renderer::CpuFallback(state) => { + let size_changed = state.size != size; + if size_changed { + state.presenter.resize(width, height); + state.size = size; + } + #[cfg(feature = "fallback-tiny-skia")] + state + .renderer + .begin(width, height, frame.scale, frame.font_embolden); + #[cfg(feature = "fallback-vello-cpu")] + { + if size_changed { + state.renderer = + CpuFallbackRenderer::new(width, height).unwrap_or_else(|err| { + panic!("Failed to recreate CPU fallback renderer: {err}") + }); + } + state.renderer.reset(); + } } Renderer::Uninitialized { .. } => {} } } - fn clip(&mut self, shape: &impl Shape) { + pub(crate) fn finish(&mut self, mode: FinishMode) -> Option { match self { - Renderer::Active(v) => { - v.clip(shape); + #[cfg(any(feature = "active-vello", feature = "active-vger"))] + Renderer::Active(state) => match state.renderer.finish(mode == FinishMode::Capture) { + Some(RenderOutput::Image(image)) => Some(image), + Some(RenderOutput::GpuTexture(output)) => { + if mode == FinishMode::Present { + state.presenter.present(&output); + None + } else { + None + } + } + None => None, + }, + #[cfg(any( + feature = "active-vello-hybrid", + feature = "active-vello-cpu", + feature = "active-tiny-skia" + ))] + Renderer::Active(state) => { + #[cfg(feature = "active-tiny-skia")] + let image = state.renderer.finish(); + #[cfg(any(feature = "active-vello-hybrid", feature = "active-vello-cpu"))] + let image = Self::finish_active_cpu_renderer(&mut state.renderer, state.size); + if mode == FinishMode::Present + && let Some(image) = image.as_ref() + { + state.presenter.present(image); + return None; + } + image } - Renderer::TinySkia(v) => { - v.clip(shape); + Renderer::CpuFallback(state) => { + #[cfg(feature = "fallback-tiny-skia")] + let image = state.renderer.finish(); + #[cfg(feature = "fallback-vello-cpu")] + let image = Self::finish_cpu_renderer(&mut state.renderer, state.size); + if mode == FinishMode::Present + && let Some(image) = image.as_ref() + { + state.presenter.present(image); + return None; + } + image } - Renderer::Uninitialized { .. } => {} + Renderer::Uninitialized { .. } => None, } } +} - fn clear_clip(&mut self) { +impl PaintSink for Renderer { + fn push_clip(&mut self, clip: ClipRef<'_>) { match self { - Renderer::Active(v) => { - v.clear_clip(); - } - Renderer::TinySkia(v) => { - v.clear_clip(); - } + Renderer::Active(state) => PaintSink::push_clip(&mut state.renderer, clip), + Renderer::CpuFallback(state) => PaintSink::push_clip(&mut state.renderer, clip), Renderer::Uninitialized { .. } => {} } } - fn stroke<'b, 's>( - &mut self, - shape: &impl Shape, - brush: impl Into>, - stroke: &'s Stroke, - ) { + fn pop_clip(&mut self) { match self { - Renderer::Active(v) => { - v.stroke(shape, brush, stroke); - } - Renderer::TinySkia(v) => { - v.stroke(shape, brush, stroke); - } + Renderer::Active(state) => PaintSink::pop_clip(&mut state.renderer), + Renderer::CpuFallback(state) => PaintSink::pop_clip(&mut state.renderer), Renderer::Uninitialized { .. } => {} } } - fn fill<'b>( - &mut self, - path: &impl peniko::kurbo::Shape, - brush: impl Into>, - blur_radius: f64, - ) { + fn push_group(&mut self, group: GroupRef<'_>) { match self { - Renderer::Active(v) => { - v.fill(path, brush, blur_radius); - } - Renderer::TinySkia(v) => { - v.fill(path, brush, blur_radius); - } + Renderer::Active(state) => PaintSink::push_group(&mut state.renderer, group), + Renderer::CpuFallback(state) => PaintSink::push_group(&mut state.renderer, group), Renderer::Uninitialized { .. } => {} } } - fn push_layer( - &mut self, - blend: impl Into, - alpha: f32, - transform: Affine, - clip: &impl Shape, - ) { + fn pop_group(&mut self) { match self { - Renderer::Active(v) => { - v.push_layer(blend, alpha, transform, clip); - } - Renderer::TinySkia(v) => v.push_layer(blend, alpha, transform, clip), + Renderer::Active(state) => PaintSink::pop_group(&mut state.renderer), + Renderer::CpuFallback(state) => PaintSink::pop_group(&mut state.renderer), Renderer::Uninitialized { .. } => {} } } - fn pop_layer(&mut self) { + fn fill(&mut self, draw: FillRef<'_>) { match self { - Renderer::Active(v) => { - v.pop_layer(); - } - Renderer::TinySkia(v) => v.pop_layer(), + Renderer::Active(state) => PaintSink::fill(&mut state.renderer, draw), + Renderer::CpuFallback(state) => PaintSink::fill(&mut state.renderer, draw), Renderer::Uninitialized { .. } => {} } } - fn draw_glyphs<'a>( - &mut self, - origin: Point, - run: &floem_renderer::text::GlyphRunRef<'a>, - glyphs: impl Iterator + 'a, - ) { + fn stroke(&mut self, draw: StrokeRef<'_>) { match self { - Renderer::Active(v) => v.draw_glyphs(origin, run, glyphs), - Renderer::TinySkia(v) => v.draw_glyphs(origin, run, glyphs), + Renderer::Active(state) => PaintSink::stroke(&mut state.renderer, draw), + Renderer::CpuFallback(state) => PaintSink::stroke(&mut state.renderer, draw), Renderer::Uninitialized { .. } => {} } } - fn draw_svg<'b>( + fn glyph_run( &mut self, - svg: floem_renderer::Svg<'b>, - rect: Rect, - brush: Option>>, + draw: GlyphRunRef<'_>, + glyphs: &mut dyn Iterator, ) { match self { - Renderer::Active(v) => { - v.draw_svg(svg, rect, brush); - } - Renderer::TinySkia(v) => { - v.draw_svg(svg, rect, brush); - } + Renderer::Active(state) => PaintSink::glyph_run(&mut state.renderer, draw, glyphs), + Renderer::CpuFallback(state) => PaintSink::glyph_run(&mut state.renderer, draw, glyphs), Renderer::Uninitialized { .. } => {} } } - fn set_transform(&mut self, transform: Affine) { + fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { match self { - Renderer::Active(v) => { - v.set_transform(transform); - } - Renderer::TinySkia(v) => { - v.set_transform(transform); + Renderer::Active(state) => PaintSink::blurred_rounded_rect(&mut state.renderer, draw), + Renderer::CpuFallback(state) => { + PaintSink::blurred_rounded_rect(&mut state.renderer, draw) } Renderer::Uninitialized { .. } => {} } } +} - fn finish(&mut self) -> Option { +impl CustomPaintSink for Renderer { + fn custom(&mut self, command: &DisplayCommandExt) { match self { - Renderer::Active(r) => r.finish(), - Renderer::TinySkia(r) => r.finish(), - Renderer::Uninitialized { .. } => None, + Renderer::Active(state) => CustomPaintSink::custom(&mut state.renderer, command), + Renderer::CpuFallback(state) => CustomPaintSink::custom(&mut state.renderer, command), + Renderer::Uninitialized { .. } => {} } } } diff --git a/src/view/mod.rs b/src/view/mod.rs index ef30dc211..3b7804c94 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1296,7 +1296,11 @@ pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, rect: Rect // Render current path with previous stroke if any if !current_path.is_empty() { cx.painter - .stroke(BezPath::from_vec(current_path.clone()), &stroke.0, &outline_color) + .stroke( + BezPath::from_vec(current_path.clone()), + &stroke.0, + &outline_color, + ) .draw(); current_path.clear(); } @@ -1471,7 +1475,11 @@ pub(crate) fn paint_border( // Render current path with previous stroke if any if !current_path.is_empty() && stroke.0.width > 0. { cx.painter - .stroke(BezPath::from_vec(current_path.to_vec()), &stroke.0, &stroke.1) + .stroke( + BezPath::from_vec(current_path.to_vec()), + &stroke.0, + &stroke.1, + ) .draw(); current_path.clear(); } else if stroke.0.width == 0. { diff --git a/src/views/editor/gutter.rs b/src/views/editor/gutter.rs index 3dcb0bd72..b9c583723 100644 --- a/src/views/editor/gutter.rs +++ b/src/views/editor/gutter.rs @@ -12,9 +12,9 @@ use crate::{ }; use floem_editor_core::{cursor::CursorMode, mode::Mode}; use floem_reactive::{RwSignal, SignalGet, SignalWith}; -use peniko::{Brush, Color}; use peniko::color::palette; use peniko::kurbo::Rect; +use peniko::{Brush, Color}; use super::{CurrentLineColor, Editor}; diff --git a/src/views/editor/view.rs b/src/views/editor/view.rs index b52c5b82f..f1b9fc4fc 100644 --- a/src/views/editor/view.rs +++ b/src/views/editor/view.rs @@ -1045,6 +1045,7 @@ impl View for EditorView { ); if editor.viewport.with_untracked(|v| v != &viewport) { editor.viewport.set(viewport); + cx.window_state.request_paint(self.id()); } } if LayoutChangedListener::extract(&cx.event).is_some() { diff --git a/src/views/resizable.rs b/src/views/resizable.rs index e8411bd4f..a33bdbf94 100644 --- a/src/views/resizable.rs +++ b/src/views/resizable.rs @@ -360,7 +360,9 @@ impl Handle { } }; - cx.painter.fill(paint_rect, &self.handle_style.color()).draw(); + cx.painter + .fill(paint_rect, &self.handle_style.color()) + .draw(); } } diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 86c427d18..5333d8852 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -288,7 +288,10 @@ impl ScrollHandle { let rounded_rect = rect_with_border.to_rounded_rect(radius); cx.painter - .fill(rounded_rect, &self.style.background().unwrap_or(HANDLE_COLOR)) + .fill( + rounded_rect, + &self.style.background().unwrap_or(HANDLE_COLOR), + ) .draw(); if edge_width > 0.0 diff --git a/src/window/handle.rs b/src/window/handle.rs index 1e294e0bb..a5df1add2 100644 --- a/src/window/handle.rs +++ b/src/window/handle.rs @@ -24,12 +24,10 @@ use winit::window::{ }; use floem_reactive::{RwSignal, Scope, SignalGet, SignalUpdate}; -use floem_renderer::Renderer; use floem_renderer::gpu_resources::GpuResources; -use peniko::BlendMode; +use imaging::{FillRef, PaintSink}; use peniko::color::palette; use peniko::kurbo::{self, Point, Size}; -use rustc_hash::FxHashSet; use winit::{ cursor::CursorIcon, dpi::{LogicalPosition, LogicalSize}, @@ -85,6 +83,7 @@ pub(crate) struct WindowHandle { size: RwSignal, default_theme: Option