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