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:
parent
072513f59f
commit
a092ff0c4f
10 changed files with 297 additions and 36 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"
|
||||
|
|
173
crates/gpui/examples/opacity.rs
Normal file
173
crates/gpui/examples/opacity.rs
Normal 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();
|
||||
});
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue