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
+151 -148
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));
let mut wrapper = div() div()
.id(id.clone()) .id(id.clone())
.key_context(COMBOBOX_CONTEXT) .key_context(COMBOBOX_CONTEXT)
.relative() .relative()
@@ -575,159 +575,162 @@ 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;
if open { div()
let rows: Vec<gpui::AnyElement> = self .id(("option", row))
.visible .debug_selector({
.iter() let id = id.clone();
.enumerate() move || format!("{id}-option-{row}")
.map(|(row, option_ix)| { })
let (value, label) = &self.options[*option_ix]; .flex()
let highlighted = self.highlight == Some(row); .items_center()
let checked = self.selected.as_ref() == Some(value); .gap_2()
let state = state.clone(); .px_2()
let row_ix = row; .py_1()
.rounded(radius.sm)
div() .when(highlighted, |row| row.bg(colors.muted))
.id(("option", row)) .when(!highlighted, |row| row.hover(|row| row.bg(colors.muted)))
.debug_selector({ .on_mouse_move({
let id = id.clone(); let state = state.clone();
move || format!("{id}-option-{row}") move |_, _window, cx| {
}) 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 = None; state.highlight = Some(row_ix);
cx.notify(); cx.notify();
}); });
} }
}); }
} })
}, .on_click({
) let state = state.clone();
.absolute() move |_, window, cx| {
.inset_0(), 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();
wrapper = wrapper.child(deferred( let rows: gpui::AnyElement = if rows.is_empty() {
anchored().offset(point(px(0.), spacing_unit)).child(panel), 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()
});
wrapper 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.highlight = None;
cx.notify();
});
}
});
}
},
)
.absolute()
.inset_0(),
);
wrapper.child(deferred(
anchored().offset(point(px(0.), spacing_unit)).child(panel),
))
})
} }
} }
+28 -30
View File
@@ -366,7 +366,15 @@ impl RenderOnce for DropdownMenu {
trigger 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() .relative()
.child( .child(
canvas( canvas(
@@ -421,34 +429,24 @@ impl RenderOnce for DropdownMenu {
.absolute() .absolute()
.inset_0(), .inset_0(),
) )
.child(trigger); .child(trigger)
.when_some(
let items = if self.open { anchored_menu_panel(
self.items window,
.map(|build| Rc::from(build(window, cx))) cx,
.unwrap_or_else(|| Rc::from([])) &self.id,
} else { self.open,
Rc::from([]) items,
}; None,
*keyboard_open.read(cx),
if let Some(panel) = anchored_menu_panel( Some(on_open),
window, label,
cx, None,
&self.id, &trigger_bounds,
self.open, &panel_chrome,
items, enter_duration,
None, ),
*keyboard_open.read(cx), |wrapper, panel| wrapper.child(panel),
Some(on_open), )
label,
None,
&trigger_bounds,
&panel_chrome,
enter_duration,
) {
wrapper = wrapper.child(panel);
}
wrapper
} }
} }
+33 -32
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 as _; use gpui::prelude::FluentBuilder;
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,6 +127,9 @@ 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 {
@@ -288,7 +291,16 @@ 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 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() .relative()
.child( .child(
canvas( canvas(
@@ -335,36 +347,25 @@ 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(
let items = if self.open { anchored_menu_panel(
match self.items { window,
Some(build) => Rc::from(build(window, cx)), cx,
None => Rc::from([]), &self.id,
} self.open,
} else { items,
Rc::from([]) self.initial_selection,
}; false,
self.on_change,
if let Some(panel) = anchored_menu_panel( fallback_label,
window, self.open_at,
cx, &trigger_bounds,
&self.id, &chrome,
self.open, enter_duration,
items, ),
self.initial_selection, |wrapper, panel| wrapper.child(panel),
false, )
self.on_change,
fallback_label,
self.open_at,
&trigger_bounds,
&chrome,
enter_duration,
) {
wrapper = wrapper.child(panel);
}
wrapper
} }
} }
+64 -67
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()));
let mut wrapper = div() div()
.relative() .relative()
.child( .child(
canvas( canvas(
@@ -161,80 +161,77 @@ 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));
if self.open { let panel = v_flex()
let on_change = self.on_change; .id("popover-panel")
let label = fallback_label.clone(); .debug_selector(|| "popover-panel".to_string())
let focus = gpui_native_utils::focus_handle(&self.id, window, cx); .occlude()
let content = self.content.map(|content| content(window, cx)); .track_focus(&focus)
.role(Role::Dialog)
let panel = v_flex() .aria_label(label)
.id("popover-panel") .min_w_32()
.debug_selector(|| "popover-panel".to_string()) .bg(colors.surface)
.occlude() .text_color(colors.surface_foreground)
.track_focus(&focus) .rounded(radius.overlay)
.role(Role::Dialog) .border_1()
.aria_label(label) .border_color(panel_border)
.min_w_32() .shadow(shadows)
.bg(colors.surface) .overflow_hidden()
.text_color(colors.surface_foreground) .on_mouse_down_out({
.rounded(radius.overlay) let on_change = on_change.clone();
.border_1() let trigger_bounds = trigger_bounds.clone();
.border_color(panel_border) move |event, window, cx| {
.shadow(shadows) if trigger_bounds
.overflow_hidden() .borrow()
.on_mouse_down_out({ .is_some_and(|bounds| bounds.contains(&event.position))
let on_change = on_change.clone(); {
let trigger_bounds = trigger_bounds.clone(); return;
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({
.when_some(content, |panel, content| panel.child(content)) let on_change = on_change.clone();
.child( move |event: &KeyDownEvent, window, cx| {
canvas(|_, _, _| {}, { if event.keystroke.key == "escape" {
let focus = focus; if let Some(on_change) = &on_change {
move |_, _, window, cx| { on_change(false, window, cx);
if !focus.contains_focused(window, cx) { }
window.focus(&focus, cx); cx.stop_propagation();
} }
} }
}) })
.absolute() .when_some(content, |panel, content| panel.child(content))
.inset_0(), .child(
); canvas(|_, _, _| {}, {
let focus = focus;
move |_, _, window, cx| {
if !focus.contains_focused(window, cx) {
window.focus(&focus, cx);
}
}
})
.absolute()
.inset_0(),
);
wrapper = wrapper.child(deferred( 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
} }
} }
+25 -28
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 mut pane = div() let 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,34 +112,31 @@ 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);
// Extend with children div()
pane.extend(self.children); .relative()
.refine_style(&self.style)
.child(pane.into_any_element())
.when_some(self.handle, |wrapper, handle| {
let id = self.id.clone();
let mut wrapper = div().relative().refine_style(&self.style); wrapper
wrapper.extend(std::iter::once(pane.into_any_element())); .when(self.axes != ScrollAxes::Horizontal, |wrapper| {
wrapper.child(
if let Some(handle) = self.handle { Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
let id = self.id.clone(); .visibility(self.visibility)
.into_any_element(),
if self.axes != ScrollAxes::Horizontal { )
wrapper.extend(std::iter::once( })
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone()) .when(self.axes != ScrollAxes::Vertical, |wrapper| {
.visibility(self.visibility) wrapper.child(
.into_any_element(), Scrollbar::horizontal((id, "h-scrollbar"), handle)
)); .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
} }
} }
+4 -5
View File
@@ -274,11 +274,10 @@ where
} }
}; };
let mut row = MenuItem::new(option.label.clone()); MenuItem::new(option.label.clone())
if let Some(icon) = option.icon.clone() { .when_some(option.icon.clone(), |row, icon| row.icon(icon))
row = row.icon(icon); .checked(is_selected)
} .handler(handler)
row.checked(is_selected).handler(handler)
}) })
.collect() .collect()
}; };
+65 -73
View File
@@ -1,6 +1,6 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder;
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 _,
@@ -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. /// A tab strip.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Tabs { pub struct Tabs {
@@ -215,7 +218,7 @@ impl RenderOnce for Tabs {
}) })
.collect(); .collect();
let mut strip_root = div() 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)
@@ -243,17 +246,14 @@ impl RenderOnce for Tabs {
} }
cx.stop_propagation(); cx.stop_propagation();
} }
}); })
.map(|root| match self.variant {
strip_root = match self.variant { TabsVariant::Underline => {
TabsVariant::Underline => strip_root root.gap_1().border_b_1().border_color(theme.colors.border)
.gap_1() }
.border_b_1() TabsVariant::Pill => root.items_center().gap_1(),
.border_color(theme.colors.border), })
TabsVariant::Pill => strip_root.items_center().gap_1(), .children(strip)
};
strip_root.children(strip)
} }
} }
@@ -295,7 +295,7 @@ fn render_tab(
} }
}; };
let mut strip_tab = div() let 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,67 +310,59 @@ 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() .rounded_tl(theme.radius.sm)
.rounded_tl(theme.radius.sm) .rounded_tr(theme.radius.sm)
.rounded_tr(theme.radius.sm) .border_b_2()
.border_b_2() .border_color(gpui::transparent_black())
.border_color(gpui::transparent_black()) .when(selected, |strip_tab| {
.when(selected, |strip_tab| { strip_tab.border_color(theme.ramps.primary.base)
strip_tab.border_color(theme.ramps.primary.base) }),
}), TabsVariant::Pill => strip_tab
TabsVariant::Pill => strip_tab .h(small.control_height + metrics.plate_inset * 2.0)
.h(small.control_height + metrics.plate_inset * 2.0) .px(metrics.dense_padding_x)
.px(metrics.dense_padding_x) .pr(metrics.plate_inset)
.pr(metrics.plate_inset) .rounded(theme.radius.control)
.rounded(theme.radius.control) .when(selected, |strip_tab| {
.when(selected, |strip_tab| { strip_tab
strip_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())
if let Some(lead) = tab.lead.take() { .when(variant == TabsVariant::Pill, |strip_tab| {
strip_tab = strip_tab.child(lead); strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5))
} })
.when(tab.busy && !tab.disabled, |strip_tab| {
strip_tab = strip_tab.child(tab.label.clone()); strip_tab.child(
Spinner::new((tab.key.clone(), "tab-busy"))
if variant == TabsVariant::Pill { .small()
strip_tab = strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5)); .accessibility_label("Loading"),
} )
})
if tab.busy && !tab.disabled { .when(
strip_tab = strip_tab.child( tab.closeable && on_close.is_some() && !tab.disabled,
Spinner::new((tab.key.clone(), "tab-busy")) |strip_tab| strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)),
.small() )
.accessibility_label("Loading"), .when(!tab.disabled, |strip_tab| {
); strip_tab.cursor_pointer().on_click({
} let on_change = on_change.clone();
let focus = focus.clone();
if tab.closeable && on_close.is_some() && !tab.disabled { move |_, window, cx| {
strip_tab = strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)); window.focus(&focus, cx);
} if index != selected_index
&& let Some(on_change) = &on_change
if !tab.disabled { {
strip_tab = strip_tab.cursor_pointer().on_click({ on_change(index, window, cx);
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),
+18 -17
View File
@@ -546,24 +546,25 @@ fn render_row(
}, },
) )
.when(options.line_numbers, |row_element| { .when(options.line_numbers, |row_element| {
let mut number = gutter_cell( row_element.child(
cx, gutter_cell(
colors.muted_foreground, cx,
row.new_number colors.muted_foreground,
.or(row.old_number) row.new_number
.map(|number| number.to_string()) .or(row.old_number)
.unwrap_or_default(), .map(|number| number.to_string())
.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,
+75 -79
View File
@@ -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);
if let Some(marker) = marker { block_frame(indent, quote, px(0.), bottom, id, cx)
let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT; .map(|frame| match marker {
let marker_cell = div() Some(marker) => {
.flex_none() let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT;
.w(theme.root_font_size * MARKER_WIDTH) let marker_cell = div()
.h(line_height) .flex_none()
.flex() .w(theme.root_font_size * MARKER_WIDTH)
.items_center() .h(line_height)
.justify_end() .flex()
.pr_1() .items_center()
.child(match marker { .justify_end()
ListMarker::Bullet => div() .pr_1()
.text_color(theme.colors.muted_foreground) .child(match marker {
.child("•") ListMarker::Bullet => div()
.into_any_element(), .text_color(theme.colors.muted_foreground)
ListMarker::Ordered(number) => div() .child("•")
.text_color(theme.colors.muted_foreground) .into_any_element(),
.whitespace_nowrap() ListMarker::Ordered(number) => div()
.child(format!("{number}.")) .text_color(theme.colors.muted_foreground)
.into_any_element(), .whitespace_nowrap()
ListMarker::Task(checked) => task_box(checked, cx), .child(format!("{number}."))
}); .into_any_element(),
ListMarker::Task(checked) => task_box(checked, cx),
});
frame = frame.child( 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)),
} })
.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 mut plate = div() let plate = div()
.w_full() .w_full()
.flex() .flex()
.flex_col() .flex_col()
@@ -906,17 +906,14 @@ 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,
@@ -961,48 +958,47 @@ fn table_row(
.unwrap_or(0); .unwrap_or(0);
let row_count = table.rows.len(); let row_count = table.rows.len();
let mut grid = div() let 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)| {
for (row_ix, row) in table.rows.iter().enumerate() { div()
let mut row_element = div().w_full().flex(); .w_full()
if row_ix + 1 < row_count { .flex()
row_element = row_element.border_b_1().border_color(colors.border); .when(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); .when(row.header, |row_element| row_element.bg(colors.muted))
} .children(
row.cells
for (column_ix, cell) in row.cells.iter().enumerate() { .iter()
let cell_id = ElementId::from(SharedString::from(format!( .enumerate()
"{id}-cell-{row_ix}-{column_ix}" .map(|(column_ix, cell)| {
))); let cell_id = ElementId::from(SharedString::from(format!(
row_element = row_element.child( "{id}-cell-{row_ix}-{column_ix}"
div() )));
.flex_1() div()
.min_w_0() .flex_1()
.px_2() .min_w_0()
.py_1() .px_2()
.when(row.header, |cell_element| { .py_1()
cell_element.font_weight(FontWeight::SEMIBOLD) .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.
// 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,
+25 -24
View File
@@ -1044,30 +1044,31 @@ fn render_pane(
tabs.iter() tabs.iter()
.map(|tab| { .map(|tab| {
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone()); Tab::new(tab.panel.clone(), tab.title.clone())
if let Some(icon) = tab.icon.clone() { .when_some(tab.icon.clone(), |entry, icon| {
entry = entry.lead(icon.size(metrics.icon_size)); entry.lead(icon.size(metrics.icon_size))
} })
entry.closeable(true).wrap({ .closeable(true)
let panel = tab.panel.clone(); .wrap({
let title = tab.title.clone(); let panel = tab.panel.clone();
move |tab| { let title = tab.title.clone();
tab.on_drag( move |tab| {
PanelDrag { tab.on_drag(
panel: panel.clone(), PanelDrag {
}, panel: panel.clone(),
{ },
let title = title.clone(); {
move |_, _, _, cx| { let title = title.clone();
cx.new(|_| DragPreview { move |_, _, _, cx| {
title: title.clone(), cx.new(|_| DragPreview {
}) title: title.clone(),
} })
}, }
) },
.into_any_element() )
} .into_any_element()
}) }
})
}) })
.collect() .collect()
} }
+47 -52
View File
@@ -220,39 +220,36 @@ 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();
let mut group = div() 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;
let mut pane_element = if remainder {
div()
.flex_1()
.overflow_hidden() .overflow_hidden()
.when(axis == Axis::Vertical, |p| p.min_h(pane.min)) .when(remainder, |pane_element| {
.when(axis == Axis::Horizontal, |p| p.min_w(pane.min)) pane_element
} else { .flex_1()
div() .when(axis == Axis::Vertical, |p| p.min_h(pane.min))
.flex_none() .when(axis == Axis::Horizontal, |p| p.min_w(pane.min))
.overflow_hidden() })
.when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full()) .when(!remainder, |pane_element| {
.when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full()) 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();
if let Some(selector) = &debug_selector { let divider = (!remainder).then(|| {
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({
@@ -268,32 +265,30 @@ 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()
});
group = group.child(divider); std::iter::once(pane_element).chain(divider)
} }))
} .when(pane_count > 0, |group| {
group.child(
if pane_count > 0 { canvas(
group = group.child( {
canvas( let state = state.clone();
{ move |bounds: Bounds<Pixels>, _, cx: &mut App| {
let state = state.clone(); let extent = match axis {
move |bounds: Bounds<Pixels>, _, cx: &mut App| { Axis::Vertical => bounds.size.height,
let extent = match axis { Axis::Horizontal => bounds.size.width,
Axis::Vertical => bounds.size.height, };
Axis::Horizontal => bounds.size.width, state.update(cx, |state, cx| state.set_measured(extent, cx));
}; }
state.update(cx, |state, cx| state.set_measured(extent, cx)); },
} |_, _, _, _| {},
}, )
|_, _, _, _| {}, .absolute()
.inset_0(),
) )
.absolute() })
.inset_0(),
);
}
group
} }
} }
+10 -6
View File
@@ -9,12 +9,16 @@ is the first downstream consumer; its screens drive the order.
## Next ## Next
- [ ] **Declarative render audit** — run the AGENTS.md render rule over the - [x] **Declarative render audit** — the AGENTS.md render rule swept the
other landed components: replace `let mut` element variables reassigned landed components: `let mut` element variables reassigned under `if` /
under `if` / `if let` with one chained expression using `.when(...)` / `if let` now build one chained expression with `.when(...)` /
`.when_some(...)` / `.map(...)`. The sidebar row is the model; sweep `.when_some(...)` / `.map(...)`. Converted: Tabs strip root and tab,
`crates/core`, `crates/data`, `crates/editor`, `crates/layout`, and Menu / DropdownMenu / Popover / Combobox wrappers, ScrollArea pane and
re-run tests plus a gallery pass after each crate. 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