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

+143 -105
View File
@@ -8,12 +8,11 @@ use gpui::{
use super::{
Choice,
check::tab_stop,
input::text_size,
options::{OnValue, Pick, Popup, step},
};
use crate::{
primitives::{Icon, IconName},
primitives::{Icon, IconName, tab_stop},
theme::{ActiveTheme, ControlSize, IconSize, Radius},
};
@@ -125,6 +124,114 @@ pub(crate) fn field_text(
.child(text.unwrap_or(placeholder))
}
/// The list a trigger opens: its rows, the one chosen, and whether the trigger holds focus.
pub(crate) struct Listing<'a> {
pub id: &'a ElementId,
pub rows: Rc<Vec<Choice>>,
pub selected: Option<&'a SharedString>,
pub focused: bool,
}
/// Opens `list` from `trigger`: a press or Down toggles it, arrows walk it, Enter picks, Escape or blur closes.
pub(crate) fn listing(
list: Listing,
trigger: Stateful<Div>,
on_pick: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
window: &mut Window,
cx: &mut App,
) -> Stateful<Div> {
let Listing {
id,
rows,
selected,
focused,
} = list;
let picker = window.use_keyed_state((id.clone(), "picker"), cx, |_, _| Picker::default());
if picker.read(cx).open && !focused {
log::info!("list {id:?}: closed on blur");
picker.update(cx, |picker, _| picker.open = false);
}
let current = selected.and_then(|value| rows.iter().position(|row| row.value == *value));
let start = current.unwrap_or_else(|| step(&rows, rows.len() - 1, 1));
if picker.read(cx).highlighted >= rows.len() {
picker.update(cx, |picker, _| picker.highlighted = start);
}
let (open, highlighted, anchor, scroll) = {
let picker = picker.read(cx);
(
picker.open,
picker.highlighted,
picker.anchor,
picker.scroll.clone(),
)
};
let pick: Pick = {
let (rows, picker) = (rows.clone(), picker.clone());
Rc::new(move |ix, window, cx| {
if rows[ix].disabled {
return;
}
Picker::show(&picker, false, ix, cx);
on_pick(&rows[ix].value, window, cx);
})
};
let chosen: Vec<SharedString> = current
.map(|ix| rows[ix].value.clone())
.into_iter()
.collect();
let (toggle, keys, close, keyed_rows, enter) = (
picker.clone(),
picker.clone(),
picker.clone(),
rows.clone(),
pick.clone(),
);
trigger
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
Picker::show(&toggle, !open, start, cx)
})
.on_key_down(move |event, window, cx| {
let at = keys.read(cx).highlighted;
let key = event.keystroke.key.as_str();
if !open {
if matches!(key, "down" | "up" | "enter" | "space") {
cx.stop_propagation();
Picker::show(&keys, true, start, cx);
}
return;
}
if let Some(to) = moved(event, &keyed_rows, at) {
cx.stop_propagation();
Picker::show(&keys, true, to, cx);
} else if matches!(key, "enter" | "space") {
cx.stop_propagation();
enter(at, window, cx);
} else if key == "escape" {
cx.stop_propagation();
Picker::show(&keys, false, at, cx);
}
})
.child(measure_anchor(picker))
.when(open, |trigger| {
trigger.child(
Popup {
id: (id.clone(), "list").into(),
anchor,
rows: &rows,
highlighted: Some(highlighted),
checked: Some(&chosen),
pick,
dismiss: Some(Rc::new(move |_, cx| {
let at = close.read(cx).highlighted;
Picker::show(&close, false, at, cx)
})),
scroll: Some(&scroll),
}
.render(window, cx),
)
})
}
/// A button that opens a list and shows the one chosen.
#[derive(IntoElement)]
pub struct Select {
@@ -192,62 +299,14 @@ impl RenderOnce for Select {
window,
cx,
);
let focused = focus.is_focused(window);
let picker =
window.use_keyed_state((self.id.clone(), "picker"), cx, |_, _| Picker::default());
if picker.read(cx).open && !focused {
log::info!("select {:?}: closed on blur", self.id);
picker.update(cx, |picker, _| picker.open = false);
}
let choices = Rc::new(self.choices);
let current = self
let shown = self
.selected
.as_ref()
.and_then(|value| choices.iter().position(|choice| choice.value == *value));
let start = current.unwrap_or_else(|| step(&choices, choices.len() - 1, 1));
if picker.read(cx).highlighted >= choices.len() {
picker.update(cx, |picker, _| picker.highlighted = start);
}
let (open, highlighted, anchor, scroll) = {
let picker = picker.read(cx);
(
picker.open,
picker.highlighted,
picker.anchor,
picker.scroll.clone(),
)
};
let pick: Pick = {
let (id, choices, picker, on_change) = (
self.id.clone(),
choices.clone(),
picker.clone(),
self.on_change,
);
Rc::new(move |ix, window, cx| {
if choices[ix].disabled {
return;
}
let value = choices[ix].value.clone();
log::info!("select {id:?}: {value}");
Picker::show(&picker, false, ix, cx);
if let Some(on_change) = &on_change {
on_change(&value, window, cx);
}
})
};
let theme = cx.theme();
let colors = &theme.colors;
let shown = current.map(|ix| &choices[ix]);
let (toggle, keys, rows, close) = (
picker.clone(),
picker.clone(),
choices.clone(),
picker.clone(),
);
let enter = pick.clone();
let chosen: Vec<SharedString> = shown.iter().map(|choice| choice.value.clone()).collect();
field_button(
.and_then(|value| choices.iter().find(|choice| choice.value == *value))
.cloned();
let colors = &cx.theme().colors;
let trigger = field_button(
self.id.clone(),
&focus,
self.size,
@@ -255,38 +314,14 @@ impl RenderOnce for Select {
window,
cx,
)
.when(!self.disabled, |trigger| {
trigger
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
Picker::show(&toggle, !open, start, cx)
})
.on_key_down(move |event, window, cx| {
let at = keys.read(cx).highlighted;
let key = event.keystroke.key.as_str();
if !open {
if matches!(key, "down" | "up" | "enter" | "space") {
cx.stop_propagation();
Picker::show(&keys, true, start, cx);
}
return;
}
if let Some(to) = moved(event, &rows, at) {
cx.stop_propagation();
Picker::show(&keys, true, to, cx);
} else if matches!(key, "enter" | "space") {
cx.stop_propagation();
enter(at, window, cx);
} else if key == "escape" {
cx.stop_propagation();
Picker::show(&keys, false, at, cx);
}
})
})
.when_some(shown.and_then(|choice| choice.icon), |trigger, icon| {
trigger.child(Icon::new(icon).size(IconSize::Sm).color(colors.fg_muted))
})
.when_some(
shown.as_ref().and_then(|choice| choice.icon),
|trigger, icon| {
trigger.child(Icon::new(icon).size(IconSize::Sm).color(colors.fg_muted))
},
)
.child(field_text(
shown.map(|choice| choice.label.clone()),
shown.map(|choice| choice.label),
self.placeholder,
self.disabled,
cx,
@@ -295,25 +330,28 @@ impl RenderOnce for Select {
Icon::new(IconName::ChevronsUpDown)
.size(IconSize::Xs)
.color(colors.fg_subtle),
)
.child(measure_anchor(picker))
.when(open, |trigger| {
trigger.child(
Popup {
id: (self.id, "list").into(),
anchor,
rows: &choices,
highlighted: Some(highlighted),
checked: Some(&chosen),
pick,
dismiss: Some(Rc::new(move |_, cx| {
let at = close.read(cx).highlighted;
Picker::show(&close, false, at, cx)
})),
scroll: Some(&scroll),
);
if self.disabled {
return trigger;
}
let (id, on_change) = (self.id.clone(), self.on_change);
let list = Listing {
id: &self.id,
rows: choices,
selected: self.selected.as_ref(),
focused: focus.is_focused(window),
};
listing(
list,
trigger,
move |value, window, cx| {
log::info!("select {id:?}: {value}");
if let Some(on_change) = &on_change {
on_change(value, window, cx);
}
.render(window, cx),
)
})
},
window,
cx,
)
}
}