Fade in windows, tooltips, popups, etc (#4587)

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`.
This commit is contained in:
Emil Ernerfeldt 2024-05-30 16:22:12 +02:00 committed by GitHub
parent 6282844f65
commit 84d204246f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 74 additions and 37 deletions

View File

@ -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<Pos2>,
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();

View File

@ -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 --------------------------------

View File

@ -496,6 +496,7 @@ impl ContextImpl {
pivot: Align2::LEFT_TOP,
size: screen_rect.size(),
interactable: true,
last_became_visible_at: f64::NEG_INFINITY,
},
);

View File

@ -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)