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:
@@ -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()),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user