diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index ab61cf04c504a4..3ce9b509f0e0c1 100644 --- a/crates/gpui/Cargo.toml +++ b/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" diff --git a/crates/gpui/examples/opacity.rs b/crates/gpui/examples/opacity.rs new file mode 100644 index 00000000000000..f360bebe64b1b6 --- /dev/null +++ b/crates/gpui/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>> { + 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> { + 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>, + opacity: f32, +} + +impl HelloWorld { + fn new(_: &mut ViewContext) -> Self { + Self { + _task: None, + opacity: 0.5, + } + } + + fn change_opacity(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + 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) -> 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(); + }); +} diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 585255b450377e..24edace5936c86 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -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 for Hsla { diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 2773c4c256cd59..4ea74aaa7d6571 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -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); + } + } + }, + ); }); }); }); diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 77f14e8264b84d..c3983c7acc6bc6 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -548,7 +548,9 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { 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 { let grayscale = dot(color.rgb, GRAYSCALE_FACTORS); color = vec4(vec3(grayscale), sample.a); } - return blend_color(color, saturate(0.5 - distance)); + return blend_color(color, sprite.opacity * saturate(0.5 - distance)); } // --- surfaces --- // diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index e7afe2c11fe691..464e4b59039777 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -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; } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 63b2bbeeeea48d..32602b7f92166e 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -640,16 +640,19 @@ impl From 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, pub content_mask: ContentMask, pub corner_radii: Corners, pub tile: AtlasTile, } +impl Eq for PolychromeSprite {} impl Ord for PolychromeSprite { fn cmp(&self, other: &Self) -> std::cmp::Ordering { diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 77cf6ac52fe733..b003f9b903437c 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -234,6 +234,9 @@ pub struct Style { /// The mouse cursor style shown when the mouse pointer is over an element. pub mouse_cursor: Option, + /// The opacity of this element + pub opacity: Option, + /// 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, diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 8196cc25986d8f..d95ecebf4f6401 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -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 { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index a7afb91594877b..1c47b98606b310 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -520,6 +520,7 @@ pub struct Window { pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, pub(crate) text_style_stack: Vec, pub(crate) element_offset_stack: Vec>, + pub(crate) element_opacity: Option, pub(crate) content_mask_stack: Vec>, pub(crate) requested_autoscroll: Option>, 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( + &mut self, + opacity: Option, + 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 { 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(()) }