diff --git a/crates/core/src/elements/rect.rs b/crates/core/src/elements/rect.rs index 55bc235c0..56cd9d6a1 100644 --- a/crates/core/src/elements/rect.rs +++ b/crates/core/src/elements/rect.rs @@ -9,6 +9,7 @@ use freya_node_state::{ ReferencesState, ShadowPosition, StyleState, + TransformState, }; use torin::{ prelude::{ @@ -337,6 +338,70 @@ impl ElementUtils for RectElement { path.add_rrect(rounded_rect, None); } + // This is only done to dupe the borrow checker since [`SaveLayerRec::backdrop`] + // takes an [`ImageFilter`] by reference and it'd be otherwise dropped in the + // if-statement below. + let blur_filter = blur( + ( + node_style.backdrop_blur * scale_factor, + node_style.backdrop_blur * scale_factor, + ), + None, + None, + rounded_rect.rect(), + ) + .unwrap(); + + // If we have a backdrop blur applied, we need to draw that by creating a new + // layer, clipping it to the rect's roundness, then blurring behind it before + // drawing the rect's initial background box. + if node_style.backdrop_blur != 0.0 { + // If we can guarantee that the node entirely draws over it's backdrop, + // we can avoid this whole (possibly intense) process, since the node's + // backdrop is never visible. + // + // There's probably more that can be done in this area, like checking individual gradient + // stops but I'm not completely sure of the diminishing returns here. + // + // Currently we verify the following: + // - Node has a single solid color background. + // - The background has 100% opacity. + // - The node has no parents with the `opacity` attribute applied. + let is_possibly_translucent = if let Fill::Color(color) = node_style.background { + let node_transform = &*node_ref.get::().unwrap(); + + color.a() != u8::MAX + || node_style.blend_mode.is_some() + || !node_transform.opacities.is_empty() + } else { + true + }; + + if is_possibly_translucent { + let layer_rec = SaveLayerRec::default() + .bounds(rounded_rect.rect()) + .backdrop(&blur_filter); + + // Depending on if the rect is rounded or not, we might need to clip the blur + // layer to the shape of the rounded rect. + if corner_radius.bottom_left == 0.0 + && corner_radius.bottom_right == 0.0 + && corner_radius.top_left == 0.0 + && corner_radius.top_right == 0.0 + { + canvas.save_layer(&layer_rec); + canvas.restore(); + } else { + canvas.save(); + canvas.clip_rrect(rounded_rect, ClipOp::Intersect, true); + canvas.save_layer(&layer_rec); + canvas.restore(); + canvas.restore(); + } + } + } + + // Paint the rect's background. canvas.draw_path(&path, &paint); // Shadows diff --git a/crates/core/src/render/pipeline.rs b/crates/core/src/render/pipeline.rs index a477aaabb..e6628ab35 100644 --- a/crates/core/src/render/pipeline.rs +++ b/crates/core/src/render/pipeline.rs @@ -8,9 +8,11 @@ use freya_engine::prelude::{ FontCollection, FontMgr, Matrix, + Paint, Point, Rect, SamplingOptions, + SaveLayerRec, Surface, }; use freya_native_core::{ @@ -23,6 +25,7 @@ use freya_native_core::{ NodeId, }; use freya_node_state::{ + StyleState, TransformState, ViewportState, }; @@ -193,6 +196,7 @@ impl RenderPipeline<'_> { ) { let dirty_canvas = self.dirty_surface.canvas(); let area = layout_node.visible_area(); + let rect = Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()); let node_type = &*node_ref.node_type(); if let NodeType::Element(ElementNode { tag, .. }) = node_type { let Some(element_utils) = tag.utils() else { @@ -201,6 +205,7 @@ impl RenderPipeline<'_> { let initial_layer = dirty_canvas.save(); let node_transform = &*node_ref.get::().unwrap(); + let node_style = &*node_ref.get::().unwrap(); // Pass rotate effect to children for (id, rotate_degs) in &node_transform.rotations { @@ -217,17 +222,18 @@ impl RenderPipeline<'_> { dirty_canvas.concat(&matrix); } + // Apply blend mode + if let Some(blend) = node_style.blend_mode { + let mut paint = Paint::default(); + paint.set_blend_mode(blend); + + let layer_rec = SaveLayerRec::default().bounds(&rect).paint(&paint); + dirty_canvas.save_layer(&layer_rec); + } + // Apply inherited opacity effects for opacity in &node_transform.opacities { - dirty_canvas.save_layer_alpha_f( - Rect::new( - self.canvas_area.min_x(), - self.canvas_area.min_y(), - self.canvas_area.max_x(), - self.canvas_area.max_y(), - ), - *opacity, - ); + dirty_canvas.save_layer_alpha_f(rect, *opacity); } // Clip all elements with their corresponding viewports diff --git a/crates/elements/src/definitions.rs b/crates/elements/src/definitions.rs index ad5c9bdcb..bdafe63d7 100644 --- a/crates/elements/src/definitions.rs +++ b/crates/elements/src/definitions.rs @@ -231,6 +231,10 @@ builder_constructors! { line_height: String, #[doc = include_str!("_docs/attributes/spacing.md")] spacing: String, + // #[doc = include_str!("_docs/attributes/blend_mode.md")] + blend_mode: String, + // #[doc = include_str!("_docs/attributes/blend_mode.md")] + backdrop_blur: String, a11y_auto_focus: String, a11y_name: String, @@ -303,6 +307,8 @@ builder_constructors! { margin: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, + // #[doc = include_str!("_docs/attributes/blend_mode.md")] + blend_mode: String, layer: String, a11y_auto_focus: String, @@ -377,6 +383,8 @@ builder_constructors! { margin: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, + // #[doc = include_str!("_docs/attributes/blend_mode.md")] + blend_mode: String, layer: String, cursor_index: String, @@ -453,6 +461,8 @@ builder_constructors! { rotate: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, + // #[doc = include_str!("_docs/attributes/blend_mode.md")] + blend_mode: String, image_data: String, image_reference: String, @@ -492,6 +502,8 @@ builder_constructors! { rotate: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, + // #[doc = include_str!("_docs/attributes/blend_mode.md")] + blend_mode: String, svg_data: String, svg_content: String, diff --git a/crates/engine/src/mocked.rs b/crates/engine/src/mocked.rs index cfeb00df0..0108eb103 100644 --- a/crates/engine/src/mocked.rs +++ b/crates/engine/src/mocked.rs @@ -1091,6 +1091,41 @@ impl Canvas { #[derive(Default)] pub struct SamplingOptions; +#[repr(C)] +#[derive(Default)] +pub struct SaveLayerRec<'_>; + +impl<'a> SaveLayerRec<'a> { + pub fn bounds(mut self, bounds: &'a Rect) -> Self { + unimplemented!("This is mocked") + } + + pub fn paint(mut self, paint: &'a Paint) -> Self { + unimplemented!("This is mocked") + } + + pub fn backdrop(mut self, backdrop: &'a ImageFilter) -> Self { + unimplemented!("This is mocked") + } + + pub fn color_space(mut self, color_space: &'a ColorSpace) -> Self { + unimplemented!("This is mocked") + } + + pub fn flags(mut self, flags: SaveLayerFlags) -> Self { + unimplemented!("This is mocked") + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SaveLayerFlags: u32 { + const PRESERVE_LCD_TEXT = sb::SkCanvas_SaveLayerFlagsSet_kPreserveLCDText_SaveLayerFlag as _; + const INIT_WITH_PREVIOUS = sb::SkCanvas_SaveLayerFlagsSet_kInitWithPrevious_SaveLayerFlag as _; + const F16_COLOR_TYPE = sb::SkCanvas_SaveLayerFlagsSet_kF16ColorType as _; + } +} + #[repr(i32)] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Default)] pub enum RectHeightStyle { @@ -1741,3 +1776,37 @@ pub enum EncodedImageFormat { AVIF = 12, JPEGXL = 13, } + +#[repr(i32)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum BlendMode { + Clear = 0, + Src = 1, + Dst = 2, + SrcOver = 3, + DstOver = 4, + SrcIn = 5, + DstIn = 6, + SrcOut = 7, + DstOut = 8, + SrcATop = 9, + DstATop = 10, + Xor = 11, + Plus = 12, + Modulate = 13, + Screen = 14, + Overlay = 15, + Darken = 16, + Lighten = 17, + ColorDodge = 18, + ColorBurn = 19, + HardLight = 20, + SoftLight = 21, + Difference = 22, + Exclusion = 23, + Multiply = 24, + Hue = 25, + Saturation = 26, + Color = 27, + Luminosity = 28, +} diff --git a/crates/engine/src/skia.rs b/crates/engine/src/skia.rs index c9c73a9b5..e7cd5abed 100644 --- a/crates/engine/src/skia.rs +++ b/crates/engine/src/skia.rs @@ -1,4 +1,8 @@ pub use skia_safe::{ + canvas::{ + SaveLayerFlags, + SaveLayerRec, + }, font_style::{ Slant, Weight, @@ -23,6 +27,7 @@ pub use skia_safe::{ set_resource_cache_single_allocation_byte_limit, set_resource_cache_total_bytes_limit, }, + image_filters::blur, path::ArcSize, rrect::Corner, runtime_effect::Uniform, @@ -56,6 +61,7 @@ pub use skia_safe::{ TypefaceFontProvider, }, Bitmap, + BlendMode, BlurStyle, Canvas, ClipOp, @@ -71,6 +77,7 @@ pub use skia_safe::{ IPoint, IRect, Image, + ImageFilter, ImageInfo, MaskFilter, Matrix, diff --git a/crates/native-core/src/attributes.rs b/crates/native-core/src/attributes.rs index f91f42fdc..07a427efc 100644 --- a/crates/native-core/src/attributes.rs +++ b/crates/native-core/src/attributes.rs @@ -67,6 +67,8 @@ pub enum AttributeName { SvgData, SvgContent, Spacing, + BlendMode, + BackdropBlur, } impl FromStr for AttributeName { @@ -139,6 +141,8 @@ impl FromStr for AttributeName { "svg_data" => Ok(AttributeName::SvgData), "svg_content" => Ok(AttributeName::SvgContent), "spacing" => Ok(AttributeName::Spacing), + "blend_mode" => Ok(AttributeName::BlendMode), + "backdrop_blur" => Ok(AttributeName::BackdropBlur), _ => Err(format!("{attr} not supported.")), } } diff --git a/crates/state/src/style.rs b/crates/state/src/style.rs index 978a57162..05832969a 100644 --- a/crates/state/src/style.rs +++ b/crates/state/src/style.rs @@ -4,6 +4,7 @@ use std::sync::{ }; use freya_common::CompositorDirtyNodes; +use freya_engine::prelude::BlendMode; use freya_native_core::{ attributes::AttributeName, exports::shipyard::Component, @@ -42,6 +43,8 @@ pub struct StyleState { pub image_data: Option, pub svg_data: Option, pub overflow: OverflowMode, + pub blend_mode: Option, + pub backdrop_blur: f32, } impl ParseAttribute for StyleState { @@ -114,6 +117,16 @@ impl ParseAttribute for StyleState { self.overflow = OverflowMode::parse(value)?; } } + AttributeName::BlendMode => { + if let Some(value) = attr.value.as_text() { + self.blend_mode = Some(BlendMode::parse(value)?); + } + } + AttributeName::BackdropBlur => { + if let Some(value) = attr.value.as_text() { + self.backdrop_blur = value.parse::().map_err(|_| ParseError)?; + } + } _ => {} } @@ -141,6 +154,8 @@ impl State for StyleState { AttributeName::SvgData, AttributeName::SvgContent, AttributeName::Overflow, + AttributeName::BackdropBlur, + AttributeName::BlendMode, ])); fn update<'a>( diff --git a/crates/state/src/values/blend_mode.rs b/crates/state/src/values/blend_mode.rs new file mode 100644 index 000000000..7712820e9 --- /dev/null +++ b/crates/state/src/values/blend_mode.rs @@ -0,0 +1,43 @@ +use freya_engine::prelude::*; + +use crate::{ + Parse, + ParseError, +}; + +impl Parse for BlendMode { + fn parse(value: &str) -> Result { + Ok(match value { + "clear" => Self::Clear, + "src" => Self::Src, + "dst" => Self::Dst, + "src-over" => Self::SrcOver, + "dst-over" => Self::DstOver, + "src-in" => Self::SrcIn, + "dst-in" => Self::DstIn, + "src-out" => Self::SrcOut, + "dst-out" => Self::DstOut, + "src-a-top" => Self::SrcATop, + "dst-a-top" => Self::DstATop, + "xor" => Self::Xor, + "plus" => Self::Plus, + "modulate" => Self::Modulate, + "screen" => Self::Screen, + "overlay" => Self::Overlay, + "darken" => Self::Darken, + "lighten" => Self::Lighten, + "color-dodge" => Self::ColorDodge, + "color-burn" => Self::ColorBurn, + "hard-light" => Self::HardLight, + "soft-light" => Self::SoftLight, + "difference" => Self::Difference, + "exclusion" => Self::Exclusion, + "multiply" => Self::Multiply, + "hue" => Self::Hue, + "saturation" => Self::Saturation, + "color" => Self::Color, + "luminosity" => Self::Luminosity, + _ => Err(ParseError)?, + }) + } +} diff --git a/crates/state/src/values/mod.rs b/crates/state/src/values/mod.rs index 4f2b435e4..fc5d2e8b0 100644 --- a/crates/state/src/values/mod.rs +++ b/crates/state/src/values/mod.rs @@ -1,4 +1,5 @@ mod alignment; +mod blend_mode; mod border; mod color; mod content; diff --git a/examples/backdrop_blur.rs b/examples/backdrop_blur.rs new file mode 100644 index 000000000..3d1e7d229 --- /dev/null +++ b/examples/backdrop_blur.rs @@ -0,0 +1,136 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch_with_props(app, "iOS-Styled Notifications", (322.5, 699.0)); +} + +static WALLPAPER: &[u8] = include_bytes!("./wallpaper.png"); + +#[component] +fn Notification(title: String, description: String, time: String) -> Element { + rsx!( + rect { + layer: "-9999", + width: "fill", + margin: "8", + corner_radius: "24", + background: "hsl(0deg, 0%, 65%, 70%)", + backdrop_blur: "150", + + rect { + direction: "vertical", + main_align: "center", + layer: "-9999", + width: "100%", + height: "auto", + padding: "14", + corner_radius: "24", + blend_mode: "color-dodge", + background: "#333333", + font_size: "15", + line_height: "1.3333", + color: "#000000", + + rect { + direction: "horizontal", + corner_radius: "9", + spacing: "10", + padding: "0 8 0 0", + + rect { + width: "38", + height: "38", + corner_radius: "8", + corner_smoothing: "60%", + background: "#3D3D3D", + blend_mode: "overlay", + } + + rect { + direction: "vertical", + + rect { + width: "fill", + direction: "horizontal", + main_align: "space-between", + + label { + font_weight: "semi-bold", + + "{title}" + } + + rect { + rect { + position: "absolute", + position_top: "0", + position_left: "0", + + label { + font_weight: "normal", + color: "hsl(0deg, 0%, 50%, 50%)", + + "{time}" + } + } + + label { + font_weight: "normal", + color: "#3D3D3D", + blend_mode: "overlay", + layer: "-9999", + + "{time}" + } + } + } + + label { + font_weight: "normal", + + "{description}" + } + } + } + } + } + ) +} + +fn app() -> Element { + let image_data = static_bytes(WALLPAPER); + + rsx!( + rect { + width: "100%", + height: "100%", + background: "#f5f5f5", + + rect { + position: "absolute", + position_top: "0", + position_left: "0", + cross_align: "center", + width: "fill", + height: "fill", + + image { + image_data: image_data, + width: "fill", + height: "fill", + } + } + + Notification { + title: "Notification Title", + description: "Hello world!", + time: "9:42 AM", + } + } + ) +} diff --git a/examples/wallpaper.png b/examples/wallpaper.png new file mode 100644 index 000000000..dd6cc1b77 Binary files /dev/null and b/examples/wallpaper.png differ