T07a navigation tabs and paths: tabs, editor tabs, tab overflow menu, breadcrumb, pagination, steps and wizard; shared slide, list trigger and tab stop
This commit is contained in:
50 files changed
+2490
-330
No files matched your search
@@ -1,5 +1,7 @@
|
||||
mod changes;
|
||||
mod curve;
|
||||
mod slide;
|
||||
|
||||
pub(crate) use changes::changes;
|
||||
pub use curve::*;
|
||||
pub(crate) use slide::{Axis, Marker, glide, measure_item, measure_origin, slide};
|
||||
@@ -0,0 +1,147 @@
|
||||
use gpui::{
|
||||
App, Bounds, ElementId, Entity, IntoElement, Pixels, SharedString, Styled, Window, canvas,
|
||||
};
|
||||
|
||||
use super::spring;
|
||||
|
||||
/// Start and length of an item along the axis a marker slides on.
|
||||
pub(crate) type Span = (Pixels, Pixels);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
fn span(self, bounds: Bounds<Pixels>, origin: Pixels) -> Span {
|
||||
match self {
|
||||
Axis::Horizontal => (bounds.left() - origin, bounds.size.width),
|
||||
Axis::Vertical => (bounds.top() - origin, bounds.size.height),
|
||||
}
|
||||
}
|
||||
|
||||
fn start(self, bounds: Bounds<Pixels>) -> Pixels {
|
||||
match self {
|
||||
Axis::Horizontal => bounds.left(),
|
||||
Axis::Vertical => bounds.top(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a sliding marker's items sit, and where it last stood.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Slide {
|
||||
origin: Pixels,
|
||||
spans: Vec<Option<Span>>,
|
||||
from: Option<Span>,
|
||||
last: Option<SharedString>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
/// A marker's trip: from the last choice to this one, keyed by `generation`.
|
||||
pub(crate) struct Marker {
|
||||
pub from: Span,
|
||||
pub to: Span,
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
/// The slide over `values` with `selected` chosen; no marker until that item is measured.
|
||||
pub(crate) fn slide(
|
||||
id: impl Into<ElementId>,
|
||||
values: &[SharedString],
|
||||
selected: &SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (Entity<Slide>, Option<Marker>) {
|
||||
let chosen = values
|
||||
.iter()
|
||||
.position(|value| value == selected)
|
||||
.unwrap_or_else(|| panic!("no item {selected} to slide to"));
|
||||
let state = window.use_keyed_state(id, cx, |_, _| Slide::default());
|
||||
if state.read(cx).spans.len() != values.len() {
|
||||
state.update(cx, |slide, _| slide.spans = vec![None; values.len()]);
|
||||
}
|
||||
if state.read(cx).last.as_ref() != Some(selected) {
|
||||
state.update(cx, |slide, _| {
|
||||
let previous = slide
|
||||
.last
|
||||
.as_ref()
|
||||
.and_then(|last| values.iter().position(|value| value == last));
|
||||
slide.from = previous.and_then(|ix| slide.spans[ix]);
|
||||
slide.last = Some(selected.clone());
|
||||
slide.generation += 1;
|
||||
});
|
||||
}
|
||||
let marker = {
|
||||
let slide = state.read(cx);
|
||||
slide.spans[chosen].map(|to| Marker {
|
||||
from: slide.from.unwrap_or(to),
|
||||
to,
|
||||
generation: slide.generation,
|
||||
})
|
||||
};
|
||||
(state, marker)
|
||||
}
|
||||
|
||||
/// Records item `ix`'s span; place it as an absolute child that fills the item.
|
||||
pub(crate) fn measure_item(state: Entity<Slide>, ix: usize, axis: Axis) -> impl IntoElement {
|
||||
canvas(
|
||||
move |bounds: Bounds<Pixels>, _, cx| {
|
||||
let span = Some(axis.span(bounds, state.read(cx).origin));
|
||||
if state.read(cx).spans.get(ix) != Some(&span) {
|
||||
state.update(cx, |slide, cx| {
|
||||
slide.spans[ix] = span;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// Records where the container starts; place it as an absolute child that fills it.
|
||||
pub(crate) fn measure_origin(state: Entity<Slide>, axis: Axis) -> impl IntoElement {
|
||||
canvas(
|
||||
move |bounds: Bounds<Pixels>, _, cx| {
|
||||
let start = axis.start(bounds);
|
||||
if state.read(cx).origin != start {
|
||||
state.update(cx, |slide, cx| {
|
||||
slide.origin = start;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// A marker's span at `t` of its trip, overshooting a little.
|
||||
pub(crate) fn glide(from: Span, to: Span, t: f32) -> Span {
|
||||
let s = spring(t);
|
||||
let lerp = |a: Pixels, b: Pixels| a + (b - a) * s;
|
||||
(lerp(from.0, to.0), lerp(from.1, to.1))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::px;
|
||||
|
||||
use super::glide;
|
||||
|
||||
#[test]
|
||||
fn glide_starts_at_from_and_settles_on_to() {
|
||||
let (from, to) = ((px(0.0), px(40.0)), (px(100.0), px(60.0)));
|
||||
assert_eq!(glide(from, to, 0.0), from);
|
||||
let (left, width) = glide(from, to, 1.0);
|
||||
assert!((f32::from(left) - 100.0).abs() < 0.5 && (f32::from(width) - 60.0).abs() < 0.5);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user