From 3b2f1644fb4245bac941218a0c413040e6bde534 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 May 2022 14:24:53 +0200 Subject: [PATCH] Constrain context menu to the width of the widest item Co-Authored-By: Nathan Sobo --- crates/context_menu/src/context_menu.rs | 119 ++++++++++---------- crates/gpui/src/elements/constrained_box.rs | 112 +++++++++++++++--- crates/gpui/src/presenter.rs | 9 ++ crates/project_panel/src/project_panel.rs | 10 +- crates/theme/src/theme.rs | 2 +- styles/src/styleTree/app.ts | 2 + styles/src/styleTree/contextMenu.ts | 23 ++++ styles/src/styleTree/projectPanel.ts | 16 --- 8 files changed, 200 insertions(+), 93 deletions(-) create mode 100644 styles/src/styleTree/contextMenu.ts diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index c3f00ac5b6fea33a1a0dcb9ddc9db487ae59fd9c..0bcc97a25dac5f802f5685ccaf0a8caf737b4b19 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,8 +1,8 @@ use gpui::{ - elements::*, geometry::vector::Vector2F, Action, Entity, RenderContext, View, ViewContext, + elements::*, geometry::vector::Vector2F, Action, Axis, Entity, RenderContext, SizeConstraint, + View, ViewContext, }; use settings::Settings; -use std::{marker::PhantomData, sync::Arc}; pub enum ContextMenuItem { Item { @@ -12,75 +12,51 @@ pub enum ContextMenuItem { Separator, } -pub struct ContextMenu { +pub struct ContextMenu { position: Vector2F, - items: Arc<[ContextMenuItem]>, - state: UniformListState, + items: Vec, + widest_item_index: usize, selected_index: Option, - widest_item_index: Option, visible: bool, - _phantom: PhantomData, } -impl Entity for ContextMenu { +impl Entity for ContextMenu { type Event = (); } -impl View for ContextMenu { +impl View for ContextMenu { fn ui_name() -> &'static str { "ContextMenu" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum Tag {} + if !self.visible { return Empty::new().boxed(); } - let theme = &cx.global::().theme; - let menu_style = &theme.project_panel.context_menu; - let separator_style = menu_style.separator; - let item_style = menu_style.item.clone(); - let items = self.items.clone(); - let selected_ix = self.selected_index; + let style = cx.global::().theme.context_menu.clone(); + + let mut widest_item = self.render_menu_item::<()>(self.widest_item_index, cx, &style); + Overlay::new( - UniformList::new( - self.state.clone(), - self.items.len(), - move |range, elements, cx| { - let start = range.start; - elements.extend(items[range].iter().enumerate().map(|(ix, item)| { - let item_ix = start + ix; - match item { - ContextMenuItem::Item { label, action } => { - let action = action.boxed_clone(); - MouseEventHandler::new::(item_ix, cx, |state, _| { - let style = - item_style.style_for(state, Some(item_ix) == selected_ix); - Flex::row() - .with_child( - Label::new(label.to_string(), style.label.clone()) - .boxed(), - ) - .boxed() - }) - .on_click(move |_, _, cx| { - cx.dispatch_any_action(action.boxed_clone()) - }) - .boxed() - } - ContextMenuItem::Separator => { - Empty::new().contained().with_style(separator_style).boxed() - } - } - })) - }, - ) - .with_width_from_item(self.widest_item_index) - .boxed(), + Flex::column() + .with_children( + (0..self.items.len()).map(|ix| self.render_menu_item::(ix, cx, &style)), + ) + .constrained() + .dynamically(move |constraint, cx| { + SizeConstraint::strict_along( + Axis::Horizontal, + widest_item.layout(constraint, cx).x(), + ) + }) + .contained() + .with_style(style.container) + .boxed(), ) .with_abs_position(self.position) - .contained() - .with_style(menu_style.container) .boxed() } @@ -90,16 +66,14 @@ impl View for ContextMenu { } } -impl ContextMenu { +impl ContextMenu { pub fn new() -> Self { Self { position: Default::default(), - items: Arc::from([]), - state: Default::default(), + items: Default::default(), selected_index: Default::default(), widest_item_index: Default::default(), visible: false, - _phantom: PhantomData, } } @@ -109,7 +83,9 @@ impl ContextMenu { items: impl IntoIterator, cx: &mut ViewContext, ) { - self.items = items.into_iter().collect(); + let mut items = items.into_iter().peekable(); + assert!(items.peek().is_some(), "must have at least one item"); + self.items = items.collect(); self.widest_item_index = self .items .iter() @@ -118,10 +94,39 @@ impl ContextMenu { ContextMenuItem::Item { label, .. } => label.chars().count(), ContextMenuItem::Separator => 0, }) - .map(|(ix, _)| ix); + .unwrap() + .0; self.position = position; self.visible = true; cx.focus_self(); cx.notify(); } + + fn render_menu_item( + &self, + ix: usize, + cx: &mut RenderContext, + style: &theme::ContextMenu, + ) -> ElementBox { + match &self.items[ix] { + ContextMenuItem::Item { label, action } => { + let action = action.boxed_clone(); + MouseEventHandler::new::(ix, cx, |state, _| { + let style = style.item.style_for(state, Some(ix) == self.selected_index); + Flex::row() + .with_child(Label::new(label.to_string(), style.label.clone()).boxed()) + .boxed() + }) + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) + .boxed() + } + ContextMenuItem::Separator => Empty::new() + .contained() + .with_style(style.separator) + .constrained() + .with_height(1.) + .flex(1., false) + .boxed(), + } + } } diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index f12ed6900abc03a50d435fbb5eba859ec02c0e89..5ab01df1e1d7547ba1d565f001c177c0edcfb863 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -9,46 +9,121 @@ use crate::{ pub struct ConstrainedBox { child: ElementBox, - constraint: SizeConstraint, + constraint: Constraint, +} + +pub enum Constraint { + Static(SizeConstraint), + Dynamic(Box SizeConstraint>), +} + +impl ToJson for Constraint { + fn to_json(&self) -> serde_json::Value { + match self { + Constraint::Static(constraint) => constraint.to_json(), + Constraint::Dynamic(_) => "dynamic".into(), + } + } } impl ConstrainedBox { pub fn new(child: ElementBox) -> Self { Self { child, - constraint: SizeConstraint { - min: Vector2F::zero(), - max: Vector2F::splat(f32::INFINITY), - }, + constraint: Constraint::Static(Default::default()), } } + pub fn dynamically( + mut self, + constraint: impl 'static + FnMut(SizeConstraint, &mut LayoutContext) -> SizeConstraint, + ) -> Self { + self.constraint = Constraint::Dynamic(Box::new(constraint)); + self + } + pub fn with_min_width(mut self, min_width: f32) -> Self { - self.constraint.min.set_x(min_width); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.min.set_x(min_width); + } else { + unreachable!() + } + self } pub fn with_max_width(mut self, max_width: f32) -> Self { - self.constraint.max.set_x(max_width); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.max.set_x(max_width); + } else { + unreachable!() + } + self } pub fn with_max_height(mut self, max_height: f32) -> Self { - self.constraint.max.set_y(max_height); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.max.set_y(max_height); + } else { + unreachable!() + } + self } pub fn with_width(mut self, width: f32) -> Self { - self.constraint.min.set_x(width); - self.constraint.max.set_x(width); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.min.set_x(width); + constraint.max.set_x(width); + } else { + unreachable!() + } + self } pub fn with_height(mut self, height: f32) -> Self { - self.constraint.min.set_y(height); - self.constraint.max.set_y(height); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.min.set_y(height); + constraint.max.set_y(height); + } else { + unreachable!() + } + self } + + fn constraint( + &mut self, + input_constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> SizeConstraint { + match &mut self.constraint { + Constraint::Static(constraint) => *constraint, + Constraint::Dynamic(compute_constraint) => compute_constraint(input_constraint, cx), + } + } } impl Element for ConstrainedBox { @@ -57,13 +132,14 @@ impl Element for ConstrainedBox { fn layout( &mut self, - mut constraint: SizeConstraint, + mut parent_constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - constraint.min = constraint.min.max(self.constraint.min); - constraint.max = constraint.max.min(self.constraint.max); - constraint.max = constraint.max.max(constraint.min); - let size = self.child.layout(constraint, cx); + let constraint = self.constraint(parent_constraint, cx); + parent_constraint.min = parent_constraint.min.max(constraint.min); + parent_constraint.max = parent_constraint.max.min(constraint.max); + parent_constraint.max = parent_constraint.max.max(parent_constraint.min); + let size = self.child.layout(parent_constraint, cx); (size, ()) } @@ -96,6 +172,6 @@ impl Element for ConstrainedBox { _: &Self::PaintState, cx: &DebugContext, ) -> json::Value { - json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(cx)}) + json!({"type": "ConstrainedBox", "assigned_constraint": self.constraint.to_json(), "child": self.child.debug(cx)}) } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 2a2da34d631101d3fef73db3469fcdca6eaa6598..053b69269c9335662c1215fa0e725d9910260eb7 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -524,6 +524,15 @@ impl SizeConstraint { } } +impl Default for SizeConstraint { + fn default() -> Self { + SizeConstraint { + min: Vector2F::zero(), + max: Vector2F::splat(f32::INFINITY), + } + } +} + impl ToJson for SizeConstraint { fn to_json(&self) -> serde_json::Value { json!({ diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 85d1ed7407d88a51748f99acfd6bec574222e42c..4b060b4e7033967d2e7ed73c99e9e6c4e260927c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -38,7 +38,7 @@ pub struct ProjectPanel { selection: Option, edit_state: Option, filename_editor: ViewHandle, - context_menu: ViewHandle>, + context_menu: ViewHandle, handle: WeakViewHandle, } @@ -220,6 +220,14 @@ impl ProjectPanel { action: Box::new(AddDirectory), }, ContextMenuItem::Separator, + ContextMenuItem::Item { + label: "Rename".to_string(), + action: Box::new(Rename), + }, + ContextMenuItem::Item { + label: "Delete".to_string(), + action: Box::new(Delete), + }, ], cx, ); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 941ae03d4d87bc45494c8d7b00832f03c7a87269..4cbe60db3c0d6998be6e317db27467bd3e85c066 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -19,6 +19,7 @@ pub struct Theme { #[serde(default)] pub name: String, pub workspace: Workspace, + pub context_menu: ContextMenu, pub chat_panel: ChatPanel, pub contacts_panel: ContactsPanel, pub contact_finder: ContactFinder, @@ -226,7 +227,6 @@ pub struct ProjectPanel { pub ignored_entry_fade: f32, pub filename_editor: FieldEditor, pub indent_width: f32, - pub context_menu: ContextMenu, } #[derive(Clone, Debug, Deserialize, Default)] diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 230bd3e57f8eb31b5b602785026c6abe927d1ffe..41266ff5f7d775551ce7cec8d7cff91e604d8658 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -9,6 +9,7 @@ import projectPanel from "./projectPanel"; import search from "./search"; import picker from "./picker"; import workspace from "./workspace"; +import contextMenu from "./contextMenu"; import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; @@ -20,6 +21,7 @@ export default function app(theme: Theme): Object { return { picker: picker(theme), workspace: workspace(theme), + contextMenu: contextMenu(theme), editor: editor(theme), projectDiagnostics: projectDiagnostics(theme), commandPalette: commandPalette(theme), diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts new file mode 100644 index 0000000000000000000000000000000000000000..5458ceda6940043fdb4f9cf1496c8e202e2b3600 --- /dev/null +++ b/styles/src/styleTree/contextMenu.ts @@ -0,0 +1,23 @@ +import Theme from "../themes/common/theme"; +import { shadow, text } from "./components"; + +export default function contextMenu(theme: Theme) { + return { + background: "#ff0000", + // background: backgroundColor(theme, 300, "base"), + cornerRadius: 6, + padding: { + bottom: 2, + left: 6, + right: 6, + top: 2, + }, + shadow: shadow(theme), + item: { + label: text(theme, "sans", "secondary", { size: "sm" }), + }, + separator: { + background: "#00ff00" + } + } +} \ No newline at end of file diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index b2d8b9d4acd613088eaefbbba37725e21ecd36d1..547414afc885188ebac64ca7c1cacd2a14e88ebf 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -32,21 +32,5 @@ export default function projectPanel(theme: Theme) { text: text(theme, "mono", "primary", { size: "sm" }), selection: player(theme, 1).selection, }, - contextMenu: { - width: 100, - // background: "#ff0000", - background: backgroundColor(theme, 300, "base"), - cornerRadius: 6, - padding: { - bottom: 2, - left: 6, - right: 6, - top: 2, - }, - item: { - label: text(theme, "sans", "secondary", { size: "sm" }), - }, - shadow: shadow(theme), - } }; }