206 lines
7.2 KiB
Rust
206 lines
7.2 KiB
Rust
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);
|
|
}
|
|
}
|