Filter API + Blur (#40)

Co-authored-by: Newspicel <newspicel+claude@pm.me>
This commit is contained in:
Leonard Seibold
2026-06-14 10:18:25 -04:00
committed by GitHub
co-authored by Newspicel
parent cfec5ff014
commit 69d467e4d1
14 changed files with 3109 additions and 70 deletions
+30 -1
View File
@@ -28,6 +28,10 @@ where
root: Option<usize>,
/// Index of the leaf with the highest ordering (for fast-path lookups).
max_leaf: Option<usize>,
/// Minimum ordering assigned to any subsequent insert. Raised before painting deferred
/// draws so overlays always sort above the main scene (and their orders can't fall inside a
/// content-filter order range from the main scene). 0 means no floor.
order_floor: u32,
/// Reusable stack for tree traversal during insertion.
insert_path: Vec<usize>,
/// Reusable stack for search operations.
@@ -109,10 +113,34 @@ where
self.nodes.clear();
self.root = None;
self.max_leaf = None;
self.order_floor = 0;
self.insert_path.clear();
self.search_stack.clear();
}
/// Raise the minimum ordering for subsequent inserts to `floor`. Relative ordering above the
/// floor is preserved (overlapping inserts still step above one another).
pub fn set_order_floor(&mut self, floor: u32) {
self.order_floor = self.order_floor.max(floor);
}
/// The highest ordering assigned to any bounds so far (0 if empty).
pub fn max_order(&self) -> u32 {
self.max_leaf.map_or(0, |idx| self.nodes[idx].max_order)
}
/// Inserts bounds with an ordering strictly greater than *every* existing bounds (not just
/// intersecting ones), and returns that ordering. Used for content-filter group boundaries
/// (which must sort after all previously-painted content so their order range can't collide
/// with unrelated non-overlapping content that reuses low orderings) and to raise the order
/// floor before painting deferred draws (so overlays always sort above the main scene).
pub fn insert_above_all(&mut self, new_bounds: Bounds<U>) -> u32 {
let ordering = self.max_order() + 1;
let new_leaf_idx = self.insert_leaf(new_bounds, ordering);
self.max_leaf = Some(new_leaf_idx);
ordering
}
/// Inserts bounds into the tree and returns its assigned ordering.
///
/// The ordering is one greater than the maximum ordering of any
@@ -120,7 +148,7 @@ where
pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
// Find maximum ordering among intersecting bounds
let max_intersecting = self.find_max_ordering(&new_bounds);
let ordering = max_intersecting + 1;
let ordering = (max_intersecting + 1).max(self.order_floor);
// Insert the new leaf
let new_leaf_idx = self.insert_leaf(new_bounds, ordering);
@@ -365,6 +393,7 @@ where
nodes: Vec::new(),
root: None,
max_leaf: None,
order_floor: 0,
insert_path: Vec::new(),
search_stack: Vec::new(),
}
+349 -7
View File
@@ -6,8 +6,9 @@ use serde::{Deserialize, Serialize};
use crate::{
AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point,
Point, Radians, ScaledFilter, ScaledPixels, Size, bounds_tree::BoundsTree, point,
};
use smallvec::SmallVec;
use std::{
fmt::Debug,
iter::Peekable,
@@ -36,6 +37,8 @@ pub struct Scene {
pub subpixel_sprites: Vec<SubpixelSprite>,
pub polychrome_sprites: Vec<PolychromeSprite>,
pub surfaces: Vec<PaintSurface>,
pub backdrop_filters: Vec<BackdropFilter>,
pub filter_boundaries: Vec<FilterBoundary>,
}
#[expect(missing_docs)]
@@ -52,6 +55,8 @@ impl Scene {
self.subpixel_sprites.clear();
self.polychrome_sprites.clear();
self.surfaces.clear();
self.backdrop_filters.clear();
self.filter_boundaries.clear();
}
pub fn len(&self) -> usize {
@@ -70,21 +75,50 @@ impl Scene {
self.paint_operations.push(PaintOperation::EndLayer);
}
/// Raise the draw-order floor so every primitive inserted afterwards sorts above everything
/// inserted before. Called before painting deferred draws so overlays (tooltips, popovers,
/// drag images) sort above the main scene — and a deferred backdrop's order can't fall inside
/// a content-filter (`filter`) order range left behind by the main scene.
pub fn raise_order_floor(&mut self) {
let floor = self.primitive_bounds.max_order() + 1;
self.primitive_bounds.set_order_floor(floor);
}
pub fn insert_primitive(&mut self, primitive: impl Into<Primitive>) {
let mut primitive = primitive.into();
let clipped_bounds = primitive
.bounds()
.intersect(&primitive.content_mask().bounds);
if clipped_bounds.is_empty() {
// Content-filter boundaries must always be inserted as matched pairs — dropping one
// (e.g. for an empty clipped region) would orphan its partner and corrupt the renderer's
// target stack. Each marker takes an order strictly above ALL prior content, so the start
// sorts after everything painted before it and the element's own children (which overlap
// the marker bounds) sort strictly above the start. This keeps a marker's order range from
// colliding with unrelated non-overlapping content that reuses low orderings (e.g. a
// background grid), which would otherwise sweep that content into the group. Content
// painted *after* the group is held above it by raising the order floor when the end
// marker is inserted (see below) — otherwise a later non-overlapping sibling could reuse a
// low order that lands inside the start..end range and be swept into the group.
let is_filter_boundary = matches!(primitive, Primitive::FilterBoundary(_));
if clipped_bounds.is_empty() && !is_filter_boundary {
return;
}
let order = self
.layer_stack
.last()
.copied()
.unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds));
let order = if is_filter_boundary {
let order_bounds = if clipped_bounds.is_empty() {
*primitive.bounds()
} else {
clipped_bounds
};
self.primitive_bounds.insert_above_all(order_bounds)
} else {
self.layer_stack
.last()
.copied()
.unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds))
};
match &mut primitive {
Primitive::Shadow(shadow) => {
shadow.order = order;
@@ -119,6 +153,22 @@ impl Scene {
surface.order = order;
self.surfaces.push(surface.clone());
}
Primitive::BackdropFilter(filter) => {
filter.order = order;
self.backdrop_filters.push(filter.clone());
}
Primitive::FilterBoundary(boundary) => {
boundary.order = order;
if !boundary.is_start {
// A closed content-filter group is a draw-order barrier: everything painted
// afterwards must sort above the group's end marker so it can't fall back
// inside the group's order range (subsequent non-overlapping content otherwise
// reuses a low order). Mirrors the floor raised before deferred draws in
// `raise_order_floor`.
self.primitive_bounds.set_order_floor(order + 1);
}
self.filter_boundaries.push(boundary.clone());
}
}
self.paint_operations
.push(PaintOperation::Primitive(primitive));
@@ -146,6 +196,13 @@ impl Scene {
self.polychrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.surfaces.sort_by_key(|surface| surface.order);
self.backdrop_filters.sort_by_key(|filter| filter.order);
// Markers normally get distinct, monotonically-increasing orders (children overlap
// their group bounds and so sort strictly between the start and end). The `!is_start`
// tiebreak only matters for a degenerate empty group whose start and end tie: it keeps
// the start (false = 0) ahead of the end (true = 1) so the pair stays well-formed.
self.filter_boundaries
.sort_by_key(|boundary| (boundary.order, !boundary.is_start));
}
#[cfg_attr(
@@ -173,6 +230,10 @@ impl Scene {
polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
surfaces_start: 0,
surfaces_iter: self.surfaces.iter().peekable(),
backdrop_filters_start: 0,
backdrop_filters_iter: self.backdrop_filters.iter().peekable(),
filter_boundaries_start: 0,
filter_boundaries_iter: self.filter_boundaries.iter().peekable(),
}
}
}
@@ -186,6 +247,9 @@ impl Scene {
allow(dead_code)
)]
pub(crate) enum PrimitiveKind {
// Lowest discriminant: at an equal order, a content-filter group-start is emitted before
// the group's own content so the renderer redirects rendering before any child draws.
FilterBoundaryStart,
Shadow,
#[default]
Quad,
@@ -195,6 +259,10 @@ pub(crate) enum PrimitiveKind {
SubpixelSprite,
PolychromeSprite,
Surface,
BackdropFilter,
// Highest discriminant: at an equal order, a group-end is emitted after the group's content
// so the renderer composites the filtered group only once every child has been drawn.
FilterBoundaryEnd,
}
pub(crate) enum PaintOperation {
@@ -214,6 +282,8 @@ pub enum Primitive {
SubpixelSprite(SubpixelSprite),
PolychromeSprite(PolychromeSprite),
Surface(PaintSurface),
BackdropFilter(BackdropFilter),
FilterBoundary(FilterBoundary),
}
#[expect(missing_docs)]
@@ -228,6 +298,8 @@ impl Primitive {
Primitive::SubpixelSprite(sprite) => &sprite.bounds,
Primitive::PolychromeSprite(sprite) => &sprite.bounds,
Primitive::Surface(surface) => &surface.bounds,
Primitive::BackdropFilter(filter) => &filter.bounds,
Primitive::FilterBoundary(boundary) => &boundary.bounds,
}
}
@@ -241,6 +313,8 @@ impl Primitive {
Primitive::SubpixelSprite(sprite) => &sprite.content_mask,
Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
Primitive::Surface(surface) => &surface.content_mask,
Primitive::BackdropFilter(filter) => &filter.content_mask,
Primitive::FilterBoundary(boundary) => &boundary.content_mask,
}
}
}
@@ -269,6 +343,10 @@ struct BatchIterator<'a> {
polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
surfaces_start: usize,
surfaces_iter: Peekable<slice::Iter<'a, PaintSurface>>,
backdrop_filters_start: usize,
backdrop_filters_iter: Peekable<slice::Iter<'a, BackdropFilter>>,
filter_boundaries_start: usize,
filter_boundaries_iter: Peekable<slice::Iter<'a, FilterBoundary>>,
}
impl<'a> Iterator for BatchIterator<'a> {
@@ -302,6 +380,20 @@ impl<'a> Iterator for BatchIterator<'a> {
self.surfaces_iter.peek().map(|s| s.order),
PrimitiveKind::Surface,
),
(
self.backdrop_filters_iter.peek().map(|f| f.order),
PrimitiveKind::BackdropFilter,
),
(
self.filter_boundaries_iter.peek().map(|b| b.order),
// The same vec yields both start and end markers; the discriminant decides
// where the next marker sorts relative to draw batches at an equal order
// (start before content, end after).
match self.filter_boundaries_iter.peek() {
Some(boundary) if boundary.is_start => PrimitiveKind::FilterBoundaryStart,
_ => PrimitiveKind::FilterBoundaryEnd,
},
),
];
orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind));
@@ -447,6 +539,30 @@ impl<'a> Iterator for BatchIterator<'a> {
self.surfaces_start = surfaces_end;
Some(PrimitiveBatch::Surfaces(surfaces_start..surfaces_end))
}
PrimitiveKind::BackdropFilter => {
let backdrop_filters_start = self.backdrop_filters_start;
let mut backdrop_filters_end = backdrop_filters_start + 1;
self.backdrop_filters_iter.next();
while self
.backdrop_filters_iter
.next_if(|filter| (filter.order, batch_kind) < max_order_and_kind)
.is_some()
{
backdrop_filters_end += 1;
}
self.backdrop_filters_start = backdrop_filters_end;
Some(PrimitiveBatch::BackdropFilters(
backdrop_filters_start..backdrop_filters_end,
))
}
// Boundaries are emitted one at a time (never merged) so the renderer can switch
// render targets at exactly the right point in the batch stream.
PrimitiveKind::FilterBoundaryStart | PrimitiveKind::FilterBoundaryEnd => {
let index = self.filter_boundaries_start;
self.filter_boundaries_iter.next();
self.filter_boundaries_start = index + 1;
Some(PrimitiveBatch::FilterBoundary(index))
}
}
}
}
@@ -479,6 +595,11 @@ pub enum PrimitiveBatch {
range: Range<usize>,
},
Surfaces(Range<usize>),
BackdropFilters(Range<usize>),
/// A single content-filter group boundary; index into [`Scene::filter_boundaries`]. Read
/// `is_start` to tell whether this opens the group (switch render target) or closes it
/// (filter the offscreen target and composite it back).
FilterBoundary(usize),
}
#[derive(Default, Debug, Copy, Clone)]
@@ -543,6 +664,59 @@ impl From<Shadow> for Primitive {
}
}
/// A backdrop filter blurs (and may otherwise filter) the content already rendered behind
/// `bounds`, compositing the result into a rounded rectangle — the frosted-glass effect.
/// Emitted by [`crate::Window::paint_backdrop_filter`]; produces the CSS `backdrop-filter` effect.
#[derive(Default, Debug, Clone)]
#[expect(missing_docs)]
pub struct BackdropFilter {
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
/// The filter chain applied to the backdrop, in scene (device-pixel) space. Identity filters
/// are dropped at paint time, so a `BackdropFilter` is only emitted when this is non-empty.
///
/// Inline capacity is 4: a `SmallVec<[ScaledFilter; 4]>` is the same size as capacity 1 here
/// (the heap repr already occupies that space), so chains up to 4 filters avoid allocating
/// at no extra struct size.
pub filters: SmallVec<[ScaledFilter; 4]>,
/// Element opacity captured at paint time, multiplied into the composited result.
pub opacity: f32,
}
impl From<BackdropFilter> for Primitive {
fn from(filter: BackdropFilter) -> Self {
Primitive::BackdropFilter(filter)
}
}
/// The start or end marker of a content-filter (`filter`) isolation group. The element's
/// subtree is painted between a matched start/end pair; the renderer redirects that span into
/// an offscreen target, filters it, and composites it back at `bounds`. Produces the CSS
/// `filter` effect (e.g. blurring the element and its children as a single group).
#[derive(Debug, Clone)]
#[expect(missing_docs)]
pub struct FilterBoundary {
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
/// The filter chain applied to the isolated group, in scene (device-pixel) space. Identity
/// filters are dropped at paint time, so a `FilterBoundary` is only emitted when non-empty.
/// Inline capacity 4 (same struct size as 1 here — see [`BackdropFilter::filters`]).
pub filters: SmallVec<[ScaledFilter; 4]>,
pub opacity: f32,
/// `true` for the start marker (opens the group), `false` for the end marker (closes it).
pub is_start: bool,
}
impl From<FilterBoundary> for Primitive {
fn from(boundary: FilterBoundary) -> Self {
Primitive::FilterBoundary(boundary)
}
}
/// The style of a border.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
@@ -903,3 +1077,171 @@ impl PathVertex<Pixels> {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Point, Size};
fn sp(value: f32) -> ScaledPixels {
ScaledPixels(value)
}
/// All test primitives cover the same region so the bounds tree assigns strictly
/// increasing orders in insertion order — making the expected batch order deterministic.
fn full_bounds() -> Bounds<ScaledPixels> {
Bounds {
origin: Point {
x: sp(0.0),
y: sp(0.0),
},
size: Size {
width: sp(100.0),
height: sp(100.0),
},
}
}
fn mask() -> ContentMask<ScaledPixels> {
ContentMask {
bounds: full_bounds(),
}
}
fn quad() -> Quad {
Quad {
bounds: full_bounds(),
content_mask: mask(),
..Default::default()
}
}
/// A 100x100 quad whose bounds don't overlap `full_bounds()` (used to exercise the
/// order-reuse path: non-overlapping content reuses low draw-orders).
fn detached_quad() -> Quad {
let bounds = Bounds {
origin: Point {
x: sp(200.0),
y: sp(200.0),
},
size: Size {
width: sp(100.0),
height: sp(100.0),
},
};
Quad {
bounds,
content_mask: ContentMask { bounds },
..Default::default()
}
}
fn boundary(is_start: bool) -> FilterBoundary {
FilterBoundary {
order: 0,
bounds: full_bounds(),
content_mask: mask(),
corner_radii: Corners::default(),
filters: smallvec::smallvec![ScaledFilter::Blur(sp(8.0))],
opacity: 1.0,
is_start,
}
}
fn backdrop() -> BackdropFilter {
BackdropFilter {
bounds: full_bounds(),
content_mask: mask(),
corner_radii: Corners::default(),
filters: smallvec::smallvec![ScaledFilter::Blur(sp(20.0))],
opacity: 1.0,
..Default::default()
}
}
fn batch_kinds(scene: &mut Scene) -> Vec<&'static str> {
scene.finish();
scene
.batches()
.map(|batch| match batch {
PrimitiveBatch::Quads(_) => "quad",
PrimitiveBatch::BackdropFilters(_) => "backdrop",
PrimitiveBatch::FilterBoundary(ix) => {
if scene.filter_boundaries[ix].is_start {
"start"
} else {
"end"
}
}
_ => "other",
})
.collect()
}
#[test]
fn content_filter_group_brackets_its_children() {
let mut scene = Scene::default();
// Background painted before the filtered element.
scene.insert_primitive(quad());
// A content-filtered element: start marker, its child, end marker.
scene.insert_primitive(boundary(true));
scene.insert_primitive(quad());
scene.insert_primitive(boundary(false));
// The start must precede the group's child and the end must follow it, so the
// renderer can redirect rendering for exactly the group's span.
assert_eq!(
batch_kinds(&mut scene),
vec!["quad", "start", "quad", "end"]
);
}
// Note: this validates only the *scene ordering* of nested filter boundaries (start/child/
// end interleaving), not that a renderer actually isolates both levels — that depends on the
// backend's group-texture pool (see MAX_FILTER_DEPTH) and is exercised by the `blur` example.
#[test]
fn nested_content_filters_emit_well_nested_ordering() {
let mut scene = Scene::default();
scene.insert_primitive(boundary(true)); // outer start
scene.insert_primitive(quad()); // outer child
scene.insert_primitive(boundary(true)); // inner start
scene.insert_primitive(quad()); // inner child
scene.insert_primitive(boundary(false)); // inner end
scene.insert_primitive(boundary(false)); // outer end
assert_eq!(
batch_kinds(&mut scene),
vec!["start", "quad", "start", "quad", "end", "end"]
);
}
#[test]
fn content_after_a_filter_group_sorts_above_it() {
let mut scene = Scene::default();
// A content-filtered element: start marker, its child, end marker.
scene.insert_primitive(boundary(true));
scene.insert_primitive(quad());
scene.insert_primitive(boundary(false));
// A sibling painted after the group that does NOT overlap it. Without the close-time
// order-floor it would reuse the lowest order, tie with the start marker, and be swept
// into the group (start, quad, quad, end); it must instead sort after the end marker.
scene.insert_primitive(detached_quad());
assert_eq!(
batch_kinds(&mut scene),
vec!["start", "quad", "end", "quad"]
);
}
#[test]
fn backdrop_filter_sorts_before_a_later_overlapping_quad() {
let mut scene = Scene::default();
// Content behind the frosted panel.
scene.insert_primitive(quad());
// The panel: its backdrop snapshot, then its (translucent) background quad on top.
scene.insert_primitive(backdrop());
scene.insert_primitive(quad());
assert_eq!(batch_kinds(&mut scene), vec!["quad", "backdrop", "quad"]);
}
}
+112 -40
View File
@@ -8,7 +8,8 @@ use crate::{
AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners,
CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point,
PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi,
PointRefinement, Rgba, ScaledPixels, SharedString, Size, SizeRefinement, Styled, TextRun,
Window, black, phi,
point, quad, rems, size,
};
use collections::HashSet;
@@ -287,6 +288,12 @@ pub struct Style {
/// Box shadow of the element
pub box_shadow: Vec<BoxShadow>,
/// Filters applied to this element's own content and children (CSS `filter`).
pub filter: Vec<Filter>,
/// Filters applied to the content rendered behind this element (CSS `backdrop-filter`).
pub backdrop_filter: Vec<Filter>,
/// The text style of this element
#[refineable]
pub text: TextStyleRefinement,
@@ -355,6 +362,50 @@ pub struct BoxShadow {
pub inset: bool,
}
/// A graphical filter that can be applied either to an element's own content
/// (via [`Styled::filter`], like CSS `filter`) or to the content rendered behind
/// it (via [`Styled::backdrop_filter`], like CSS `backdrop-filter`).
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum Filter {
/// A gaussian blur with the given radius, in logical pixels. Maps to CSS `blur(<px>)`.
Blur(Pixels),
}
impl Filter {
/// Whether this filter has no visible effect, so painting can skip it entirely (and the
/// element can avoid the offscreen isolation pass when *all* of its filters are identities).
///
/// Each variant declares its own no-op case here rather than the pipeline special-casing
/// blur — adding a filter that this returns `true` for is silently dropped before it ever
/// reaches the renderer.
pub fn is_identity(&self) -> bool {
match self {
Filter::Blur(radius) => *radius <= Pixels::ZERO,
}
}
/// Lower this logical-pixel filter into its scene-space ([`ScaledFilter`]) form for the
/// renderer, scaling any pixel magnitudes by `factor` (the window scale factor).
pub fn scale(&self, factor: f32) -> ScaledFilter {
match self {
Filter::Blur(radius) => ScaledFilter::Blur(radius.scale(factor)),
}
}
}
/// The scene-space (device-pixel) form of a [`Filter`], carried on the scene primitives that the
/// renderers consume. Produced by [`Filter::scale`]; pixel magnitudes are in [`ScaledPixels`].
///
/// This is intentionally a separate enum from [`Filter`] (rather than reusing it) so the scene
/// stays in device space like every other primitive, and so the renderers `match` on it
/// exhaustively — adding a filter variant breaks each backend's match, forcing a deliberate
/// implement-or-decline decision per backend instead of silently rendering nothing.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ScaledFilter {
/// A gaussian blur with the given radius, in scaled (device) pixels.
Blur(ScaledPixels),
}
/// How to handle whitespace in text
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum WhiteSpace {
@@ -669,49 +720,68 @@ impl Style {
window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow);
let background_color = self.background.as_ref().and_then(Fill::color);
if background_color.is_some_and(|color| !color.is_transparent()) {
let mut border_color = match background_color {
Some(color) => match color.tag {
BackgroundTag::Solid
| BackgroundTag::PatternSlash
| BackgroundTag::Checkerboard => color.solid,
BackgroundTag::LinearGradient => color
.colors
.first()
.map(|stop| stop.color)
.unwrap_or_default(),
},
None => Hsla::default(),
};
border_color.a = 0.;
window.paint_quad(quad(
bounds,
corner_radii,
background_color.unwrap_or_default(),
Edges::default(),
border_color,
self.border_style,
));
// Blur the content behind this element before its (typically translucent) background
// is painted on top, so the background tints the frosted backdrop (CSS `backdrop-filter`).
if !self.backdrop_filter.is_empty() {
window.paint_backdrop_filter(bounds, corner_radii, &self.backdrop_filter);
}
window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
// The element's own box — background, inset shadows, children, and border — painted as a
// unit. A `filter` (CSS `filter`) wraps this whole unit so the renderer blurs the element
// and its children together as one group; without a filter it paints directly.
let paint_box = |window: &mut Window, cx: &mut App| {
let background_color = self.background.as_ref().and_then(Fill::color);
if background_color.is_some_and(|color| !color.is_transparent()) {
let mut border_color = match background_color {
Some(color) => match color.tag {
BackgroundTag::Solid
| BackgroundTag::PatternSlash
| BackgroundTag::Checkerboard => color.solid,
continuation(window, cx);
BackgroundTag::LinearGradient => color
.colors
.first()
.map(|stop| stop.color)
.unwrap_or_default(),
},
None => Hsla::default(),
};
border_color.a = 0.;
window.paint_quad(quad(
bounds,
corner_radii,
background_color.unwrap_or_default(),
Edges::default(),
border_color,
self.border_style,
));
}
if self.is_border_visible() {
let border_widths = self.border_widths.to_pixels(rem_size);
let mut background = self.border_color.unwrap_or_default();
background.a = 0.;
window.paint_quad(quad(
bounds,
corner_radii,
background,
border_widths,
self.border_color.unwrap_or_default(),
self.border_style,
));
window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
continuation(window, cx);
if self.is_border_visible() {
let border_widths = self.border_widths.to_pixels(rem_size);
let mut background = self.border_color.unwrap_or_default();
background.a = 0.;
window.paint_quad(quad(
bounds,
corner_radii,
background,
border_widths,
self.border_color.unwrap_or_default(),
self.border_style,
));
}
};
if self.filter.is_empty() {
paint_box(window, cx);
} else {
window.with_filter_layer(bounds, corner_radii, &self.filter, |window| {
paint_box(window, cx);
});
}
#[cfg(debug_assertions)]
@@ -765,6 +835,8 @@ impl Default for Style {
border_style: BorderStyle::default(),
corner_radii: Corners::default(),
box_shadow: Default::default(),
filter: Default::default(),
backdrop_filter: Default::default(),
text: TextStyleRefinement::default(),
mouse_cursor: None,
opacity: None,
+49 -2
View File
@@ -1,7 +1,7 @@
use crate::{
self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle,
FontWeight, GridPlacement, GridTemplate, Hsla, JustifyContent, Length, SharedString,
DefiniteLength, Display, Fill, Filter, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle,
FontWeight, GridPlacement, GridTemplate, Hsla, JustifyContent, Length, Pixels, SharedString,
StrikethroughStyle, StyleRefinement, TemplateColumnMinSize, TextAlign, TextOverflow,
TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
};
@@ -33,6 +33,53 @@ pub trait Styled: Sized {
gpui_macros::border_style_methods!();
gpui_macros::box_shadow_style_methods!();
/// Blur this element's own content and children, like CSS `filter: blur(<radius>)`.
///
/// This isolates the element's subtree, blurs it as a group, and composites the
/// result back. To blur the content *behind* the element instead (frosted glass),
/// use [`Styled::backdrop_blur`].
///
/// *Appends* to the element's filter chain, so it composes with other convenience
/// setters (`.blur(8.).<other_filter>()`). To replace the whole chain, use
/// [`Styled::filter`].
fn blur(mut self, radius: impl Into<Pixels>) -> Self {
self.style()
.filter
.get_or_insert_with(Vec::new)
.push(Filter::Blur(radius.into()));
self
}
/// Set (replacing any existing) the full list of filters applied to this element's own
/// content, like CSS `filter`. To add a single filter to the chain instead, use the
/// convenience setters such as [`Styled::blur`].
fn filter(mut self, filters: impl Into<Vec<Filter>>) -> Self {
self.style().filter = Some(filters.into());
self
}
/// Blur the content rendered behind this element — a frosted-glass effect — like CSS
/// `backdrop-filter: blur(<radius>)`. Typically paired with a translucent [`Styled::bg`]
/// so the background tints the blurred backdrop.
///
/// *Appends* to the element's backdrop-filter chain. To replace the whole chain, use
/// [`Styled::backdrop_filter`].
fn backdrop_blur(mut self, radius: impl Into<Pixels>) -> Self {
self.style()
.backdrop_filter
.get_or_insert_with(Vec::new)
.push(Filter::Blur(radius.into()));
self
}
/// Set (replacing any existing) the full list of filters applied to the content behind this
/// element, like CSS `backdrop-filter`. To add a single filter to the chain instead, use the
/// convenience setters such as [`Styled::backdrop_blur`].
fn backdrop_filter(mut self, filters: impl Into<Vec<Filter>>) -> Self {
self.style().backdrop_filter = Some(filters.into());
self
}
/// Sets the display type of the element to `block`.
/// [Docs](https://tailwindcss.com/docs/display)
fn block(mut self) -> Self {
+104 -6
View File
@@ -2,17 +2,18 @@
use crate::Inspector;
use crate::{
Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock,
Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels,
AsyncWindowContext, AvailableSpace, BackdropFilter, Background, BorderStyle, Bounds, BoxShadow,
Capslock, Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels,
DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs,
Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke,
KeystrokeEvent, LayoutId, Lerp, LineLayoutIndex, Modifiers, ModifiersChangedEvent,
EntityId, EventEmitter, FileDropEvent, Filter, FilterBoundary, FontId, Global, GlobalElementId,
GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent,
Keystroke, KeystrokeEvent, LayoutId, Lerp, LineLayoutIndex, Modifiers, ModifiersChangedEvent,
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, Priority, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size,
SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledFilter, ScaledPixels, Scene, Shadow,
SharedString, Size,
StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab,
SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextRenderingMode, TextStyle,
TextStyleRefinement, ThermalState, TransformationMatrix, Transition, TransitionState,
@@ -2938,6 +2939,11 @@ impl Window {
return;
}
// Deferred draws are overlays (tooltips, popovers, drag images) and must sort above the
// whole main scene. Raise the order floor so they do — this also keeps a deferred
// backdrop's order from falling inside a content-filter order range left by the main scene.
self.next_frame.scene.raise_order_floor();
let traversal_order = self.deferred_draw_traversal_order();
let mut deferred_draws = mem::take(&mut self.next_frame.deferred_draws);
for deferred_draw_ix in traversal_order {
@@ -3677,6 +3683,98 @@ impl Window {
}
}
/// Paint a backdrop filter into the scene for the next frame at the current z-index. The
/// renderer blurs the content already painted behind `bounds` and composites the result
/// into the rounded rectangle described by `bounds` and `corner_radii` — the CSS
/// `backdrop-filter` effect (frosted glass). Typically the element then paints a translucent
/// background quad on top so its color tints the blurred backdrop.
///
/// Does nothing when `filters` produce no visible blur.
///
/// This method should only be called as part of the paint phase of element drawing.
pub fn paint_backdrop_filter(
&mut self,
bounds: Bounds<Pixels>,
corner_radii: Corners<Pixels>,
filters: &[Filter],
) {
self.invalidator.debug_assert_paint();
let scale_factor = self.scale_factor();
let filters: SmallVec<[ScaledFilter; 4]> = filters
.iter()
.filter(|filter| !filter.is_identity())
.map(|filter| filter.scale(scale_factor))
.collect();
if filters.is_empty() {
return;
}
self.next_frame.scene.insert_primitive(BackdropFilter {
order: 0,
bounds: self.snap_bounds(bounds),
content_mask: self.snapped_content_mask(),
corner_radii: corner_radii.scale(scale_factor),
filters,
opacity: self.element_opacity(),
});
}
/// Isolate the painting performed by `f` into a content-filter group: the renderer renders
/// everything `f` paints into an offscreen target, blurs it as a single layer, and
/// composites the result back into the rounded rectangle described by `bounds` and
/// `corner_radii` — the CSS `filter` effect (e.g. blurring an element and its children).
///
/// When `filters` produce no visible blur this simply runs `f` with no offscreen
/// indirection.
///
/// This method should only be called as part of the paint phase of element drawing.
pub fn with_filter_layer<R>(
&mut self,
bounds: Bounds<Pixels>,
corner_radii: Corners<Pixels>,
filters: &[Filter],
f: impl FnOnce(&mut Self) -> R,
) -> R {
self.invalidator.debug_assert_paint();
let scale_factor = self.scale_factor();
let filters: SmallVec<[ScaledFilter; 4]> = filters
.iter()
.filter(|filter| !filter.is_identity())
.map(|filter| filter.scale(scale_factor))
.collect();
if filters.is_empty() {
return f(self);
}
// Snapshot the (scaled) group parameters once so the start and end markers agree.
//
// `opacity` is 1.0 — NOT `element_opacity()`. The group's children/bg/border are painted
// through the normal paint methods while `element_opacity` is still in effect, so they
// already carry the element's opacity (consistent with gpui's per-primitive opacity for
// non-filtered elements). Re-applying it at composite time would double it (e.g.
// `.blur(r).opacity(0.5)` would render at 0.25 instead of 0.5).
let boundary = FilterBoundary {
order: 0,
bounds: self.snap_bounds(bounds),
content_mask: self.snapped_content_mask(),
corner_radii: corner_radii.scale(scale_factor),
filters,
opacity: 1.0,
is_start: true,
};
self.next_frame.scene.insert_primitive(boundary.clone());
let result = f(self);
self.next_frame.scene.insert_primitive(FilterBoundary {
is_start: false,
..boundary
});
result
}
/// Paint one or more quads into the scene for the next frame at the current stacking context.
/// Quads are colored rectangular regions with an optional background, border, and corner radius.
/// see [`fill`], [`outline`], and [`quad`] to construct this type.