gpui: Fix broken rendering with nested opacity (#35407)

Rendering breaks when both an element and its parent have opacity set.
The following code reproduces the issue:

```rust
struct Repro;

impl Render for Repro {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        fn make_box(bg: impl Into<Fill>) -> impl IntoElement {
            div().size_8().bg(bg).hover(|style| style.opacity(0.5))
        }

        div()
            .flex()
            .items_center()
            .justify_center()
            .size(px(500.0))
            .hover(|style| style.opacity(0.5))
            .child(make_box(gpui::red()))
            .child(make_box(gpui::green()))
            .child(make_box(gpui::blue()))
    }
}
```

Before (broken behavior):


https://github.com/user-attachments/assets/2c5c1e31-88b2-4f39-81f8-40060e3fe958

The child element resets its parent and siblings' opacity, which is an
unexpected behavior.

After (fixed behavior):


https://github.com/user-attachments/assets/48527033-b06f-4737-b6c3-0ee3d133f138

Release Notes:

- Fixed an issue where nested opacity is rendered incorrectly.
This commit is contained in:
Cyandev
2025-10-10 23:17:20 +02:00
committed by GitHub
parent 924e7e61a5
commit d6becab3be
2 changed files with 39 additions and 38 deletions
+27 -29
View File
@@ -1,10 +1,9 @@
use std::{fs, path::PathBuf, time::Duration};
use std::{fs, path::PathBuf};
use anyhow::Result;
use gpui::{
App, Application, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task,
Timer, Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size,
svg,
Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg,
};
struct Assets {
@@ -37,6 +36,7 @@ impl AssetSource for Assets {
struct HelloWorld {
_task: Option<Task<()>>,
opacity: f32,
animating: bool,
}
impl HelloWorld {
@@ -44,39 +44,29 @@ impl HelloWorld {
Self {
_task: None,
opacity: 0.5,
animating: false,
}
}
fn change_opacity(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
self.opacity = 0.0;
self.animating = true;
cx.notify();
self._task = Some(cx.spawn_in(window, async move |view, cx| {
loop {
Timer::after(Duration::from_secs_f32(0.05)).await;
let mut stop = false;
let _ = cx.update(|_, cx| {
view.update(cx, |view, cx| {
if view.opacity >= 1.0 {
stop = true;
return;
}
view.opacity += 0.1;
cx.notify();
})
});
if stop {
break;
}
}
}));
}
}
impl Render for HelloWorld {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.animating {
self.opacity += 0.005;
if self.opacity >= 1.0 {
self.animating = false;
self.opacity = 1.0;
} else {
window.request_animation_frame();
}
}
div()
.flex()
.flex_row()
@@ -96,7 +86,7 @@ impl Render for HelloWorld {
.child(
div()
.id("panel")
.on_click(cx.listener(Self::change_opacity))
.on_click(cx.listener(Self::start_animation))
.absolute()
.top_8()
.left_8()
@@ -150,7 +140,15 @@ impl Render for HelloWorld {
.text_2xl()
.size_8(),
)
.child("🎊✈️🎉🎈🎁🎂")
.child(
div()
.flex()
.children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| {
div()
.child(emoji.to_string())
.hover(|style| style.opacity(0.5))
})),
)
.child(img("image/black-cat-typing.gif").size_12()),
),
)
+12 -9
View File
@@ -837,7 +837,7 @@ pub struct Window {
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
pub(crate) rendered_entity_stack: Vec<EntityId>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) element_opacity: Option<f32>,
pub(crate) element_opacity: f32,
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) requested_autoscroll: Option<Bounds<Pixels>>,
pub(crate) image_cache_stack: Vec<AnyImageCache>,
@@ -1222,7 +1222,7 @@ impl Window {
rendered_entity_stack: Vec::new(),
element_offset_stack: Vec::new(),
content_mask_stack: Vec::new(),
element_opacity: None,
element_opacity: 1.0,
requested_autoscroll: None,
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
@@ -2435,14 +2435,16 @@ impl Window {
opacity: Option<f32>,
f: impl FnOnce(&mut Self) -> R,
) -> R {
if opacity.is_none() {
return f(self);
}
self.invalidator.debug_assert_paint_or_prepaint();
self.element_opacity = opacity;
let Some(opacity) = opacity else {
return f(self);
};
let previous_opacity = self.element_opacity;
self.element_opacity = previous_opacity * opacity;
let result = f(self);
self.element_opacity = None;
self.element_opacity = previous_opacity;
result
}
@@ -2539,9 +2541,10 @@ impl Window {
/// Obtain the current element opacity. This method should only be called during the
/// prepaint phase of element drawing.
#[inline]
pub(crate) fn element_opacity(&self) -> f32 {
self.invalidator.debug_assert_paint_or_prepaint();
self.element_opacity.unwrap_or(1.0)
self.element_opacity
}
/// Obtain the current content mask. This method should only be called during element drawing.