From c530504a04252105fd8d0da0af3b5ff2876366f4 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 26 Mar 2024 10:37:12 +0100 Subject: [PATCH] CSS-like shadows with offset, spread, and blur (#4232) This makes `epaint::Shadow` more like CSS's box-shadow, adding `offset` and replacing `extrusion` with `blur` and `spread`. * Closes https://github.com/emilk/egui/pull/3047 The offsets make for nice drop-shadow effects. Old shadows: old-shadows New shadows: new-shadows-full --- crates/egui/src/containers/area.rs | 2 +- crates/egui/src/style.rs | 33 +++++++-- crates/egui/src/widgets/mod.rs | 47 ++++++++++--- crates/epaint/src/shadow.rs | 107 ++++++++++++++--------------- 4 files changed, 120 insertions(+), 69 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 3c6b711b..c66be433 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -423,7 +423,7 @@ impl Prepared { .at_least(self.state.left_top_pos() + Vec2::splat(32.0)), ); - let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky + let shadow_radius = ctx.style().visuals.window_shadow.margin().sum().max_elem(); // hacky let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius); let clip_rect = Rect::from_min_max(self.state.left_top_pos(), constrain_rect.max) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index c270add6..da507ae2 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1071,7 +1071,12 @@ impl Visuals { error_fg_color: Color32::from_rgb(255, 0, 0), // red window_rounding: Rounding::same(6.0), - window_shadow: Shadow::big_dark(), + window_shadow: Shadow { + offset: vec2(10.0, 20.0), + blur: 15.0, + spread: 0.0, + color: Color32::from_black_alpha(96), + }, window_fill: Color32::from_gray(27), window_stroke: Stroke::new(1.0, Color32::from_gray(60)), window_highlight_topmost: true, @@ -1080,10 +1085,18 @@ impl Visuals { panel_fill: Color32::from_gray(27), - popup_shadow: Shadow::small_dark(), + popup_shadow: Shadow { + offset: vec2(6.0, 10.0), + blur: 8.0, + spread: 0.0, + color: Color32::from_black_alpha(96), + }, + resize_corner_size: 12.0, + text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), text_cursor_preview: false, + clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion button_frame: true, collapsing_header_frame: false, @@ -1115,14 +1128,26 @@ impl Visuals { warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background. error_fg_color: Color32::from_rgb(255, 0, 0), // red - window_shadow: Shadow::big_light(), + window_shadow: Shadow { + offset: vec2(10.0, 20.0), + blur: 15.0, + spread: 0.0, + color: Color32::from_black_alpha(25), + }, window_fill: Color32::from_gray(248), window_stroke: Stroke::new(1.0, Color32::from_gray(190)), panel_fill: Color32::from_gray(248), - popup_shadow: Shadow::small_light(), + popup_shadow: Shadow { + offset: vec2(6.0, 10.0), + blur: 8.0, + spread: 0.0, + color: Color32::from_black_alpha(25), + }, + text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)), + ..Self::dark() } } diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index d589e386..77ee8692 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -126,15 +126,44 @@ pub fn stroke_ui(ui: &mut crate::Ui, stroke: &mut epaint::Stroke, text: &str) { } pub(crate) fn shadow_ui(ui: &mut Ui, shadow: &mut epaint::Shadow, text: &str) { - let epaint::Shadow { extrusion, color } = shadow; - ui.horizontal(|ui| { - ui.label(text); - ui.add( - DragValue::new(extrusion) - .speed(1.0) - .clamp_range(0.0..=100.0), - ) - .on_hover_text("Extrusion"); + let epaint::Shadow { + offset, + blur, + spread, + color, + } = shadow; + + ui.label(text); + ui.indent(text, |ui| { + crate::Grid::new("shadow_ui").show(ui, |ui| { + ui.add( + DragValue::new(&mut offset.x) + .speed(1.0) + .clamp_range(-100.0..=100.0) + .prefix("x: "), + ); + ui.add( + DragValue::new(&mut offset.y) + .speed(1.0) + .clamp_range(-100.0..=100.0) + .prefix("y: "), + ); + ui.end_row(); + + ui.add( + DragValue::new(blur) + .speed(1.0) + .clamp_range(0.0..=100.0) + .prefix("Blur:"), + ); + + ui.add( + DragValue::new(spread) + .speed(1.0) + .clamp_range(0.0..=100.0) + .prefix("Spread:"), + ); + }); ui.color_edit_button_srgba(color); }); } diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index fb6a9a4a..71ce76c9 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,84 +1,65 @@ use super::*; /// The color and fuzziness of a fuzzy shape. +/// /// Can be used for a rectangular shadow with a soft penumbra. +/// +/// Very similar to a box-shadow in CSS. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Shadow { - /// The shadow extends this much outside the rect. - /// The size of the fuzzy penumbra. - pub extrusion: f32, + /// Move the shadow by this much. + /// + /// For instance, a value of `[1.0, 2.0]` will move the shadow 1 point to the right and 2 points down, + /// causing a drop-shadow effet. + pub offset: Vec2, + + /// The width of the blur, i.e. the width of the fuzzy penumbra. + /// + /// A value of 0.0 means no blur. + pub blur: f32, + + /// Expand the shadow in all directions by this much. + pub spread: f32, /// Color of the opaque center of the shadow. pub color: Color32, } impl Shadow { + /// No shadow at all. pub const NONE: Self = Self { - extrusion: 0.0, + offset: Vec2::ZERO, + blur: 0.0, + spread: 0.0, color: Color32::TRANSPARENT, }; - pub const fn new(extrusion: f32, color: Color32) -> Self { - Self { extrusion, color } - } - - /// Tooltips, menus, …, for dark mode. - pub const fn small_dark() -> Self { - Self { - extrusion: 16.0, - color: Color32::from_black_alpha(96), - } - } - - /// Tooltips, menus, …, for light mode. - pub const fn small_light() -> Self { - Self { - extrusion: 16.0, - color: Color32::from_black_alpha(20), - } - } - - /// Used for egui windows in dark mode. - pub const fn big_dark() -> Self { - Self { - extrusion: 32.0, - color: Color32::from_black_alpha(96), - } - } - - /// Used for egui windows in light mode. - pub const fn big_light() -> Self { - Self { - extrusion: 32.0, - color: Color32::from_black_alpha(16), - } - } - pub fn tessellate(&self, rect: Rect, rounding: impl Into) -> Mesh { // tessellator.clip_rect = clip_rect; // TODO(emilk): culling - let Self { extrusion, color } = *self; - - let rounding: Rounding = rounding.into(); - let half_ext = 0.5 * extrusion; - - let ext_rounding = Rounding { - nw: rounding.nw + half_ext, - ne: rounding.ne + half_ext, - sw: rounding.sw + half_ext, - se: rounding.se + half_ext, - }; - use crate::tessellator::*; - let rect = RectShape::filled(rect.expand(half_ext), ext_rounding, color); + + let Self { + offset, + blur, + spread, + color, + } = *self; + + let rect = rect.translate(offset); + + let rounding_expansion = spread.abs() + 0.5 * blur; + let rounding = rounding.into() + Rounding::from(rounding_expansion); + + let rect = RectShape::filled(rect.expand(spread), rounding, color); let pixels_per_point = 1.0; // doesn't matter here - let font_tex_size = [1; 2]; // unused size we are not tessellating text. + let font_tex_size = [1; 2]; // unused since we are not tessellating text. let mut tessellator = Tessellator::new( pixels_per_point, TessellationOptions { feathering: true, - feathering_size_in_pixels: extrusion * pixels_per_point, + feathering_size_in_pixels: blur * pixels_per_point, ..Default::default() }, font_tex_size, @@ -88,4 +69,20 @@ impl Shadow { tessellator.tessellate_rect(&rect, &mut mesh); mesh } + + /// How much larger than the parent rect are we in each direction? + pub fn margin(&self) -> Margin { + let Self { + offset, + blur, + spread, + color: _, + } = *self; + Margin { + left: spread + 0.5 * blur - offset.x, + right: spread + 0.5 * blur + offset.x, + top: spread + 0.5 * blur - offset.y, + bottom: spread + 0.5 * blur + offset.y, + } + } }