T12c Data Display: Carousel, Gallery, BeforeAfter, Watermark (review pending: codex 401)
This commit is contained in:
19 files changed
+1004
-12
No files matched your search
@@ -0,0 +1,205 @@
|
||||
use gpui::{
|
||||
AnyElement, App, Bounds, Div, DragMoveEvent, ElementId, EmptyView, EntityId,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Window, canvas, div, prelude::*, relative,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
primitives::{FocusRing, Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, Elevation, IconSize},
|
||||
};
|
||||
|
||||
/// A drag of one comparison's divider.
|
||||
struct Divider {
|
||||
owner: EntityId,
|
||||
}
|
||||
|
||||
/// Where the divider stands, as a share of its travel, and the box it travels in.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Split {
|
||||
share: f32,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
/// The share of the travel under `at`; the travel stops `inset` short of each side. None when the box is narrower than the handle.
|
||||
fn share_at(at: Point<Pixels>, bounds: Bounds<Pixels>, inset: Pixels) -> Option<f32> {
|
||||
let travel = bounds.size.width - inset * 2.0;
|
||||
(travel > Pixels::ZERO).then(|| ((at.x - bounds.left() - inset) / travel).clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// Two versions of a picture, one over the other: `before` shows left of the divider, `after` right of it. Drag the divider, press anywhere, or use Left and Right. Give it a size.
|
||||
#[derive(IntoElement)]
|
||||
pub struct BeforeAfter {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
before: AnyElement,
|
||||
after: AnyElement,
|
||||
}
|
||||
|
||||
impl BeforeAfter {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
before: impl IntoElement,
|
||||
after: impl IntoElement,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: div(),
|
||||
before: before.into_any_element(),
|
||||
after: after.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for BeforeAfter {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BeforeAfter {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let focus = tab_stop((self.id.clone(), "focus").into(), true, window, cx);
|
||||
let split = window.use_keyed_state((self.id.clone(), "split"), cx, |_, _| Split {
|
||||
share: 0.5,
|
||||
bounds: Bounds::default(),
|
||||
});
|
||||
let Split { share, bounds } = *split.read(cx);
|
||||
let owner = split.entity_id();
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let handle = theme.control_height(ControlSize::Lg);
|
||||
let inset = handle.to_pixels(window.rem_size()) / 2.0;
|
||||
let width = bounds.size.width;
|
||||
let x = inset + (width - inset * 2.0) * share;
|
||||
let set = {
|
||||
let (split, id) = (split.clone(), self.id.clone());
|
||||
move |share: f32, cx: &mut App| {
|
||||
split.update(cx, |split, cx| {
|
||||
split.share = share;
|
||||
log::debug!("before-after {id:?}: divider at {share:.2}");
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
};
|
||||
let (press, drag, keys, measured) = (set.clone(), set.clone(), set, split.clone());
|
||||
let overlay = (width > Pixels::ZERO).then(|| {
|
||||
[
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w(x)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w(width)
|
||||
.child(self.before),
|
||||
)
|
||||
.into_any_element(),
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.left(x)
|
||||
.border_l_1()
|
||||
.border_color(colors.on_media)
|
||||
.into_any_element(),
|
||||
div()
|
||||
.absolute()
|
||||
.top(relative(0.5))
|
||||
.mt(handle * -0.5)
|
||||
.left(x - inset)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(handle)
|
||||
.rounded_full()
|
||||
.bg(colors.on_media)
|
||||
.shadow(theme.elevation(Elevation::Raised))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronsLeftRight)
|
||||
.size(IconSize::Sm)
|
||||
.color(colors.media_backdrop),
|
||||
)
|
||||
.into_any_element(),
|
||||
]
|
||||
});
|
||||
self.base
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(gpui::transparent_black())
|
||||
.track_focus(&focus)
|
||||
.focus_ring(cx)
|
||||
.cursor_ew_resize()
|
||||
.on_mouse_down(MouseButton::Left, move |event, _, cx| {
|
||||
let bounds = measured.read(cx).bounds;
|
||||
if let Some(share) = share_at(event.position, bounds, inset) {
|
||||
press(share, cx);
|
||||
}
|
||||
})
|
||||
.on_drag(Divider { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
|
||||
.on_drag_move(move |event: &DragMoveEvent<Divider>, _, cx| {
|
||||
if event.drag(cx).owner != owner {
|
||||
return;
|
||||
}
|
||||
if let Some(share) = share_at(event.event.position, bounds, inset) {
|
||||
drag(share, cx);
|
||||
}
|
||||
})
|
||||
.on_key_down(move |event, _, cx| {
|
||||
let to = match event.keystroke.key.as_str() {
|
||||
"left" => share - 0.05,
|
||||
"right" => share + 0.05,
|
||||
_ => return,
|
||||
};
|
||||
cx.stop_propagation();
|
||||
keys(to.clamp(0.0, 1.0), cx);
|
||||
})
|
||||
.child(div().absolute().inset_0().child(self.after))
|
||||
.children(overlay.into_iter().flatten())
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, window, cx| {
|
||||
if split.read(cx).bounds != bounds {
|
||||
split.update(cx, |split, cx| {
|
||||
split.bounds = bounds;
|
||||
cx.notify();
|
||||
});
|
||||
window.request_animation_frame();
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{Bounds, point, px, size};
|
||||
|
||||
use super::share_at;
|
||||
|
||||
#[test]
|
||||
fn the_divider_travels_inside_the_handle_inset() {
|
||||
let bounds = Bounds::new(point(px(100.0), px(0.0)), size(px(240.0), px(100.0)));
|
||||
let at = |x: f32| share_at(point(px(x), px(50.0)), bounds, px(20.0));
|
||||
assert_eq!(at(120.0), Some(0.0));
|
||||
assert_eq!(at(220.0), Some(0.5));
|
||||
assert_eq!(at(320.0), Some(1.0));
|
||||
assert_eq!(at(90.0), Some(0.0), "left of the box holds at the start");
|
||||
assert_eq!(at(400.0), Some(1.0));
|
||||
let narrow = Bounds::new(point(px(0.0), px(0.0)), size(px(30.0), px(30.0)));
|
||||
assert_eq!(share_at(point(px(10.0), px(10.0)), narrow, px(20.0)), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user