T03 layout: stacks, grids, surfaces, scroll, splits, sheets, dock, panes, workspace, viewport
This commit is contained in:
37 files changed
+5283
-50
No files matched your search
@@ -0,0 +1,251 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Axis, Div, ElementId, EmptyView, Entity, EntityId, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, RenderOnce, ScrollHandle,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Window, div, point, prelude::*,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::ScrollShadow;
|
||||
use crate::{motion, theme::ActiveTheme};
|
||||
|
||||
const IDLE: Duration = Duration::from_millis(900);
|
||||
|
||||
struct Activity {
|
||||
handle: ScrollHandle,
|
||||
last: Point<Pixels>,
|
||||
active_at: Option<Instant>,
|
||||
hovered: bool,
|
||||
}
|
||||
|
||||
/// Visibility 0..=1: full while active, fading after `IDLE`.
|
||||
fn presence(activity: &Activity, fade: Duration) -> f32 {
|
||||
if activity.hovered {
|
||||
return 1.0;
|
||||
}
|
||||
let Some(at) = activity.active_at else {
|
||||
return 0.0;
|
||||
};
|
||||
let quiet = at.elapsed().saturating_sub(IDLE);
|
||||
1.0 - (quiet.as_secs_f32() / fade.as_secs_f32()).min(1.0)
|
||||
}
|
||||
|
||||
/// Thumb start at press, and the pointer's first position.
|
||||
struct ThumbDrag {
|
||||
owner: EntityId,
|
||||
start: Pixels,
|
||||
anchor: Rc<Cell<Option<Pixels>>>,
|
||||
}
|
||||
|
||||
/// Overlay thumb for a `ScrollHandle`; drag it to scroll.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Scrollbar {
|
||||
id: ElementId,
|
||||
handle: ScrollHandle,
|
||||
axis: Axis,
|
||||
presence: f32,
|
||||
}
|
||||
|
||||
impl Scrollbar {
|
||||
pub fn new(id: impl Into<ElementId>, handle: &ScrollHandle, axis: Axis) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
handle: handle.clone(),
|
||||
axis,
|
||||
presence: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fades the thumb; 0 hides it.
|
||||
pub fn presence(mut self, presence: f32) -> Self {
|
||||
self.presence = presence.clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Scrollbar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let owner = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_, _| ())
|
||||
.entity_id();
|
||||
let theme = cx.theme();
|
||||
let rem = window.rem_size();
|
||||
let thickness = theme.scrollbar_thickness().to_pixels(rem);
|
||||
let min_thumb = theme.scrollbar_min_thumb().to_pixels(rem);
|
||||
let (view, max, offset) = (
|
||||
self.handle.bounds().size,
|
||||
self.handle.max_offset(),
|
||||
self.handle.offset(),
|
||||
);
|
||||
let (span, reach, scrolled) = match self.axis {
|
||||
Axis::Vertical => (view.height, max.height, -offset.y),
|
||||
Axis::Horizontal => (view.width, max.width, -offset.x),
|
||||
};
|
||||
let length = (span * (span / (span + reach))).max(min_thumb).min(span);
|
||||
let travel = span - length;
|
||||
if reach <= Pixels::ZERO || travel <= Pixels::ZERO || self.presence <= 0.0 {
|
||||
return div().into_any_element();
|
||||
}
|
||||
let start = travel * (scrolled / reach).clamp(0.0, 1.0);
|
||||
let axis = self.axis;
|
||||
let handle = self.handle.clone();
|
||||
let (idle, busy) = (theme.colors.fg.opacity(0.22), theme.colors.fg.opacity(0.42));
|
||||
let thumb = div()
|
||||
.id("thumb")
|
||||
.absolute()
|
||||
.rounded_full()
|
||||
.bg(idle)
|
||||
.hover(|style| style.bg(busy))
|
||||
.on_drag(
|
||||
ThumbDrag {
|
||||
owner,
|
||||
start,
|
||||
anchor: Rc::new(Cell::new(None)),
|
||||
},
|
||||
|_, _, _, cx| cx.new(|_| EmptyView),
|
||||
)
|
||||
.map(|thumb| match axis {
|
||||
Axis::Vertical => thumb.top(start).left_0().w(thickness).h(length),
|
||||
Axis::Horizontal => thumb.left(start).top_0().h(thickness).w(length),
|
||||
});
|
||||
let track = div()
|
||||
.id(self.id)
|
||||
.absolute()
|
||||
.opacity(self.presence)
|
||||
.on_drag_move(move |event: &gpui::DragMoveEvent<ThumbDrag>, window, cx| {
|
||||
let drag = event.drag(cx);
|
||||
if drag.owner != owner {
|
||||
return;
|
||||
}
|
||||
let local = event.event.position - event.bounds.origin;
|
||||
let along = match axis {
|
||||
Axis::Vertical => local.y,
|
||||
Axis::Horizontal => local.x,
|
||||
};
|
||||
let anchor = match drag.anchor.get() {
|
||||
Some(anchor) => anchor,
|
||||
None => {
|
||||
drag.anchor.set(Some(along));
|
||||
along
|
||||
}
|
||||
};
|
||||
let ratio = ((drag.start + along - anchor) / travel).clamp(0.0, 1.0);
|
||||
let target = -(reach * ratio);
|
||||
let current = handle.offset();
|
||||
handle.set_offset(match axis {
|
||||
Axis::Vertical => point(current.x, target),
|
||||
Axis::Horizontal => point(target, current.y),
|
||||
});
|
||||
window.refresh();
|
||||
});
|
||||
match axis {
|
||||
Axis::Vertical => track
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.w(thickness * 2.0)
|
||||
.pl(thickness * 0.5),
|
||||
Axis::Horizontal => track
|
||||
.left_0()
|
||||
.right_0()
|
||||
.bottom_0()
|
||||
.h(thickness * 2.0)
|
||||
.pt(thickness * 0.5),
|
||||
}
|
||||
.child(thumb)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll region with overlay scrollbars that show while in use.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ScrollArea {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
shadows: bool,
|
||||
body: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: div(),
|
||||
shadows: false,
|
||||
body: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft edges where content continues.
|
||||
pub fn shadows(mut self) -> Self {
|
||||
self.shadows = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ScrollArea {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ScrollArea {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.body.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
fn track_activity(state: &Entity<Activity>, window: &mut Window, cx: &mut App) -> f32 {
|
||||
let offset = state.read(cx).handle.offset();
|
||||
if state.read(cx).last != offset {
|
||||
state.update(cx, |activity, _| {
|
||||
activity.last = offset;
|
||||
activity.active_at = Some(Instant::now());
|
||||
});
|
||||
}
|
||||
let fade = motion::duration(motion::SLOW, cx);
|
||||
let presence = presence(state.read(cx), fade);
|
||||
if presence > 0.0 && !state.read(cx).hovered {
|
||||
window.request_animation_frame();
|
||||
}
|
||||
presence
|
||||
}
|
||||
|
||||
impl RenderOnce for ScrollArea {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| Activity {
|
||||
handle: ScrollHandle::new(),
|
||||
last: Point::default(),
|
||||
active_at: None,
|
||||
hovered: false,
|
||||
});
|
||||
let handle = state.read(cx).handle.clone();
|
||||
let presence = track_activity(&state, window, cx);
|
||||
self.base
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.on_hover(move |hovered, _, cx| {
|
||||
state.update(cx, |activity, cx| {
|
||||
activity.hovered = *hovered;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id("scroll-body")
|
||||
.size_full()
|
||||
.overflow_scroll()
|
||||
.track_scroll(&handle)
|
||||
.children(self.body),
|
||||
)
|
||||
.when(self.shadows, |area| area.child(ScrollShadow::new(&handle)))
|
||||
.child(Scrollbar::new("scrollbar-y", &handle, Axis::Vertical).presence(presence))
|
||||
.child(Scrollbar::new("scrollbar-x", &handle, Axis::Horizontal).presence(presence))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user