diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 217953562740a2210269754d59ef407c30091960..4a43761aa54beae1af34d6fc08715695d22949bb 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -495,6 +495,205 @@ impl ContextMenu { self._on_blur_subscription = new_subscription; self } + + fn render_menu_item( + &self, + ix: usize, + item: &ContextMenuItem, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + match item { + ContextMenuItem::Separator => ListSeparator.into_any_element(), + ContextMenuItem::Header(header) => ListSubHeader::new(header.clone()) + .inset(true) + .into_any_element(), + ContextMenuItem::Label(label) => ListItem::new(ix) + .inset(true) + .disabled(true) + .child(Label::new(label.clone())) + .into_any_element(), + ContextMenuItem::Entry(entry) => self + .render_menu_entry(ix, entry, window, cx) + .into_any_element(), + ContextMenuItem::CustomEntry { + entry_render, + handler, + selectable, + } => { + let handler = handler.clone(); + let menu = cx.entity().downgrade(); + let selectable = *selectable; + ListItem::new(ix) + .inset(true) + .toggle_state(if selectable { + Some(ix) == self.selected_index + } else { + false + }) + .selectable(selectable) + .when(selectable, |item| { + item.on_click({ + let context = self.action_context.clone(); + move |_, window, cx| { + handler(context.as_ref(), window, cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } + }) + }) + .child(entry_render(window, cx)) + .into_any_element() + } + } + } + + fn render_menu_entry( + &self, + ix: usize, + entry: &ContextMenuEntry, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let ContextMenuEntry { + toggle, + label, + handler, + icon, + icon_position, + icon_size, + icon_color, + action, + disabled, + documentation_aside, + } = entry; + + let handler = handler.clone(); + let menu = cx.entity().downgrade(); + + let icon_color = if *disabled { + Color::Muted + } else if toggle.is_some() { + icon_color.unwrap_or(Color::Accent) + } else { + icon_color.unwrap_or(Color::Default) + }; + + let label_color = if *disabled { + Color::Disabled + } else { + Color::Default + }; + + let label_element = if let Some(icon_name) = icon { + h_flex() + .gap_1p5() + .when( + *icon_position == IconPosition::Start && toggle.is_none(), + |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)), + ) + .child(Label::new(label.clone()).color(label_color)) + .when(*icon_position == IconPosition::End, |flex| { + flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)) + }) + .into_any_element() + } else { + Label::new(label.clone()) + .color(label_color) + .into_any_element() + }; + + let documentation_aside_callback = documentation_aside.clone(); + + div() + .id(("context-menu-child", ix)) + .when_some( + documentation_aside_callback.clone(), + |this, documentation_aside_callback| { + this.occlude() + .on_hover(cx.listener(move |menu, hovered, _, cx| { + if *hovered { + menu.documentation_aside = + Some((ix, documentation_aside_callback.clone())); + cx.notify(); + } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) + { + menu.documentation_aside = None; + cx.notify(); + } + })) + }, + ) + .child( + ListItem::new(ix) + .inset(true) + .disabled(*disabled) + .toggle_state(Some(ix) == self.selected_index) + .when_some(*toggle, |list_item, (position, toggled)| { + let contents = div() + .flex_none() + .child( + Icon::new(icon.unwrap_or(IconName::Check)) + .color(icon_color) + .size(*icon_size), + ) + .when(!toggled, |contents| contents.invisible()); + + match position { + IconPosition::Start => list_item.start_slot(contents), + IconPosition::End => list_item.end_slot(contents), + } + }) + .child( + h_flex() + .w_full() + .justify_between() + .child(label_element) + .debug_selector(|| format!("MENU_ITEM-{}", label)) + .children(action.as_ref().and_then(|action| { + self.action_context + .as_ref() + .map(|focus| { + KeyBinding::for_action_in(&**action, focus, window, cx) + }) + .unwrap_or_else(|| { + KeyBinding::for_action(&**action, window, cx) + }) + .map(|binding| { + div().ml_4().child(binding).when( + *disabled && documentation_aside_callback.is_some(), + |parent| parent.invisible(), + ) + }) + })) + .when( + *disabled && documentation_aside_callback.is_some(), + |parent| { + parent.child( + Icon::new(IconName::Info) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + }, + ), + ) + .on_click({ + let context = self.action_context.clone(); + move |_, window, cx| { + handler(context.as_ref(), window, cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } + }), + ) + .into_any_element() + } } impl ContextMenuItem { @@ -522,23 +721,21 @@ impl Render for ContextMenu { .map(|(_, callback)| callback.clone()); h_flex() - .when(is_wide_window, |this| {this.flex_row()}) - .when(!is_wide_window, |this| {this.flex_col()}) + .when(is_wide_window, |this| this.flex_row()) + .when(!is_wide_window, |this| this.flex_col()) .w_full() .items_start() .gap_1() - .child( - div().children(aside.map(|aside| - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .p_2() - .overflow_hidden() - .when(is_wide_window, |this| {this.max_w_96()}) - .when(!is_wide_window, |this| {this.max_w_48()}) - .child(aside(cx)) - )) - ) + .child(div().children(aside.map(|aside| { + WithRemSize::new(ui_font_size) + .occlude() + .elevation_2(cx) + .p_2() + .overflow_hidden() + .when(is_wide_window, |this| this.max_w_96()) + .when(!is_wide_window, |this| this.max_w_48()) + .child(aside(cx)) + }))) .child( WithRemSize::new(ui_font_size) .occlude() @@ -579,229 +776,13 @@ impl Render for ContextMenu { } el }) - .child(List::new().children(self.items.iter_mut().enumerate().map( - |(ix, item)| { - match item { - ContextMenuItem::Separator => { - ListSeparator.into_any_element() - } - ContextMenuItem::Header(header) => { - ListSubHeader::new(header.clone()) - .inset(true) - .into_any_element() - } - ContextMenuItem::Label(label) => ListItem::new(ix) - .inset(true) - .disabled(true) - .child(Label::new(label.clone())) - .into_any_element(), - ContextMenuItem::Entry(ContextMenuEntry { - toggle, - label, - handler, - icon, - icon_position, - icon_size, - icon_color, - action, - disabled, - documentation_aside, - }) => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - - let icon_color = if *disabled { - Color::Muted - } else if toggle.is_some() { - icon_color.unwrap_or(Color::Accent) - } else { - icon_color.unwrap_or(Color::Default) - }; - - let label_color = if *disabled { - Color::Disabled - } else { - Color::Default - }; - - let label_element = if let Some(icon_name) = icon { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| { - flex.child( - Icon::new(*icon_name) - .size(*icon_size) - .color(icon_color), - ) - }, - ) - .child( - Label::new(label.clone()) - .color(label_color), - ) - .when( - *icon_position == IconPosition::End, - |flex| { - flex.child( - Icon::new(*icon_name) - .size(*icon_size) - .color(icon_color), - ) - }, - ) - .into_any_element() - } else { - Label::new(label.clone()) - .color(label_color) - .into_any_element() - }; - - let documentation_aside_callback = - documentation_aside.clone(); - - div() - .id(("context-menu-child", ix)) - .when_some( - documentation_aside_callback.clone(), - |this, documentation_aside_callback| { - this.occlude().on_hover(cx.listener( - move |menu, hovered, _, cx| { - if *hovered { - menu.documentation_aside = Some((ix, documentation_aside_callback.clone())); - cx.notify(); - } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { - menu.documentation_aside = None; - cx.notify(); - } - }, - )) - }, - ) - .child( - ListItem::new(ix) - .inset(true) - .disabled(*disabled) - .toggle_state( - Some(ix) == self.selected_index, - ) - .when_some( - *toggle, - |list_item, (position, toggled)| { - let contents = - div().flex_none().child( - Icon::new(icon.unwrap_or(IconName::Check)) - .color(icon_color) - .size(*icon_size) - ) - .when(!toggled, |contents| - contents.invisible() - ); - - match position { - IconPosition::Start => { - list_item - .start_slot(contents) - } - IconPosition::End => { - list_item.end_slot(contents) - } - } - }, - ) - .child( - h_flex() - .w_full() - .justify_between() - .child(label_element) - .debug_selector(|| { - format!("MENU_ITEM-{}", label) - }) - .children( - action.as_ref().and_then( - |action| { - self.action_context - .as_ref() - .map(|focus| { - KeyBinding::for_action_in( - &**action, focus, - window, - cx - ) - }) - .unwrap_or_else(|| { - KeyBinding::for_action( - &**action, window, cx - ) - }) - .map(|binding| { - div().ml_4().child(binding) - .when(*disabled && documentation_aside_callback.is_some(), |parent| { - parent.invisible() - }) - }) - }, - ), - ) - .when(*disabled && documentation_aside_callback.is_some(), |parent| { - parent.child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted)) - }), - ) - .on_click({ - let context = - self.action_context.clone(); - move |_, window, cx| { - handler( - context.as_ref(), - window, - cx, - ); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); - }) - .ok(); - } - }), - ) - .into_any_element() - } - ContextMenuItem::CustomEntry { - entry_render, - handler, - selectable, - } => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let selectable = *selectable; - ListItem::new(ix) - .inset(true) - .toggle_state(if selectable { - Some(ix) == self.selected_index - } else { - false - }) - .selectable(selectable) - .when(selectable, |item| { - item.on_click({ - let context = self.action_context.clone(); - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); - }) - .ok(); - } - }) - }) - .child(entry_render(window, cx)) - .into_any_element() - } - } - }, - ))) + .child( + List::new().children( + self.items.iter().enumerate().map(|(ix, item)| { + self.render_menu_item(ix, item, window, cx) + }), + ), + ), ), ) }