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:
ZacharyZhang-NY committed 2026-09-25 11:37:38 -04:00
1 parent 05ff67ad9f
commit 0883a999f4
50 files changed
+2490 -330

No files matched your search

+2
View File
@@ -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};
+147
View File
@@ -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);
}
}