Split out Interactive<T> into Toggle<T> and Interactive<T>

Piotr Osiewicz created

Change summary

crates/ai/src/assistant.rs                             |   8 
crates/auto_update/src/update_notification.rs          |   4 
crates/breadcrumbs/src/breadcrumbs.rs                  |   2 
crates/collab_ui/src/collab_titlebar_item.rs           |  29 ++
crates/collab_ui/src/contact_finder.rs                 |   3 
crates/collab_ui/src/contact_list.rs                   |  41 ++-
crates/collab_ui/src/notifications.rs                  |   4 
crates/command_palette/src/command_palette.rs          |   8 
crates/context_menu/src/context_menu.rs                |  32 ++-
crates/copilot/src/sign_in.rs                          |   6 
crates/copilot_button/src/copilot_button.rs            |   5 
crates/diagnostics/src/items.rs                        |   4 
crates/editor/src/editor.rs                            |  18 +
crates/editor/src/element.rs                           |   2 
crates/feedback/src/deploy_feedback_button.rs          |   3 
crates/feedback/src/submit_feedback_button.rs          |   2 
crates/file_finder/src/file_finder.rs                  |   2 
crates/language_selector/src/active_buffer_language.rs |   2 
crates/language_selector/src/language_selector.rs      |   2 
crates/language_tools/src/lsp_log.rs                   |   8 
crates/language_tools/src/syntax_tree_view.rs          |   5 
crates/outline/src/outline.rs                          |   2 
crates/project_panel/src/project_panel.rs              |  11 
crates/project_symbols/src/project_symbols.rs          |   4 
crates/recent_projects/src/recent_projects.rs          |   2 
crates/search/src/buffer_search.rs                     |  10 
crates/search/src/project_search.rs                    |   8 
crates/theme/src/theme.rs                              | 105 ++++++-----
crates/theme/src/ui.rs                                 |   6 
crates/theme_selector/src/theme_selector.rs            |   2 
crates/welcome/src/base_keymap_picker.rs               |   2 
crates/workspace/src/dock.rs                           |  11 +
crates/workspace/src/notifications.rs                  |   4 
crates/workspace/src/pane.rs                           |   4 
crates/workspace/src/toolbar.rs                        |   6 
35 files changed, 223 insertions(+), 144 deletions(-)

Detailed changes

crates/ai/src/assistant.rs 🔗

@@ -1233,19 +1233,19 @@ impl AssistantEditor {
                                 cx,
                                 |state, _| match message.role {
                                     Role::User => {
-                                        let style = style.user_sender.style_for(state, false);
+                                        let style = style.user_sender.style_for(state);
                                         Label::new("You", style.text.clone())
                                             .contained()
                                             .with_style(style.container)
                                     }
                                     Role::Assistant => {
-                                        let style = style.assistant_sender.style_for(state, false);
+                                        let style = style.assistant_sender.style_for(state);
                                         Label::new("Assistant", style.text.clone())
                                             .contained()
                                             .with_style(style.container)
                                     }
                                     Role::System => {
-                                        let style = style.system_sender.style_for(state, false);
+                                        let style = style.system_sender.style_for(state);
                                         Label::new("System", style.text.clone())
                                             .contained()
                                             .with_style(style.container)
@@ -1484,7 +1484,7 @@ impl View for AssistantEditor {
                 Flex::row()
                     .with_child(
                         MouseEventHandler::<Model, _>::new(0, cx, |state, _| {
-                            let style = theme.model.style_for(state, false);
+                            let style = theme.model.style_for(state);
                             Label::new(model, style.text.clone())
                                 .contained()
                                 .with_style(style.container)

crates/auto_update/src/update_notification.rs 🔗

@@ -49,7 +49,7 @@ impl View for UpdateNotification {
                         )
                         .with_child(
                             MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
-                                let style = theme.dismiss_button.style_for(state, false);
+                                let style = theme.dismiss_button.style_for(state);
                                 Svg::new("icons/x_mark_8.svg")
                                     .with_color(style.color)
                                     .constrained()
@@ -74,7 +74,7 @@ impl View for UpdateNotification {
                         ),
                 )
                 .with_child({
-                    let style = theme.action_message.style_for(state, false);
+                    let style = theme.action_message.style_for(state);
                     Text::new("View the release notes", style.text.clone())
                         .contained()
                         .with_style(style.container)

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -83,7 +83,7 @@ impl View for Breadcrumbs {
         }
 
         MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
-            let style = style.style_for(state, false);
+            let style = style.style_for(state);
             crumbs.with_style(style.container)
         })
         .on_click(MouseButton::Left, |_, this, cx| {

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -299,7 +299,7 @@ impl CollabTitlebarItem {
     pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
         let theme = theme::current(cx).clone();
         let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
-        let item_style = theme.context_menu.item.disabled_style().clone();
+        let item_style = theme.context_menu.item.off_state().disabled_style().clone();
         self.user_menu.update(cx, |user_menu, cx| {
             let items = if let Some(user) = self.user_store.read(cx).current_user() {
                 vec![
@@ -361,8 +361,20 @@ impl CollabTitlebarItem {
                     .contained()
                     .with_style(titlebar.toggle_contacts_badge)
                     .contained()
-                    .with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
-                    .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
+                    .with_margin_left(
+                        titlebar
+                            .toggle_contacts_button
+                            .off_state()
+                            .default
+                            .icon_width,
+                    )
+                    .with_margin_top(
+                        titlebar
+                            .toggle_contacts_button
+                            .off_state()
+                            .default
+                            .icon_width,
+                    )
                     .aligned(),
             )
         };
@@ -372,7 +384,8 @@ impl CollabTitlebarItem {
                 MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
                     let style = titlebar
                         .toggle_contacts_button
-                        .style_for(state, self.contacts_popover.is_some());
+                        .in_state(self.contacts_popover.is_some())
+                        .style_for(state);
                     Svg::new("icons/user_plus_16.svg")
                         .with_color(style.color)
                         .constrained()
@@ -419,7 +432,7 @@ impl CollabTitlebarItem {
 
         let titlebar = &theme.workspace.titlebar;
         MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
-            let style = titlebar.call_control.style_for(state, false);
+            let style = titlebar.call_control.style_for(state);
             Svg::new(icon)
                 .with_color(style.color)
                 .constrained()
@@ -473,7 +486,7 @@ impl CollabTitlebarItem {
                 .with_child(
                     MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
                         //TODO: Ensure this button has consistent width for both text variations
-                        let style = titlebar.share_button.style_for(state, false);
+                        let style = titlebar.share_button.style_for(state);
                         Label::new(label, style.text.clone())
                             .contained()
                             .with_style(style.container)
@@ -511,7 +524,7 @@ impl CollabTitlebarItem {
         Stack::new()
             .with_child(
                 MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
-                    let style = titlebar.call_control.style_for(state, false);
+                    let style = titlebar.call_control.style_for(state);
                     Svg::new("icons/ellipsis_14.svg")
                         .with_color(style.color)
                         .constrained()
@@ -549,7 +562,7 @@ impl CollabTitlebarItem {
     fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let titlebar = &theme.workspace.titlebar;
         MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
-            let style = titlebar.sign_in_prompt.style_for(state, false);
+            let style = titlebar.sign_in_prompt.style_for(state);
             Label::new("Sign In", style.text.clone())
                 .contained()
                 .with_style(style.container)

crates/collab_ui/src/contact_finder.rs 🔗

@@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate {
             .contact_finder
             .picker
             .item
-            .style_for(mouse_state, selected);
+            .in_state(selected)
+            .style_for(mouse_state);
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
                 Image::from_data(avatar)

crates/collab_ui/src/contact_list.rs 🔗

@@ -774,7 +774,8 @@ impl ContactList {
             .with_style(
                 *theme
                     .contact_row
-                    .style_for(&mut Default::default(), is_selected),
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
             )
             .into_any()
     }
@@ -797,7 +798,7 @@ impl ContactList {
             .width
             .or(theme.contact_avatar.height)
             .unwrap_or(0.);
-        let row = &theme.project_row.default;
+        let row = &theme.project_row.off_state().default;
         let tree_branch = theme.tree_branch;
         let line_height = row.name.text.line_height(font_cache);
         let cap_height = row.name.text.cap_height(font_cache);
@@ -810,8 +811,11 @@ impl ContactList {
         };
 
         MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
-            let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
-            let row = theme.project_row.style_for(mouse_state, is_selected);
+            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+            let row = theme
+                .project_row
+                .in_state(is_selected)
+                .style_for(mouse_state);
 
             Flex::row()
                 .with_child(
@@ -893,7 +897,7 @@ impl ContactList {
             .width
             .or(theme.contact_avatar.height)
             .unwrap_or(0.);
-        let row = &theme.project_row.default;
+        let row = &theme.project_row.off_state().default;
         let tree_branch = theme.tree_branch;
         let line_height = row.name.text.line_height(font_cache);
         let cap_height = row.name.text.cap_height(font_cache);
@@ -904,8 +908,11 @@ impl ContactList {
             peer_id.as_u64() as usize,
             cx,
             |mouse_state, _| {
-                let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
-                let row = theme.project_row.style_for(mouse_state, is_selected);
+                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+                let row = theme
+                    .project_row
+                    .in_state(is_selected)
+                    .style_for(mouse_state);
 
                 Flex::row()
                     .with_child(
@@ -989,7 +996,8 @@ impl ContactList {
 
         let header_style = theme
             .header_row
-            .style_for(&mut Default::default(), is_selected);
+            .in_state(is_selected)
+            .style_for(&mut Default::default());
         let text = match section {
             Section::ActiveCall => "Collaborators",
             Section::Requests => "Contact Requests",
@@ -999,7 +1007,7 @@ impl ContactList {
         let leave_call = if section == Section::ActiveCall {
             Some(
                 MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
-                    let style = theme.leave_call.style_for(state, false);
+                    let style = theme.leave_call.style_for(state);
                     Label::new("Leave Call", style.text.clone())
                         .contained()
                         .with_style(style.container)
@@ -1110,8 +1118,7 @@ impl ContactList {
                             contact.user.id as usize,
                             cx,
                             |mouse_state, _| {
-                                let button_style =
-                                    theme.contact_button.style_for(mouse_state, false);
+                                let button_style = theme.contact_button.style_for(mouse_state);
                                 render_icon_button(button_style, "icons/x_mark_8.svg")
                                     .aligned()
                                     .flex_float()
@@ -1146,7 +1153,8 @@ impl ContactList {
                     .with_style(
                         *theme
                             .contact_row
-                            .style_for(&mut Default::default(), is_selected),
+                            .in_state(is_selected)
+                            .style_for(&mut Default::default()),
                     )
             })
             .on_click(MouseButton::Left, move |_, this, cx| {
@@ -1204,7 +1212,7 @@ impl ContactList {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
-                        theme.contact_button.style_for(mouse_state, false)
+                        theme.contact_button.style_for(mouse_state)
                     };
                     render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
                 })
@@ -1227,7 +1235,7 @@ impl ContactList {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
-                        theme.contact_button.style_for(mouse_state, false)
+                        theme.contact_button.style_for(mouse_state)
                     };
                     render_icon_button(button_style, "icons/check_8.svg")
                         .aligned()
@@ -1250,7 +1258,7 @@ impl ContactList {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
-                        theme.contact_button.style_for(mouse_state, false)
+                        theme.contact_button.style_for(mouse_state)
                     };
                     render_icon_button(button_style, "icons/x_mark_8.svg")
                         .aligned()
@@ -1277,7 +1285,8 @@ impl ContactList {
             .with_style(
                 *theme
                     .contact_row
-                    .style_for(&mut Default::default(), is_selected),
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
             )
             .into_any()
     }

crates/collab_ui/src/notifications.rs 🔗

@@ -53,7 +53,7 @@ where
                 )
                 .with_child(
                     MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
-                        let style = theme.dismiss_button.style_for(state, false);
+                        let style = theme.dismiss_button.style_for(state);
                         Svg::new("icons/x_mark_8.svg")
                             .with_color(style.color)
                             .constrained()
@@ -93,7 +93,7 @@ where
                     .with_children(buttons.into_iter().enumerate().map(
                         |(ix, (message, handler))| {
                             MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
-                                let button = theme.button.style_for(state, false);
+                                let button = theme.button.style_for(state);
                                 Label::new(message, button.text.clone())
                                     .contained()
                                     .with_style(button.container)

crates/command_palette/src/command_palette.rs 🔗

@@ -185,8 +185,12 @@ impl PickerDelegate for CommandPaletteDelegate {
         let mat = &self.matches[ix];
         let command = &self.actions[mat.candidate_id];
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
-        let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+        let key_style = &theme
+            .command_palette
+            .key
+            .in_state(selected)
+            .style_for(mouse_state);
         let keystroke_spacing = theme.command_palette.keystroke_spacing;
 
         Flex::row()

crates/context_menu/src/context_menu.rs 🔗

@@ -9,6 +9,7 @@ use gpui::{
 };
 use menu::*;
 use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
+use theme::ToggleState;
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(ContextMenu::select_first);
@@ -328,10 +329,13 @@ impl ContextMenu {
                 Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
                     match item {
                         ContextMenuItem::Item { label, .. } => {
-                            let style = style.item.style_for(
-                                &mut Default::default(),
-                                Some(ix) == self.selected_index,
-                            );
+                            let toggle_state = if Some(ix) == self.selected_index {
+                                ToggleState::On
+                            } else {
+                                ToggleState::Off
+                            };
+                            let style = style.item.in_state(toggle_state);
+                            let style = style.style_for(&mut Default::default());
 
                             match label {
                                 ContextMenuItemLabel::String(label) => {
@@ -363,10 +367,13 @@ impl ContextMenu {
                     .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                         match item {
                             ContextMenuItem::Item { action, .. } => {
-                                let style = style.item.style_for(
-                                    &mut Default::default(),
-                                    Some(ix) == self.selected_index,
-                                );
+                                let toggle_state = if Some(ix) == self.selected_index {
+                                    ToggleState::On
+                                } else {
+                                    ToggleState::Off
+                                };
+                                let style = style.item.in_state(toggle_state);
+                                let style = style.style_for(&mut Default::default());
 
                                 match action {
                                     ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
@@ -412,8 +419,13 @@ impl ContextMenu {
                             let action = action.clone();
                             let view_id = self.parent_view_id;
                             MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
-                                let style =
-                                    style.item.style_for(state, Some(ix) == self.selected_index);
+                                let toggle_state = if Some(ix) == self.selected_index {
+                                    ToggleState::On
+                                } else {
+                                    ToggleState::Off
+                                };
+                                let style = style.item.in_state(toggle_state);
+                                let style = style.style_for(state);
                                 let keystroke = match &action {
                                     ContextMenuItemAction::Action(action) => Some(
                                         KeystrokeLabel::new(

crates/copilot/src/sign_in.rs 🔗

@@ -127,16 +127,16 @@ impl CopilotCodeVerification {
                 .with_child(
                     Label::new(
                         if copied { "Copied!" } else { "Copy" },
-                        device_code_style.cta.style_for(state, false).text.clone(),
+                        device_code_style.cta.style_for(state).text.clone(),
                     )
                     .aligned()
                     .contained()
-                    .with_style(*device_code_style.right_container.style_for(state, false))
+                    .with_style(*device_code_style.right_container.style_for(state))
                     .constrained()
                     .with_width(device_code_style.right),
                 )
                 .contained()
-                .with_style(device_code_style.cta.style_for(state, false).container)
+                .with_style(device_code_style.cta.style_for(state).container)
         })
         .on_click(gpui::platform::MouseButton::Left, {
             let user_code = data.user_code.clone();

crates/copilot_button/src/copilot_button.rs 🔗

@@ -71,7 +71,8 @@ impl View for CopilotButton {
                             .status_bar
                             .panel_buttons
                             .button
-                            .style_for(state, active);
+                            .in_state(active)
+                            .style_for(state);
 
                         Flex::row()
                             .with_child(
@@ -255,7 +256,7 @@ impl CopilotButton {
             move |state: &mut MouseState, style: &theme::ContextMenuItem| {
                 Flex::row()
                     .with_child(Label::new("Copilot Settings", style.label.clone()))
-                    .with_child(theme::ui::icon(icon_style.style_for(state, false)))
+                    .with_child(theme::ui::icon(icon_style.style_for(state)))
                     .align_children_center()
                     .into_any()
             },

crates/diagnostics/src/items.rs 🔗

@@ -100,7 +100,7 @@ impl View for DiagnosticIndicator {
                     .workspace
                     .status_bar
                     .diagnostic_summary
-                    .style_for(state, false);
+                    .style_for(state);
 
                 let mut summary_row = Flex::row();
                 if self.summary.error_count > 0 {
@@ -198,7 +198,7 @@ impl View for DiagnosticIndicator {
                 MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
                     Label::new(
                         diagnostic.message.split('\n').next().unwrap().to_string(),
-                        message_style.style_for(state, false).text.clone(),
+                        message_style.style_for(state).text.clone(),
                     )
                     .aligned()
                     .contained()

crates/editor/src/editor.rs 🔗

@@ -3320,15 +3320,21 @@ impl Editor {
     pub fn render_code_actions_indicator(
         &self,
         style: &EditorStyle,
-        active: bool,
+        is_active: bool,
         cx: &mut ViewContext<Self>,
     ) -> Option<AnyElement<Self>> {
         if self.available_code_actions.is_some() {
             enum CodeActions {}
             Some(
                 MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
-                    Svg::new("icons/bolt_8.svg")
-                        .with_color(style.code_actions.indicator.style_for(state, active).color)
+                    Svg::new("icons/bolt_8.svg").with_color(
+                        style
+                            .code_actions
+                            .indicator
+                            .in_state(is_active)
+                            .style_for(state)
+                            .color,
+                    )
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .with_padding(Padding::uniform(3.))
@@ -3378,10 +3384,8 @@ impl Editor {
                                     .with_color(
                                         style
                                             .indicator
-                                            .style_for(
-                                                mouse_state,
-                                                fold_status == FoldStatus::Folded,
-                                            )
+                                            .in_state(fold_status == FoldStatus::Folded)
+                                            .style_for(mouse_state)
                                             .color,
                                     )
                                     .constrained()

crates/editor/src/element.rs 🔗

@@ -2090,7 +2090,7 @@ impl Element<Editor> for EditorElement {
                     .folds
                     .ellipses
                     .background
-                    .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
+                    .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
                     .color;
 
                 (id, fold, color)

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -41,7 +41,8 @@ impl View for DeployFeedbackButton {
                         .status_bar
                         .panel_buttons
                         .button
-                        .style_for(state, active);
+                        .in_state(active)
+                        .style_for(state);
 
                     Svg::new("icons/feedback_16.svg")
                         .with_color(style.icon_color)

crates/feedback/src/submit_feedback_button.rs 🔗

@@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton {
         let theme = theme::current(cx).clone();
         enum SubmitFeedbackButton {}
         MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
-            let style = theme.feedback.submit_button.style_for(state, false);
+            let style = theme.feedback.submit_button.style_for(state);
             Label::new("Submit as Markdown", style.text.clone())
                 .contained()
                 .with_style(style.container)

crates/file_finder/src/file_finder.rs 🔗

@@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate {
             .get(ix)
             .expect("Invalid matches state: no element for index {ix}");
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let (file_name, file_name_positions, full_path, full_path_positions) =
             self.labels_for_match(path_match, cx, ix);
         Flex::column()

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage {
 
             MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
                 let theme = &theme::current(cx).workspace.status_bar;
-                let style = theme.active_language.style_for(state, false);
+                let style = theme.active_language.style_for(state);
                 Label::new(active_language_text, style.text.clone())
                     .contained()
                     .with_style(style.container)

crates/language_selector/src/language_selector.rs 🔗

@@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
         let mat = &self.matches[ix];
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
         let mut label = mat.string.clone();
         if buffer_language_name.as_deref() == Some(mat.string.as_str()) {

crates/language_tools/src/lsp_log.rs 🔗

@@ -681,7 +681,7 @@ impl LspLogToolbarItemView {
                     )
                 })
                 .unwrap_or_else(|| "No server selected".into());
-            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
+            let style = theme.toolbar_dropdown_menu.header.style_for(state);
             Label::new(label, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -722,7 +722,8 @@ impl LspLogToolbarItemView {
                     let style = theme
                         .toolbar_dropdown_menu
                         .item
-                        .style_for(state, logs_selected);
+                        .in_state(logs_selected)
+                        .style_for(state);
                     Label::new(SERVER_LOGS, style.text.clone())
                         .contained()
                         .with_style(style.container)
@@ -739,7 +740,8 @@ impl LspLogToolbarItemView {
                     let style = theme
                         .toolbar_dropdown_menu
                         .item
-                        .style_for(state, rpc_trace_selected);
+                        .in_state(rpc_trace_selected)
+                        .style_for(state);
                     Flex::row()
                         .with_child(
                             Label::new(RPC_MESSAGES, style.text.clone())

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView {
     ) -> impl Element<Self> {
         enum ToggleMenu {}
         MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
-            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
+            let style = theme.toolbar_dropdown_menu.header.style_for(state);
             Flex::row()
                 .with_child(
                     Label::new(active_layer.language.name().to_string(), style.text.clone())
@@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView {
             let style = theme
                 .toolbar_dropdown_menu
                 .item
-                .style_for(state, is_selected);
+                .in_state(is_selected)
+                .style_for(state);
             Flex::row()
                 .with_child(
                     Label::new(layer.language.name().to_string(), style.text.clone())

crates/outline/src/outline.rs 🔗

@@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate {
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let string_match = &self.matches[ix];
         let outline_item = &self.outline.items[string_match.candidate_id];
 

crates/project_panel/src/project_panel.rs 🔗

@@ -1253,7 +1253,10 @@ impl ProjectPanel {
         let show_editor = details.is_editing && !details.is_processing;
 
         MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
-            let mut style = entry_style.style_for(state, details.is_selected).clone();
+            let mut style = entry_style
+                .in_state(details.is_selected)
+                .style_for(state)
+                .clone();
 
             if cx
                 .global::<DragAndDrop<Workspace>>()
@@ -1264,7 +1267,7 @@ impl ProjectPanel {
                     .filter(|destination| details.path.starts_with(destination))
                     .is_some()
             {
-                style = entry_style.active.clone().unwrap();
+                style = entry_style.on_state().default.clone();
             }
 
             let row_container_style = if show_editor {
@@ -1405,9 +1408,9 @@ impl View for ProjectPanel {
                         let button_style = theme.open_project_button.clone();
                         let context_menu_item_style = theme::current(cx).context_menu.item.clone();
                         move |state, cx| {
-                            let button_style = button_style.style_for(state, false).clone();
+                            let button_style = button_style.style_for(state).clone();
                             let context_menu_item =
-                                context_menu_item_style.style_for(state, true).clone();
+                                context_menu_item_style.on_state().style_for(state).clone();
 
                             theme::ui::keystroke_label(
                                 "Open a project",

crates/project_symbols/src/project_symbols.rs 🔗

@@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
         let style = &theme.picker.item;
-        let current_style = style.style_for(mouse_state, selected);
+        let current_style = style.in_state(selected).style_for(mouse_state);
 
         let string_match = &self.matches[ix];
         let symbol = &self.symbols[string_match.candidate_id];
@@ -229,7 +229,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
             .with_child(
                 // Avoid styling the path differently when it is selected, since
                 // the symbol's syntax highlighting doesn't change when selected.
-                Label::new(path.to_string(), style.default.label.clone()),
+                Label::new(path.to_string(), style.off_state().default.label.clone()),
             )
             .contained()
             .with_style(current_style.container)

crates/recent_projects/src/recent_projects.rs 🔗

@@ -173,7 +173,7 @@ impl PickerDelegate for RecentProjectsDelegate {
         cx: &gpui::AppContext,
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
         let string_match = &self.matches[ix];
 

crates/search/src/buffer_search.rs 🔗

@@ -328,7 +328,11 @@ impl BufferSearchBar {
         Some(
             MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
                 let theme = theme::current(cx);
-                let style = theme.search.option_button.style_for(state, is_active);
+                let style = theme
+                    .search
+                    .option_button
+                    .in_state(is_active)
+                    .style_for(state);
                 Label::new(icon, style.text.clone())
                     .contained()
                     .with_style(style.container)
@@ -371,7 +375,7 @@ impl BufferSearchBar {
         enum NavButton {}
         MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.option_button.style_for(state, false);
+            let style = theme.search.option_button.off_state().style_for(state);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -403,7 +407,7 @@ impl BufferSearchBar {
 
         enum CloseButton {}
         MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
-            let style = theme.dismiss_button.style_for(state, false);
+            let style = theme.dismiss_button.style_for(state);
             Svg::new("icons/x_mark_8.svg")
                 .with_color(style.color)
                 .constrained()

crates/search/src/project_search.rs 🔗

@@ -896,7 +896,7 @@ impl ProjectSearchBar {
         enum NavButton {}
         MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.option_button.style_for(state, false);
+            let style = theme.search.option_button.off_state().style_for(state);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -927,7 +927,11 @@ impl ProjectSearchBar {
         let is_active = self.is_option_enabled(option, cx);
         MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.option_button.style_for(state, is_active);
+            let style = theme
+                .search
+                .option_button
+                .in_state(is_active)
+                .style_for(state);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)

crates/theme/src/theme.rs 🔗

@@ -132,7 +132,7 @@ pub struct Titlebar {
     pub outdated_warning: ContainedText,
     pub share_button: Interactive<ContainedText>,
     pub call_control: Interactive<IconButton>,
-    pub toggle_contacts_button: Interactive<IconButton>,
+    pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
     pub user_menu_button: Interactive<IconButton>,
     pub toggle_contacts_badge: ContainerStyle,
 }
@@ -204,12 +204,12 @@ pub struct ContactList {
     pub user_query_editor: FieldEditor,
     pub user_query_editor_height: f32,
     pub add_contact_button: IconButton,
-    pub header_row: Interactive<ContainedText>,
+    pub header_row: Toggleable<Interactive<ContainedText>>,
     pub leave_call: Interactive<ContainedText>,
-    pub contact_row: Interactive<ContainerStyle>,
+    pub contact_row: Toggleable<Interactive<ContainerStyle>>,
     pub row_height: f32,
-    pub project_row: Interactive<ProjectRow>,
-    pub tree_branch: Interactive<TreeBranch>,
+    pub project_row: Toggleable<Interactive<ProjectRow>>,
+    pub tree_branch: Toggleable<Interactive<TreeBranch>>,
     pub contact_avatar: ImageStyle,
     pub contact_status_free: ContainerStyle,
     pub contact_status_busy: ContainerStyle,
@@ -251,7 +251,7 @@ pub struct DropdownMenu {
     pub container: ContainerStyle,
     pub header: Interactive<DropdownMenuItem>,
     pub section_header: ContainedText,
-    pub item: Interactive<DropdownMenuItem>,
+    pub item: Toggleable<Interactive<DropdownMenuItem>>,
     pub row_height: f32,
 }
 
@@ -270,7 +270,7 @@ pub struct DropdownMenuItem {
 pub struct TabBar {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub pane_button: Interactive<IconButton>,
+    pub pane_button: Toggleable<Interactive<IconButton>>,
     pub pane_button_container: ContainerStyle,
     pub active_pane: TabStyles,
     pub inactive_pane: TabStyles,
@@ -339,7 +339,7 @@ pub struct Toolbar {
     pub container: ContainerStyle,
     pub height: f32,
     pub item_spacing: f32,
-    pub nav_button: Interactive<IconButton>,
+    pub nav_button: Toggleable<Interactive<IconButton>>,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -359,7 +359,7 @@ pub struct Search {
     pub include_exclude_editor: FindEditor,
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
-    pub option_button: Interactive<ContainedText>,
+    pub option_button: Toggleable<Interactive<ContainedText>>,
     pub match_background: Color,
     pub match_index: ContainedText,
     pub results_status: TextStyle,
@@ -395,7 +395,7 @@ pub struct StatusBarPanelButtons {
     pub group_left: ContainerStyle,
     pub group_bottom: ContainerStyle,
     pub group_right: ContainerStyle,
-    pub button: Interactive<PanelButton>,
+    pub button: Toggleable<Interactive<PanelButton>>,
 }
 
 #[derive(Deserialize, Default)]
@@ -444,10 +444,10 @@ pub struct PanelButton {
 pub struct ProjectPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub entry: Interactive<ProjectPanelEntry>,
+    pub entry: Toggleable<Interactive<ProjectPanelEntry>>,
     pub dragged_entry: ProjectPanelEntry,
-    pub ignored_entry: Interactive<ProjectPanelEntry>,
-    pub cut_entry: Interactive<ProjectPanelEntry>,
+    pub ignored_entry: Toggleable<Interactive<ProjectPanelEntry>>,
+    pub cut_entry: Toggleable<Interactive<ProjectPanelEntry>>,
     pub filename_editor: FieldEditor,
     pub indent_width: f32,
     pub open_project_button: Interactive<ContainedText>,
@@ -481,7 +481,7 @@ pub struct GitProjectStatus {
 pub struct ContextMenu {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub item: Interactive<ContextMenuItem>,
+    pub item: Toggleable<Interactive<ContextMenuItem>>,
     pub keystroke_margin: f32,
     pub separator: ContainerStyle,
 }
@@ -498,7 +498,7 @@ pub struct ContextMenuItem {
 
 #[derive(Debug, Deserialize, Default)]
 pub struct CommandPalette {
-    pub key: Interactive<ContainedLabel>,
+    pub key: Toggleable<Interactive<ContainedLabel>>,
     pub keystroke_spacing: f32,
 }
 
@@ -565,7 +565,7 @@ pub struct Picker {
     pub input_editor: FieldEditor,
     pub empty_input_editor: FieldEditor,
     pub no_matches: ContainedLabel,
-    pub item: Interactive<ContainedLabel>,
+    pub item: Toggleable<Interactive<ContainedLabel>>,
 }
 
 #[derive(Clone, Debug, Deserialize, Default)]
@@ -771,13 +771,13 @@ pub struct InteractiveColor {
 #[derive(Clone, Deserialize, Default)]
 pub struct CodeActions {
     #[serde(default)]
-    pub indicator: Interactive<InteractiveColor>,
+    pub indicator: Toggleable<Interactive<InteractiveColor>>,
     pub vertical_scale: f32,
 }
 
 #[derive(Clone, Deserialize, Default)]
 pub struct Folds {
-    pub indicator: Interactive<InteractiveColor>,
+    pub indicator: Toggleable<Interactive<InteractiveColor>>,
     pub ellipses: FoldEllipses,
     pub fold_background: Color,
     pub icon_margin_scale: f32,
@@ -806,29 +806,52 @@ pub struct DiffStyle {
 pub struct Interactive<T> {
     pub default: T,
     pub hover: Option<T>,
-    pub hover_and_active: Option<T>,
     pub clicked: Option<T>,
-    pub click_and_active: Option<T>,
-    pub active: Option<T>,
     pub disabled: Option<T>,
 }
 
+#[derive(Clone, Copy, Debug, Default, Deserialize)]
+pub struct Toggleable<T> {
+    on: T,
+    off: T,
+}
+
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
+pub enum ToggleState {
+    Off,
+    On,
+}
+
+impl<T: std::borrow::Borrow<bool>> From<T> for ToggleState {
+    fn from(item: T) -> Self {
+        match *item.borrow() {
+            true => Self::On,
+            false => Self::Off,
+        }
+    }
+}
+
+impl<T> Toggleable<T> {
+    pub fn new(on: T, off: T) -> Self {
+        Self { on, off }
+    }
+    pub fn in_state(&self, state: impl Into<ToggleState>) -> &T {
+        match state.into() {
+            ToggleState::Off => &self.off,
+            ToggleState::On => &self.on,
+        }
+    }
+    pub fn on_state(&self) -> &T {
+        self.in_state(ToggleState::On)
+    }
+    pub fn off_state(&self) -> &T {
+        self.in_state(ToggleState::Off)
+    }
+}
+
 impl<T> Interactive<T> {
-    pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
-        if active {
-            if state.hovered() {
-                self.hover_and_active
-                    .as_ref()
-                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
-            } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some()
-            {
-                self.click_and_active
-                    .as_ref()
-                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
-            } else {
-                self.active.as_ref().unwrap_or(&self.default)
-            }
-        } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
+    pub fn style_for(&self, state: &mut MouseState) -> &T {
+        if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
             self.clicked.as_ref().unwrap()
         } else if state.hovered() {
             self.hover.as_ref().unwrap_or(&self.default)
@@ -836,7 +859,6 @@ impl<T> Interactive<T> {
             &self.default
         }
     }
-
     pub fn disabled_style(&self) -> &T {
         self.disabled.as_ref().unwrap_or(&self.default)
     }
@@ -852,10 +874,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
             #[serde(flatten)]
             default: Value,
             hover: Option<Value>,
-            hover_and_active: Option<Value>,
             clicked: Option<Value>,
-            click_and_active: Option<Value>,
-            active: Option<Value>,
             disabled: Option<Value>,
         }
 
@@ -881,20 +900,14 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
         };
 
         let hover = deserialize_state(json.hover)?;
-        let hover_and_active = deserialize_state(json.hover_and_active)?;
         let clicked = deserialize_state(json.clicked)?;
-        let click_and_active = deserialize_state(json.click_and_active)?;
-        let active = deserialize_state(json.active)?;
         let disabled = deserialize_state(json.disabled)?;
         let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
 
         Ok(Interactive {
             default,
             hover,
-            hover_and_active,
             clicked,
-            click_and_active,
-            active,
             disabled,
         })
     }

crates/theme/src/ui.rs 🔗

@@ -170,7 +170,7 @@ where
     F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
 {
     MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
-        let style = style.style_for(state, false);
+        let style = style.style_for(state);
         Label::new(label, style.text.to_owned())
             .aligned()
             .contained()
@@ -220,13 +220,13 @@ where
                     title,
                     style
                         .title_text
-                        .style_for(&mut MouseState::default(), false)
+                        .style_for(&mut MouseState::default())
                         .clone(),
                 ))
                 .with_child(
                     // FIXME: Get a better tag type
                     MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
-                        let style = style.close_icon.style_for(state, false);
+                        let style = style.close_icon.style_for(state);
                         icon(style)
                     })
                     .on_click(platform::MouseButton::Left, move |_, _, cx| {

crates/theme_selector/src/theme_selector.rs 🔗

@@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
         let theme_match = &self.matches[ix];
         Label::new(theme_match.string.clone(), style.label.clone())

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -141,7 +141,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
     ) -> gpui::AnyElement<Picker<Self>> {
         let theme = &theme::current(cx);
         let keymap_match = &self.matches[ix];
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
         Label::new(keymap_match.string.clone(), style.label.clone())
             .with_highlights(keymap_match.positions.clone())

crates/workspace/src/dock.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
 };
 use serde::Deserialize;
 use std::rc::Rc;
-use theme::ThemeSettings;
+use theme::{ThemeSettings, ToggleState};
 
 pub trait Panel: View {
     fn position(&self, cx: &WindowContext) -> DockPosition;
@@ -498,7 +498,14 @@ impl View for PanelButtons {
                     Stack::new()
                         .with_child(
                             MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
-                                let style = button_style.style_for(state, is_active);
+                                let toggle_state = if is_active {
+                                    ToggleState::On
+                                } else {
+                                    ToggleState::Off
+                                };
+                                let style = button_style.in_state(toggle_state);
+
+                                let style = style.style_for(state);
                                 Flex::row()
                                     .with_child(
                                         Svg::new(view.icon_path(cx))

crates/workspace/src/notifications.rs 🔗

@@ -291,7 +291,7 @@ pub mod simple_message_notification {
                         )
                         .with_child(
                             MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
-                                let style = theme.dismiss_button.style_for(state, false);
+                                let style = theme.dismiss_button.style_for(state);
                                 Svg::new("icons/x_mark_8.svg")
                                     .with_color(style.color)
                                     .constrained()
@@ -323,7 +323,7 @@ pub mod simple_message_notification {
                                 0,
                                 cx,
                                 |state, _| {
-                                    let style = theme.action_message.style_for(state, false);
+                                    let style = theme.action_message.style_for(state);
 
                                     Flex::row()
                                         .with_child(

crates/workspace/src/pane.rs 🔗

@@ -1410,7 +1410,7 @@ impl Pane {
     pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
         index: usize,
         icon: &'static str,
-        active: bool,
+        is_active: bool,
         tooltip: Option<(String, Option<Box<dyn Action>>)>,
         cx: &mut ViewContext<Pane>,
         on_click: F,
@@ -1420,7 +1420,7 @@ impl Pane {
 
         let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
             let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
-            let style = theme.pane_button.style_for(mouse_state, active);
+            let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
             Svg::new(icon)
                 .with_color(style.color)
                 .constrained()

crates/workspace/src/toolbar.rs 🔗

@@ -219,7 +219,7 @@ impl View for Toolbar {
 #[allow(clippy::too_many_arguments)]
 fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
     svg_path: &'static str,
-    style: theme::Interactive<theme::IconButton>,
+    style: theme::Toggleable<theme::Interactive<theme::IconButton>>,
     nav_button_height: f32,
     tooltip_style: TooltipStyle,
     enabled: bool,
@@ -231,9 +231,9 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
 ) -> AnyElement<Toolbar> {
     MouseEventHandler::<A, _>::new(0, cx, |state, _| {
         let style = if enabled {
-            style.style_for(state, false)
+            style.off_state().style_for(state)
         } else {
-            style.disabled_style()
+            style.off_state().disabled_style()
         };
         Svg::new(svg_path)
             .with_color(style.color)