Pull out fluent builder helpers into re-usable trait

Mikayla created

Change summary

crates/collab_ui/src/face_pile.rs                      |  1 
crates/gpui/src/element.rs                             | 38 -------
crates/gpui/src/prelude.rs                             |  6 
crates/gpui/src/util.rs                                | 52 ++++++++++++
crates/language_selector/src/active_buffer_language.rs |  2 
crates/project_panel/src/project_panel.rs              | 24 ++---
crates/ui/src/components/context_menu.rs               | 30 ------
crates/ui/src/components/popover_menu.rs               |  6 
8 files changed, 77 insertions(+), 82 deletions(-)

Detailed changes

crates/collab_ui/src/face_pile.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{div, AnyElement, IntoElement, ParentElement, RenderOnce, Styled, WindowContext};
 use smallvec::SmallVec;
+use ui::FluentBuilder;
 
 #[derive(Default, IntoElement)]
 pub struct FacePile {

crates/gpui/src/element.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size,
-    ViewContext, WindowContext, ELEMENT_ARENA,
+    util::FluentBuilder, ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId,
+    Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
 };
 use derive_more::{Deref, DerefMut};
 pub(crate) use smallvec::SmallVec;
@@ -77,40 +77,10 @@ pub trait IntoElement: Sized {
             })
         }
     }
-
-    /// Convert self to another type by calling the given closure. Useful in rendering code.
-    fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
-    where
-        Self: Sized,
-        U: IntoElement,
-    {
-        f(self)
-    }
-
-    /// Conditionally chain onto self with the given closure. Useful in rendering code.
-    fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
-    where
-        Self: Sized,
-    {
-        self.map(|this| if condition { then(this) } else { this })
-    }
-
-    /// Conditionally chain onto self with the given closure if the given option is Some.
-    /// The contents of the option are provided to the closure.
-    fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
-    where
-        Self: Sized,
-    {
-        self.map(|this| {
-            if let Some(value) = option {
-                then(this, value)
-            } else {
-                this
-            }
-        })
-    }
 }
 
+impl<T: IntoElement> FluentBuilder for T {}
+
 pub trait Render: 'static + Sized {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
 }

crates/gpui/src/prelude.rs 🔗

@@ -1,5 +1,5 @@
 pub use crate::{
-    BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement,
-    IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
-    VisualContext,
+    util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
+    InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
+    StatefulInteractiveElement, Styled, VisualContext,
 };

crates/gpui/src/util.rs 🔗

@@ -9,6 +9,58 @@ use smol::future::FutureExt;
 
 pub use util::*;
 
+/// A helper trait for building complex objects with imperative conditionals in a fluent style.
+pub trait FluentBuilder {
+    /// Imperatively modify self with the given closure.
+    fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
+    where
+        Self: Sized,
+    {
+        f(self)
+    }
+
+    /// Conditionally modify self with the given closure.
+    fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+    where
+        Self: Sized,
+    {
+        self.map(|this| if condition { then(this) } else { this })
+    }
+
+    /// Conditionally unwrap and modify self with the given closure, if the given option is Some.
+    fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
+    where
+        Self: Sized,
+    {
+        self.map(|this| {
+            if let Some(value) = option {
+                then(this, value)
+            } else {
+                this
+            }
+        })
+    }
+
+    /// Conditionally modify self with one closure or another
+    fn when_else(
+        self,
+        condition: bool,
+        then: impl FnOnce(Self) -> Self,
+        otherwise: impl FnOnce(Self) -> Self,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.map(|this| {
+            if condition {
+                then(this)
+            } else {
+                otherwise(this)
+            }
+        })
+    }
+}
+
 #[cfg(any(test, feature = "test-support"))]
 pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
 where

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
 use std::sync::Arc;
-use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
+use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 use crate::LanguageSelector;

crates/project_panel/src/project_panel.rs 🔗

@@ -382,21 +382,21 @@ impl ProjectPanel {
             let is_read_only = project.is_read_only();
 
             let context_menu = ContextMenu::build(cx, |menu, cx| {
-                menu.context(self.focus_handle.clone()).then_if_else(
+                menu.context(self.focus_handle.clone()).when_else(
                     is_read_only,
                     |menu| {
                         menu.action("Copy Relative Path", Box::new(CopyRelativePath))
-                            .then_if(is_dir, |menu| {
+                            .when(is_dir, |menu| {
                                 menu.action("Search Inside", Box::new(NewSearchInDirectory))
                             })
                     },
                     |menu| {
-                        menu.then_if(is_local, |menu| {
+                        menu.when(is_local, |menu| {
                             menu.action(
                                 "Add Folder to Project",
                                 Box::new(workspace::AddFolderToProject),
                             )
-                            .then_if(is_root, |menu| {
+                            .when(is_root, |menu| {
                                 menu.entry(
                                     "Remove from Project",
                                     None,
@@ -413,8 +413,8 @@ impl ProjectPanel {
                         .separator()
                         .action("Cut", Box::new(Cut))
                         .action("Copy", Box::new(Copy))
-                        .if_some(self.clipboard_entry, |menu, entry| {
-                            menu.then_if(entry.worktree_id() == worktree_id, |menu| {
+                        .when_some(self.clipboard_entry, |menu, entry| {
+                            menu.when(entry.worktree_id() == worktree_id, |menu| {
                                 menu.action("Paste", Box::new(Paste))
                             })
                         })
@@ -423,15 +423,13 @@ impl ProjectPanel {
                         .action("Copy Relative Path", Box::new(CopyRelativePath))
                         .separator()
                         .action("Reveal in Finder", Box::new(RevealInFinder))
-                        .then_if(is_dir, |menu| {
-                            menu
-                                .action("Open in Terminal", Box::new(OpenInTerminal))
+                        .when(is_dir, |menu| {
+                            menu.action("Open in Terminal", Box::new(OpenInTerminal))
                                 .action("Search Inside", Box::new(NewSearchInDirectory))
                         })
-                        .separator().action("Rename", Box::new(Rename))
-                        .then_if(!is_root, |menu| {
-                            menu.action("Delete", Box::new(Delete))
-                        })
+                        .separator()
+                        .action("Rename", Box::new(Rename))
+                        .when(!is_root, |menu| menu.action("Delete", Box::new(Delete)))
                     },
                 )
             });

crates/ui/src/components/context_menu.rs 🔗

@@ -42,6 +42,8 @@ impl FocusableView for ContextMenu {
 
 impl EventEmitter<DismissEvent> for ContextMenu {}
 
+impl FluentBuilder for ContextMenu {}
+
 impl ContextMenu {
     pub fn build(
         cx: &mut WindowContext,
@@ -68,34 +70,6 @@ impl ContextMenu {
         })
     }
 
-    pub fn if_some<T>(self, condition: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
-        if let Some(t) = condition {
-            f(self, t)
-        } else {
-            self
-        }
-    }
-
-    pub fn then_if_else(self, condition: bool, then: impl FnOnce(Self) -> Self, otherwise: impl FnOnce(Self) -> Self) -> Self {
-        if condition {
-            then(self)
-        } else {
-            otherwise(self)
-        }
-    }
-
-    pub fn then_if(self, condition: bool, f: impl FnOnce(Self) -> Self) -> Self {
-        if condition {
-            f(self)
-        } else {
-            self
-        }
-    }
-
-    pub fn map(self, f: impl FnOnce(Self) -> Self) -> Self {
-        f(self)
-    }
-
     pub fn context(mut self, focus: FocusHandle) -> Self {
         self.action_context = Some(focus);
         self

crates/ui/src/components/popover_menu.rs 🔗

@@ -1,9 +1,9 @@
 use std::{cell::RefCell, rc::Rc};
 
 use gpui::{
-    overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
-    Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent,
-    ParentElement, Pixels, Point, View, VisualContext, WindowContext,
+    overlay, point, prelude::FluentBuilder, px, rems, AnchorCorner, AnyElement, Bounds,
+    DismissEvent, DispatchPhase, Element, ElementId, InteractiveBounds, IntoElement, LayoutId,
+    ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, WindowContext,
 };
 
 use crate::{Clickable, Selectable};