Rework extension filtering to use a `ToggleButton` (#8387)

Marshall Bowers created

This PR reworks the extension filtering to use a `ToggleButton`, since
the filter states are mutually-exclusive.

<img width="1136" alt="Screenshot 2024-02-25 at 10 04 59 AM"
src="https://github.com/zed-industries/zed/assets/1486634/52c621da-201c-42b9-805d-62e3ab66f94b">

Release Notes:

- N/A

Change summary

crates/extensions_ui/src/extensions_ui.rs | 139 +++++++++++++++---------
1 file changed, 84 insertions(+), 55 deletions(-)

Detailed changes

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -11,7 +11,7 @@ use settings::Settings;
 use std::time::Duration;
 use std::{ops::Range, sync::Arc};
 use theme::ThemeSettings;
-use ui::{prelude::*, CheckboxWithLabel, Tooltip};
+use ui::{prelude::*, ToggleButton, Tooltip};
 
 use workspace::{
     item::{Item, ItemEvent},
@@ -30,12 +30,18 @@ pub fn init(cx: &mut AppContext) {
     .detach();
 }
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+enum ExtensionFilter {
+    All,
+    Installed,
+    NotInstalled,
+}
+
 pub struct ExtensionsPage {
     list: UniformListScrollHandle,
     telemetry: Arc<Telemetry>,
     is_fetching_extensions: bool,
-    is_showing_installed_extensions: bool,
-    is_showing_not_installed_extensions: bool,
+    filter: ExtensionFilter,
     extension_entries: Vec<Extension>,
     query_editor: View<Editor>,
     query_contains_error: bool,
@@ -56,8 +62,7 @@ impl ExtensionsPage {
                 list: UniformListScrollHandle::new(),
                 telemetry: workspace.client().telemetry().clone(),
                 is_fetching_extensions: false,
-                is_showing_installed_extensions: true,
-                is_showing_not_installed_extensions: true,
+                filter: ExtensionFilter::All,
                 extension_entries: Vec::new(),
                 query_contains_error: false,
                 extension_fetch_task: None,
@@ -74,17 +79,17 @@ impl ExtensionsPage {
 
         self.extension_entries
             .iter()
-            .filter(|extension| {
-                let status = extension_store.extension_status(&extension.id);
-
-                match [
-                    self.is_showing_installed_extensions,
-                    self.is_showing_not_installed_extensions,
-                ] {
-                    [true, true] => true,
-                    [true, false] => matches!(status, ExtensionStatus::Installed(_)),
-                    [false, true] => matches!(status, ExtensionStatus::NotInstalled),
-                    [false, false] => false,
+            .filter(|extension| match self.filter {
+                ExtensionFilter::All => true,
+                ExtensionFilter::Installed => {
+                    let status = extension_store.extension_status(&extension.id);
+
+                    matches!(status, ExtensionStatus::Installed(_))
+                }
+                ExtensionFilter::NotInstalled => {
+                    let status = extension_store.extension_status(&extension.id);
+
+                    matches!(status, ExtensionStatus::NotInstalled)
                 }
             })
             .cloned()
@@ -421,17 +426,33 @@ impl ExtensionsPage {
     }
 
     fn render_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let is_filtering = self.search_query(cx).is_some()
-            || self.is_showing_installed_extensions
-            || self.is_showing_not_installed_extensions;
+        let has_search = self.search_query(cx).is_some();
 
         let message = if self.is_fetching_extensions {
             "Loading extensions..."
         } else {
-            if is_filtering {
-                "No extensions that match your search criteria."
-            } else {
-                "No extensions."
+            match self.filter {
+                ExtensionFilter::All => {
+                    if has_search {
+                        "No extensions that match your search."
+                    } else {
+                        "No extensions."
+                    }
+                }
+                ExtensionFilter::Installed => {
+                    if has_search {
+                        "No installed extensions that match your search."
+                    } else {
+                        "No installed extensions."
+                    }
+                }
+                ExtensionFilter::NotInstalled => {
+                    if has_search {
+                        "No not installed extensions that match your search."
+                    } else {
+                        "No not installed extensions."
+                    }
+                }
             }
         };
 
@@ -456,38 +477,46 @@ impl Render for ExtensionsPage {
                     .w_full()
                     .gap_2()
                     .child(h_flex().child(self.render_search(cx)))
-                    .child(CheckboxWithLabel::new(
-                        "Installed",
-                        Label::new("Installed"),
-                        if self.is_showing_installed_extensions {
-                            Selection::Selected
-                        } else {
-                            Selection::Unselected
-                        },
-                        cx.listener(|this, selection, _cx| {
-                            this.is_showing_installed_extensions = match selection {
-                                Selection::Selected => true,
-                                Selection::Unselected => false,
-                                Selection::Indeterminate => return,
-                            }
-                        }),
-                    ))
-                    .child(CheckboxWithLabel::new(
-                        "not installed",
-                        Label::new("Not installed"),
-                        if self.is_showing_not_installed_extensions {
-                            Selection::Selected
-                        } else {
-                            Selection::Unselected
-                        },
-                        cx.listener(|this, selection, _cx| {
-                            this.is_showing_not_installed_extensions = match selection {
-                                Selection::Selected => true,
-                                Selection::Unselected => false,
-                                Selection::Indeterminate => return,
-                            }
-                        }),
-                    )),
+                    .child(
+                        h_flex()
+                            .child(
+                                ToggleButton::new("filter-all", "All")
+                                    .style(ButtonStyle::Filled)
+                                    .size(ButtonSize::Large)
+                                    .selected(self.filter == ExtensionFilter::All)
+                                    .on_click(cx.listener(|this, _event, _cx| {
+                                        this.filter = ExtensionFilter::All;
+                                    }))
+                                    .tooltip(move |cx| Tooltip::text("Show all extensions", cx))
+                                    .first(),
+                            )
+                            .child(
+                                ToggleButton::new("filter-installed", "Installed")
+                                    .style(ButtonStyle::Filled)
+                                    .size(ButtonSize::Large)
+                                    .selected(self.filter == ExtensionFilter::Installed)
+                                    .on_click(cx.listener(|this, _event, _cx| {
+                                        this.filter = ExtensionFilter::Installed;
+                                    }))
+                                    .tooltip(move |cx| {
+                                        Tooltip::text("Show installed extensions", cx)
+                                    })
+                                    .middle(),
+                            )
+                            .child(
+                                ToggleButton::new("filter-not-installed", "Not Installed")
+                                    .style(ButtonStyle::Filled)
+                                    .size(ButtonSize::Large)
+                                    .selected(self.filter == ExtensionFilter::NotInstalled)
+                                    .on_click(cx.listener(|this, _event, _cx| {
+                                        this.filter = ExtensionFilter::NotInstalled;
+                                    }))
+                                    .tooltip(move |cx| {
+                                        Tooltip::text("Show not installed extensions", cx)
+                                    })
+                                    .last(),
+                            ),
+                    ),
             )
             .child(v_flex().size_full().overflow_y_hidden().map(|this| {
                 let entries = self.filtered_extension_entries(cx);