feat: replace sidebar row icon with prefix and suffix slots
This commit is contained in:
@@ -64,7 +64,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; use `.when(...)` / `.when_some(...)` for small conditional refinements.
|
- 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`.
|
||||||
- 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.
|
||||||
|
|||||||
+64
-58
@@ -10,7 +10,6 @@ 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};
|
||||||
|
|
||||||
@@ -19,15 +18,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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -50,23 +49,13 @@ 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
|
||||||
@@ -81,6 +70,28 @@ 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 {
|
||||||
@@ -207,7 +218,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
|
||||||
.iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, entry)| {
|
.map(|(index, entry)| {
|
||||||
let is_row = matches!(entry, SidebarEntry::Row { .. });
|
let is_row = matches!(entry, SidebarEntry::Row { .. });
|
||||||
@@ -274,7 +285,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,
|
||||||
@@ -293,17 +304,18 @@ 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.clone())
|
.child(label)
|
||||||
.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);
|
||||||
|
|
||||||
let mut row = div()
|
div()
|
||||||
.id(("sidebar-row", index))
|
.id(("sidebar-row", index))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -316,51 +328,45 @@ 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| {
|
||||||
if !*row_disabled {
|
row.cursor_pointer().on_click({
|
||||||
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| {
|
window.focus(&focus, cx);
|
||||||
window.focus(&focus, cx);
|
if let Some(on_change) = &on_change {
|
||||||
if let Some(on_change) = &on_change {
|
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(
|
||||||
});
|
div()
|
||||||
|
.flex_none()
|
||||||
row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
|
.text_sm()
|
||||||
|
.text_color(if selected {
|
||||||
if let Some(detail) = detail {
|
theme.colors.primary_foreground
|
||||||
row = row.child(
|
} else {
|
||||||
div()
|
theme.colors.muted_foreground
|
||||||
.flex_none()
|
})
|
||||||
.text_sm()
|
.child(detail),
|
||||||
.text_color(if selected {
|
)
|
||||||
theme.colors.primary_foreground
|
})
|
||||||
} else {
|
.when_some(suffix, |row, suffix| row.child(suffix))
|
||||||
theme.colors.muted_foreground
|
.into_any_element()
|
||||||
})
|
|
||||||
.child(detail.clone()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
row.into_any_element()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ is the first downstream consumer; its screens drive the order.
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
- [ ] **Declarative render audit** — run the AGENTS.md render rule over the
|
||||||
|
other landed components: replace `let mut` element variables reassigned
|
||||||
|
under `if` / `if let` with one chained expression using `.when(...)` /
|
||||||
|
`.when_some(...)` / `.map(...)`. The sidebar row is the model; sweep
|
||||||
|
`crates/core`, `crates/data`, `crates/editor`, `crates/layout`, and
|
||||||
|
re-run tests plus a gallery pass after each crate.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -3739,16 +3739,27 @@ impl Render for SidebarDemo {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.entries(|_, _| {
|
.entries(|_, cx| {
|
||||||
|
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").icon(IconRole::Folder),
|
SidebarEntry::row("Documents").prefix(row_icon()),
|
||||||
SidebarEntry::row("Pictures")
|
SidebarEntry::row("Pictures")
|
||||||
.icon(IconRole::Folder)
|
.prefix(row_icon())
|
||||||
.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").icon(IconRole::Folder),
|
SidebarEntry::row("iCloud").prefix(row_icon()).suffix(
|
||||||
|
StatusDot::new("sidebar-demo-icloud-status")
|
||||||
|
.small()
|
||||||
|
.variant(StatusDotVariant::Success),
|
||||||
|
),
|
||||||
SidebarEntry::row("Trash"),
|
SidebarEntry::row("Trash"),
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user