crates/gpui/Cargo.toml 🔗
@@ -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"
Jason Lee created
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">
crates/gpui/Cargo.toml | 4
crates/gpui/examples/opacity.rs | 173 +++++++++++++++++++++++
crates/gpui/src/color.rs | 10 +
crates/gpui/src/elements/div.rs | 61 ++++---
crates/gpui/src/platform/blade/shaders.wgsl | 4
crates/gpui/src/platform/mac/shaders.metal | 2
crates/gpui/src/scene.rs | 5
crates/gpui/src/style.rs | 4
crates/gpui/src/styled.rs | 6
crates/gpui/src/window.rs | 66 +++++++-
10 files changed, 298 insertions(+), 37 deletions(-)
@@ -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"
@@ -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();
+ });
+}
@@ -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);
-
- if !cx.has_active_drag() {
- if let Some(mouse_cursor) = style.mouse_cursor {
- cx.set_cursor_style(mouse_cursor, hitbox);
+ 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 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_mouse_listeners(hitbox, element_state.as_mut(), cx);
- self.paint_scroll_listener(hitbox, &style, cx);
- }
- self.paint_keyboard_listeners(cx);
- f(&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(())
}