diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index df9e9db8668..33e3fae9783 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -26,6 +26,11 @@ pub struct AreaState { /// If false, clicks goes straight through to what is behind us. Useful for tooltips etc. pub interactable: bool, + + /// At what time was this area first shown? + /// + /// Used to fade in the area. + pub last_became_visible_at: f64, } impl AreaState { @@ -88,6 +93,7 @@ pub struct Area { pivot: Align2, anchor: Option<(Align2, Vec2)>, new_pos: Option, + fade_in: bool, } impl WidgetWithState for Area { @@ -111,6 +117,7 @@ impl Area { new_pos: None, pivot: Align2::LEFT_TOP, anchor: None, + fade_in: true, } } @@ -282,6 +289,15 @@ impl Area { Align2::LEFT_TOP } } + + /// If `true`, quickly fade in the area. + /// + /// Default: `true`. + #[inline] + pub fn fade_in(mut self, fade_in: bool) -> Self { + self.fade_in = fade_in; + self + } } pub(crate) struct Prepared { @@ -298,6 +314,8 @@ pub(crate) struct Prepared { /// and then can correctly position the window and its contents the next frame, /// without having one frame where the window is wrongly positioned or sized. sizing_pass: bool, + + fade_in: bool, } impl Area { @@ -328,6 +346,7 @@ impl Area { anchor, constrain, constrain_rect, + fade_in, } = self; let layer_id = LayerId::new(order, id); @@ -363,11 +382,19 @@ impl Area { pivot, size, interactable, + last_became_visible_at: ctx.input(|i| i.time), } }); state.pivot_pos = new_pos.unwrap_or(state.pivot_pos); state.interactable = interactable; + // TODO(emilk): if last frame was sizing pass, it should be considered invisible for smmother fade-in + let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)); + + if !visible_last_frame { + state.last_became_visible_at = ctx.input(|i| i.time); + } + if let Some((anchor, offset)) = anchor { let screen = ctx.available_rect(); state.set_left_top_pos( @@ -421,7 +448,7 @@ impl Area { state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos())); - // Update responsbe with posisbly moved/constrained rect: + // Update response with possibly moved/constrained rect: move_response.rect = state.rect(); move_response.interact_rect = state.rect(); @@ -433,34 +460,7 @@ impl Area { constrain, constrain_rect, sizing_pass: is_new, - } - } - - pub fn show_open_close_animation(&self, ctx: &Context, frame: &Frame, is_open: bool) { - // must be called first so animation managers know the latest state - let visibility_factor = ctx.animate_bool(self.id.with("close_animation"), is_open); - - if is_open { - // we actually only show close animations. - // when opening a window we show it right away. - return; - } - if visibility_factor <= 0.0 { - return; - } - - let layer_id = LayerId::new(self.order, self.id); - let area_rect = AreaState::load(ctx, self.id).map(|state| state.rect()); - if let Some(area_rect) = area_rect { - let clip_rect = Rect::EVERYTHING; - let painter = Painter::new(ctx.clone(), layer_id, clip_rect); - - // shrinkage: looks kinda a bad on its own - // let area_rect = - // Rect::from_center_size(area_rect.center(), visibility_factor * area_rect.size()); - - let frame = frame.multiply_with_opacity(visibility_factor); - painter.add(frame.paint(area_rect)); + fade_in, } } } @@ -509,6 +509,17 @@ impl Prepared { max_rect, clip_rect, ); + + if self.fade_in { + let age = + ctx.input(|i| (i.time - self.state.last_became_visible_at) as f32 + i.predicted_dt); + let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0); + ui.multiply_opacity(opacity); + if opacity < 1.0 { + ctx.request_repaint(); + } + } + ui.set_enabled(self.enabled); if self.sizing_pass { ui.set_sizing_pass(); @@ -522,10 +533,7 @@ impl Prepared { layer_id, mut state, move_response, - enabled: _, - constrain: _, - constrain_rect: _, - sizing_pass: _, + .. } = self; state.size = content_ui.min_size(); diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 9bf3924e0f2..cc6cea3f9e3 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -40,6 +40,7 @@ pub struct Window<'open> { collapsible: bool, default_open: bool, with_title_bar: bool, + fade_out: bool, } impl<'open> Window<'open> { @@ -62,6 +63,7 @@ impl<'open> Window<'open> { collapsible: true, default_open: true, with_title_bar: true, + fade_out: true, } } @@ -111,6 +113,26 @@ impl<'open> Window<'open> { self } + /// If `true`, quickly fade in the `Window` when it first appears. + /// + /// Default: `true`. + #[inline] + pub fn fade_in(mut self, fade_in: bool) -> Self { + self.area = self.area.fade_in(fade_in); + self + } + + /// If `true`, quickly fade out the `Window` when it closes. + /// + /// This only works if you use [`Self::open`] to close the window. + /// + /// Default: `true`. + #[inline] + pub fn fade_out(mut self, fade_out: bool) -> Self { + self.fade_out = fade_out; + self + } + /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))` // TODO(emilk): I'm not sure this is a good interface for this. #[inline] @@ -402,6 +424,7 @@ impl<'open> Window<'open> { collapsible, default_open, with_title_bar, + fade_out, } = self; let header_color = @@ -415,9 +438,8 @@ impl<'open> Window<'open> { let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); - area.show_open_close_animation(ctx, &window_frame, is_open); - - if !is_open { + let opacity = ctx.animate_bool(area.id.with("fade-out"), is_open); + if opacity <= 0.0 { return None; } @@ -477,6 +499,12 @@ impl<'open> Window<'open> { ); let mut area_content_ui = area.content_ui(ctx); + if is_open { + // `Area` already takes care of fade-in animations, + // so we only need to handle fade-out animations here. + } else if fade_out { + area_content_ui.multiply_opacity(opacity); + } let content_inner = { // BEGIN FRAME -------------------------------- diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 54cdae84de9..9884de41fff 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -496,6 +496,7 @@ impl ContextImpl { pivot: Align2::LEFT_TOP, size: screen_rect.size(), interactable: true, + last_became_visible_at: f64::NEG_INFINITY, }, ); diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index babc8dd9568..0496d8963ef 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -63,7 +63,7 @@ impl super::View for WidgetGallery { fn ui(&mut self, ui: &mut egui::Ui) { ui.add_enabled_ui(self.enabled, |ui| { ui.set_visible(self.visible); - ui.set_opacity(self.opacity); + ui.multiply_opacity(self.opacity); egui::Grid::new("my_grid") .num_columns(2)