gpui: Add opacity to support transparency of the entire element (#17132)

Release Notes:

- N/A

---

Add this for let GPUI element to support fade in-out animation.

## Platform test

- [x] macOS
- [x] blade `cargo run -p gpui --example opacity --features macos-blade`

## Usage

```rs
div()
    .opacity(0.5)
    .bg(gpui::black())
    .text_color(gpui::black())
    .child("Hello world")
```

This will apply the `opacity` it self and all children to use `opacity`
value to render colors.

## Example

```
cargo run -p gpui --example opacity
cargo run -p gpui --example opacity --features macos-blade
```

<img width="612" alt="image"
src="https://github.com/user-attachments/assets/f1da87ed-31f5-4b55-a023-39e8ee1ba349">
This commit is contained in:
Jason Lee 2024-09-04 18:53:45 +08:00 committed by GitHub
parent 072513f59f
commit a092ff0c4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 297 additions and 36 deletions

View file

@ -188,3 +188,7 @@ path = "examples/svg/svg.rs"
[[example]]
name = "text_wrapper"
path = "examples/text_wrapper.rs"
[[example]]
name = "opacity"
path = "examples/opacity.rs"

View file

@ -0,0 +1,173 @@
use std::{fs, path::PathBuf, time::Duration};
use gpui::*;
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|e| e.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|e| e.into())
}
}
struct HelloWorld {
_task: Option<Task<()>>,
opacity: f32,
}
impl HelloWorld {
fn new(_: &mut ViewContext<Self>) -> Self {
Self {
_task: None,
opacity: 0.5,
}
}
fn change_opacity(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.opacity = 0.0;
cx.notify();
self._task = Some(cx.spawn(|view, mut cx| async move {
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, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.bg(rgb(0xE0E0E0))
.text_xl()
.child(
div()
.flex()
.size_full()
.justify_center()
.items_center()
.border_1()
.text_color(gpui::blue())
.child(div().child("This is background text.")),
)
.child(
div()
.id("panel")
.on_click(cx.listener(Self::change_opacity))
.absolute()
.top_8()
.left_8()
.right_8()
.bottom_8()
.opacity(self.opacity)
.flex()
.justify_center()
.items_center()
.bg(gpui::white())
.border_3()
.border_color(gpui::red())
.text_color(gpui::yellow())
.child(
div()
.flex()
.flex_col()
.gap_2()
.justify_center()
.items_center()
.size(px(300.))
.bg(gpui::blue())
.border_3()
.border_color(gpui::black())
.shadow(smallvec::smallvec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.5),
blur_radius: px(1.0),
spread_radius: px(5.0),
offset: point(px(10.0), px(10.0)),
}])
.child(img("image/app-icon.png").size_8())
.child("Opacity Panel (Click to test)")
.child(
div()
.id("deep-level-text")
.flex()
.justify_center()
.items_center()
.p_4()
.bg(gpui::black())
.text_color(gpui::white())
.text_decoration_2()
.text_decoration_wavy()
.text_decoration_color(gpui::red())
.child(format!("opacity: {:.1}", self.opacity)),
)
.child(
svg()
.path("image/arrow_circle.svg")
.text_color(gpui::black())
.text_2xl()
.size_8(),
)
.child("🎊✈️🎉🎈🎁🎂")
.child(img("image/black-cat-typing.gif").size_12()),
),
)
}
}
fn main() {
App::new()
.with_assets(Assets {
base: PathBuf::from("crates/gpui/examples"),
})
.run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(HelloWorld::new),
)
.unwrap();
});
}

View file

@ -461,6 +461,16 @@ impl Hsla {
pub fn fade_out(&mut self, factor: f32) {
self.a *= 1.0 - factor.clamp(0., 1.);
}
/// Returns a new HSLA color with the same hue, saturation, and lightness, but with a modified alpha value.
pub fn opacity(&self, factor: f32) -> Self {
Hsla {
h: self.h,
s: self.s,
l: self.l,
a: self.a * factor.clamp(0., 1.),
}
}
}
impl From<Rgba> for Hsla {

View file

@ -1500,35 +1500,44 @@ impl Interactivity {
return ((), element_state);
}
style.paint(bounds, cx, |cx: &mut WindowContext| {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(global_id, hitbox, &style, cx);
cx.with_element_opacity(style.opacity, |cx| {
style.paint(bounds, cx, |cx: &mut WindowContext| {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(
style.overflow_mask(bounds, cx.rem_size()),
|cx| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(global_id, hitbox, &style, cx);
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
cx.set_cursor_style(mouse_cursor, hitbox);
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
cx.set_cursor_style(mouse_cursor, hitbox);
}
}
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
cx,
);
self.paint_scroll_listener(hitbox, &style, cx);
}
}
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
self.paint_keyboard_listeners(cx);
f(&style, cx);
self.paint_mouse_listeners(hitbox, element_state.as_mut(), cx);
self.paint_scroll_listener(hitbox, &style, cx);
}
self.paint_keyboard_listeners(cx);
f(&style, cx);
if hitbox.is_some() {
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
if hitbox.is_some() {
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
},
);
});
});
});

View file

@ -548,7 +548,9 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
struct PolychromeSprite {
order: u32,
pad: u32,
grayscale: u32,
opacity: f32,
bounds: Bounds,
content_mask: Bounds,
corner_radii: Corners,
@ -592,7 +594,7 @@ fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
color = vec4<f32>(vec3<f32>(grayscale), sample.a);
}
return blend_color(color, saturate(0.5 - distance));
return blend_color(color, sprite.opacity * saturate(0.5 - distance));
}
// --- surfaces --- //

View file

@ -385,7 +385,7 @@ fragment float4 polychrome_sprite_fragment(
color.g = grayscale;
color.b = grayscale;
}
color.a *= saturate(0.5 - distance);
color.a *= sprite.opacity * saturate(0.5 - distance);
return color;
}

View file

@ -640,16 +640,19 @@ impl From<MonochromeSprite> for Primitive {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub order: DrawOrder,
pub pad: u32, // align to 8 bytes
pub grayscale: bool,
pub opacity: f32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
pub tile: AtlasTile,
}
impl Eq for PolychromeSprite {}
impl Ord for PolychromeSprite {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {

View file

@ -234,6 +234,9 @@ pub struct Style {
/// The mouse cursor style shown when the mouse pointer is over an element.
pub mouse_cursor: Option<CursorStyle>,
/// The opacity of this element
pub opacity: Option<f32>,
/// Whether to draw a red debugging outline around this element
#[cfg(debug_assertions)]
pub debug: bool,
@ -694,6 +697,7 @@ impl Default for Style {
box_shadow: Default::default(),
text: TextStyleRefinement::default(),
mouse_cursor: None,
opacity: None,
#[cfg(debug_assertions)]
debug: false,

View file

@ -547,6 +547,12 @@ pub trait Styled: Sized {
self
}
/// Set opacity on this element and its children.
fn opacity(mut self, opacity: f32) -> Self {
self.style().opacity = Some(opacity);
self
}
/// Draw a debug border around this element.
#[cfg(debug_assertions)]
fn debug(mut self) -> Self {

View file

@ -520,6 +520,7 @@ pub struct Window {
pub(crate) element_id_stack: SmallVec<[ElementId; 32]>,
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) element_opacity: Option<f32>,
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) requested_autoscroll: Option<Bounds<Pixels>>,
pub(crate) rendered_frame: Frame,
@ -799,6 +800,7 @@ impl Window {
text_style_stack: Vec::new(),
element_offset_stack: Vec::new(),
content_mask_stack: Vec::new(),
element_opacity: None,
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())),
@ -1908,6 +1910,28 @@ impl<'a> WindowContext<'a> {
result
}
pub(crate) fn with_element_opacity<R>(
&mut self,
opacity: Option<f32>,
f: impl FnOnce(&mut Self) -> R,
) -> R {
if opacity.is_none() {
return f(self);
}
debug_assert!(
matches!(
self.window.draw_phase,
DrawPhase::Prepaint | DrawPhase::Paint
),
"this method can only be called during prepaint, or paint"
);
self.window_mut().element_opacity = opacity;
let result = f(self);
self.window_mut().element_opacity = None;
result
}
/// Perform prepaint on child elements in a "retryable" manner, so that any side effects
/// of prepaints can be discarded before prepainting again. This is used to support autoscroll
/// where we need to prepaint children to detect the autoscroll bounds, then adjust the
@ -2021,6 +2045,19 @@ impl<'a> WindowContext<'a> {
.unwrap_or_default()
}
/// Obtain the current element opacity. This method should only be called during the
/// prepaint phase of element drawing.
pub(crate) fn element_opacity(&self) -> f32 {
debug_assert!(
matches!(
self.window.draw_phase,
DrawPhase::Prepaint | DrawPhase::Paint
),
"this method can only be called during prepaint, or paint"
);
self.window().element_opacity.unwrap_or(1.0)
}
/// Obtain the current content mask. This method should only be called during element drawing.
pub fn content_mask(&self) -> ContentMask<Pixels> {
debug_assert!(
@ -2258,6 +2295,7 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let opacity = self.element_opacity();
for shadow in shadows {
let mut shadow_bounds = bounds;
shadow_bounds.origin += shadow.offset;
@ -2268,7 +2306,7 @@ impl<'a> WindowContext<'a> {
bounds: shadow_bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
corner_radii: corner_radii.scale(scale_factor),
color: shadow.color,
color: shadow.color.opacity(opacity),
});
}
}
@ -2287,13 +2325,14 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let opacity = self.element_opacity();
self.window.next_frame.scene.insert_primitive(Quad {
order: 0,
pad: 0,
bounds: quad.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
background: quad.background,
border_color: quad.border_color,
background: quad.background.opacity(opacity),
border_color: quad.border_color.opacity(opacity),
corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: quad.border_widths.scale(scale_factor),
});
@ -2311,8 +2350,9 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let opacity = self.element_opacity();
path.content_mask = content_mask;
path.color = color.into();
path.color = color.into().opacity(opacity);
self.window
.next_frame
.scene
@ -2345,13 +2385,14 @@ impl<'a> WindowContext<'a> {
size: size(width, height),
};
let content_mask = self.content_mask();
let element_opacity = self.element_opacity();
self.window.next_frame.scene.insert_primitive(Underline {
order: 0,
pad: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
color: style.color.unwrap_or_default(),
color: style.color.unwrap_or_default().opacity(element_opacity),
thickness: style.thickness.scale(scale_factor),
wavy: style.wavy,
});
@ -2379,6 +2420,7 @@ impl<'a> WindowContext<'a> {
size: size(width, height),
};
let content_mask = self.content_mask();
let opacity = self.element_opacity();
self.window.next_frame.scene.insert_primitive(Underline {
order: 0,
@ -2386,7 +2428,7 @@ impl<'a> WindowContext<'a> {
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
thickness: style.thickness.scale(scale_factor),
color: style.color.unwrap_or_default(),
color: style.color.unwrap_or_default().opacity(opacity),
wavy: false,
});
}
@ -2413,6 +2455,7 @@ impl<'a> WindowContext<'a> {
"this method can only be called during paint"
);
let element_opacity = self.element_opacity();
let scale_factor = self.scale_factor();
let glyph_origin = origin.scale(scale_factor);
let subpixel_variant = Point {
@ -2451,7 +2494,7 @@ impl<'a> WindowContext<'a> {
pad: 0,
bounds,
content_mask,
color,
color: color.opacity(element_opacity),
tile,
transformation: TransformationMatrix::unit(),
});
@ -2508,17 +2551,20 @@ impl<'a> WindowContext<'a> {
size: tile.bounds.size.map(Into::into),
};
let content_mask = self.content_mask().scale(scale_factor);
let opacity = self.element_opacity();
self.window
.next_frame
.scene
.insert_primitive(PolychromeSprite {
order: 0,
pad: 0,
grayscale: false,
bounds,
corner_radii: Default::default(),
content_mask,
tile,
opacity,
});
}
Ok(())
@ -2540,6 +2586,7 @@ impl<'a> WindowContext<'a> {
"this method can only be called during paint"
);
let element_opacity = self.element_opacity();
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
// Render the SVG at twice the size to get a higher quality result.
@ -2574,7 +2621,7 @@ impl<'a> WindowContext<'a> {
.map_origin(|origin| origin.floor())
.map_size(|size| size.ceil()),
content_mask,
color,
color: color.opacity(element_opacity),
tile,
transformation,
});
@ -2622,17 +2669,20 @@ impl<'a> WindowContext<'a> {
.expect("Callback above only returns Some");
let content_mask = self.content_mask().scale(scale_factor);
let corner_radii = corner_radii.scale(scale_factor);
let opacity = self.element_opacity();
self.window
.next_frame
.scene
.insert_primitive(PolychromeSprite {
order: 0,
pad: 0,
grayscale,
bounds,
content_mask,
corner_radii,
tile,
opacity,
});
Ok(())
}