diff --git a/crates/core/src/combobox.rs b/crates/core/src/combobox.rs index 8329c10..5e00775 100644 --- a/crates/core/src/combobox.rs +++ b/crates/core/src/combobox.rs @@ -518,7 +518,7 @@ where let wrapper_bounds: Rc>>> = Rc::new(RefCell::new(None)); - let mut wrapper = div() + div() .id(id.clone()) .key_context(COMBOBOX_CONTEXT) .relative() @@ -575,159 +575,162 @@ where .right(metrics.dense_padding_x) .top((metrics.input_height + BORDER_WIDTH * 2.0 - icon_size) / 2.) .child(chevron), - ); + ) + .when(open, |wrapper| { + let rows: Vec = 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 { - let rows: Vec = self - .visible - .iter() - .enumerate() - .map(|(row, option_ix)| { - let (value, label) = &self.options[*option_ix]; - let highlighted = self.highlight == Some(row); - let checked = self.selected.as_ref() == Some(value); - let state = state.clone(); - let row_ix = row; - - div() - .id(("option", row)) - .debug_selector({ - let id = id.clone(); - move || format!("{id}-option-{row}") - }) - .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>>> = 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; - } + div() + .id(("option", row)) + .debug_selector({ + let id = id.clone(); + move || format!("{id}-option-{row}") + }) + .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 = None; + state.highlight = Some(row_ix); cx.notify(); }); } - }); - } - }, - ) - .absolute() - .inset_0(), - ); + } + }) + .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(); - wrapper = wrapper.child(deferred( - anchored().offset(point(px(0.), spacing_unit)).child(panel), - )); - } + 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() + }); - 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>>> = 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), + )) + }) } } diff --git a/crates/core/src/dropdown_menu.rs b/crates/core/src/dropdown_menu.rs index 9560543..f4557b5 100644 --- a/crates/core/src/dropdown_menu.rs +++ b/crates/core/src/dropdown_menu.rs @@ -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,34 +429,24 @@ 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( - window, - cx, - &self.id, - self.open, - items, - None, - *keyboard_open.read(cx), - Some(on_open), - label, - None, - &trigger_bounds, - &panel_chrome, - enter_duration, - ) { - wrapper = wrapper.child(panel); - } - - wrapper + .child(trigger) + .when_some( + anchored_menu_panel( + window, + cx, + &self.id, + self.open, + items, + None, + *keyboard_open.read(cx), + Some(on_open), + label, + None, + &trigger_bounds, + &panel_chrome, + enter_duration, + ), + |wrapper, panel| wrapper.child(panel), + ) } } diff --git a/crates/core/src/menu.rs b/crates/core/src/menu.rs index 6061ed3..c00ec24 100644 --- a/crates/core/src/menu.rs +++ b/crates/core/src/menu.rs @@ -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,36 +347,25 @@ 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( - window, - cx, - &self.id, - self.open, - items, - self.initial_selection, - false, - self.on_change, - fallback_label, - self.open_at, - &trigger_bounds, - &chrome, - enter_duration, - ) { - wrapper = wrapper.child(panel); - } - - wrapper + }) + .when_some( + anchored_menu_panel( + window, + cx, + &self.id, + self.open, + items, + self.initial_selection, + false, + self.on_change, + fallback_label, + self.open_at, + &trigger_bounds, + &chrome, + enter_duration, + ), + |wrapper, panel| wrapper.child(panel), + ) } } diff --git a/crates/core/src/popover.rs b/crates/core/src/popover.rs index bc226f8..630673f 100644 --- a/crates/core/src/popover.rs +++ b/crates/core/src/popover.rs @@ -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,80 +161,77 @@ impl RenderOnce for Popover { ) .when_some(self.trigger, |wrapper, trigger| { 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 on_change = self.on_change; - let label = fallback_label.clone(); - let focus = gpui_native_utils::focus_handle(&self.id, window, cx); - let content = self.content.map(|content| content(window, cx)); - - let panel = v_flex() - .id("popover-panel") - .debug_selector(|| "popover-panel".to_string()) - .occlude() - .track_focus(&focus) - .role(Role::Dialog) - .aria_label(label) - .min_w_32() - .bg(colors.surface) - .text_color(colors.surface_foreground) - .rounded(radius.overlay) - .border_1() - .border_color(panel_border) - .shadow(shadows) - .overflow_hidden() - .on_mouse_down_out({ - let on_change = on_change.clone(); - let trigger_bounds = trigger_bounds.clone(); - move |event, window, cx| { - if trigger_bounds - .borrow() - .is_some_and(|bounds| bounds.contains(&event.position)) - { - return; - } - if let Some(on_change) = &on_change { - on_change(false, window, cx); - } - } - }) - .on_key_down({ - let on_change = on_change.clone(); - move |event: &KeyDownEvent, window, cx| { - if event.keystroke.key == "escape" { + let panel = v_flex() + .id("popover-panel") + .debug_selector(|| "popover-panel".to_string()) + .occlude() + .track_focus(&focus) + .role(Role::Dialog) + .aria_label(label) + .min_w_32() + .bg(colors.surface) + .text_color(colors.surface_foreground) + .rounded(radius.overlay) + .border_1() + .border_color(panel_border) + .shadow(shadows) + .overflow_hidden() + .on_mouse_down_out({ + let on_change = on_change.clone(); + let trigger_bounds = trigger_bounds.clone(); + move |event, window, cx| { + if trigger_bounds + .borrow() + .is_some_and(|bounds| bounds.contains(&event.position)) + { + return; + } if let Some(on_change) = &on_change { on_change(false, window, cx); } - cx.stop_propagation(); } - } - }) - .when_some(content, |panel, content| panel.child(content)) - .child( - canvas(|_, _, _| {}, { - let focus = focus; - move |_, _, window, cx| { - if !focus.contains_focused(window, cx) { - window.focus(&focus, 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 { + on_change(false, window, cx); + } + cx.stop_propagation(); } } }) - .absolute() - .inset_0(), - ); + .when_some(content, |panel, content| panel.child(content)) + .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( - anchored() - .offset(point(px(0.), anchor_gap)) - .child(panel.with_animation( - "popover-enter", - Animation::new(enter_duration).with_easing(ease_out_quint()), - |panel, delta| panel.opacity(delta), - )), - )); - } - - wrapper + wrapper.child(deferred( + anchored() + .offset(point(px(0.), anchor_gap)) + .child(panel.with_animation( + "popover-enter", + Animation::new(enter_duration).with_easing(ease_out_quint()), + |panel, delta| panel.opacity(delta), + )), + )) + }) } } diff --git a/crates/core/src/scroll_area.rs b/crates/core/src/scroll_area.rs index bc540c7..5f2835d 100644 --- a/crates/core/src/scroll_area.rs +++ b/crates/core/src/scroll_area.rs @@ -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); + div() + .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.extend(std::iter::once(pane.into_any_element())); - - if let Some(handle) = self.handle { - let id = self.id.clone(); - - if self.axes != ScrollAxes::Horizontal { - wrapper.extend(std::iter::once( - Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone()) - .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 + wrapper + .when(self.axes != ScrollAxes::Horizontal, |wrapper| { + wrapper.child( + Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone()) + .visibility(self.visibility) + .into_any_element(), + ) + }) + .when(self.axes != ScrollAxes::Vertical, |wrapper| { + wrapper.child( + Scrollbar::horizontal((id, "h-scrollbar"), handle) + .visibility(self.visibility) + .into_any_element(), + ) + }) + }) } } diff --git a/crates/core/src/select.rs b/crates/core/src/select.rs index 1a72b09..cc9c041 100644 --- a/crates/core/src/select.rs +++ b/crates/core/src/select.rs @@ -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() }; diff --git a/crates/data/src/tabs.rs b/crates/data/src/tabs.rs index 6f72dca..46d884b 100644 --- a/crates/data/src/tabs.rs +++ b/crates/data/src/tabs.rs @@ -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,67 +310,59 @@ 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 { - TabsVariant::Underline => strip_tab - .px_2() - .py_1() - .rounded_tl(theme.radius.sm) - .rounded_tr(theme.radius.sm) - .border_b_2() - .border_color(gpui::transparent_black()) - .when(selected, |strip_tab| { - strip_tab.border_color(theme.ramps.primary.base) - }), - TabsVariant::Pill => strip_tab - .h(small.control_height + metrics.plate_inset * 2.0) - .px(metrics.dense_padding_x) - .pr(metrics.plate_inset) - .rounded(theme.radius.control) - .when(selected, |strip_tab| { - strip_tab - .bg(theme.colors.muted) - .hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover)) - }), - }; - - 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( - Spinner::new((tab.key.clone(), "tab-busy")) - .small() - .accessibility_label("Loading"), - ); - } - - if tab.closeable && on_close.is_some() && !tab.disabled { - strip_tab = strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)); - } - - if !tab.disabled { - strip_tab = strip_tab.cursor_pointer().on_click({ - let on_change = on_change.clone(); - let focus = focus.clone(); - move |_, window, cx| { - window.focus(&focus, cx); - if index != selected_index - && let Some(on_change) = &on_change - { - on_change(index, window, cx); + }) + .map(|strip_tab| match variant { + TabsVariant::Underline => strip_tab + .px_2() + .py_1() + .rounded_tl(theme.radius.sm) + .rounded_tr(theme.radius.sm) + .border_b_2() + .border_color(gpui::transparent_black()) + .when(selected, |strip_tab| { + strip_tab.border_color(theme.ramps.primary.base) + }), + TabsVariant::Pill => strip_tab + .h(small.control_height + metrics.plate_inset * 2.0) + .px(metrics.dense_padding_x) + .pr(metrics.plate_inset) + .rounded(theme.radius.control) + .when(selected, |strip_tab| { + strip_tab + .bg(theme.colors.muted) + .hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover)) + }), + }) + .when_some(tab.lead.take(), |strip_tab, lead| strip_tab.child(lead)) + .child(tab.label.clone()) + .when(variant == TabsVariant::Pill, |strip_tab| { + strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5)) + }) + .when(tab.busy && !tab.disabled, |strip_tab| { + strip_tab.child( + Spinner::new((tab.key.clone(), "tab-busy")) + .small() + .accessibility_label("Loading"), + ) + }) + .when( + tab.closeable && on_close.is_some() && !tab.disabled, + |strip_tab| strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)), + ) + .when(!tab.disabled, |strip_tab| { + strip_tab.cursor_pointer().on_click({ + let on_change = on_change.clone(); + let focus = focus.clone(); + move |_, window, cx| { + window.focus(&focus, cx); + if index != selected_index + && let Some(on_change) = &on_change + { + on_change(index, window, cx); + } } - } + }) }); - } match tab.wrap { Some(wrap) => wrap(strip_tab), diff --git a/crates/editor/src/diff_view.rs b/crates/editor/src/diff_view.rs index 9c3e980..8fe3004 100644 --- a/crates/editor/src/diff_view.rs +++ b/crates/editor/src/diff_view.rs @@ -546,24 +546,25 @@ fn render_row( }, ) .when(options.line_numbers, |row_element| { - let mut number = gutter_cell( - cx, - colors.muted_foreground, - row.new_number - .or(row.old_number) - .map(|number| number.to_string()) - .unwrap_or_default(), + row_element.child( + gutter_cell( + cx, + colors.muted_foreground, + row.new_number + .or(row.old_number) + .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( options.indicators == DiffIndicators::Bars && !options.line_numbers, diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs index fd8dcba..faeaea7 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/editor/src/markdown.rs @@ -817,43 +817,43 @@ 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 { - let line_height = theme.sizes.small.text_size * BODY_LINE_HEIGHT; - let marker_cell = div() - .flex_none() - .w(theme.root_font_size * MARKER_WIDTH) - .h(line_height) - .flex() - .items_center() - .justify_end() - .pr_1() - .child(match marker { - ListMarker::Bullet => div() - .text_color(theme.colors.muted_foreground) - .child("•") - .into_any_element(), - ListMarker::Ordered(number) => div() - .text_color(theme.colors.muted_foreground) - .whitespace_nowrap() - .child(format!("{number}.")) - .into_any_element(), - ListMarker::Task(checked) => task_box(checked, cx), - }); + 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() + .w(theme.root_font_size * MARKER_WIDTH) + .h(line_height) + .flex() + .items_center() + .justify_end() + .pr_1() + .child(match marker { + ListMarker::Bullet => div() + .text_color(theme.colors.muted_foreground) + .child("•") + .into_any_element(), + ListMarker::Ordered(number) => div() + .text_color(theme.colors.muted_foreground) + .whitespace_nowrap() + .child(format!("{number}.")) + .into_any_element(), + ListMarker::Task(checked) => task_box(checked, cx), + }); - frame = 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() + frame.child( + div() + .flex() + .items_start() + .child(marker_cell) + .child(div().flex_1().min_w_0().child(text_child(spans, id, cx))), + ) + } + 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,48 +958,47 @@ 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() { - 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() - .px_2() - .py_1() - .when(row.header, |cell_element| { - cell_element.font_weight(FontWeight::SEMIBOLD) - }) - .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); - } + .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}" + ))); + div() + .flex_1() + .min_w_0() + .px_2() + .py_1() + .when(row.header, |cell_element| { + cell_element.font_weight(FontWeight::SEMIBOLD) + }) + .child(text_child(&cell.spans, &cell_id, cx)) + }) + // Ragged tables pad missing cells so rows align. + .chain( + (row.cells.len()..width) + .map(|_| div().flex_1().min_w_0().px_2().py_1()), + ), + ) + })); block_frame( indent, diff --git a/crates/layout/src/dock.rs b/crates/layout/src/dock.rs index d3327c0..6372ce6 100644 --- a/crates/layout/src/dock.rs +++ b/crates/layout/src/dock.rs @@ -1044,30 +1044,31 @@ 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({ - let panel = tab.panel.clone(); - let title = tab.title.clone(); - move |tab| { - tab.on_drag( - PanelDrag { - panel: panel.clone(), - }, - { - let title = title.clone(); - move |_, _, _, cx| { - cx.new(|_| DragPreview { - title: title.clone(), - }) - } - }, - ) - .into_any_element() - } - }) + 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| { + tab.on_drag( + PanelDrag { + panel: panel.clone(), + }, + { + let title = title.clone(); + move |_, _, _, cx| { + cx.new(|_| DragPreview { + title: title.clone(), + }) + } + }, + ) + .into_any_element() + } + }) }) .collect() } diff --git a/crates/layout/src/resizable.rs b/crates/layout/src/resizable.rs index b7c73ea..a769f0d 100644 --- a/crates/layout/src/resizable.rs +++ b/crates/layout/src/resizable.rs @@ -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() { - let remainder = ix + 1 == pane_count; - let content = pane.content; - let mut pane_element = if remainder { - div() - .flex_1() + .refine_style(&self.style) + .children(self.panes.into_iter().enumerate().flat_map(|(ix, pane)| { + let remainder = ix + 1 == pane_count; + let pane_element = div() .overflow_hidden() - .when(axis == Axis::Vertical, |p| p.min_h(pane.min)) - .when(axis == Axis::Horizontal, |p| p.min_w(pane.min)) - } else { - div() - .flex_none() - .overflow_hidden() - .when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full()) - .when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full()) - }; + .when(remainder, |pane_element| { + pane_element + .flex_1() + .when(axis == Axis::Vertical, |p| p.min_h(pane.min)) + .when(axis == Axis::Horizontal, |p| p.min_w(pane.min)) + }) + .when(!remainder, |pane_element| { + pane_element + .flex_none() + .when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full()) + .when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full()) + }) + .when_some(debug_selector.clone(), |pane_element, selector| { + let prefix = selector(); + pane_element.debug_selector(move || format!("{prefix}-{ix}")) + }) + .child(pane.content) + .into_any_element(); - 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 = + let divider = (!remainder).then(|| { ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id))) .axis(axis) .on_resize({ @@ -268,32 +265,30 @@ 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( - canvas( - { - let state = state.clone(); - move |bounds: Bounds, _, cx: &mut App| { - let extent = match axis { - Axis::Vertical => bounds.size.height, - Axis::Horizontal => bounds.size.width, - }; - state.update(cx, |state, cx| state.set_measured(extent, cx)); - } - }, - |_, _, _, _| {}, + std::iter::once(pane_element).chain(divider) + })) + .when(pane_count > 0, |group| { + group.child( + canvas( + { + let state = state.clone(); + move |bounds: Bounds, _, cx: &mut App| { + let extent = match axis { + Axis::Vertical => bounds.size.height, + Axis::Horizontal => bounds.size.width, + }; + state.update(cx, |state, cx| state.set_measured(extent, cx)); + } + }, + |_, _, _, _| {}, + ) + .absolute() + .inset_0(), ) - .absolute() - .inset_0(), - ); - } - - group + }) } } diff --git a/docs/CHECKLIST.md b/docs/CHECKLIST.md index 0f65713..48b556f 100644 --- a/docs/CHECKLIST.md +++ b/docs/CHECKLIST.md @@ -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