Render editor fold indicators using `Disclosure`s (#13008)

Marshall Bowers created

This PR updates the spots where we render the fold indicators in editors
to use the `Disclosure` component instead of re-implementing similar UI.

This makes this UI more consistent across Zed.

Release Notes:

- N/A

Change summary

crates/assistant/src/assistant_panel.rs | 19 +++++-----------
crates/editor/src/editor.rs             | 31 ++++++++++----------------
crates/ui/src/components/disclosure.rs  | 17 ++++++++++++++
3 files changed, 35 insertions(+), 32 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -54,8 +54,8 @@ use std::{
 };
 use telemetry_events::AssistantKind;
 use ui::{
-    popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding, ListItem,
-    ListItemSpacing, PopoverMenuHandle, Tab, TabBar, Tooltip,
+    popover_menu, prelude::*, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding,
+    ListItem, ListItemSpacing, PopoverMenuHandle, Tab, TabBar, Tooltip,
 };
 use util::{paths::CONTEXTS_DIR, post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
@@ -3132,17 +3132,10 @@ fn render_slash_command_output_toggle(
     fold: ToggleFold,
     _cx: &mut WindowContext,
 ) -> AnyElement {
-    IconButton::new(
-        ("slash-command-output-fold-indicator", row.0),
-        ui::IconName::ChevronDown,
-    )
-    .on_click(move |_e, cx| fold(!is_folded, cx))
-    .icon_color(ui::Color::Muted)
-    .icon_size(ui::IconSize::Small)
-    .selected(is_folded)
-    .selected_icon(ui::IconName::ChevronRight)
-    .size(ui::ButtonSize::None)
-    .into_any_element()
+    Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
+        .selected(is_folded)
+        .on_click(move |_e, cx| fold(!is_folded, cx))
+        .into_any_element()
 }
 
 fn render_pending_slash_command_gutter_decoration(

crates/editor/src/editor.rs 🔗

@@ -138,8 +138,8 @@ use theme::{
     ThemeColors, ThemeSettings,
 };
 use ui::{
-    h_flex, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem, Popover,
-    Tooltip,
+    h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
+    ListItem, Popover, Tooltip,
 };
 use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::item::{ItemHandle, PreviewTabsSettings};
@@ -11752,23 +11752,16 @@ impl EditorSnapshot {
             || (self.starts_indent(buffer_row) && (row_contains_cursor || self.gutter_hovered))
         {
             Some(
-                IconButton::new(
-                    ("indent-fold-indicator", buffer_row.0),
-                    ui::IconName::ChevronDown,
-                )
-                .on_click(cx.listener_for(&editor, move |this, _e, cx| {
-                    if folded {
-                        this.unfold_at(&UnfoldAt { buffer_row }, cx);
-                    } else {
-                        this.fold_at(&FoldAt { buffer_row }, cx);
-                    }
-                }))
-                .icon_color(ui::Color::Muted)
-                .icon_size(ui::IconSize::Small)
-                .selected(folded)
-                .selected_icon(ui::IconName::ChevronRight)
-                .size(ui::ButtonSize::None)
-                .into_any_element(),
+                Disclosure::new(("indent-fold-indicator", buffer_row.0), !folded)
+                    .selected(folded)
+                    .on_click(cx.listener_for(&editor, move |this, _e, cx| {
+                        if folded {
+                            this.unfold_at(&UnfoldAt { buffer_row }, cx);
+                        } else {
+                            this.fold_at(&FoldAt { buffer_row }, cx);
+                        }
+                    }))
+                    .into_any_element(),
             )
         } else {
             None

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

@@ -8,6 +8,7 @@ use crate::{prelude::*, Color, IconButton, IconButtonShape, IconName, IconSize};
 pub struct Disclosure {
     id: ElementId,
     is_open: bool,
+    selected: bool,
     on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
 }
 
@@ -16,6 +17,7 @@ impl Disclosure {
         Self {
             id: id.into(),
             is_open,
+            selected: false,
             on_toggle: None,
         }
     }
@@ -29,6 +31,20 @@ impl Disclosure {
     }
 }
 
+impl Selectable for Disclosure {
+    fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
+impl Clickable for Disclosure {
+    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+        self.on_toggle = Some(Arc::new(handler));
+        self
+    }
+}
+
 impl RenderOnce for Disclosure {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         IconButton::new(
@@ -41,6 +57,7 @@ impl RenderOnce for Disclosure {
         .shape(IconButtonShape::Square)
         .icon_color(Color::Muted)
         .icon_size(IconSize::Small)
+        .selected(self.selected)
         .when_some(self.on_toggle, move |this, on_toggle| {
             this.on_click(move |event, cx| on_toggle(event, cx))
         })