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
+2
-2
@@ -8,13 +8,13 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
CheckState,
|
||||
check::{check_mark, tab_stop},
|
||||
check::check_mark,
|
||||
options::{OnFlag, Run},
|
||||
radio::radio_mark,
|
||||
};
|
||||
use crate::{
|
||||
motion,
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
Choice,
|
||||
check::tab_stop,
|
||||
options::{OnValues, float, option_row, surface},
|
||||
select::{field_button, field_text},
|
||||
};
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
|
||||
|
||||
+4
-18
@@ -1,9 +1,9 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, ElementId, FocusHandle, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement,
|
||||
Styled, Window, div, prelude::*,
|
||||
Animation, AnimationExt, AnyElement, App, ElementId, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -12,7 +12,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
motion,
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, IconSize, Mix, Radius, TextSize},
|
||||
};
|
||||
|
||||
@@ -31,20 +31,6 @@ impl From<bool> for CheckState {
|
||||
}
|
||||
}
|
||||
|
||||
/// A focus handle kept for `id`, a Tab stop while enabled.
|
||||
pub(crate) fn tab_stop(
|
||||
id: ElementId,
|
||||
enabled: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> FocusHandle {
|
||||
window
|
||||
.use_keyed_state(id, cx, |_, cx| cx.focus_handle())
|
||||
.read(cx)
|
||||
.clone()
|
||||
.tab_stop(enabled)
|
||||
}
|
||||
|
||||
/// The box alone, shared by checkboxes, cards and lists. `changes` replays its motion.
|
||||
pub(crate) fn check_mark(
|
||||
state: CheckState,
|
||||
|
||||
@@ -6,9 +6,10 @@ use gpui::{
|
||||
Styled, Window, canvas, div, linear_color_stop, linear_gradient, prelude::*, relative,
|
||||
};
|
||||
|
||||
use super::{super::check::tab_stop, ColorPicker, swatch::checker};
|
||||
use super::{ColorPicker, swatch::checker};
|
||||
use crate::{
|
||||
layout::seeded::{Seeded, use_seeded},
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, ControlSize, Mix, Radius},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,15 +7,12 @@ use gpui::{
|
||||
};
|
||||
use jiff::{ToSpan, civil::Date};
|
||||
|
||||
use super::{
|
||||
super::{check::tab_stop, options::Run},
|
||||
zoned_now,
|
||||
};
|
||||
use super::{super::options::Run, zoned_now};
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
layout::seeded::use_seeded,
|
||||
motion,
|
||||
primitives::IconName,
|
||||
primitives::{IconName, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use jiff::civil::Date;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
check::tab_stop,
|
||||
options::{Run, float, surface},
|
||||
select::{field_button, field_text},
|
||||
},
|
||||
@@ -18,7 +17,7 @@ use super::{
|
||||
show_date, show_span,
|
||||
};
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
|
||||
|
||||
+1
-2
@@ -10,13 +10,12 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
check::tab_stop,
|
||||
path::choose,
|
||||
select::{field_button, field_text},
|
||||
};
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
|
||||
+1
-2
@@ -9,11 +9,10 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
Input, TextInput,
|
||||
check::tab_stop,
|
||||
text::{Down, Enter},
|
||||
};
|
||||
use crate::{
|
||||
primitives::{Icon, IconName, Tooltip},
|
||||
primitives::{Icon, IconName, Tooltip, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
typography::Emoji,
|
||||
};
|
||||
|
||||
+4
-2
@@ -7,11 +7,13 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
check::tab_stop,
|
||||
options::OnNumber,
|
||||
slider::{fraction, keyed, value_at},
|
||||
};
|
||||
use crate::theme::{ActiveTheme, Elevation};
|
||||
use crate::{
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, Elevation},
|
||||
};
|
||||
|
||||
/// Where the dial starts, at lower left, and how far it turns.
|
||||
const START: f32 = 0.75 * PI;
|
||||
|
||||
@@ -7,10 +7,13 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
Choice,
|
||||
check::{tab_stop, toggled},
|
||||
check::toggled,
|
||||
options::{OnValues, Pick, option_row, step},
|
||||
};
|
||||
use crate::theme::{ActiveTheme, Radius, TextSize};
|
||||
use crate::{
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
};
|
||||
|
||||
/// `selected` after choosing row `ix`: one row replaces, several toggle.
|
||||
pub(crate) fn chosen(
|
||||
|
||||
@@ -70,6 +70,7 @@ pub use mention::{MentionInput, mention_highlights};
|
||||
pub use multi::MultiSelect;
|
||||
pub use number::{NumberInput, ScrubInput};
|
||||
pub use options::Choice;
|
||||
pub(crate) use options::{Pick, Run, step};
|
||||
pub use pairs::{FieldArray, KeyValueInput, ListInput};
|
||||
pub use path::PathInput;
|
||||
pub use pattern::{RegexInput, regex_highlights};
|
||||
@@ -78,6 +79,7 @@ pub use radio::{Radio, RadioGroup};
|
||||
pub use rating::Rating;
|
||||
pub use search::SearchInput;
|
||||
pub use select::Select;
|
||||
pub(crate) use select::{Listing, listing};
|
||||
pub use signature::{SignaturePad, Stroke};
|
||||
pub use slider::{RangeSlider, Slider};
|
||||
pub use stepper::Stepper;
|
||||
|
||||
+1
-2
@@ -7,14 +7,13 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
Choice,
|
||||
check::tab_stop,
|
||||
listbox::chosen,
|
||||
options::{OnValues, Pick, Popup, step},
|
||||
select::{Picker, measure_anchor, moved},
|
||||
tags::chip,
|
||||
};
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, tab_stop},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -8,11 +8,11 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
Choice,
|
||||
check::tab_stop,
|
||||
options::{OnValue, Run, step},
|
||||
};
|
||||
use crate::{
|
||||
motion,
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, TextSize},
|
||||
};
|
||||
|
||||
|
||||
+4
-2
@@ -9,8 +9,10 @@ use gpui::{
|
||||
point, prelude::*, transparent_black,
|
||||
};
|
||||
|
||||
use super::check::tab_stop;
|
||||
use crate::theme::{ActiveTheme, IconSize, Radius};
|
||||
use crate::{
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, IconSize, Radius},
|
||||
};
|
||||
|
||||
/// A five-point star inside `bounds`, filled or outlined with Lucide's stroke ratio.
|
||||
fn star(bounds: Bounds<Pixels>, fill: bool) -> Path<Pixels> {
|
||||
|
||||
+143
-105
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -6,8 +6,11 @@ use gpui::{
|
||||
Styled, Window, canvas, div, prelude::*, relative,
|
||||
};
|
||||
|
||||
use super::{check::tab_stop, options::OnNumber};
|
||||
use crate::theme::{ActiveTheme, Elevation};
|
||||
use super::options::OnNumber;
|
||||
use crate::{
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, Elevation},
|
||||
};
|
||||
|
||||
/// `value` as a fraction of `min..=max`.
|
||||
pub(crate) fn fraction(value: f64, min: f64, max: f64) -> f32 {
|
||||
|
||||
+2
-1
@@ -6,9 +6,10 @@ use gpui::{
|
||||
prelude::*, transparent_black,
|
||||
};
|
||||
|
||||
use super::{check::tab_stop, options::OnFlag};
|
||||
use super::options::OnFlag;
|
||||
use crate::{
|
||||
motion,
|
||||
primitives::tab_stop,
|
||||
theme::{ActiveTheme, Elevation, Mix, TextSize},
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user