Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b75968ee0f |
@@ -1,6 +1,5 @@
|
|||||||
# Coding guidelines
|
# Coding guidelines
|
||||||
|
|
||||||
- Prefer existing components (e.g. build a close affordance out of `Button`) over custom elements; build a custom element only when no existing component fits.
|
|
||||||
- Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
- Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
||||||
- Do not write organizational or comments that summarize the code. Comments should only be written to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
- Do not write organizational or comments that summarize the code. Comments should only be written to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
||||||
- Keep doc comments and comments simple, one line, straightforward, and unbloated.
|
- Keep doc comments and comments simple, one line, straightforward, and unbloated.
|
||||||
@@ -64,7 +63,7 @@ GPUI is a UI framework which also provides primitives for state and concurrency
|
|||||||
## Rendering and state
|
## Rendering and state
|
||||||
|
|
||||||
- `Render` builds an element tree from entity state; `RenderOnce` (with `#[derive(IntoElement)]`) is for value-like components whose `render` takes `self` and `&mut App`.
|
- `Render` builds an element tree from entity state; `RenderOnce` (with `#[derive(IntoElement)]`) is for value-like components whose `render` takes `self` and `&mut App`.
|
||||||
- Keep `render` declarative: build elements as one chained expression, using `.when(...)` / `.when_some(...)` / `.map(...)` for conditional refinements instead of `let mut` element variables reassigned under `if` / `if let`.
|
- Keep `render` declarative; use `.when(...)` / `.when_some(...)` for small conditional refinements.
|
||||||
- Controlled components: the owner passes the current value, callbacks report intent, the owner updates state and calls `cx.notify()`. No second source of truth.
|
- Controlled components: the owner passes the current value, callbacks report intent, the owner updates state and calls `cx.notify()`. No second source of truth.
|
||||||
- Call `cx.notify()` after a mutation that changes rendering; notify the narrowest owning entity. Never notify from `render`.
|
- Call `cx.notify()` after a mutation that changes rendering; notify the narrowest owning entity. Never notify from `render`.
|
||||||
- `render` is side-effect free: no `cx.notify()`, no `entity.update(...)`, no writing derived text or state back into other entities during render. A write there forces another frame, and a write that emits an event re-enters the event handler — an unbounded redraw loop. Reconcile state in event handlers instead, with silent setters (`set_value_silent`-style) so a control's own write does not re-enter the events it mirrors.
|
- `render` is side-effect free: no `cx.notify()`, no `entity.update(...)`, no writing derived text or state back into other entities during render. A write there forces another frame, and a write that emits an event re-enters the event handler — an unbounded redraw loop. Reconcile state in event handlers instead, with silent setters (`set_value_silent`-style) so a control's own write does not re-enter the events it mirrors.
|
||||||
|
|||||||
Generated
+688
-709
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -24,9 +24,9 @@ gpui-native-icons = { path = "crates/icons" }
|
|||||||
gpui-native-theme = { path = "crates/theme" }
|
gpui-native-theme = { path = "crates/theme" }
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
gpui = { package = "gpui-pre", version = "0.3.10" }
|
||||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit"] }
|
gpui_platform = { package = "gpui-pre-platform", version = "0.3.10", features = ["font-kit"] }
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.10" }
|
||||||
|
|
||||||
gpui-native-core = { path = "crates/core", default-features = false }
|
gpui-native-core = { path = "crates/core", default-features = false }
|
||||||
gpui-native-data = { path = "crates/data", default-features = false }
|
gpui-native-data = { path = "crates/data", default-features = false }
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ badge = []
|
|||||||
input = ["button"]
|
input = ["button"]
|
||||||
clipboard = ["button"]
|
clipboard = ["button"]
|
||||||
popover = []
|
popover = []
|
||||||
scroll_area = ["scrollbar"]
|
scroll_area = []
|
||||||
scrollbar = []
|
scrollbar = ["scroll_area"]
|
||||||
segmented = []
|
segmented = []
|
||||||
separator = []
|
separator = []
|
||||||
spinner = []
|
spinner = []
|
||||||
@@ -76,7 +76,7 @@ tooltip = []
|
|||||||
menu = ["scroll_area"]
|
menu = ["scroll_area"]
|
||||||
dropdown_menu = ["menu", "button"]
|
dropdown_menu = ["menu", "button"]
|
||||||
select = ["menu"]
|
select = ["menu"]
|
||||||
dialog = ["button"]
|
dialog = []
|
||||||
empty_state = []
|
empty_state = []
|
||||||
alert = []
|
alert = []
|
||||||
toast = []
|
toast = []
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, App, ElementId, Hsla, InteractiveElement as _, IntoElement, ParentElement as _,
|
App, Hsla, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement,
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _,
|
Styled as _, Window,
|
||||||
Window, transparent_black,
|
|
||||||
};
|
};
|
||||||
use gpui_native_icons::Icon;
|
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size, Theme};
|
use gpui_native_theme::{ActiveTheme as _, Size, Theme};
|
||||||
|
|
||||||
use crate::common::{Sizable, StyledExt, h_flex};
|
use crate::common::{Sizable, StyledExt, h_flex};
|
||||||
|
|
||||||
/// The tooltip view builder gpui's `.tooltip(...)` expects.
|
|
||||||
type TooltipBuilder = Rc<dyn Fn(&mut Window, &mut App) -> AnyView>;
|
|
||||||
|
|
||||||
/// How loud a badge is.
|
/// How loud a badge is.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum BadgeVariant {
|
pub enum BadgeVariant {
|
||||||
@@ -28,42 +20,18 @@ pub enum BadgeVariant {
|
|||||||
Warning,
|
Warning,
|
||||||
/// The destructive ramp.
|
/// The destructive ramp.
|
||||||
Danger,
|
Danger,
|
||||||
/// No fill, just a border; the quietest badge.
|
|
||||||
Outline,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BadgeVariant {
|
impl BadgeVariant {
|
||||||
fn tokens(self, theme: &Theme) -> (Hsla, Hsla, Hsla) {
|
fn tokens(self, theme: &Theme) -> (Hsla, Hsla) {
|
||||||
match self {
|
match self {
|
||||||
Self::Neutral => (
|
Self::Neutral => (theme.colors.muted, theme.colors.muted_foreground),
|
||||||
theme.colors.muted,
|
Self::Accent => (theme.ramps.primary.base, theme.colors.primary_foreground),
|
||||||
theme.colors.muted_foreground,
|
Self::Success => (theme.ramps.success.base, theme.colors.success_foreground),
|
||||||
transparent_black(),
|
Self::Warning => (theme.ramps.warning.base, theme.colors.warning_foreground),
|
||||||
),
|
|
||||||
Self::Accent => (
|
|
||||||
theme.ramps.primary.base,
|
|
||||||
theme.colors.primary_foreground,
|
|
||||||
transparent_black(),
|
|
||||||
),
|
|
||||||
Self::Success => (
|
|
||||||
theme.ramps.success.base,
|
|
||||||
theme.colors.success_foreground,
|
|
||||||
transparent_black(),
|
|
||||||
),
|
|
||||||
Self::Warning => (
|
|
||||||
theme.ramps.warning.base,
|
|
||||||
theme.colors.warning_foreground,
|
|
||||||
transparent_black(),
|
|
||||||
),
|
|
||||||
Self::Danger => (
|
Self::Danger => (
|
||||||
theme.ramps.destructive.base,
|
theme.ramps.destructive.base,
|
||||||
theme.colors.destructive_foreground,
|
theme.colors.destructive_foreground,
|
||||||
transparent_black(),
|
|
||||||
),
|
|
||||||
Self::Outline => (
|
|
||||||
transparent_black(),
|
|
||||||
theme.colors.foreground,
|
|
||||||
theme.colors.border,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,29 +40,22 @@ impl BadgeVariant {
|
|||||||
/// A small pill for a count or a state.
|
/// A small pill for a count or a state.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Badge {
|
pub struct Badge {
|
||||||
id: ElementId,
|
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<Icon>,
|
|
||||||
variant: BadgeVariant,
|
variant: BadgeVariant,
|
||||||
size: Size,
|
size: Size,
|
||||||
tooltip: Option<TooltipBuilder>,
|
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Badge {
|
impl Badge {
|
||||||
/// Creates a badge showing `label` — a count or state word the caller formats.
|
/// Creates a badge showing `label` — a count or state word the caller formats.
|
||||||
pub fn new<I, L>(id: I, label: L) -> Self
|
pub fn new<L>(label: L) -> Self
|
||||||
where
|
where
|
||||||
I: Into<ElementId>,
|
|
||||||
L: Into<SharedString>,
|
L: Into<SharedString>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
icon: None,
|
|
||||||
variant: BadgeVariant::default(),
|
variant: BadgeVariant::default(),
|
||||||
size: Size::default(),
|
size: Size::default(),
|
||||||
tooltip: None,
|
|
||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,24 +65,6 @@ impl Badge {
|
|||||||
self.variant = variant;
|
self.variant = variant;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon before the label.
|
|
||||||
pub fn icon<I>(mut self, icon: I) -> Self
|
|
||||||
where
|
|
||||||
I: Into<Icon>,
|
|
||||||
{
|
|
||||||
self.icon = Some(icon.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows a tooltip view next to the badge after it is hovered.
|
|
||||||
pub fn tooltip<F>(mut self, tooltip: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
|
||||||
{
|
|
||||||
self.tooltip = Some(Rc::new(tooltip));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for Badge {
|
impl Sizable for Badge {
|
||||||
@@ -141,28 +84,18 @@ impl RenderOnce for Badge {
|
|||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let metrics = theme.sizes.resolve(self.size);
|
let metrics = theme.sizes.resolve(self.size);
|
||||||
let label_size = metrics.caption_size;
|
let label_size = metrics.caption_size;
|
||||||
let (background, foreground, border) = self.variant.tokens(theme);
|
let (background, foreground) = self.variant.tokens(theme);
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(self.id.clone())
|
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_1()
|
|
||||||
.rounded(theme.radius.full)
|
.rounded(theme.radius.full)
|
||||||
.border_1()
|
|
||||||
.border_color(border)
|
|
||||||
.bg(background)
|
.bg(background)
|
||||||
.text_color(foreground)
|
.text_color(foreground)
|
||||||
.text_size(label_size)
|
.text_size(label_size)
|
||||||
.px(metrics.dense_padding_x)
|
.px(metrics.dense_padding_x)
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.when_some(self.icon, |badge, icon| {
|
|
||||||
badge.child(icon.size(metrics.dense_icon_size).color(foreground))
|
|
||||||
})
|
|
||||||
.child(self.label)
|
.child(self.label)
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.when_some(self.tooltip, |badge, tooltip| {
|
|
||||||
badge.tooltip(move |window, cx| tooltip(window, cx))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+148
-151
@@ -518,7 +518,7 @@ where
|
|||||||
|
|
||||||
let wrapper_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
|
let wrapper_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
div()
|
let mut wrapper = div()
|
||||||
.id(id.clone())
|
.id(id.clone())
|
||||||
.key_context(COMBOBOX_CONTEXT)
|
.key_context(COMBOBOX_CONTEXT)
|
||||||
.relative()
|
.relative()
|
||||||
@@ -575,162 +575,159 @@ where
|
|||||||
.right(metrics.dense_padding_x)
|
.right(metrics.dense_padding_x)
|
||||||
.top((metrics.input_height + BORDER_WIDTH * 2.0 - icon_size) / 2.)
|
.top((metrics.input_height + BORDER_WIDTH * 2.0 - icon_size) / 2.)
|
||||||
.child(chevron),
|
.child(chevron),
|
||||||
)
|
);
|
||||||
.when(open, |wrapper| {
|
|
||||||
let rows: Vec<gpui::AnyElement> = self
|
|
||||||
.visible
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(row, option_ix)| {
|
|
||||||
let (value, label) = &self.options[*option_ix];
|
|
||||||
let highlighted = self.highlight == Some(row);
|
|
||||||
let checked = self.selected.as_ref() == Some(value);
|
|
||||||
let state = state.clone();
|
|
||||||
let row_ix = row;
|
|
||||||
|
|
||||||
div()
|
if open {
|
||||||
.id(("option", row))
|
let rows: Vec<gpui::AnyElement> = self
|
||||||
.debug_selector({
|
.visible
|
||||||
let id = id.clone();
|
.iter()
|
||||||
move || format!("{id}-option-{row}")
|
.enumerate()
|
||||||
})
|
.map(|(row, option_ix)| {
|
||||||
.flex()
|
let (value, label) = &self.options[*option_ix];
|
||||||
.items_center()
|
let highlighted = self.highlight == Some(row);
|
||||||
.gap_2()
|
let checked = self.selected.as_ref() == Some(value);
|
||||||
.px_2()
|
let state = state.clone();
|
||||||
.py_1()
|
let row_ix = row;
|
||||||
.rounded(radius.sm)
|
|
||||||
.when(highlighted, |row| row.bg(colors.muted))
|
div()
|
||||||
.when(!highlighted, |row| row.hover(|row| row.bg(colors.muted)))
|
.id(("option", row))
|
||||||
.on_mouse_move({
|
.debug_selector({
|
||||||
let state = state.clone();
|
let id = id.clone();
|
||||||
move |_, _window, cx| {
|
move || format!("{id}-option-{row}")
|
||||||
if state.read(cx).highlight != Some(row_ix) {
|
})
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded(radius.sm)
|
||||||
|
.when(highlighted, |row| row.bg(colors.muted))
|
||||||
|
.when(!highlighted, |row| row.hover(|row| row.bg(colors.muted)))
|
||||||
|
.on_mouse_move({
|
||||||
|
let state = state.clone();
|
||||||
|
move |_, _window, cx| {
|
||||||
|
if state.read(cx).highlight != Some(row_ix) {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.highlight = Some(row_ix);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
|
let state = state.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.text_ellipsis()
|
||||||
|
.child(label.clone()),
|
||||||
|
)
|
||||||
|
.when(checked, |row| {
|
||||||
|
row.child(
|
||||||
|
icon(IconRole::Check)
|
||||||
|
.size(icon_size)
|
||||||
|
.color(colors.foreground),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let rows: gpui::AnyElement = if rows.is_empty() {
|
||||||
|
div()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.text_size(metrics.caption_size)
|
||||||
|
.text_color(colors.muted_foreground)
|
||||||
|
.child("No matches")
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
let scroll = window
|
||||||
|
.use_keyed_state((id.clone(), "rows-scroll"), cx, |_, _| ScrollHandle::new());
|
||||||
|
|
||||||
|
ScrollArea::new((id.clone(), "rows"))
|
||||||
|
.handle(scroll.read(cx).clone())
|
||||||
|
.axes(crate::scroll_area::ScrollAxes::Vertical)
|
||||||
|
.max_h(POPUP_MAX_HEIGHT)
|
||||||
|
.children(rows)
|
||||||
|
.into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
|
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
|
||||||
|
let popup_width = wrapper_bounds
|
||||||
|
.borrow()
|
||||||
|
.map(|bounds| bounds.size.width)
|
||||||
|
.unwrap_or_else(|| window.rem_size() * 12.0);
|
||||||
|
|
||||||
|
let panel = v_flex()
|
||||||
|
.id((id.clone(), "panel"))
|
||||||
|
.debug_selector({
|
||||||
|
let id = id.clone();
|
||||||
|
move || format!("{id}-panel")
|
||||||
|
})
|
||||||
|
.w(popup_width)
|
||||||
|
.p_1()
|
||||||
|
.bg(colors.surface)
|
||||||
|
.text_color(colors.foreground)
|
||||||
|
.rounded(radius.overlay)
|
||||||
|
.border_1()
|
||||||
|
.border_color(panel_border)
|
||||||
|
.shadow(shadows)
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(rows)
|
||||||
|
.child(
|
||||||
|
canvas(
|
||||||
|
{
|
||||||
|
let cell = panel_bounds.clone();
|
||||||
|
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let state = state.clone();
|
||||||
|
let panel_bounds = panel_bounds.clone();
|
||||||
|
let wrapper_bounds = wrapper_bounds.clone();
|
||||||
|
move |_, _, window, _cx| {
|
||||||
|
window.on_mouse_event({
|
||||||
|
let state = state.clone();
|
||||||
|
let panel_bounds = panel_bounds.clone();
|
||||||
|
let wrapper_bounds = wrapper_bounds.clone();
|
||||||
|
move |event: &MouseDownEvent, phase, _window, cx| {
|
||||||
|
if phase != gpui::DispatchPhase::Bubble {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let on_panel = panel_bounds
|
||||||
|
.borrow()
|
||||||
|
.is_some_and(|bounds| bounds.contains(&event.position));
|
||||||
|
let on_wrapper = wrapper_bounds
|
||||||
|
.borrow()
|
||||||
|
.is_some_and(|bounds| bounds.contains(&event.position));
|
||||||
|
if on_panel || on_wrapper {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
state.highlight = Some(row_ix);
|
state.highlight = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
}
|
||||||
.on_click({
|
},
|
||||||
let state = state.clone();
|
)
|
||||||
move |_, window, cx| {
|
.absolute()
|
||||||
state.update(cx, |state, cx| {
|
.inset_0(),
|
||||||
state.choose_row(row_ix, window, cx)
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.overflow_hidden()
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.text_ellipsis()
|
|
||||||
.child(label.clone()),
|
|
||||||
)
|
|
||||||
.when(checked, |row| {
|
|
||||||
row.child(
|
|
||||||
icon(IconRole::Check)
|
|
||||||
.size(icon_size)
|
|
||||||
.color(colors.foreground),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rows: gpui::AnyElement = if rows.is_empty() {
|
wrapper = wrapper.child(deferred(
|
||||||
div()
|
anchored().offset(point(px(0.), spacing_unit)).child(panel),
|
||||||
.px_2()
|
));
|
||||||
.py_1()
|
}
|
||||||
.text_size(metrics.caption_size)
|
|
||||||
.text_color(colors.muted_foreground)
|
|
||||||
.child("No matches")
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
let scroll = window.use_keyed_state((id.clone(), "rows-scroll"), cx, |_, _| {
|
|
||||||
ScrollHandle::new()
|
|
||||||
});
|
|
||||||
|
|
||||||
ScrollArea::new((id.clone(), "rows"))
|
wrapper
|
||||||
.handle(scroll.read(cx).clone())
|
|
||||||
.axes(crate::scroll_area::ScrollAxes::Vertical)
|
|
||||||
.max_h(POPUP_MAX_HEIGHT)
|
|
||||||
.children(rows)
|
|
||||||
.into_any_element()
|
|
||||||
};
|
|
||||||
|
|
||||||
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
|
|
||||||
|
|
||||||
let popup_width = wrapper_bounds
|
|
||||||
.borrow()
|
|
||||||
.map(|bounds| bounds.size.width)
|
|
||||||
.unwrap_or_else(|| window.rem_size() * 12.0);
|
|
||||||
|
|
||||||
let panel = v_flex()
|
|
||||||
.id((id.clone(), "panel"))
|
|
||||||
.debug_selector({
|
|
||||||
let id = id.clone();
|
|
||||||
move || format!("{id}-panel")
|
|
||||||
})
|
|
||||||
.w(popup_width)
|
|
||||||
.p_1()
|
|
||||||
.bg(colors.surface)
|
|
||||||
.text_color(colors.foreground)
|
|
||||||
.rounded(radius.overlay)
|
|
||||||
.border_1()
|
|
||||||
.border_color(panel_border)
|
|
||||||
.shadow(shadows)
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(rows)
|
|
||||||
.child(
|
|
||||||
canvas(
|
|
||||||
{
|
|
||||||
let cell = panel_bounds.clone();
|
|
||||||
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let state = state.clone();
|
|
||||||
let panel_bounds = panel_bounds.clone();
|
|
||||||
let wrapper_bounds = wrapper_bounds.clone();
|
|
||||||
move |_, _, window, _cx| {
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = state.clone();
|
|
||||||
let panel_bounds = panel_bounds.clone();
|
|
||||||
let wrapper_bounds = wrapper_bounds.clone();
|
|
||||||
move |event: &MouseDownEvent, phase, _window, cx| {
|
|
||||||
if phase != gpui::DispatchPhase::Bubble {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let on_panel =
|
|
||||||
panel_bounds.borrow().is_some_and(|bounds| {
|
|
||||||
bounds.contains(&event.position)
|
|
||||||
});
|
|
||||||
let on_wrapper =
|
|
||||||
wrapper_bounds.borrow().is_some_and(|bounds| {
|
|
||||||
bounds.contains(&event.position)
|
|
||||||
});
|
|
||||||
if on_panel || on_wrapper {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.highlight = None;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.absolute()
|
|
||||||
.inset_0(),
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper.child(deferred(
|
|
||||||
anchored().offset(point(px(0.), spacing_unit)).child(panel),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ use gpui::{
|
|||||||
IntoElement, KeyDownEvent, MouseButton, ParentElement as _, RenderOnce, Role, SharedString,
|
IntoElement, KeyDownEvent, MouseButton, ParentElement as _, RenderOnce, Role, SharedString,
|
||||||
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative,
|
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::IconRole;
|
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariant};
|
use crate::common::v_flex;
|
||||||
use crate::common::{Sizable, v_flex};
|
|
||||||
|
|
||||||
/// Requests the dialog's close: `false` closes.
|
/// Requests the dialog's close: `false` closes.
|
||||||
pub type ChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
pub type ChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||||
type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
|
type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
|
||||||
type ActionsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<AnyElement>>;
|
type ActionsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<AnyElement>>;
|
||||||
|
|
||||||
/// A centred panel over a dim scrim: the platform's modal.
|
/// A centred panel over a dim scrim: the platform's modal.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Dialog {
|
pub struct Dialog {
|
||||||
@@ -60,7 +57,7 @@ impl Dialog {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests close through `on_change` on escape, scrim clicks, and the close button.
|
/// Requests close through `on_change` on escape and scrim clicks.
|
||||||
pub fn on_change<F>(mut self, handler: F) -> Self
|
pub fn on_change<F>(mut self, handler: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(bool, &mut Window, &mut App) + 'static,
|
F: Fn(bool, &mut Window, &mut App) + 'static,
|
||||||
@@ -157,7 +154,6 @@ impl RenderOnce for Dialog {
|
|||||||
|
|
||||||
let on_change = self.on_change.clone();
|
let on_change = self.on_change.clone();
|
||||||
let alert = self.alert;
|
let alert = self.alert;
|
||||||
let show_close = !alert && on_change.is_some();
|
|
||||||
|
|
||||||
// Keyboard focus rides the panel across frames.
|
// Keyboard focus rides the panel across frames.
|
||||||
let focus = gpui_native_utils::focus_handle(&self.id, window, cx);
|
let focus = gpui_native_utils::focus_handle(&self.id, window, cx);
|
||||||
@@ -194,40 +190,12 @@ impl RenderOnce for Dialog {
|
|||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when(self.title.is_some() || show_close, |panel| {
|
.when_some(self.title, |panel, title| {
|
||||||
panel.child(
|
panel.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.text_size(title_size)
|
||||||
.items_center()
|
.font_weight(FontWeight::SEMIBOLD)
|
||||||
.justify_end()
|
.child(title),
|
||||||
.gap_2()
|
|
||||||
.when_some(self.title.clone(), |row, title| {
|
|
||||||
row.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.text_size(title_size)
|
|
||||||
.font_weight(FontWeight::SEMIBOLD)
|
|
||||||
.child(title),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(show_close, |row| {
|
|
||||||
row.child(
|
|
||||||
Button::new("dialog-close")
|
|
||||||
.icon(IconRole::Close)
|
|
||||||
.variant(ButtonVariant::Ghost)
|
|
||||||
.small()
|
|
||||||
.debug_selector(|| "dialog-close".to_string())
|
|
||||||
.accessibility_label("Close")
|
|
||||||
.on_click({
|
|
||||||
let on_change = on_change.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
if let Some(on_change) = &on_change {
|
|
||||||
on_change(false, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when_some(content, |panel, content| {
|
.when_some(content, |panel, content| {
|
||||||
|
|||||||
@@ -366,15 +366,7 @@ impl RenderOnce for DropdownMenu {
|
|||||||
trigger
|
trigger
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = if self.open {
|
let mut wrapper = div()
|
||||||
self.items
|
|
||||||
.map(|build| Rc::from(build(window, cx)))
|
|
||||||
.unwrap_or_else(|| Rc::from([]))
|
|
||||||
} else {
|
|
||||||
Rc::from([])
|
|
||||||
};
|
|
||||||
|
|
||||||
div()
|
|
||||||
.relative()
|
.relative()
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(
|
||||||
@@ -429,24 +421,34 @@ impl RenderOnce for DropdownMenu {
|
|||||||
.absolute()
|
.absolute()
|
||||||
.inset_0(),
|
.inset_0(),
|
||||||
)
|
)
|
||||||
.child(trigger)
|
.child(trigger);
|
||||||
.when_some(
|
|
||||||
anchored_menu_panel(
|
let items = if self.open {
|
||||||
window,
|
self.items
|
||||||
cx,
|
.map(|build| Rc::from(build(window, cx)))
|
||||||
&self.id,
|
.unwrap_or_else(|| Rc::from([]))
|
||||||
self.open,
|
} else {
|
||||||
items,
|
Rc::from([])
|
||||||
None,
|
};
|
||||||
*keyboard_open.read(cx),
|
|
||||||
Some(on_open),
|
if let Some(panel) = anchored_menu_panel(
|
||||||
label,
|
window,
|
||||||
None,
|
cx,
|
||||||
&trigger_bounds,
|
&self.id,
|
||||||
&panel_chrome,
|
self.open,
|
||||||
enter_duration,
|
items,
|
||||||
),
|
None,
|
||||||
|wrapper, panel| wrapper.child(panel),
|
*keyboard_open.read(cx),
|
||||||
)
|
Some(on_open),
|
||||||
|
label,
|
||||||
|
None,
|
||||||
|
&trigger_bounds,
|
||||||
|
&panel_chrome,
|
||||||
|
enter_duration,
|
||||||
|
) {
|
||||||
|
wrapper = wrapper.child(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-33
@@ -1,7 +1,7 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, DispatchPhase, ElementId,
|
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, DispatchPhase, ElementId,
|
||||||
Entity, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
|
Entity, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
|
||||||
@@ -127,9 +127,6 @@ impl MenuItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MenuItem is a data builder, not an element, adopt the combinators explicitly.
|
|
||||||
impl FluentBuilder for MenuItem {}
|
|
||||||
|
|
||||||
/// Menu chrome styles.
|
/// Menu chrome styles.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct MenuChrome {
|
pub(crate) struct MenuChrome {
|
||||||
@@ -291,16 +288,7 @@ impl RenderOnce for Menu {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||||
|
|
||||||
let items = if self.open {
|
let mut wrapper = div()
|
||||||
match self.items {
|
|
||||||
Some(build) => Rc::from(build(window, cx)),
|
|
||||||
None => Rc::from([]),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Rc::from([])
|
|
||||||
};
|
|
||||||
|
|
||||||
div()
|
|
||||||
.relative()
|
.relative()
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(
|
||||||
@@ -347,25 +335,36 @@ impl RenderOnce for Menu {
|
|||||||
)
|
)
|
||||||
.when_some(self.trigger, |wrapper, trigger| {
|
.when_some(self.trigger, |wrapper, trigger| {
|
||||||
wrapper.child(trigger(window, cx))
|
wrapper.child(trigger(window, cx))
|
||||||
})
|
});
|
||||||
.when_some(
|
|
||||||
anchored_menu_panel(
|
let items = if self.open {
|
||||||
window,
|
match self.items {
|
||||||
cx,
|
Some(build) => Rc::from(build(window, cx)),
|
||||||
&self.id,
|
None => Rc::from([]),
|
||||||
self.open,
|
}
|
||||||
items,
|
} else {
|
||||||
self.initial_selection,
|
Rc::from([])
|
||||||
false,
|
};
|
||||||
self.on_change,
|
|
||||||
fallback_label,
|
if let Some(panel) = anchored_menu_panel(
|
||||||
self.open_at,
|
window,
|
||||||
&trigger_bounds,
|
cx,
|
||||||
&chrome,
|
&self.id,
|
||||||
enter_duration,
|
self.open,
|
||||||
),
|
items,
|
||||||
|wrapper, panel| wrapper.child(panel),
|
self.initial_selection,
|
||||||
)
|
false,
|
||||||
|
self.on_change,
|
||||||
|
fallback_label,
|
||||||
|
self.open_at,
|
||||||
|
&trigger_bounds,
|
||||||
|
&chrome,
|
||||||
|
enter_duration,
|
||||||
|
) {
|
||||||
|
wrapper = wrapper.child(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+67
-64
@@ -114,7 +114,7 @@ impl RenderOnce for Popover {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||||
|
|
||||||
div()
|
let mut wrapper = div()
|
||||||
.relative()
|
.relative()
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(
|
||||||
@@ -161,77 +161,80 @@ impl RenderOnce for Popover {
|
|||||||
)
|
)
|
||||||
.when_some(self.trigger, |wrapper, trigger| {
|
.when_some(self.trigger, |wrapper, trigger| {
|
||||||
wrapper.child(trigger(window, cx))
|
wrapper.child(trigger(window, cx))
|
||||||
})
|
});
|
||||||
.when(self.open, |wrapper| {
|
|
||||||
let on_change = self.on_change;
|
|
||||||
let label = fallback_label.clone();
|
|
||||||
let focus = gpui_native_utils::focus_handle(&self.id, window, cx);
|
|
||||||
let content = self.content.map(|content| content(window, cx));
|
|
||||||
|
|
||||||
let panel = v_flex()
|
if self.open {
|
||||||
.id("popover-panel")
|
let on_change = self.on_change;
|
||||||
.debug_selector(|| "popover-panel".to_string())
|
let label = fallback_label.clone();
|
||||||
.occlude()
|
let focus = gpui_native_utils::focus_handle(&self.id, window, cx);
|
||||||
.track_focus(&focus)
|
let content = self.content.map(|content| content(window, cx));
|
||||||
.role(Role::Dialog)
|
|
||||||
.aria_label(label)
|
let panel = v_flex()
|
||||||
.min_w_32()
|
.id("popover-panel")
|
||||||
.bg(colors.surface)
|
.debug_selector(|| "popover-panel".to_string())
|
||||||
.text_color(colors.surface_foreground)
|
.occlude()
|
||||||
.rounded(radius.overlay)
|
.track_focus(&focus)
|
||||||
.border_1()
|
.role(Role::Dialog)
|
||||||
.border_color(panel_border)
|
.aria_label(label)
|
||||||
.shadow(shadows)
|
.min_w_32()
|
||||||
.overflow_hidden()
|
.bg(colors.surface)
|
||||||
.on_mouse_down_out({
|
.text_color(colors.surface_foreground)
|
||||||
let on_change = on_change.clone();
|
.rounded(radius.overlay)
|
||||||
let trigger_bounds = trigger_bounds.clone();
|
.border_1()
|
||||||
move |event, window, cx| {
|
.border_color(panel_border)
|
||||||
if trigger_bounds
|
.shadow(shadows)
|
||||||
.borrow()
|
.overflow_hidden()
|
||||||
.is_some_and(|bounds| bounds.contains(&event.position))
|
.on_mouse_down_out({
|
||||||
{
|
let on_change = on_change.clone();
|
||||||
return;
|
let trigger_bounds = trigger_bounds.clone();
|
||||||
}
|
move |event, window, cx| {
|
||||||
|
if trigger_bounds
|
||||||
|
.borrow()
|
||||||
|
.is_some_and(|bounds| bounds.contains(&event.position))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(on_change) = &on_change {
|
||||||
|
on_change(false, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_key_down({
|
||||||
|
let on_change = on_change.clone();
|
||||||
|
move |event: &KeyDownEvent, window, cx| {
|
||||||
|
if event.keystroke.key == "escape" {
|
||||||
if let Some(on_change) = &on_change {
|
if let Some(on_change) = &on_change {
|
||||||
on_change(false, window, cx);
|
on_change(false, window, cx);
|
||||||
}
|
}
|
||||||
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.on_key_down({
|
})
|
||||||
let on_change = on_change.clone();
|
.when_some(content, |panel, content| panel.child(content))
|
||||||
move |event: &KeyDownEvent, window, cx| {
|
.child(
|
||||||
if event.keystroke.key == "escape" {
|
canvas(|_, _, _| {}, {
|
||||||
if let Some(on_change) = &on_change {
|
let focus = focus;
|
||||||
on_change(false, window, cx);
|
move |_, _, window, cx| {
|
||||||
}
|
if !focus.contains_focused(window, cx) {
|
||||||
cx.stop_propagation();
|
window.focus(&focus, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when_some(content, |panel, content| panel.child(content))
|
.absolute()
|
||||||
.child(
|
.inset_0(),
|
||||||
canvas(|_, _, _| {}, {
|
);
|
||||||
let focus = focus;
|
|
||||||
move |_, _, window, cx| {
|
|
||||||
if !focus.contains_focused(window, cx) {
|
|
||||||
window.focus(&focus, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.absolute()
|
|
||||||
.inset_0(),
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper.child(deferred(
|
wrapper = wrapper.child(deferred(
|
||||||
anchored()
|
anchored()
|
||||||
.offset(point(px(0.), anchor_gap))
|
.offset(point(px(0.), anchor_gap))
|
||||||
.child(panel.with_animation(
|
.child(panel.with_animation(
|
||||||
"popover-enter",
|
"popover-enter",
|
||||||
Animation::new(enter_duration).with_easing(ease_out_quint()),
|
Animation::new(enter_duration).with_easing(ease_out_quint()),
|
||||||
|panel, delta| panel.opacity(delta),
|
|panel, delta| panel.opacity(delta),
|
||||||
)),
|
)),
|
||||||
))
|
));
|
||||||
})
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ impl RenderOnce for ScrollArea {
|
|||||||
let mut pane_style = self.style.clone();
|
let mut pane_style = self.style.clone();
|
||||||
pane_style.padding = Default::default();
|
pane_style.padding = Default::default();
|
||||||
|
|
||||||
let pane = div()
|
let mut pane = div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.when(self.axes != ScrollAxes::Horizontal, |area| {
|
.when(self.axes != ScrollAxes::Horizontal, |area| {
|
||||||
@@ -112,31 +112,34 @@ impl RenderOnce for ScrollArea {
|
|||||||
// Keep wheel events from scrolling an outer scroll area too.
|
// Keep wheel events from scrolling an outer scroll area too.
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
})
|
})
|
||||||
.refine_style(&pane_style)
|
.refine_style(&pane_style);
|
||||||
.children(self.children);
|
|
||||||
|
|
||||||
div()
|
// Extend with children
|
||||||
.relative()
|
pane.extend(self.children);
|
||||||
.refine_style(&self.style)
|
|
||||||
.child(pane.into_any_element())
|
|
||||||
.when_some(self.handle, |wrapper, handle| {
|
|
||||||
let id = self.id.clone();
|
|
||||||
|
|
||||||
wrapper
|
let mut wrapper = div().relative().refine_style(&self.style);
|
||||||
.when(self.axes != ScrollAxes::Horizontal, |wrapper| {
|
wrapper.extend(std::iter::once(pane.into_any_element()));
|
||||||
wrapper.child(
|
|
||||||
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
|
if let Some(handle) = self.handle {
|
||||||
.visibility(self.visibility)
|
let id = self.id.clone();
|
||||||
.into_any_element(),
|
|
||||||
)
|
if self.axes != ScrollAxes::Horizontal {
|
||||||
})
|
wrapper.extend(std::iter::once(
|
||||||
.when(self.axes != ScrollAxes::Vertical, |wrapper| {
|
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
|
||||||
wrapper.child(
|
.visibility(self.visibility)
|
||||||
Scrollbar::horizontal((id, "h-scrollbar"), handle)
|
.into_any_element(),
|
||||||
.visibility(self.visibility)
|
));
|
||||||
.into_any_element(),
|
}
|
||||||
)
|
|
||||||
})
|
if self.axes != ScrollAxes::Vertical {
|
||||||
})
|
wrapper.extend(std::iter::once(
|
||||||
|
Scrollbar::horizontal((id, "h-scrollbar"), handle)
|
||||||
|
.visibility(self.visibility)
|
||||||
|
.into_any_element(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,10 +274,11 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuItem::new(option.label.clone())
|
let mut row = MenuItem::new(option.label.clone());
|
||||||
.when_some(option.icon.clone(), |row, icon| row.icon(icon))
|
if let Some(icon) = option.icon.clone() {
|
||||||
.checked(is_selected)
|
row = row.icon(icon);
|
||||||
.handler(handler)
|
}
|
||||||
|
row.checked(is_selected).handler(handler)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -158,26 +158,3 @@ fn clicking_the_panel_does_not_dismiss() {
|
|||||||
click(&mut visual, point(px(960.), px(540.)));
|
click(&mut visual, point(px(960.), px(540.)));
|
||||||
assert!(changes.borrow().is_empty());
|
assert!(changes.borrow().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clicking the close button reports close like escape and the scrim; the
|
|
||||||
/// panel then unmounts the button with the rest of the dialog.
|
|
||||||
#[test]
|
|
||||||
fn the_close_button_reports_close_and_disappears() {
|
|
||||||
let (_cx, mut visual, changes) = dialog_window(true, false);
|
|
||||||
|
|
||||||
let bounds = visual
|
|
||||||
.debug_bounds("dialog-close")
|
|
||||||
.expect("the close button rendered");
|
|
||||||
click(&mut visual, bounds.center());
|
|
||||||
assert_eq!(*changes.borrow(), vec![false]);
|
|
||||||
|
|
||||||
draw(&mut visual);
|
|
||||||
assert!(visual.debug_bounds("dialog-close").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An alert answers only through its actions, so it renders no close button.
|
|
||||||
#[test]
|
|
||||||
fn an_alert_hides_the_close_button() {
|
|
||||||
let (_cx, mut visual, _changes) = dialog_window(true, true);
|
|
||||||
assert!(visual.debug_bounds("dialog-close").is_none());
|
|
||||||
}
|
|
||||||
|
|||||||
+58
-64
@@ -10,6 +10,7 @@ use gpui::{
|
|||||||
Styled as _, Window, div,
|
Styled as _, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_core::scroll_area::ScrollArea;
|
use gpui_native_core::scroll_area::ScrollArea;
|
||||||
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
||||||
|
|
||||||
@@ -18,15 +19,15 @@ type ChangeHandler = Rc<dyn Fn(Option<usize>, &mut Window, &mut App)>;
|
|||||||
type EntriesFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<SidebarEntry>>;
|
type EntriesFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<SidebarEntry>>;
|
||||||
|
|
||||||
/// One entry of a [`Sidebar`], a section header or a selectable row.
|
/// One entry of a [`Sidebar`], a section header or a selectable row.
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum SidebarEntry {
|
pub enum SidebarEntry {
|
||||||
/// A non-selectable section label.
|
/// A non-selectable section label.
|
||||||
Header { label: SharedString },
|
Header { label: SharedString },
|
||||||
/// A selectable row.
|
/// A selectable row.
|
||||||
Row {
|
Row {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
|
icon: Option<Icon>,
|
||||||
detail: Option<SharedString>,
|
detail: Option<SharedString>,
|
||||||
prefix: Option<AnyElement>,
|
|
||||||
suffix: Option<AnyElement>,
|
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -49,13 +50,23 @@ impl SidebarEntry {
|
|||||||
{
|
{
|
||||||
Self::Row {
|
Self::Row {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
|
icon: None,
|
||||||
detail: None,
|
detail: None,
|
||||||
prefix: None,
|
|
||||||
suffix: None,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows an icon leading the label, a role or any custom [`IconNamed`].
|
||||||
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
if let Self::Row { icon: slot, .. } = &mut self {
|
||||||
|
*slot = Some(icon.into());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows muted trailing text.
|
/// Shows muted trailing text.
|
||||||
pub fn detail<D>(mut self, detail: D) -> Self
|
pub fn detail<D>(mut self, detail: D) -> Self
|
||||||
where
|
where
|
||||||
@@ -70,28 +81,6 @@ impl SidebarEntry {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an element leading the row.
|
|
||||||
pub fn prefix<E>(mut self, prefix: E) -> Self
|
|
||||||
where
|
|
||||||
E: IntoElement,
|
|
||||||
{
|
|
||||||
if let Self::Row { prefix: slot, .. } = &mut self {
|
|
||||||
*slot = Some(prefix.into_any_element());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows an element trailing the row.
|
|
||||||
pub fn suffix<E>(mut self, suffix: E) -> Self
|
|
||||||
where
|
|
||||||
E: IntoElement,
|
|
||||||
{
|
|
||||||
if let Self::Row { suffix: slot, .. } = &mut self {
|
|
||||||
*slot = Some(suffix.into_any_element());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders the row dimmed; clicks and keyboard skip it.
|
/// Renders the row dimmed; clicks and keyboard skip it.
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||||
if let Self::Row {
|
if let Self::Row {
|
||||||
@@ -218,7 +207,7 @@ impl RenderOnce for Sidebar {
|
|||||||
|
|
||||||
let mut row_position = 0usize;
|
let mut row_position = 0usize;
|
||||||
let children: Vec<AnyElement> = entries
|
let children: Vec<AnyElement> = entries
|
||||||
.into_iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, entry)| {
|
.map(|(index, entry)| {
|
||||||
let is_row = matches!(entry, SidebarEntry::Row { .. });
|
let is_row = matches!(entry, SidebarEntry::Row { .. });
|
||||||
@@ -285,7 +274,7 @@ impl RenderOnce for Sidebar {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_entry(
|
fn render_entry(
|
||||||
cx: &App,
|
cx: &App,
|
||||||
entry: SidebarEntry,
|
entry: &SidebarEntry,
|
||||||
index: usize,
|
index: usize,
|
||||||
position_in_set: usize,
|
position_in_set: usize,
|
||||||
size_of_set: usize,
|
size_of_set: usize,
|
||||||
@@ -304,18 +293,17 @@ fn render_entry(
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.font_weight(gpui::FontWeight::MEDIUM)
|
.font_weight(gpui::FontWeight::MEDIUM)
|
||||||
.text_color(theme.colors.muted_foreground)
|
.text_color(theme.colors.muted_foreground)
|
||||||
.child(label)
|
.child(label.clone())
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
SidebarEntry::Row {
|
SidebarEntry::Row {
|
||||||
label,
|
label,
|
||||||
|
icon: row_icon,
|
||||||
detail,
|
detail,
|
||||||
prefix,
|
|
||||||
suffix,
|
|
||||||
disabled: row_disabled,
|
disabled: row_disabled,
|
||||||
} => {
|
} => {
|
||||||
let label_color = selectable_label_color(cx, selected, row_disabled);
|
let label_color = selectable_label_color(cx, selected, *row_disabled);
|
||||||
|
|
||||||
div()
|
let mut row = div()
|
||||||
.id(("sidebar-row", index))
|
.id(("sidebar-row", index))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -328,45 +316,51 @@ fn render_entry(
|
|||||||
.aria_position_in_set(position_in_set)
|
.aria_position_in_set(position_in_set)
|
||||||
.aria_size_of_set(size_of_set)
|
.aria_size_of_set(size_of_set)
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
if material && !row_disabled {
|
if material && !*row_disabled {
|
||||||
if selected {
|
if selected {
|
||||||
row.bg(theme.material.selection)
|
row.bg(theme.material.selection)
|
||||||
} else {
|
} else {
|
||||||
row.hover(|row| row.bg(theme.material.hover))
|
row.hover(|row| row.bg(theme.material.hover))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
paint_selectable_row(cx, row, selected, row_disabled)
|
paint_selectable_row(cx, row, selected, *row_disabled)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.when(!row_disabled, |row| {
|
|
||||||
row.cursor_pointer().on_click({
|
if !*row_disabled {
|
||||||
let on_change = on_change.clone();
|
row = row.cursor_pointer().on_click({
|
||||||
let focus = focus.clone();
|
let on_change = on_change.clone();
|
||||||
move |_, window, cx| {
|
let focus = focus.clone();
|
||||||
window.focus(&focus, cx);
|
move |_, window, cx| {
|
||||||
if let Some(on_change) = &on_change {
|
window.focus(&focus, cx);
|
||||||
on_change(Some(index), window, cx);
|
if let Some(on_change) = &on_change {
|
||||||
}
|
on_change(Some(index), window, cx);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
});
|
||||||
.when_some(prefix, |row, prefix| row.child(prefix))
|
}
|
||||||
.child(div().flex_1().text_color(label_color).child(label))
|
|
||||||
.when_some(detail, |row, detail| {
|
row = row.when_some(row_icon.clone(), |row, icon| {
|
||||||
row.child(
|
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||||
div()
|
});
|
||||||
.flex_none()
|
|
||||||
.text_sm()
|
row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
|
||||||
.text_color(if selected {
|
|
||||||
theme.colors.primary_foreground
|
if let Some(detail) = detail {
|
||||||
} else {
|
row = row.child(
|
||||||
theme.colors.muted_foreground
|
div()
|
||||||
})
|
.flex_none()
|
||||||
.child(detail),
|
.text_sm()
|
||||||
)
|
.text_color(if selected {
|
||||||
})
|
theme.colors.primary_foreground
|
||||||
.when_some(suffix, |row, suffix| row.child(suffix))
|
} else {
|
||||||
.into_any_element()
|
theme.colors.muted_foreground
|
||||||
|
})
|
||||||
|
.child(detail.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-68
@@ -1,6 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Div, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent,
|
AnyElement, App, Div, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent,
|
||||||
ParentElement as _, RenderOnce, Role, SharedString, Stateful, StatefulInteractiveElement as _,
|
ParentElement as _, RenderOnce, Role, SharedString, Stateful, StatefulInteractiveElement as _,
|
||||||
@@ -10,7 +10,7 @@ use gpui_native_core::button::{Button, ButtonVariant};
|
|||||||
use gpui_native_core::common::Sizable as _;
|
use gpui_native_core::common::Sizable as _;
|
||||||
use gpui_native_core::spinner::Spinner;
|
use gpui_native_core::spinner::Spinner;
|
||||||
use gpui_native_core::tooltip::Tooltip;
|
use gpui_native_core::tooltip::Tooltip;
|
||||||
use gpui_native_icons::IconRole;
|
use gpui_native_icons::{Icon, IconRole};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
use gpui_native_utils::adjacent_selectable;
|
use gpui_native_utils::adjacent_selectable;
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ pub struct Tab {
|
|||||||
/// The stable identity for element state; must survive reordering.
|
/// The stable identity for element state; must survive reordering.
|
||||||
key: ElementId,
|
key: ElementId,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
|
icon: Option<Icon>,
|
||||||
lead: Option<AnyElement>,
|
lead: Option<AnyElement>,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
@@ -52,6 +53,7 @@ impl Tab {
|
|||||||
Self {
|
Self {
|
||||||
key: key.into(),
|
key: key.into(),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
|
icon: None,
|
||||||
lead: None,
|
lead: None,
|
||||||
busy: false,
|
busy: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -60,7 +62,16 @@ impl Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the leading element, e.g. an icon or an avatar.
|
/// Shows an icon leading the label.
|
||||||
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the leading element.
|
||||||
pub fn lead<E>(mut self, lead: E) -> Self
|
pub fn lead<E>(mut self, lead: E) -> Self
|
||||||
where
|
where
|
||||||
E: IntoElement + 'static,
|
E: IntoElement + 'static,
|
||||||
@@ -97,9 +108,6 @@ impl Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab is a data builder, not an element, adopt the combinators explicitly.
|
|
||||||
impl FluentBuilder for Tab {}
|
|
||||||
|
|
||||||
/// A tab strip.
|
/// A tab strip.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Tabs {
|
pub struct Tabs {
|
||||||
@@ -218,7 +226,7 @@ impl RenderOnce for Tabs {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
div()
|
let mut strip_root = div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.flex()
|
.flex()
|
||||||
.text_size(theme.sizes.medium.text_size)
|
.text_size(theme.sizes.medium.text_size)
|
||||||
@@ -246,14 +254,17 @@ impl RenderOnce for Tabs {
|
|||||||
}
|
}
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.map(|root| match self.variant {
|
|
||||||
TabsVariant::Underline => {
|
strip_root = match self.variant {
|
||||||
root.gap_1().border_b_1().border_color(theme.colors.border)
|
TabsVariant::Underline => strip_root
|
||||||
}
|
.gap_1()
|
||||||
TabsVariant::Pill => root.items_center().gap_1(),
|
.border_b_1()
|
||||||
})
|
.border_color(theme.colors.border),
|
||||||
.children(strip)
|
TabsVariant::Pill => strip_root.items_center().gap_1(),
|
||||||
|
};
|
||||||
|
|
||||||
|
strip_root.children(strip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +306,7 @@ fn render_tab(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let strip_tab = div()
|
let mut strip_tab = div()
|
||||||
.id((tab.key.clone(), "tab"))
|
.id((tab.key.clone(), "tab"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let tabs_id = tabs_id.clone();
|
let tabs_id = tabs_id.clone();
|
||||||
@@ -310,60 +321,81 @@ fn render_tab(
|
|||||||
.text_color(label_color)
|
.text_color(label_color)
|
||||||
.when(!tab.disabled && !selected, |strip_tab| {
|
.when(!tab.disabled && !selected, |strip_tab| {
|
||||||
strip_tab.hover(|strip_tab| strip_tab.bg(theme.colors.muted))
|
strip_tab.hover(|strip_tab| strip_tab.bg(theme.colors.muted))
|
||||||
})
|
|
||||||
.map(|strip_tab| match variant {
|
|
||||||
TabsVariant::Underline => strip_tab
|
|
||||||
.px_2()
|
|
||||||
.py_1()
|
|
||||||
.rounded_tl(theme.radius.sm)
|
|
||||||
.rounded_tr(theme.radius.sm)
|
|
||||||
.border_b_2()
|
|
||||||
.border_color(gpui::transparent_black())
|
|
||||||
.when(selected, |strip_tab| {
|
|
||||||
strip_tab.border_color(theme.ramps.primary.base)
|
|
||||||
}),
|
|
||||||
TabsVariant::Pill => strip_tab
|
|
||||||
.h(small.control_height + metrics.plate_inset * 2.0)
|
|
||||||
.px(metrics.dense_padding_x)
|
|
||||||
.pr(metrics.plate_inset)
|
|
||||||
.rounded(theme.radius.control)
|
|
||||||
.when(selected, |strip_tab| {
|
|
||||||
strip_tab
|
|
||||||
.bg(theme.colors.muted)
|
|
||||||
.hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover))
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.when_some(tab.lead.take(), |strip_tab, lead| strip_tab.child(lead))
|
|
||||||
.child(tab.label.clone())
|
|
||||||
.when(variant == TabsVariant::Pill, |strip_tab| {
|
|
||||||
strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5))
|
|
||||||
})
|
|
||||||
.when(tab.busy && !tab.disabled, |strip_tab| {
|
|
||||||
strip_tab.child(
|
|
||||||
Spinner::new((tab.key.clone(), "tab-busy"))
|
|
||||||
.small()
|
|
||||||
.accessibility_label("Loading"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(
|
|
||||||
tab.closeable && on_close.is_some() && !tab.disabled,
|
|
||||||
|strip_tab| strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)),
|
|
||||||
)
|
|
||||||
.when(!tab.disabled, |strip_tab| {
|
|
||||||
strip_tab.cursor_pointer().on_click({
|
|
||||||
let on_change = on_change.clone();
|
|
||||||
let focus = focus.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
window.focus(&focus, cx);
|
|
||||||
if index != selected_index
|
|
||||||
&& let Some(on_change) = &on_change
|
|
||||||
{
|
|
||||||
on_change(index, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
strip_tab = match variant {
|
||||||
|
TabsVariant::Underline => strip_tab
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded_tl(theme.radius.sm)
|
||||||
|
.rounded_tr(theme.radius.sm)
|
||||||
|
.border_b_2()
|
||||||
|
.border_color(gpui::transparent_black())
|
||||||
|
.when(selected, |strip_tab| {
|
||||||
|
strip_tab.border_color(theme.ramps.primary.base)
|
||||||
|
}),
|
||||||
|
TabsVariant::Pill => strip_tab
|
||||||
|
.h(small.control_height + metrics.plate_inset * 2.0)
|
||||||
|
.px(metrics.dense_padding_x)
|
||||||
|
.pr(metrics.plate_inset)
|
||||||
|
.rounded(theme.radius.control)
|
||||||
|
.when(selected, |strip_tab| {
|
||||||
|
strip_tab
|
||||||
|
.bg(theme.colors.muted)
|
||||||
|
.hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover))
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let lead = tab.lead.take();
|
||||||
|
let icon = tab.icon.take();
|
||||||
|
|
||||||
|
strip_tab = match lead {
|
||||||
|
Some(lead) => strip_tab.child(lead),
|
||||||
|
None => match variant {
|
||||||
|
TabsVariant::Underline => {
|
||||||
|
strip_tab.child(div().flex_none().pb_0p5().when_some(icon, |slot, icon| {
|
||||||
|
slot.child(icon.size(metrics.dense_icon_size).color(label_color))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
TabsVariant::Pill => strip_tab.when_some(icon, |strip_tab, icon| {
|
||||||
|
strip_tab.child(icon.size(metrics.dense_icon_size).color(label_color))
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
strip_tab = strip_tab.child(tab.label.clone());
|
||||||
|
|
||||||
|
if variant == TabsVariant::Pill {
|
||||||
|
strip_tab = strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
if tab.busy && !tab.disabled {
|
||||||
|
strip_tab = strip_tab.child(
|
||||||
|
Spinner::new((tab.key.clone(), "tab-busy"))
|
||||||
|
.small()
|
||||||
|
.accessibility_label("Loading"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if tab.closeable && on_close.is_some() && !tab.disabled {
|
||||||
|
strip_tab = strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tab.disabled {
|
||||||
|
strip_tab = strip_tab.cursor_pointer().on_click({
|
||||||
|
let on_change = on_change.clone();
|
||||||
|
let focus = focus.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
window.focus(&focus, cx);
|
||||||
|
if index != selected_index
|
||||||
|
&& let Some(on_change) = &on_change
|
||||||
|
{
|
||||||
|
on_change(index, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
match tab.wrap {
|
match tab.wrap {
|
||||||
Some(wrap) => wrap(strip_tab),
|
Some(wrap) => wrap(strip_tab),
|
||||||
None => strip_tab.into_any_element(),
|
None => strip_tab.into_any_element(),
|
||||||
|
|||||||
@@ -546,25 +546,24 @@ fn render_row(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.when(options.line_numbers, |row_element| {
|
.when(options.line_numbers, |row_element| {
|
||||||
row_element.child(
|
let mut number = gutter_cell(
|
||||||
gutter_cell(
|
cx,
|
||||||
cx,
|
colors.muted_foreground,
|
||||||
colors.muted_foreground,
|
row.new_number
|
||||||
row.new_number
|
.or(row.old_number)
|
||||||
.or(row.old_number)
|
.map(|number| number.to_string())
|
||||||
.map(|number| number.to_string())
|
.unwrap_or_default(),
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.w_10()
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.bg(match row.kind {
|
|
||||||
DiffRowKind::Context => wash_gutter(&colors),
|
|
||||||
_ => wash_strong(change_color),
|
|
||||||
})
|
|
||||||
.when(options.indicators == DiffIndicators::Bars, |number| {
|
|
||||||
number.border_l_4().border_color(change_color)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
.w_10()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.bg(match row.kind {
|
||||||
|
DiffRowKind::Context => wash_gutter(&colors),
|
||||||
|
_ => wash_strong(change_color),
|
||||||
|
});
|
||||||
|
if options.indicators == DiffIndicators::Bars {
|
||||||
|
number = number.border_l_4().border_color(change_color);
|
||||||
|
}
|
||||||
|
row_element.child(number)
|
||||||
})
|
})
|
||||||
.when(
|
.when(
|
||||||
options.indicators == DiffIndicators::Bars && !options.line_numbers,
|
options.indicators == DiffIndicators::Bars && !options.line_numbers,
|
||||||
|
|||||||
@@ -817,43 +817,43 @@ fn text_row(
|
|||||||
cx: &App,
|
cx: &App,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
|
let mut frame = block_frame(indent, quote, px(0.), bottom, id, cx);
|
||||||
|
|
||||||
block_frame(indent, quote, px(0.), bottom, id, cx)
|
if let Some(marker) = marker {
|
||||||
.map(|frame| match marker {
|
let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT;
|
||||||
Some(marker) => {
|
let marker_cell = div()
|
||||||
let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT;
|
.flex_none()
|
||||||
let marker_cell = div()
|
.w(theme.root_font_size * MARKER_WIDTH)
|
||||||
.flex_none()
|
.h(line_height)
|
||||||
.w(theme.root_font_size * MARKER_WIDTH)
|
.flex()
|
||||||
.h(line_height)
|
.items_center()
|
||||||
.flex()
|
.justify_end()
|
||||||
.items_center()
|
.pr_1()
|
||||||
.justify_end()
|
.child(match marker {
|
||||||
.pr_1()
|
ListMarker::Bullet => div()
|
||||||
.child(match marker {
|
.text_color(theme.colors.muted_foreground)
|
||||||
ListMarker::Bullet => div()
|
.child("•")
|
||||||
.text_color(theme.colors.muted_foreground)
|
.into_any_element(),
|
||||||
.child("•")
|
ListMarker::Ordered(number) => div()
|
||||||
.into_any_element(),
|
.text_color(theme.colors.muted_foreground)
|
||||||
ListMarker::Ordered(number) => div()
|
.whitespace_nowrap()
|
||||||
.text_color(theme.colors.muted_foreground)
|
.child(format!("{number}."))
|
||||||
.whitespace_nowrap()
|
.into_any_element(),
|
||||||
.child(format!("{number}."))
|
ListMarker::Task(checked) => task_box(checked, cx),
|
||||||
.into_any_element(),
|
});
|
||||||
ListMarker::Task(checked) => task_box(checked, cx),
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.child(
|
frame = frame.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.child(marker_cell)
|
.child(marker_cell)
|
||||||
.child(div().flex_1().min_w_0().child(text_child(spans, id, cx))),
|
.child(div().flex_1().min_w_0().child(text_child(spans, id, cx))),
|
||||||
)
|
);
|
||||||
}
|
} else {
|
||||||
None => frame.child(text_child(spans, id, cx)),
|
frame = frame.child(text_child(spans, id, cx));
|
||||||
})
|
}
|
||||||
.into_any_element()
|
|
||||||
|
frame.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A read-only task item indicator, matching the checkbox component.
|
/// A read-only task item indicator, matching the checkbox component.
|
||||||
@@ -896,7 +896,7 @@ fn code_row(
|
|||||||
let text_size = theme.sizes.small.text_size;
|
let text_size = theme.sizes.small.text_size;
|
||||||
let line_height = text_size * CODE_LINE_HEIGHT;
|
let line_height = text_size * CODE_LINE_HEIGHT;
|
||||||
|
|
||||||
let plate = div()
|
let mut plate = div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -906,14 +906,17 @@ fn code_row(
|
|||||||
.font_family(theme.mono_font)
|
.font_family(theme.mono_font)
|
||||||
.text_size(text_size)
|
.text_size(text_size)
|
||||||
.line_height(relative(CODE_LINE_HEIGHT))
|
.line_height(relative(CODE_LINE_HEIGHT))
|
||||||
.overflow_hidden()
|
.overflow_hidden();
|
||||||
.children(lines.lines.iter().map(|line| {
|
|
||||||
|
for line in &lines.lines {
|
||||||
|
plate = plate.child(
|
||||||
div()
|
div()
|
||||||
.h(line_height)
|
.h(line_height)
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(code_line(line, theme.colors, theme.ramps))
|
.child(code_line(line, theme.colors, theme.ramps)),
|
||||||
}));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
block_frame(
|
block_frame(
|
||||||
indent,
|
indent,
|
||||||
@@ -958,47 +961,48 @@ fn table_row(
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let row_count = table.rows.len();
|
let row_count = table.rows.len();
|
||||||
|
|
||||||
let grid = div()
|
let mut grid = div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(colors.border)
|
.border_color(colors.border)
|
||||||
.rounded(theme.radius.control)
|
.rounded(theme.radius.control)
|
||||||
.overflow_hidden()
|
.overflow_hidden();
|
||||||
.children(table.rows.iter().enumerate().map(|(row_ix, row)| {
|
|
||||||
div()
|
for (row_ix, row) in table.rows.iter().enumerate() {
|
||||||
.w_full()
|
let mut row_element = div().w_full().flex();
|
||||||
.flex()
|
if row_ix + 1 < row_count {
|
||||||
.when(row_ix + 1 < row_count, |row_element| {
|
row_element = row_element.border_b_1().border_color(colors.border);
|
||||||
row_element.border_b_1().border_color(colors.border)
|
}
|
||||||
})
|
if row.header {
|
||||||
.when(row.header, |row_element| row_element.bg(colors.muted))
|
row_element = row_element.bg(colors.muted);
|
||||||
.children(
|
}
|
||||||
row.cells
|
|
||||||
.iter()
|
for (column_ix, cell) in row.cells.iter().enumerate() {
|
||||||
.enumerate()
|
let cell_id = ElementId::from(SharedString::from(format!(
|
||||||
.map(|(column_ix, cell)| {
|
"{id}-cell-{row_ix}-{column_ix}"
|
||||||
let cell_id = ElementId::from(SharedString::from(format!(
|
)));
|
||||||
"{id}-cell-{row_ix}-{column_ix}"
|
row_element = row_element.child(
|
||||||
)));
|
div()
|
||||||
div()
|
.flex_1()
|
||||||
.flex_1()
|
.min_w_0()
|
||||||
.min_w_0()
|
.px_2()
|
||||||
.px_2()
|
.py_1()
|
||||||
.py_1()
|
.when(row.header, |cell_element| {
|
||||||
.when(row.header, |cell_element| {
|
cell_element.font_weight(FontWeight::SEMIBOLD)
|
||||||
cell_element.font_weight(FontWeight::SEMIBOLD)
|
})
|
||||||
})
|
.child(text_child(&cell.spans, &cell_id, cx)),
|
||||||
.child(text_child(&cell.spans, &cell_id, cx))
|
);
|
||||||
})
|
}
|
||||||
// Ragged tables pad missing cells so rows align.
|
|
||||||
.chain(
|
// Ragged tables pad missing cells so rows align.
|
||||||
(row.cells.len()..width)
|
for _ in row.cells.len()..width {
|
||||||
.map(|_| div().flex_1().min_w_0().px_2().py_1()),
|
row_element = row_element.child(div().flex_1().min_w_0().px_2().py_1());
|
||||||
),
|
}
|
||||||
)
|
|
||||||
}));
|
grid = grid.child(row_element);
|
||||||
|
}
|
||||||
|
|
||||||
block_frame(
|
block_frame(
|
||||||
indent,
|
indent,
|
||||||
|
|||||||
+25
-29
@@ -1038,37 +1038,33 @@ fn render_pane(
|
|||||||
.selected(selected)
|
.selected(selected)
|
||||||
.tabs({
|
.tabs({
|
||||||
let tabs = pane.tabs.clone();
|
let tabs = pane.tabs.clone();
|
||||||
move |_, cx| {
|
move |_, _| {
|
||||||
let theme = cx.theme();
|
|
||||||
let metrics = theme.sizes.resolve(Size::default());
|
|
||||||
|
|
||||||
tabs.iter()
|
tabs.iter()
|
||||||
.map(|tab| {
|
.map(|tab| {
|
||||||
Tab::new(tab.panel.clone(), tab.title.clone())
|
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone());
|
||||||
.when_some(tab.icon.clone(), |entry, icon| {
|
if let Some(icon) = tab.icon.clone() {
|
||||||
entry.lead(icon.size(metrics.icon_size))
|
entry = entry.icon(icon);
|
||||||
})
|
}
|
||||||
.closeable(true)
|
entry.closeable(true).wrap({
|
||||||
.wrap({
|
let panel = tab.panel.clone();
|
||||||
let panel = tab.panel.clone();
|
let title = tab.title.clone();
|
||||||
let title = tab.title.clone();
|
move |tab| {
|
||||||
move |tab| {
|
tab.on_drag(
|
||||||
tab.on_drag(
|
PanelDrag {
|
||||||
PanelDrag {
|
panel: panel.clone(),
|
||||||
panel: panel.clone(),
|
},
|
||||||
},
|
{
|
||||||
{
|
let title = title.clone();
|
||||||
let title = title.clone();
|
move |_, _, _, cx| {
|
||||||
move |_, _, _, cx| {
|
cx.new(|_| DragPreview {
|
||||||
cx.new(|_| DragPreview {
|
title: title.clone(),
|
||||||
title: title.clone(),
|
})
|
||||||
})
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
)
|
.into_any_element()
|
||||||
.into_any_element()
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,36 +220,39 @@ impl RenderOnce for Resizable {
|
|||||||
.map(|ix| state.read(cx).pane_size(ix, pane_count))
|
.map(|ix| state.read(cx).pane_size(ix, pane_count))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
div()
|
let mut group = div()
|
||||||
.flex()
|
.flex()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.when(axis == Axis::Vertical, |group| group.flex_col())
|
.when(axis == Axis::Vertical, |group| group.flex_col())
|
||||||
.when(axis == Axis::Horizontal, |group| group.flex_row())
|
.when(axis == Axis::Horizontal, |group| group.flex_row())
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style);
|
||||||
.children(self.panes.into_iter().enumerate().flat_map(|(ix, pane)| {
|
|
||||||
let remainder = ix + 1 == pane_count;
|
|
||||||
let pane_element = div()
|
|
||||||
.overflow_hidden()
|
|
||||||
.when(remainder, |pane_element| {
|
|
||||||
pane_element
|
|
||||||
.flex_1()
|
|
||||||
.when(axis == Axis::Vertical, |p| p.min_h(pane.min))
|
|
||||||
.when(axis == Axis::Horizontal, |p| p.min_w(pane.min))
|
|
||||||
})
|
|
||||||
.when(!remainder, |pane_element| {
|
|
||||||
pane_element
|
|
||||||
.flex_none()
|
|
||||||
.when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full())
|
|
||||||
.when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full())
|
|
||||||
})
|
|
||||||
.when_some(debug_selector.clone(), |pane_element, selector| {
|
|
||||||
let prefix = selector();
|
|
||||||
pane_element.debug_selector(move || format!("{prefix}-{ix}"))
|
|
||||||
})
|
|
||||||
.child(pane.content)
|
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
let divider = (!remainder).then(|| {
|
for (ix, pane) in self.panes.into_iter().enumerate() {
|
||||||
|
let remainder = ix + 1 == pane_count;
|
||||||
|
let content = pane.content;
|
||||||
|
let mut pane_element = if remainder {
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.overflow_hidden()
|
||||||
|
.when(axis == Axis::Vertical, |p| p.min_h(pane.min))
|
||||||
|
.when(axis == Axis::Horizontal, |p| p.min_w(pane.min))
|
||||||
|
} else {
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.overflow_hidden()
|
||||||
|
.when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full())
|
||||||
|
.when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(selector) = &debug_selector {
|
||||||
|
let prefix = selector();
|
||||||
|
pane_element = pane_element.debug_selector(move || format!("{prefix}-{ix}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
group = group.child(pane_element.child(content));
|
||||||
|
|
||||||
|
if !remainder {
|
||||||
|
let divider =
|
||||||
ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id)))
|
ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id)))
|
||||||
.axis(axis)
|
.axis(axis)
|
||||||
.on_resize({
|
.on_resize({
|
||||||
@@ -265,30 +268,32 @@ impl RenderOnce for Resizable {
|
|||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
state.update(cx, |state, cx| state.reset_pane(ix, mins.len(), cx));
|
state.update(cx, |state, cx| state.reset_pane(ix, mins.len(), cx));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.into_any_element()
|
|
||||||
});
|
|
||||||
|
|
||||||
std::iter::once(pane_element).chain(divider)
|
group = group.child(divider);
|
||||||
}))
|
}
|
||||||
.when(pane_count > 0, |group| {
|
}
|
||||||
group.child(
|
|
||||||
canvas(
|
if pane_count > 0 {
|
||||||
{
|
group = group.child(
|
||||||
let state = state.clone();
|
canvas(
|
||||||
move |bounds: Bounds<Pixels>, _, cx: &mut App| {
|
{
|
||||||
let extent = match axis {
|
let state = state.clone();
|
||||||
Axis::Vertical => bounds.size.height,
|
move |bounds: Bounds<Pixels>, _, cx: &mut App| {
|
||||||
Axis::Horizontal => bounds.size.width,
|
let extent = match axis {
|
||||||
};
|
Axis::Vertical => bounds.size.height,
|
||||||
state.update(cx, |state, cx| state.set_measured(extent, cx));
|
Axis::Horizontal => bounds.size.width,
|
||||||
}
|
};
|
||||||
},
|
state.update(cx, |state, cx| state.set_measured(extent, cx));
|
||||||
|_, _, _, _| {},
|
}
|
||||||
)
|
},
|
||||||
.absolute()
|
|_, _, _, _| {},
|
||||||
.inset_0(),
|
|
||||||
)
|
)
|
||||||
})
|
.absolute()
|
||||||
|
.inset_0(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ impl SizeTable {
|
|||||||
padding: 0.75,
|
padding: 0.75,
|
||||||
text: 0.85,
|
text: 0.85,
|
||||||
icon: 0.875,
|
icon: 0.875,
|
||||||
avatar: 1.125,
|
avatar: 1.5,
|
||||||
strip: 1.5,
|
strip: 1.5,
|
||||||
inset: 0.125,
|
inset: 0.125,
|
||||||
floor_icon: false,
|
floor_icon: false,
|
||||||
@@ -435,7 +435,7 @@ impl SizeTable {
|
|||||||
padding: 1.0,
|
padding: 1.0,
|
||||||
text: 1.0,
|
text: 1.0,
|
||||||
icon: 1.0,
|
icon: 1.0,
|
||||||
avatar: 1.5,
|
avatar: 2.0,
|
||||||
strip: 2.0,
|
strip: 2.0,
|
||||||
inset: 0.25,
|
inset: 0.25,
|
||||||
floor_icon: true,
|
floor_icon: true,
|
||||||
@@ -446,7 +446,7 @@ impl SizeTable {
|
|||||||
padding: 1.25,
|
padding: 1.25,
|
||||||
text: 1.15,
|
text: 1.15,
|
||||||
icon: 1.25,
|
icon: 1.25,
|
||||||
avatar: 2.0,
|
avatar: 3.0,
|
||||||
strip: 3.0,
|
strip: 3.0,
|
||||||
inset: 0.375,
|
inset: 0.375,
|
||||||
floor_icon: true,
|
floor_icon: true,
|
||||||
|
|||||||
@@ -390,9 +390,7 @@ mod tests {
|
|||||||
assert_eq!(resolved.sizes.medium.control_height, px(28.));
|
assert_eq!(resolved.sizes.medium.control_height, px(28.));
|
||||||
assert_eq!(resolved.sizes.medium.input_height, px(30.));
|
assert_eq!(resolved.sizes.medium.input_height, px(30.));
|
||||||
assert_eq!(resolved.sizes.medium.text_size, px(16.));
|
assert_eq!(resolved.sizes.medium.text_size, px(16.));
|
||||||
assert_eq!(resolved.sizes.small.avatar_size, px(18.));
|
assert_eq!(resolved.sizes.large.avatar_size, px(48.));
|
||||||
assert_eq!(resolved.sizes.medium.avatar_size, px(24.));
|
|
||||||
assert_eq!(resolved.sizes.large.avatar_size, px(32.));
|
|
||||||
assert_eq!(resolved.sizes.small.caption_size, px(13.6 * 0.85));
|
assert_eq!(resolved.sizes.small.caption_size, px(13.6 * 0.85));
|
||||||
assert_eq!(resolved.sizes.medium.caption_size, px(16.));
|
assert_eq!(resolved.sizes.medium.caption_size, px(16.));
|
||||||
assert_eq!(resolved.sizes.large.caption_size, px(16.));
|
assert_eq!(resolved.sizes.large.caption_size, px(16.));
|
||||||
|
|||||||
@@ -9,17 +9,6 @@ is the first downstream consumer; its screens drive the order.
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- [x] **Declarative render audit** — the AGENTS.md render rule swept the
|
|
||||||
landed components: `let mut` element variables reassigned under `if` /
|
|
||||||
`if let` now build one chained expression with `.when(...)` /
|
|
||||||
`.when_some(...)` / `.map(...)`. Converted: Tabs strip root and tab,
|
|
||||||
Menu / DropdownMenu / Popover / Combobox wrappers, ScrollArea pane and
|
|
||||||
scrollbars, Select rows, Dock tab entries, Resizable panes and dividers,
|
|
||||||
the Markdown text / code / table rows, and the Diff gutter cell.
|
|
||||||
`MenuItem` and `Tab` adopted `FluentBuilder` so their builders chain
|
|
||||||
too; data accumulators (`Vec`/`String` builders, style clones) stay
|
|
||||||
imperative — they are state, not element refinement.
|
|
||||||
|
|
||||||
## Queue — to follow
|
## Queue — to follow
|
||||||
|
|
||||||
Platform metric adoption continues one component at a time; each stop goes
|
Platform metric adoption continues one component at a time; each stop goes
|
||||||
|
|||||||
@@ -509,7 +509,6 @@ impl Gallery {
|
|||||||
SharedString::from("Blame"),
|
SharedString::from("Blame"),
|
||||||
],
|
],
|
||||||
pill_selected: 0,
|
pill_selected: 0,
|
||||||
lead_selected: 0,
|
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}));
|
}));
|
||||||
@@ -1869,27 +1868,13 @@ impl Render for BadgeDemo {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(Badge::new("badge-count", "3"))
|
.child(Badge::new("3"))
|
||||||
.child(Badge::new("badge-new", "new").variant(BadgeVariant::Accent))
|
.child(Badge::new("new").variant(BadgeVariant::Accent))
|
||||||
.child(
|
.child(Badge::new("synced").variant(BadgeVariant::Success))
|
||||||
Badge::new("badge-synced", "synced")
|
.child(Badge::new("drift").variant(BadgeVariant::Warning))
|
||||||
.icon(IconRole::Check)
|
.child(Badge::new("12").variant(BadgeVariant::Danger))
|
||||||
.variant(BadgeVariant::Success),
|
.child(Badge::new("small").small())
|
||||||
)
|
.child(Badge::new("large").large()),
|
||||||
.child(Badge::new("badge-drift", "drift").variant(BadgeVariant::Warning))
|
|
||||||
.child(Badge::new("badge-danger", "12").variant(BadgeVariant::Danger))
|
|
||||||
.child(Badge::new("badge-outline", "outline").variant(BadgeVariant::Outline))
|
|
||||||
.child(Badge::new("badge-small", "small").small())
|
|
||||||
.child(Badge::new("badge-large", "large").large()),
|
|
||||||
))
|
|
||||||
.child(demo_section(
|
|
||||||
"tooltip",
|
|
||||||
colors,
|
|
||||||
h_flex().flex_wrap().gap_3().child(
|
|
||||||
Badge::new("badge-help", "beta")
|
|
||||||
.variant(BadgeVariant::Accent)
|
|
||||||
.tooltip(Tooltip::text("This build is not ready for release")),
|
|
||||||
),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2208,7 +2193,6 @@ struct TabsDemo {
|
|||||||
selected: usize,
|
selected: usize,
|
||||||
pill_tabs: Vec<SharedString>,
|
pill_tabs: Vec<SharedString>,
|
||||||
pill_selected: usize,
|
pill_selected: usize,
|
||||||
lead_selected: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for TabsDemo {
|
impl Render for TabsDemo {
|
||||||
@@ -2216,7 +2200,6 @@ impl Render for TabsDemo {
|
|||||||
let view = cx.entity();
|
let view = cx.entity();
|
||||||
let view_pill = view.clone();
|
let view_pill = view.clone();
|
||||||
let view_close = view.clone();
|
let view_close = view.clone();
|
||||||
let view_leads = view.clone();
|
|
||||||
let colors = cx.theme().colors;
|
let colors = cx.theme().colors;
|
||||||
let pill_count = self.pill_tabs.len();
|
let pill_count = self.pill_tabs.len();
|
||||||
|
|
||||||
@@ -2298,31 +2281,7 @@ impl Render for TabsDemo {
|
|||||||
.child(format!(
|
.child(format!(
|
||||||
"{pill_count} tabs left; \"Syncing\" spins while it loads"
|
"{pill_count} tabs left; \"Syncing\" spins while it loads"
|
||||||
)),
|
)),
|
||||||
)
|
),
|
||||||
.child(demo_section(
|
|
||||||
"leads",
|
|
||||||
colors,
|
|
||||||
Tabs::new("tabs-demo-leads")
|
|
||||||
.variant(TabsVariant::Pill)
|
|
||||||
.selected(self.lead_selected)
|
|
||||||
.on_change(move |selected, _, cx| {
|
|
||||||
view_leads.update(cx, |demo, cx| {
|
|
||||||
demo.lead_selected = selected;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.tabs(|_, cx| {
|
|
||||||
let theme = cx.theme();
|
|
||||||
let metrics = theme.sizes.resolve(Size::default());
|
|
||||||
vec![
|
|
||||||
Tab::new("files", "Files")
|
|
||||||
.lead(icon(IconRole::Folder).size(metrics.dense_icon_size)),
|
|
||||||
Tab::new("people", "People")
|
|
||||||
.lead(Avatar::new("ada").name("Ada Lovelace").small()),
|
|
||||||
Tab::new("plain", "Plain"),
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2755,11 +2714,7 @@ impl Render for DropdownMenuDemo {
|
|||||||
MenuItem::new("Save as template"),
|
MenuItem::new("Save as template"),
|
||||||
MenuItem::new("Save a copy"),
|
MenuItem::new("Save a copy"),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Export").submenu(vec![
|
MenuItem::new("Export"),
|
||||||
MenuItem::new("PNG"),
|
|
||||||
MenuItem::new("SVG"),
|
|
||||||
MenuItem::new("PDF"),
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3757,28 +3712,16 @@ impl Render for SidebarDemo {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.entries(|_, cx| {
|
.entries(|_, _| {
|
||||||
let theme = cx.theme();
|
|
||||||
let row_icon = || {
|
|
||||||
icon(IconRole::Folder).size(theme.sizes.medium.icon_size)
|
|
||||||
};
|
|
||||||
vec![
|
vec![
|
||||||
SidebarEntry::header("Favorites"),
|
SidebarEntry::header("Favorites"),
|
||||||
SidebarEntry::row("Documents").prefix(row_icon()),
|
SidebarEntry::row("Documents").icon(IconRole::Folder),
|
||||||
SidebarEntry::row("Pictures")
|
SidebarEntry::row("Pictures")
|
||||||
.prefix(row_icon())
|
.icon(IconRole::Folder)
|
||||||
.detail("24")
|
.detail("24"),
|
||||||
.suffix(
|
|
||||||
Badge::new("sidebar-demo-new-badge", "new")
|
|
||||||
.variant(BadgeVariant::Accent),
|
|
||||||
),
|
|
||||||
SidebarEntry::row("Archive").disabled(true),
|
SidebarEntry::row("Archive").disabled(true),
|
||||||
SidebarEntry::header("Locations"),
|
SidebarEntry::header("Locations"),
|
||||||
SidebarEntry::row("iCloud").prefix(row_icon()).suffix(
|
SidebarEntry::row("iCloud").icon(IconRole::Folder),
|
||||||
StatusDot::new("sidebar-demo-icloud-status")
|
|
||||||
.small()
|
|
||||||
.variant(StatusDotVariant::Success),
|
|
||||||
),
|
|
||||||
SidebarEntry::row("Trash"),
|
SidebarEntry::row("Trash"),
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ pub use gpui_native_data as data;
|
|||||||
#[cfg(feature = "layout")]
|
#[cfg(feature = "layout")]
|
||||||
pub use gpui_native_layout as layout;
|
pub use gpui_native_layout as layout;
|
||||||
|
|
||||||
|
#[cfg(feature = "editor")]
|
||||||
|
pub use gpui_native_editor as editor;
|
||||||
|
|
||||||
use gpui::App;
|
use gpui::App;
|
||||||
|
|
||||||
/// Initializes the framework.
|
/// Initializes the framework.
|
||||||
|
|||||||
Reference in New Issue
Block a user