feat: apply declarative render rule across components

This commit is contained in:
2026-11-10 08:04:24 +07:00
parent fbc4f24d16
commit 44abaf47a9
12 changed files with 545 additions and 561 deletions
+21 -18
View File
@@ -518,7 +518,7 @@ where
let wrapper_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let mut wrapper = div()
div()
.id(id.clone())
.key_context(COMBOBOX_CONTEXT)
.relative()
@@ -575,9 +575,8 @@ where
.right(metrics.dense_padding_x)
.top((metrics.input_height + BORDER_WIDTH * 2.0 - icon_size) / 2.)
.child(chevron),
);
if open {
)
.when(open, |wrapper| {
let rows: Vec<gpui::AnyElement> = self
.visible
.iter()
@@ -617,7 +616,9 @@ where
.on_click({
let state = state.clone();
move |_, window, cx| {
state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
state.update(cx, |state, cx| {
state.choose_row(row_ix, window, cx)
});
}
})
.child(
@@ -649,8 +650,9 @@ where
.child("No matches")
.into_any_element()
} else {
let scroll = window
.use_keyed_state((id.clone(), "rows-scroll"), cx, |_, _| ScrollHandle::new());
let scroll = window.use_keyed_state((id.clone(), "rows-scroll"), cx, |_, _| {
ScrollHandle::new()
});
ScrollArea::new((id.clone(), "rows"))
.handle(scroll.read(cx).clone())
@@ -661,6 +663,7 @@ where
};
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let popup_width = wrapper_bounds
.borrow()
.map(|bounds| bounds.size.width)
@@ -701,12 +704,14 @@ where
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));
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;
}
@@ -723,11 +728,9 @@ where
.inset_0(),
);
wrapper = wrapper.child(deferred(
wrapper.child(deferred(
anchored().offset(point(px(0.), spacing_unit)).child(panel),
));
}
wrapper
))
})
}
}
+15 -17
View File
@@ -366,7 +366,15 @@ impl RenderOnce for DropdownMenu {
trigger
};
let mut wrapper = div()
let items = if self.open {
self.items
.map(|build| Rc::from(build(window, cx)))
.unwrap_or_else(|| Rc::from([]))
} else {
Rc::from([])
};
div()
.relative()
.child(
canvas(
@@ -421,17 +429,9 @@ impl RenderOnce for DropdownMenu {
.absolute()
.inset_0(),
)
.child(trigger);
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(
.child(trigger)
.when_some(
anchored_menu_panel(
window,
cx,
&self.id,
@@ -445,10 +445,8 @@ impl RenderOnce for DropdownMenu {
&trigger_bounds,
&panel_chrome,
enter_duration,
) {
wrapper = wrapper.child(panel);
}
wrapper
),
|wrapper, panel| wrapper.child(panel),
)
}
}
+20 -19
View File
@@ -1,7 +1,7 @@
use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::prelude::FluentBuilder;
use gpui::{
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, DispatchPhase, ElementId,
Entity, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
@@ -127,6 +127,9 @@ impl MenuItem {
}
}
// MenuItem is a data builder, not an element, adopt the combinators explicitly.
impl FluentBuilder for MenuItem {}
/// Menu chrome styles.
#[derive(Clone)]
pub(crate) struct MenuChrome {
@@ -288,7 +291,16 @@ impl RenderOnce for Menu {
.clone()
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
let mut wrapper = div()
let items = if self.open {
match self.items {
Some(build) => Rc::from(build(window, cx)),
None => Rc::from([]),
}
} else {
Rc::from([])
};
div()
.relative()
.child(
canvas(
@@ -335,18 +347,9 @@ impl RenderOnce for Menu {
)
.when_some(self.trigger, |wrapper, trigger| {
wrapper.child(trigger(window, cx))
});
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(
})
.when_some(
anchored_menu_panel(
window,
cx,
&self.id,
@@ -360,11 +363,9 @@ impl RenderOnce for Menu {
&trigger_bounds,
&chrome,
enter_duration,
) {
wrapper = wrapper.child(panel);
}
wrapper
),
|wrapper, panel| wrapper.child(panel),
)
}
}
+6 -9
View File
@@ -114,7 +114,7 @@ impl RenderOnce for Popover {
.clone()
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
let mut wrapper = div()
div()
.relative()
.child(
canvas(
@@ -161,9 +161,8 @@ impl RenderOnce for Popover {
)
.when_some(self.trigger, |wrapper, trigger| {
wrapper.child(trigger(window, cx))
});
if self.open {
})
.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);
@@ -224,7 +223,7 @@ impl RenderOnce for Popover {
.inset_0(),
);
wrapper = wrapper.child(deferred(
wrapper.child(deferred(
anchored()
.offset(point(px(0.), anchor_gap))
.child(panel.with_animation(
@@ -232,9 +231,7 @@ impl RenderOnce for Popover {
Animation::new(enter_duration).with_easing(ease_out_quint()),
|panel, delta| panel.opacity(delta),
)),
));
}
wrapper
))
})
}
}
+18 -21
View File
@@ -95,7 +95,7 @@ impl RenderOnce for ScrollArea {
let mut pane_style = self.style.clone();
pane_style.padding = Default::default();
let mut pane = div()
let pane = div()
.id(self.id.clone())
.overflow_hidden()
.when(self.axes != ScrollAxes::Horizontal, |area| {
@@ -112,34 +112,31 @@ impl RenderOnce for ScrollArea {
// Keep wheel events from scrolling an outer scroll area too.
cx.stop_propagation();
})
.refine_style(&pane_style);
.refine_style(&pane_style)
.children(self.children);
// Extend with children
pane.extend(self.children);
let mut wrapper = div().relative().refine_style(&self.style);
wrapper.extend(std::iter::once(pane.into_any_element()));
if let Some(handle) = self.handle {
div()
.relative()
.refine_style(&self.style)
.child(pane.into_any_element())
.when_some(self.handle, |wrapper, handle| {
let id = self.id.clone();
if self.axes != ScrollAxes::Horizontal {
wrapper.extend(std::iter::once(
wrapper
.when(self.axes != ScrollAxes::Horizontal, |wrapper| {
wrapper.child(
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
.visibility(self.visibility)
.into_any_element(),
));
}
if self.axes != ScrollAxes::Vertical {
wrapper.extend(std::iter::once(
)
})
.when(self.axes != ScrollAxes::Vertical, |wrapper| {
wrapper.child(
Scrollbar::horizontal((id, "h-scrollbar"), handle)
.visibility(self.visibility)
.into_any_element(),
));
}
}
wrapper
)
})
})
}
}
+4 -5
View File
@@ -274,11 +274,10 @@ where
}
};
let mut row = MenuItem::new(option.label.clone());
if let Some(icon) = option.icon.clone() {
row = row.icon(icon);
}
row.checked(is_selected).handler(handler)
MenuItem::new(option.label.clone())
.when_some(option.icon.clone(), |row, icon| row.icon(icon))
.checked(is_selected)
.handler(handler)
})
.collect()
};
+33 -41
View File
@@ -1,6 +1,6 @@
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, Div, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent,
ParentElement as _, RenderOnce, Role, SharedString, Stateful, StatefulInteractiveElement as _,
@@ -97,6 +97,9 @@ impl Tab {
}
}
// Tab is a data builder, not an element, adopt the combinators explicitly.
impl FluentBuilder for Tab {}
/// A tab strip.
#[derive(IntoElement)]
pub struct Tabs {
@@ -215,7 +218,7 @@ impl RenderOnce for Tabs {
})
.collect();
let mut strip_root = div()
div()
.id(self.id.clone())
.flex()
.text_size(theme.sizes.medium.text_size)
@@ -243,17 +246,14 @@ impl RenderOnce for Tabs {
}
cx.stop_propagation();
}
});
strip_root = match self.variant {
TabsVariant::Underline => strip_root
.gap_1()
.border_b_1()
.border_color(theme.colors.border),
TabsVariant::Pill => strip_root.items_center().gap_1(),
};
strip_root.children(strip)
})
.map(|root| match self.variant {
TabsVariant::Underline => {
root.gap_1().border_b_1().border_color(theme.colors.border)
}
TabsVariant::Pill => root.items_center().gap_1(),
})
.children(strip)
}
}
@@ -295,7 +295,7 @@ fn render_tab(
}
};
let mut strip_tab = div()
let strip_tab = div()
.id((tab.key.clone(), "tab"))
.debug_selector({
let tabs_id = tabs_id.clone();
@@ -310,9 +310,8 @@ fn render_tab(
.text_color(label_color)
.when(!tab.disabled && !selected, |strip_tab| {
strip_tab.hover(|strip_tab| strip_tab.bg(theme.colors.muted))
});
strip_tab = match variant {
})
.map(|strip_tab| match variant {
TabsVariant::Underline => strip_tab
.px_2()
.py_1()
@@ -333,32 +332,25 @@ fn render_tab(
.bg(theme.colors.muted)
.hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover))
}),
};
if let Some(lead) = tab.lead.take() {
strip_tab = strip_tab.child(lead);
}
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(
})
.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"),
);
}
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({
)
})
.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| {
@@ -369,8 +361,8 @@ fn render_tab(
on_change(index, window, cx);
}
}
})
});
}
match tab.wrap {
Some(wrap) => wrap(strip_tab),
+7 -6
View File
@@ -546,7 +546,8 @@ fn render_row(
},
)
.when(options.line_numbers, |row_element| {
let mut number = gutter_cell(
row_element.child(
gutter_cell(
cx,
colors.muted_foreground,
row.new_number
@@ -559,11 +560,11 @@ fn render_row(
.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(options.indicators == DiffIndicators::Bars, |number| {
number.border_l_4().border_color(change_color)
}),
)
})
.when(
options.indicators == DiffIndicators::Bars && !options.line_numbers,
+36 -40
View File
@@ -817,9 +817,10 @@ fn text_row(
cx: &App,
) -> AnyElement {
let theme = cx.theme();
let mut frame = block_frame(indent, quote, px(0.), bottom, id, cx);
if let Some(marker) = marker {
block_frame(indent, quote, px(0.), bottom, id, cx)
.map(|frame| match marker {
Some(marker) => {
let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT;
let marker_cell = div()
.flex_none()
@@ -842,18 +843,17 @@ fn text_row(
ListMarker::Task(checked) => task_box(checked, cx),
});
frame = frame.child(
frame.child(
div()
.flex()
.items_start()
.child(marker_cell)
.child(div().flex_1().min_w_0().child(text_child(spans, id, cx))),
);
} else {
frame = frame.child(text_child(spans, id, cx));
)
}
frame.into_any_element()
None => frame.child(text_child(spans, id, cx)),
})
.into_any_element()
}
/// 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 line_height = text_size * CODE_LINE_HEIGHT;
let mut plate = div()
let plate = div()
.w_full()
.flex()
.flex_col()
@@ -906,17 +906,14 @@ fn code_row(
.font_family(theme.mono_font)
.text_size(text_size)
.line_height(relative(CODE_LINE_HEIGHT))
.overflow_hidden();
for line in &lines.lines {
plate = plate.child(
.overflow_hidden()
.children(lines.lines.iter().map(|line| {
div()
.h(line_height)
.whitespace_nowrap()
.overflow_hidden()
.child(code_line(line, theme.colors, theme.ramps)),
);
}
.child(code_line(line, theme.colors, theme.ramps))
}));
block_frame(
indent,
@@ -961,29 +958,30 @@ fn table_row(
.unwrap_or(0);
let row_count = table.rows.len();
let mut grid = div()
let grid = div()
.w_full()
.flex()
.flex_col()
.border_1()
.border_color(colors.border)
.rounded(theme.radius.control)
.overflow_hidden();
for (row_ix, row) in table.rows.iter().enumerate() {
let mut row_element = div().w_full().flex();
if row_ix + 1 < row_count {
row_element = row_element.border_b_1().border_color(colors.border);
}
if row.header {
row_element = row_element.bg(colors.muted);
}
for (column_ix, cell) in row.cells.iter().enumerate() {
.overflow_hidden()
.children(table.rows.iter().enumerate().map(|(row_ix, row)| {
div()
.w_full()
.flex()
.when(row_ix + 1 < row_count, |row_element| {
row_element.border_b_1().border_color(colors.border)
})
.when(row.header, |row_element| row_element.bg(colors.muted))
.children(
row.cells
.iter()
.enumerate()
.map(|(column_ix, cell)| {
let cell_id = ElementId::from(SharedString::from(format!(
"{id}-cell-{row_ix}-{column_ix}"
)));
row_element = row_element.child(
div()
.flex_1()
.min_w_0()
@@ -992,17 +990,15 @@ fn table_row(
.when(row.header, |cell_element| {
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.
for _ in row.cells.len()..width {
row_element = row_element.child(div().flex_1().min_w_0().px_2().py_1());
}
grid = grid.child(row_element);
}
.chain(
(row.cells.len()..width)
.map(|_| div().flex_1().min_w_0().px_2().py_1()),
),
)
}));
block_frame(
indent,
+6 -5
View File
@@ -1044,11 +1044,12 @@ fn render_pane(
tabs.iter()
.map(|tab| {
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone());
if let Some(icon) = tab.icon.clone() {
entry = entry.lead(icon.size(metrics.icon_size));
}
entry.closeable(true).wrap({
Tab::new(tab.panel.clone(), tab.title.clone())
.when_some(tab.icon.clone(), |entry, icon| {
entry.lead(icon.size(metrics.icon_size))
})
.closeable(true)
.wrap({
let panel = tab.panel.clone();
let title = tab.title.clone();
move |tab| {
+25 -30
View File
@@ -220,39 +220,36 @@ impl RenderOnce for Resizable {
.map(|ix| state.read(cx).pane_size(ix, pane_count))
.collect();
let mut group = div()
div()
.flex()
.overflow_hidden()
.when(axis == Axis::Vertical, |group| group.flex_col())
.when(axis == Axis::Horizontal, |group| group.flex_row())
.refine_style(&self.style);
for (ix, pane) in self.panes.into_iter().enumerate() {
.refine_style(&self.style)
.children(self.panes.into_iter().enumerate().flat_map(|(ix, pane)| {
let remainder = ix + 1 == pane_count;
let content = pane.content;
let mut pane_element = if remainder {
div()
.flex_1()
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))
} else {
div()
})
.when(!remainder, |pane_element| {
pane_element
.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 {
})
.when_some(debug_selector.clone(), |pane_element, selector| {
let prefix = selector();
pane_element = pane_element.debug_selector(move || format!("{prefix}-{ix}"));
}
pane_element.debug_selector(move || format!("{prefix}-{ix}"))
})
.child(pane.content)
.into_any_element();
group = group.child(pane_element.child(content));
if !remainder {
let divider =
let divider = (!remainder).then(|| {
ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id)))
.axis(axis)
.on_resize({
@@ -268,14 +265,14 @@ impl RenderOnce for Resizable {
move |_, cx| {
state.update(cx, |state, cx| state.reset_pane(ix, mins.len(), cx));
}
})
.into_any_element()
});
group = group.child(divider);
}
}
if pane_count > 0 {
group = group.child(
std::iter::once(pane_element).chain(divider)
}))
.when(pane_count > 0, |group| {
group.child(
canvas(
{
let state = state.clone();
@@ -291,9 +288,7 @@ impl RenderOnce for Resizable {
)
.absolute()
.inset_0(),
);
}
group
)
})
}
}
+10 -6
View File
@@ -9,12 +9,16 @@ is the first downstream consumer; its screens drive the order.
## 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.
- [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