use std::collections::HashMap;
use gpui::canvas;
use gpui::{
App, Bounds, Div, ElementId, Entity, InteractiveElement as _, ParentElement as _, Pixels,
SharedString, StatefulInteractiveElement, Styled as _, Window, div, px,
};
use gpui_base::motion::{Transition, transition};
use gpui_component::ActiveTheme as _;
fn capture<E: gpui::ParentElement>(
element: E,
mut record: impl FnMut(Bounds<Pixels>, &mut Window, &mut App) + 'static,
) -> E {
element.child(
canvas(
move |bounds, window, cx| record(bounds, window, cx),
|_, _, _, _| {},
)
.absolute()
.inset_0(),
)
}
#[derive(Clone, Copy)]
struct RowGeometry {
bounds: Bounds<Pixels>,
painted: u64,
}
pub(crate) struct GlideHover {
rows: HashMap<SharedString, RowGeometry>,
layer: Option<Bounds<Pixels>>,
hovered: Option<SharedString>,
generation: u64,
epoch: u64,
departed: Option<u64>,
}
impl GlideHover {
fn new() -> Self {
Self {
rows: HashMap::new(),
layer: None,
hovered: None,
generation: 0,
epoch: 0,
departed: None,
}
}
}
pub(crate) fn glide_hover_state(
list: ElementId,
window: &mut Window,
cx: &mut App,
) -> Entity<GlideHover> {
window.use_keyed_state((list, "glide-hover"), cx, |_, _| GlideHover::new())
}
pub(crate) fn glide_frame<E>(frame: E, state: &Entity<GlideHover>) -> E
where
E: gpui::ParentElement,
{
let state = state.clone();
capture(frame, move |_, window, cx| {
state.update(cx, |state, cx| {
let painted = state.epoch;
state.rows.retain(|_, row| row.painted == painted);
if state
.hovered
.as_ref()
.is_some_and(|key| !state.rows.contains_key(key))
{
state.hovered = None;
cx.notify();
}
state.epoch = painted.wrapping_add(1);
});
if state.read(cx).hovered.is_some() {
window.request_animation_frame();
}
})
}
pub(crate) fn glide_row<E>(row: E, key: SharedString, state: &Entity<GlideHover>) -> E
where
E: StatefulInteractiveElement + gpui::ParentElement,
{
let bounds_state = state.clone();
let bounds_key = key.clone();
let hover_state = state.clone();
capture(row, move |bounds, window, cx| {
let moved = bounds_state.update(cx, |state, _| {
let epoch = state.epoch;
let previous = state.rows.insert(
bounds_key.clone(),
RowGeometry {
bounds,
painted: epoch,
},
);
let moved = state.hovered.as_ref() == Some(&bounds_key)
&& previous.is_some_and(|previous| previous.bounds != bounds);
if moved {
state.generation += 1;
}
moved
});
if moved {
window.request_animation_frame();
}
})
.on_hover(move |hovered, _, cx| {
hover_state.update(cx, |state, cx| {
if *hovered {
let arriving_from_rest =
state.hovered.is_none() && state.departed != Some(state.epoch);
if arriving_from_rest {
state.generation += 1;
}
if state.hovered.as_ref() != Some(&key) {
state.hovered = Some(key.clone());
cx.notify();
}
} else if state.hovered.as_ref() == Some(&key) {
state.hovered = None;
state.departed = Some(state.epoch);
cx.notify();
}
});
})
}
pub(crate) fn glide_highlight(
list: ElementId,
state: &Entity<GlideHover>,
radius: Pixels,
debug_selector: &'static str,
window: &mut Window,
cx: &mut App,
) -> Div {
let placed = placement(state, cx);
let Some((target, generation)) = placed else {
return highlight_layer(state);
};
let duration = if crate::motion::motion_is_full(cx) {
crate::motion::MotionTokens::read(cx).hover_glide()
} else {
std::time::Duration::ZERO
};
let mut channel = |name: &str, value: f32| {
transition(
ElementId::Name(format!("{list:?}-glide-{name}-{generation}").into()),
value,
Transition::new(duration).ease(crate::motion::ease_out_quint),
window,
cx,
)
};
let x = channel("x", f32::from(target.origin.x));
let y = channel("y", f32::from(target.origin.y));
let w = channel("w", f32::from(target.size.width));
let h = channel("h", f32::from(target.size.height));
highlight_layer(state).child(
div()
.absolute()
.left(px(x))
.top(px(y))
.w(px(w))
.h(px(h))
.rounded(radius)
.bg(cx.theme().list_hover)
.debug_selector(move || debug_selector.to_owned()),
)
}
fn placement(state: &Entity<GlideHover>, cx: &App) -> Option<(Bounds<Pixels>, u64)> {
let state = state.read(cx);
let layer = state.layer?;
let hovered = state.hovered.as_ref()?;
let row = state
.rows
.get(hovered)
.filter(|row| row.painted == state.epoch)?
.bounds;
Some((
Bounds::new(row.origin - layer.origin, row.size),
state.generation,
))
}
fn highlight_layer(state: &Entity<GlideHover>) -> Div {
let state = state.clone();
capture(div().absolute().inset_0(), move |bounds, window, cx| {
let moved = state.update(cx, |state, _| {
let moved = state
.layer
.is_some_and(|layer| layer.origin != bounds.origin);
state.layer = Some(bounds);
moved
});
if moved {
window.request_animation_frame();
}
})
}