Skip to content

Commit

Permalink
gpui: Add opacity to support transparency of the entire element (ze…
Browse files Browse the repository at this point in the history
…d-industries#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">
  • Loading branch information
huacnlee authored and osiewicz committed Sep 5, 2024
1 parent aecb2ef commit 890dba6
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 37 deletions.
4 changes: 4 additions & 0 deletions crates/gpui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
173 changes: 173 additions & 0 deletions crates/gpui/examples/opacity.rs
Original file line number Diff line number Diff line change
@@ -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();
});
}
10 changes: 10 additions & 0 deletions crates/gpui/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
61 changes: 35 additions & 26 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
},
);
});
});
});
Expand Down
4 changes: 3 additions & 1 deletion crates/gpui/src/platform/blade/shaders.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 --- //
Expand Down
2 changes: 1 addition & 1 deletion crates/gpui/src/platform/mac/shaders.metal
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
5 changes: 4 additions & 1 deletion crates/gpui/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions crates/gpui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions crates/gpui/src/styled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 890dba6

Please sign in to comment.