Files
Ely-GPUI-Components/src/layout/sheet.rs
T

303 lines
9.6 KiB
Rust

use std::{cell::Cell, rc::Rc};
use gpui::{
Animation, AnimationExt, AnyElement, App, DragMoveEvent, ElementId, EmptyView, EntityId,
FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window, canvas, div, prelude::*, relative,
};
use smallvec::SmallVec;
use crate::{
buttons::IconButton,
motion,
primitives::{Backdrop, IconName, Place},
theme::{ActiveTheme, Elevation, Radius, TextSize},
};
type Close = Rc<dyn Fn(&mut Window, &mut App)>;
fn title_row(title: SharedString, close: Close, cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
div()
.flex()
.items_center()
.justify_between()
.gap_3()
.child(
div()
.text_size(theme.text_size(TextSize::Lg))
.font_weight(FontWeight::SEMIBOLD)
.child(title),
)
.child(
IconButton::new("close", IconName::X).on_click(move |_, window, cx| close(window, cx)),
)
}
/// Panel that slides in from an edge over a scrim.
#[derive(IntoElement)]
pub struct Sheet {
id: ElementId,
side: Place,
title: SharedString,
on_close: Close,
body: SmallVec<[AnyElement; 2]>,
}
impl Sheet {
/// `side` must be an edge, not `Place::Center`.
pub fn new(
id: impl Into<ElementId>,
side: Place,
title: impl Into<SharedString>,
on_close: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
assert!(side != Place::Center, "a sheet needs an edge");
Self {
id: id.into(),
side,
title: title.into(),
on_close: Rc::new(on_close),
body: SmallVec::new(),
}
}
}
impl ParentElement for Sheet {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.body.extend(elements);
}
}
impl RenderOnce for Sheet {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let size = theme.sheet_size();
let travel = size.to_pixels(window.rem_size());
let side = self.side;
let panel = div()
.flex()
.flex_col()
.gap_5()
.p_6()
.bg(theme.colors.overlay)
.shadow(theme.elevation(Elevation::Modal))
.map(|panel| match side {
Place::Left | Place::Right => panel.w(size).h_full(),
Place::Top | Place::Bottom | Place::Center => panel.h(size).w_full(),
})
.child(title_row(self.title, self.on_close.clone(), cx))
.children(self.body)
.with_animation(
"sheet-in",
Animation::new(motion::duration(motion::BASE, cx))
.with_easing(motion::ease_out_cubic),
move |panel, t| {
let hidden = -(travel * (1.0 - t));
match side {
Place::Left => panel.ml(hidden),
Place::Right => panel.mr(hidden),
Place::Top => panel.mt(hidden),
Place::Bottom | Place::Center => panel.mb(hidden),
}
},
);
let close = self.on_close;
Backdrop::new(self.id)
.place(side)
.on_dismiss(move |window, cx| close(window, cx))
.child(panel)
}
}
#[derive(Default)]
struct Pull {
pulling: bool,
offset: Pixels,
released: Pixels,
snaps: u64,
height: Pixels,
}
struct DrawerPull {
owner: EntityId,
anchor: Rc<Cell<Option<Pixels>>>,
}
/// Share of the drawer's height a pull must pass to dismiss it.
const DISMISS: f32 = 0.3;
/// Bottom panel with a grab handle; pull it down to dismiss.
#[derive(IntoElement)]
pub struct Drawer {
id: ElementId,
on_close: Close,
body: SmallVec<[AnyElement; 2]>,
}
impl Drawer {
pub fn new(
id: impl Into<ElementId>,
on_close: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
on_close: Rc::new(on_close),
body: SmallVec::new(),
}
}
}
impl ParentElement for Drawer {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.body.extend(elements);
}
}
impl RenderOnce for Drawer {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| Pull::default());
let owner = state.entity_id();
let (offset, released, snaps, threshold) = {
let pull = state.read(cx);
(
pull.offset,
pull.released,
pull.snaps,
pull.height * DISMISS,
)
};
let theme = cx.theme();
let grip = theme.grip();
let travel = theme.sheet_size().to_pixels(window.rem_size());
let quick = motion::duration(motion::BASE, cx);
let (move_state, release, out, measure) =
(state.clone(), state.clone(), state.clone(), state);
let close = self.on_close.clone();
let finish = move |pull: &mut Pull| -> bool {
let dismiss = pull.pulling && pull.offset > threshold;
log::info!("drawer: pulled {:?} of {threshold:?}", pull.offset);
pull.pulling = false;
pull.released = pull.offset;
pull.offset = Pixels::ZERO;
pull.snaps += 1;
dismiss
};
let finish_out = finish;
let (close_up, close_out) = (close.clone(), close);
let panel = div()
.id("drawer-panel")
.relative()
.w_full()
.max_h(relative(0.85))
.flex()
.flex_col()
.gap_4()
.px_6()
.pb_8()
.rounded_t(theme.radius(Radius::Xl))
.bg(theme.colors.overlay)
.shadow(theme.elevation(Elevation::Modal))
.mb(-offset)
.on_drag_move(move |event: &DragMoveEvent<DrawerPull>, _, cx| {
let pull = event.drag(cx);
if pull.owner != owner {
return;
}
let y = event.event.position.y;
let anchor = pull.anchor.get().unwrap_or_else(|| {
pull.anchor.set(Some(y));
y
});
move_state.update(cx, |pull, cx| {
pull.pulling = true;
pull.offset = (y - anchor).max(Pixels::ZERO);
cx.notify();
})
})
.on_mouse_up(MouseButton::Left, move |_, window, cx| {
let pulled = release.read(cx).pulling;
if pulled
&& release.update(cx, |pull, cx| {
cx.notify();
finish(pull)
})
{
close_up(window, cx);
}
})
.on_mouse_up_out(MouseButton::Left, move |_, window, cx| {
let pulled = out.read(cx).pulling;
if pulled
&& out.update(cx, |pull, cx| {
cx.notify();
finish_out(pull)
})
{
close_out(window, cx);
}
})
.child(
canvas(
move |bounds, _, cx| {
if measure.read(cx).height != bounds.size.height {
measure.update(cx, |pull, _| pull.height = bounds.size.height);
}
},
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_full(),
)
.child(
div()
.id("drawer-grab")
.flex()
.justify_center()
.pt_3()
.pb_2()
.cursor_grab()
.on_drag(
DrawerPull {
owner,
anchor: Rc::new(Cell::new(None)),
},
|_, _, _, cx| cx.new(|_| EmptyView),
)
.child(
div()
.w(grip.width)
.h(grip.height)
.rounded_full()
.bg(theme.colors.border_strong),
),
)
.children(self.body);
let snapping = snaps > 0 && released > Pixels::ZERO;
let settled = div().w_full().child(panel);
let settled = if snapping {
settled
.with_animation(
("drawer-snap", snaps),
Animation::new(quick).with_easing(motion::ease_out_cubic),
move |wrap, t| wrap.mb(-(released * (1.0 - t))),
)
.into_any_element()
} else {
settled.into_any_element()
};
let entering = div().w_full().child(settled).with_animation(
"drawer-in",
Animation::new(quick).with_easing(motion::ease_out_cubic),
move |wrap, t| wrap.mb(-(travel * (1.0 - t))),
);
let close = self.on_close;
Backdrop::new(self.id)
.place(Place::Bottom)
.on_dismiss(move |window, cx| close(window, cx))
.child(entering)
}
}