Skip to content

Commit

Permalink
Fade in windows, tooltips, popups, etc (#4587)
Browse files Browse the repository at this point in the history
All `Area`s now have a quick fade-in animation. You can turn it off with
`Area::fade_in` or `Window::fade_in` .

The `Window` fade-out animation is now nicer: it fades all elements of
the window, not just the frame.
It can be controlled with `Window::fade_out`.
  • Loading branch information
emilk authored May 30, 2024
1 parent 6282844 commit 84d2042
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 37 deletions.
74 changes: 41 additions & 33 deletions crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -88,6 +93,7 @@ pub struct Area {
pivot: Align2,
anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>,
fade_in: bool,
}

impl WidgetWithState for Area {
Expand All @@ -111,6 +117,7 @@ impl Area {
new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None,
fade_in: true,
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -328,6 +346,7 @@ impl Area {
anchor,
constrain,
constrain_rect,
fade_in,
} = self;

let layer_id = LayerId::new(order, id);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();

Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
34 changes: 31 additions & 3 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct Window<'open> {
collapsible: bool,
default_open: bool,
with_title_bar: bool,
fade_out: bool,
}

impl<'open> Window<'open> {
Expand All @@ -62,6 +63,7 @@ impl<'open> Window<'open> {
collapsible: true,
default_open: true,
with_title_bar: true,
fade_out: true,
}
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -402,6 +424,7 @@ impl<'open> Window<'open> {
collapsible,
default_open,
with_title_bar,
fade_out,
} = self;

let header_color =
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 --------------------------------
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ impl ContextImpl {
pivot: Align2::LEFT_TOP,
size: screen_rect.size(),
interactable: true,
last_became_visible_at: f64::NEG_INFINITY,
},
);

Expand Down
2 changes: 1 addition & 1 deletion crates/egui_demo_lib/src/demo/widget_gallery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 84d2042

Please sign in to comment.