From 19d6e067af782ee2281f2fb3d689f012ccafec6d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 13 Dec 2024 14:23:02 -0500 Subject: [PATCH] Toggle & Switch (#21979) ![CleanShot 2024-12-13 at 11 27 39@2x](https://github.com/user-attachments/assets/7c7828c0-c5c7-4dc6-931e-722366d4f15a) - Adds the Switch component - Updates `Selected`, `Selectable` -> `ToggleState`, `Toggleable` - Adds `checkbox` and `switch` functions to align better with other elements in our layout system. We decided not to merge Switch and Checkbox. However, in a followup I'll introduce a Toggle or AnyToggle enum so we can update `CheckboxWithLabel` -> `ToggleWithLabel` as this component will work exactly the same with either a Checkbox or a Switch. Release Notes: - N/A --- crates/assistant/src/assistant_panel.rs | 12 +- crates/assistant/src/inline_assistant.rs | 12 +- crates/assistant/src/prompt_library.rs | 8 +- crates/assistant/src/slash_command_picker.rs | 4 +- crates/assistant2/src/context_picker.rs | 2 +- .../src/context_picker/file_context_picker.rs | 2 +- crates/assistant2/src/inline_assistant.rs | 12 +- crates/assistant2/src/message_editor.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 22 +- .../src/collab_panel/channel_modal.rs | 14 +- .../src/collab_panel/contact_finder.rs | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/editor/src/code_context_menus.rs | 6 +- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_settings_controls.rs | 12 +- crates/editor/src/hunk_diff.rs | 2 +- .../src/extension_version_selector.rs | 2 +- crates/extensions_ui/src/extensions_ui.rs | 18 +- crates/file_finder/src/file_finder.rs | 2 +- crates/file_finder/src/new_path_prompt.rs | 2 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/git_ui/src/git_panel.rs | 2 +- .../src/language_model_selector.rs | 2 +- .../src/language_selector.rs | 2 +- crates/language_tools/src/lsp_log.rs | 8 +- .../markdown_preview/src/markdown_renderer.rs | 10 +- crates/outline/src/outline.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 4 +- crates/project_symbols/src/project_symbols.rs | 4 +- crates/recent_projects/src/recent_projects.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 16 +- crates/repl/src/components/kernel_options.rs | 2 +- crates/search/src/buffer_search.rs | 4 +- crates/search/src/project_search.rs | 8 +- crates/search/src/search.rs | 2 +- .../src/appearance_settings_controls.rs | 10 +- crates/snippets_ui/src/snippets_ui.rs | 2 +- crates/storybook/src/stories/picker.rs | 2 +- crates/tab_switcher/src/tab_switcher.rs | 2 +- crates/tasks_ui/src/modal.rs | 4 +- crates/terminal_view/src/terminal_panel.rs | 4 +- crates/theme_selector/src/theme_selector.rs | 2 +- crates/title_bar/src/collab.rs | 8 +- .../src/toolchain_selector.rs | 2 +- crates/ui/src/components.rs | 4 +- crates/ui/src/components/button/button.rs | 12 +- .../ui/src/components/button/button_icon.rs | 4 +- .../ui/src/components/button/button_like.rs | 6 +- .../ui/src/components/button/icon_button.rs | 8 +- .../ui/src/components/button/toggle_button.rs | 6 +- crates/ui/src/components/checkbox.rs | 248 ----------- crates/ui/src/components/context_menu.rs | 4 +- crates/ui/src/components/disclosure.rs | 6 +- crates/ui/src/components/dropdown_menu.rs | 4 +- crates/ui/src/components/list/list_header.rs | 4 +- crates/ui/src/components/list/list_item.rs | 4 +- .../ui/src/components/list/list_sub_header.rs | 4 +- crates/ui/src/components/popover_menu.rs | 6 +- crates/ui/src/components/stories/button.rs | 6 +- .../ui/src/components/stories/icon_button.rs | 6 +- crates/ui/src/components/stories/tab.rs | 6 +- crates/ui/src/components/stories/tab_bar.rs | 2 +- .../src/components/stories/toggle_button.rs | 2 +- crates/ui/src/components/tab.rs | 4 +- crates/ui/src/components/toggle.rs | 409 ++++++++++++++++++ crates/ui/src/prelude.rs | 2 +- crates/ui/src/traits.rs | 2 +- .../traits/{selectable.rs => toggleable.rs} | 17 +- crates/ui/src/utils.rs | 8 + crates/vcs_menu/src/lib.rs | 2 +- crates/welcome/src/base_keymap_picker.rs | 2 +- crates/welcome/src/welcome.rs | 18 +- crates/workspace/src/dock.rs | 2 +- crates/workspace/src/pane.rs | 6 +- crates/workspace/src/theme_preview.rs | 5 +- crates/zed/src/zed/quick_action_bar.rs | 6 +- crates/zeta/src/rate_completion_modal.rs | 2 +- 77 files changed, 626 insertions(+), 453 deletions(-) delete mode 100644 crates/ui/src/components/checkbox.rs create mode 100644 crates/ui/src/components/toggle.rs rename crates/ui/src/traits/{selectable.rs => toggleable.rs} (74%) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 285676e222f9f..da3e77f8dc577 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -305,7 +305,7 @@ impl PickerDelegate for SavedContextPickerDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(item), ) } @@ -442,7 +442,7 @@ impl AssistantPanel { ) } }) - .selected( + .toggle_state( pane.active_item() .map_or(false, |item| item.downcast::().is_some()), ); @@ -4956,7 +4956,7 @@ fn render_slash_command_output_toggle( ("slash-command-output-fold-indicator", row.0 as u64), !is_folded, ) - .selected(is_folded) + .toggle_state(is_folded) .on_click(move |_e, cx| fold(!is_folded, cx)) .into_any_element() } @@ -4971,7 +4971,7 @@ fn fold_toggle( ) -> AnyElement { move |row, is_folded, fold, _cx| { Disclosure::new((name, row.0 as u64), !is_folded) - .selected(is_folded) + .toggle_state(is_folded) .on_click(move |_e, cx| fold(!is_folded, cx)) .into_any_element() } @@ -5013,7 +5013,7 @@ fn render_quote_selection_output_toggle( _cx: &mut WindowContext, ) -> AnyElement { Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded) - .selected(is_folded) + .toggle_state(is_folded) .on_click(move |_e, cx| fold(!is_folded, cx)) .into_any_element() } @@ -5036,7 +5036,7 @@ fn render_pending_slash_command_gutter_decoration( icon = icon.icon_color(Color::Muted); } PendingSlashCommandStatus::Running { .. } => { - icon = icon.selected(true); + icon = icon.toggle_state(true); } PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error), } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index bfbdd21677c9f..acca7ae1afac2 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1534,7 +1534,7 @@ impl Render for PromptEditor { v_flex() .child( IconButton::new("rate-limit-error", IconName::XCircle) - .selected(self.show_rate_limit_notice) + .toggle_state(self.show_rate_limit_notice) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .on_click(cx.listener(Self::toggle_rate_limit_notice)), @@ -2133,15 +2133,15 @@ impl PromptEditor { "dont-show-again", Label::new("Don't show again"), if dismissed_rate_limit_notice() { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, |selection, cx| { let is_dismissed = match selection { - ui::Selection::Unselected => false, - ui::Selection::Indeterminate => return, - ui::Selection::Selected => true, + ui::ToggleState::Unselected => false, + ui::ToggleState::Indeterminate => return, + ui::ToggleState::Selected => true, }; set_rate_limit_notice_dismissed(is_dismissed, cx) diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 6d8dee8fb56d4..578e0c647fa5f 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -232,13 +232,13 @@ impl PickerDelegate for PromptPickerDelegate { let element = ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(h_flex().h_5().line_height(relative(1.)).child(Label::new( prompt.title.clone().unwrap_or("Untitled".into()), ))) .end_slot::(default.then(|| { IconButton::new("toggle-default-prompt", IconName::SparkleFilled) - .selected(true) + .toggle_state(true) .icon_color(Color::Accent) .shape(IconButtonShape::Square) .tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx)) @@ -274,7 +274,7 @@ impl PickerDelegate for PromptPickerDelegate { }) .child( IconButton::new("toggle-default-prompt", IconName::Sparkle) - .selected(default) + .toggle_state(default) .selected_icon(IconName::SparkleFilled) .icon_color(if default { Color::Accent } else { Color::Muted }) .shape(IconButtonShape::Square) @@ -1053,7 +1053,7 @@ impl PromptLibrary { IconName::Sparkle, ) .style(ButtonStyle::Transparent) - .selected(prompt_metadata.default) + .toggle_state(prompt_metadata.default) .selected_icon(IconName::SparkleFilled) .icon_color(if prompt_metadata.default { Color::Accent diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant/src/slash_command_picker.rs index 215888540a93c..612eb5d4250cd 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant/src/slash_command_picker.rs @@ -176,7 +176,7 @@ impl PickerDelegate for SlashCommandDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Dense) - .selected(selected) + .toggle_state(selected) .tooltip({ let description = info.description.clone(); move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into() @@ -229,7 +229,7 @@ impl PickerDelegate for SlashCommandDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Dense) - .selected(selected) + .toggle_state(selected) .child(renderer(cx)), ), } diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index 0e2e5aae0ad6d..1e4d007e761dd 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -174,7 +174,7 @@ impl PickerDelegate for ContextPickerDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Dense) - .selected(selected) + .toggle_state(selected) .tooltip({ let description = entry.description.clone(); move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into() diff --git a/crates/assistant2/src/context_picker/file_context_picker.rs b/crates/assistant2/src/context_picker/file_context_picker.rs index 920b47f624c78..13950b267ade0 100644 --- a/crates/assistant2/src/context_picker/file_context_picker.rs +++ b/crates/assistant2/src/context_picker/file_context_picker.rs @@ -260,7 +260,7 @@ impl PickerDelegate for FileContextPickerDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(mat.path.to_string_lossy().to_string()), ) } diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 1cc5fda5d30a0..45d1fa9a691a7 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -1623,7 +1623,7 @@ impl Render for PromptEditor { v_flex() .child( IconButton::new("rate-limit-error", IconName::XCircle) - .selected(self.show_rate_limit_notice) + .toggle_state(self.show_rate_limit_notice) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .on_click(cx.listener(Self::toggle_rate_limit_notice)), @@ -2065,15 +2065,15 @@ impl PromptEditor { "dont-show-again", Label::new("Don't show again"), if dismissed_rate_limit_notice() { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, |selection, cx| { let is_dismissed = match selection { - ui::Selection::Unselected => false, - ui::Selection::Indeterminate => return, - ui::Selection::Selected => true, + ui::ToggleState::Unselected => false, + ui::ToggleState::Indeterminate => return, + ui::ToggleState::Selected => true, }; set_rate_limit_notice_dismissed(is_dismissed, cx) diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 2bb9b783357ab..7cb605cd62e44 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -268,8 +268,8 @@ impl Render for MessageEditor { self.use_tools.into(), cx.listener(|this, selection, _cx| { this.use_tools = match selection { - Selection::Selected => true, - Selection::Unselected | Selection::Indeterminate => false, + ToggleState::Selected => true, + ToggleState::Unselected | ToggleState::Indeterminate => false, }; }), ))) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index fa3ab0219b77d..3af4eec893dc5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -841,7 +841,7 @@ impl CollabPanel { ListItem::new(SharedString::from(user.github_login.clone())) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) - .selected(is_selected) + .toggle_state(is_selected) .end_slot(if is_pending { Label::new("Calling").color(Color::Muted).into_any_element() } else if is_current_user { @@ -894,7 +894,7 @@ impl CollabPanel { .into(); ListItem::new(project_id as usize) - .selected(is_selected) + .toggle_state(is_selected) .on_click(cx.listener(move |this, _, cx| { this.workspace .update(cx, |workspace, cx| { @@ -924,7 +924,7 @@ impl CollabPanel { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); ListItem::new(("screen", id)) - .selected(is_selected) + .toggle_state(is_selected) .start_slot( h_flex() .gap_1() @@ -964,7 +964,7 @@ impl CollabPanel { let channel_store = self.channel_store.read(cx); let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id); ListItem::new("channel-notes") - .selected(is_selected) + .toggle_state(is_selected) .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx); })) @@ -996,7 +996,7 @@ impl CollabPanel { let channel_store = self.channel_store.read(cx); let has_messages_notification = channel_store.has_new_messages(channel_id); ListItem::new("channel-chat") - .selected(is_selected) + .toggle_state(is_selected) .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx); })) @@ -2253,7 +2253,7 @@ impl CollabPanel { }) .inset(true) .end_slot::(button) - .selected(is_selected), + .toggle_state(is_selected), ) } @@ -2270,7 +2270,7 @@ impl CollabPanel { let item = ListItem::new(github_login.clone()) .indent_level(1) .indent_step_size(px(20.)) - .selected(is_selected) + .toggle_state(is_selected) .child( h_flex() .w_full() @@ -2381,7 +2381,7 @@ impl CollabPanel { ListItem::new(github_login.clone()) .indent_level(1) .indent_step_size(px(20.)) - .selected(is_selected) + .toggle_state(is_selected) .child( h_flex() .w_full() @@ -2425,7 +2425,7 @@ impl CollabPanel { ]; ListItem::new(("channel-invite", channel.id.0 as usize)) - .selected(is_selected) + .toggle_state(is_selected) .child( h_flex() .w_full() @@ -2448,7 +2448,7 @@ impl CollabPanel { ListItem::new("contact-placeholder") .child(Icon::new(IconName::Plus)) .child(Label::new("Add a Contact")) - .selected(is_selected) + .toggle_state(is_selected) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) } @@ -2547,7 +2547,7 @@ impl CollabPanel { // Add one level of depth for the disclosure arrow. .indent_level(depth + 1) .indent_step_size(px(20.)) - .selected(is_selected || is_active) + .toggle_state(is_selected || is_active) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| { diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index f41dddceddadb..d0bcf49a0bfff 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -89,15 +89,15 @@ impl ChannelModal { cx.notify() } - fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext) { + fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext) { self.channel_store.update(cx, |channel_store, cx| { channel_store .set_channel_visibility( self.channel_id, match selection { - Selection::Unselected => ChannelVisibility::Members, - Selection::Selected => ChannelVisibility::Public, - Selection::Indeterminate => return, + ToggleState::Unselected => ChannelVisibility::Members, + ToggleState::Selected => ChannelVisibility::Public, + ToggleState::Indeterminate => return, }, cx, ) @@ -159,9 +159,9 @@ impl Render for ChannelModal { "is-public", Label::new("Public").size(LabelSize::Small), if visibility == ChannelVisibility::Public { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, cx.listener(Self::set_channel_visibility), )) @@ -386,7 +386,7 @@ impl PickerDelegate for ChannelModalDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) .end_slot(h_flex().gap_2().map(|slot| { diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 3a80112f3aacd..96fe44092d53a 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -151,7 +151,7 @@ impl PickerDelegate for ContactFinderDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) .end_slot::(icon_path.map(Icon::from_path)), diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 11bc6848fe115..36ca20cd4f270 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -397,7 +397,7 @@ impl PickerDelegate for CommandPaletteDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child( h_flex() .w_full() diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 1817190e426b4..cd473bd69596c 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -16,8 +16,8 @@ use project::{CodeAction, Completion, TaskSourceKind}; use task::ResolvedTask; use ui::{ h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement, - Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _, - StatefulInteractiveElement as _, Styled, StyledExt as _, + Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, + StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable as _, }; use util::ResultExt as _; use workspace::Workspace; @@ -473,7 +473,7 @@ impl CompletionsMenu { div().min_w(px(220.)).max_w(px(540.)).child( ListItem::new(mat.candidate_id) .inset(true) - .selected(item_ix == selected_item) + .toggle_state(item_ix == selected_item) .on_click(cx.listener(move |editor, _event, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_completion( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4e85c20a4405d..8be0767edd080 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4811,7 +4811,7 @@ impl Editor { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .selected(is_active) + .toggle_state(is_active) .tooltip({ let focus_handle = self.focus_handle.clone(); move |cx| { @@ -4988,7 +4988,7 @@ impl Editor { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .selected(is_active) + .toggle_state(is_active) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); editor.toggle_code_actions( @@ -13772,7 +13772,7 @@ impl EditorSnapshot { if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) { Some( Disclosure::new(("gutter_crease", buffer_row.0), !folded) - .selected(folded) + .toggle_state(folded) .on_click(cx.listener_for(&editor, move |this, _e, cx| { if folded { this.unfold_at(&UnfoldAt { buffer_row }, cx); diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index bbe1b00324a78..50392ab2c9dfe 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -265,8 +265,8 @@ impl RenderOnce for BufferFontLigaturesControl { |selection, cx| { Self::write( match selection { - Selection::Selected => true, - Selection::Unselected | Selection::Indeterminate => false, + ToggleState::Selected => true, + ToggleState::Unselected | ToggleState::Indeterminate => false, }, cx, ); @@ -318,8 +318,8 @@ impl RenderOnce for InlineGitBlameControl { |selection, cx| { Self::write( match selection { - Selection::Selected => true, - Selection::Unselected | Selection::Indeterminate => false, + ToggleState::Selected => true, + ToggleState::Unselected | ToggleState::Indeterminate => false, }, cx, ); @@ -371,8 +371,8 @@ impl RenderOnce for LineNumbersControl { |selection, cx| { Self::write( match selection { - Selection::Selected => true, - Selection::Unselected | Selection::Indeterminate => false, + ToggleState::Selected => true, + ToggleState::Unselected | ToggleState::Indeterminate => false, }, cx, ); diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 2102c111f66e8..b52086f71d968 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -726,7 +726,7 @@ impl Editor { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .selected( + .toggle_state( hunk_controls_menu_handle .is_deployed(), ) diff --git a/crates/extensions_ui/src/extension_version_selector.rs b/crates/extensions_ui/src/extension_version_selector.rs index 1041e9524fdd1..91bc1d72ac524 100644 --- a/crates/extensions_ui/src/extension_version_selector.rs +++ b/crates/extensions_ui/src/extension_version_selector.rs @@ -210,7 +210,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .disabled(disabled) .child( HighlightedLabel::new( diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index aef99e6167779..3adde9140e319 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -933,7 +933,7 @@ impl ExtensionsPage { fn update_settings( &mut self, - selection: &Selection, + selection: &ToggleState, cx: &mut ViewContext, callback: impl 'static + Send + Fn(&mut T::FileContent, bool), ) { @@ -942,8 +942,8 @@ impl ExtensionsPage { let selection = *selection; settings::update_settings_file::(fs, cx, move |settings, _| { let value = match selection { - Selection::Unselected => false, - Selection::Selected => true, + ToggleState::Unselected => false, + ToggleState::Selected => true, _ => return, }; @@ -998,9 +998,9 @@ impl ExtensionsPage { "enable-vim", Label::new("Enable vim mode"), if VimModeSetting::get_global(cx).0 { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, cx.listener(move |this, selection, cx| { this.telemetry @@ -1090,7 +1090,7 @@ impl Render for ExtensionsPage { ToggleButton::new("filter-all", "All") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(self.filter == ExtensionFilter::All) + .toggle_state(self.filter == ExtensionFilter::All) .on_click(cx.listener(|this, _event, cx| { this.filter = ExtensionFilter::All; this.filter_extension_entries(cx); @@ -1104,7 +1104,7 @@ impl Render for ExtensionsPage { ToggleButton::new("filter-installed", "Installed") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(self.filter == ExtensionFilter::Installed) + .toggle_state(self.filter == ExtensionFilter::Installed) .on_click(cx.listener(|this, _event, cx| { this.filter = ExtensionFilter::Installed; this.filter_extension_entries(cx); @@ -1118,7 +1118,9 @@ impl Render for ExtensionsPage { ToggleButton::new("filter-not-installed", "Not Installed") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(self.filter == ExtensionFilter::NotInstalled) + .toggle_state( + self.filter == ExtensionFilter::NotInstalled, + ) .on_click(cx.listener(|this, _event, cx| { this.filter = ExtensionFilter::NotInstalled; this.filter_extension_entries(cx); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 10cde076e15c2..b88be232913f0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1228,7 +1228,7 @@ impl PickerDelegate for FileFinderDelegate { .start_slot::(file_icon) .end_slot::(history_icon) .inset(true) - .selected(selected) + .toggle_state(selected) .child( h_flex() .gap_2() diff --git a/crates/file_finder/src/new_path_prompt.rs b/crates/file_finder/src/new_path_prompt.rs index 6a1b08e2056e5..7c36b67c36ce6 100644 --- a/crates/file_finder/src/new_path_prompt.rs +++ b/crates/file_finder/src/new_path_prompt.rs @@ -414,7 +414,7 @@ impl PickerDelegate for NewPathDelegate { ListItem::new(ix) .spacing(ListItemSpacing::Sparse) .inset(true) - .selected(selected) + .toggle_state(selected) .child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))), ) } diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index be1e91b482fc9..5906f62d71199 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -283,7 +283,7 @@ impl PickerDelegate for OpenPathDelegate { ListItem::new(ix) .spacing(ListItemSpacing::Sparse) .inset(true) - .selected(selected) + .toggle_state(selected) .child(LabelLike::new().child(candidate.string.clone())), ) } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 30cfee9b259dd..ea7585d978540 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -658,7 +658,7 @@ impl GitPanel { ) -> impl IntoElement { let id = id.to_proto() as usize; let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into()); - let is_staged = Selection::Selected; + let is_staged = ToggleState::Selected; h_flex() .id(id) diff --git a/crates/language_model_selector/src/language_model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs index b76f88e557d01..fb41eb6a127a0 100644 --- a/crates/language_model_selector/src/language_model_selector.rs +++ b/crates/language_model_selector/src/language_model_selector.rs @@ -267,7 +267,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .start_slot( div().pr_0p5().child( Icon::new(model_info.icon) diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 760a94000dd0b..9e90c43a7b3e6 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -281,7 +281,7 @@ impl PickerDelegate for LanguageSelectorDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .start_slot::(language_icon) .child(HighlightedLabel::new(label, mat.positions.clone())), ) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 892745f4d31b2..bd8df70e13876 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -14,7 +14,7 @@ use lsp::{ }; use project::{search::SearchQuery, Project, WorktreeId}; use std::{borrow::Cow, sync::Arc}; -use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection}; +use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState}; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, @@ -1251,9 +1251,9 @@ impl Render for LspLogToolbarItemView { Checkbox::new( "LspLogEnableRpcTrace", if rpc_trace_enabled { - Selection::Selected + ToggleState::Selected } else { - Selection::Unselected + ToggleState::Unselected }, ) .on_click(cx.listener_for( @@ -1261,7 +1261,7 @@ impl Render for LspLogToolbarItemView { move |view, selection, cx| { let enabled = matches!( selection, - Selection::Selected + ToggleState::Selected ); view.toggle_rpc_logging_for_server( server_id, enabled, cx, diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 5183f361b6293..eff109df07d0c 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -20,7 +20,7 @@ use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ h_flex, relative, tooltip_container, v_flex, Checkbox, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, - Selection, StatefulInteractiveElement, StyledExt, StyledImage, ViewContext, VisibleOnHover, + StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, ViewContext, VisibleOnHover, VisualContext as _, }; use workspace::Workspace; @@ -180,9 +180,9 @@ fn render_markdown_list_item( Checkbox::new( "checkbox", if *checked { - Selection::Selected + ToggleState::Selected } else { - Selection::Unselected + ToggleState::Unselected }, ) .when_some( @@ -192,8 +192,8 @@ fn render_markdown_list_item( let range = range.clone(); move |selection, cx| { let checked = match selection { - Selection::Selected => true, - Selection::Unselected => false, + ToggleState::Selected => true, + ToggleState::Unselected => false, _ => return, }; diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 154b9297a3930..3c7b9780e732a 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -280,7 +280,7 @@ impl PickerDelegate for OutlineViewDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child( div() .text_ui(cx) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index f36e144c88245..21c40807b8e7b 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -51,7 +51,7 @@ use workspace::{ ui::{ h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label, - LabelCommon, ListItem, Scrollbar, ScrollbarState, Selectable, StyledExt, StyledTypography, + LabelCommon, ListItem, Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, }, OpenInTerminal, WeakItemHandle, Workspace, @@ -2045,7 +2045,7 @@ impl OutlinePanel { ListItem::new(item_id) .indent_level(depth) .indent_step_size(px(settings.indent_size)) - .selected(is_active) + .toggle_state(is_active) .when_some(icon_element, |list_item, icon_element| { list_item.child(h_flex().child(icon_element)) }) diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index fce4f6b7eda3d..753980a26e9a2 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -11,7 +11,7 @@ use std::{borrow::Cow, cmp::Reverse, sync::Arc}; use theme::ActiveTheme; use util::ResultExt; use workspace::{ - ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable}, + ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Toggleable}, Workspace, }; @@ -240,7 +240,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child( v_flex() .child( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 404bf26b6271a..8deae201ba8aa 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -395,7 +395,7 @@ impl PickerDelegate for RecentProjectsDelegate { Some( ListItem::new(ix) - .selected(selected) + .toggle_state(selected) .inset(true) .spacing(ListItemSpacing::Sparse) .child( diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 49d870f56bcc7..099fbbf5353f7 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -653,7 +653,7 @@ impl RemoteServerProjects { })) .child( ListItem::new(("new-remote-project", ix)) - .selected( + .toggle_state( ssh_server.open_folder.focus_handle.contains_focused(cx), ) .inset(true) @@ -688,7 +688,7 @@ impl RemoteServerProjects { })) .child( ListItem::new(("server-options", ix)) - .selected( + .toggle_state( ssh_server.configure.focus_handle.contains_focused(cx), ) .inset(true) @@ -772,7 +772,7 @@ impl RemoteServerProjects { })) .child( ListItem::new((element_id_base, ix)) - .selected(navigation.focus_handle.contains_focused(cx)) + .toggle_state(navigation.focus_handle.contains_focused(cx)) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .start_slot( @@ -984,7 +984,7 @@ impl RemoteServerProjects { })) .child( ListItem::new("add-nickname") - .selected(entries[0].focus_handle.contains_focused(cx)) + .toggle_state(entries[0].focus_handle.contains_focused(cx)) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) @@ -1043,7 +1043,7 @@ impl RemoteServerProjects { }) .child( ListItem::new("copy-server-address") - .selected(entries[1].focus_handle.contains_focused(cx)) + .toggle_state(entries[1].focus_handle.contains_focused(cx)) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) @@ -1116,7 +1116,7 @@ impl RemoteServerProjects { })) .child( ListItem::new("remove-server") - .selected(entries[2].focus_handle.contains_focused(cx)) + .toggle_state(entries[2].focus_handle.contains_focused(cx)) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Trash).color(Color::Error)) @@ -1144,7 +1144,7 @@ impl RemoteServerProjects { })) .child( ListItem::new("go-back") - .selected(entries[3].focus_handle.contains_focused(cx)) + .toggle_state(entries[3].focus_handle.contains_focused(cx)) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .start_slot( @@ -1233,7 +1233,7 @@ impl RemoteServerProjects { .anchor_scroll(state.add_new_server.scroll_anchor.clone()) .child( ListItem::new("register-remove-server-button") - .selected(state.add_new_server.focus_handle.contains_focused(cx)) + .toggle_state(state.add_new_server.focus_handle.contains_focused(cx)) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 8fd9b412eaa20..85cd758f406c3 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -150,7 +150,7 @@ impl PickerDelegate for KernelPickerDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child( h_flex() .w_full() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 794737bd802bd..8e83b459fc722 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -272,7 +272,7 @@ impl Render for BufferSearchBar { .on_click(cx.listener(|this, _: &ClickEvent, cx| { this.toggle_replace(&ToggleReplace, cx); })) - .selected(self.replace_enabled) + .toggle_state(self.replace_enabled) .tooltip({ let focus_handle = focus_handle.clone(); move |cx| { @@ -300,7 +300,7 @@ impl Render for BufferSearchBar { .on_click(cx.listener(|this, _: &ClickEvent, cx| { this.toggle_selection(&ToggleSelection, cx); })) - .selected(self.selection_search_enabled) + .toggle_state(self.selection_search_enabled) .tooltip({ let focus_handle = focus_handle.clone(); move |cx| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2a90ff41c7725..b8eee4fe53a93 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -35,7 +35,7 @@ use std::{ use theme::ThemeSettings; use ui::{ h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape, - IconName, KeyBinding, Label, LabelCommon, LabelSize, Selectable, Tooltip, + IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip, }; use util::paths::PathMatcher; use workspace::{ @@ -1645,7 +1645,7 @@ impl Render for ProjectSearchBar { .on_click(cx.listener(|this, _, cx| { this.toggle_filters(cx); })) - .selected( + .toggle_state( self.active_project_search .as_ref() .map(|search| search.read(cx).filters_enabled) @@ -1669,7 +1669,7 @@ impl Render for ProjectSearchBar { .on_click(cx.listener(|this, _, cx| { this.toggle_replace(&ToggleReplace, cx); })) - .selected( + .toggle_state( self.active_project_search .as_ref() .map(|search| search.read(cx).replace_enabled) @@ -1878,7 +1878,7 @@ impl Render for ProjectSearchBar { .child( IconButton::new("project-search-opened-only", IconName::FileSearch) .shape(IconButtonShape::Square) - .selected(self.is_opened_only_enabled(cx)) + .toggle_state(self.is_opened_only_enabled(cx)) .tooltip(|cx| Tooltip::text("Only Search Open Files", cx)) .on_click(cx.listener(|this, _, cx| { this.toggle_opened_only(cx); diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 60ff80834feed..adca7bd049ae9 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -113,7 +113,7 @@ impl SearchOptions { .on_click(action) .style(ButtonStyle::Subtle) .shape(IconButtonShape::Square) - .selected(active) + .toggle_state(active) .tooltip({ let action = self.to_toggle_action(); let label = self.label(); diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index 39bfda081671b..c9360de8406ec 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -145,7 +145,7 @@ impl RenderOnce for ThemeModeControl { ToggleButton::new("light", "Light") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(value == ThemeMode::Light) + .toggle_state(value == ThemeMode::Light) .on_click(|_, cx| Self::write(ThemeMode::Light, cx)) .first(), ) @@ -153,7 +153,7 @@ impl RenderOnce for ThemeModeControl { ToggleButton::new("system", "System") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(value == ThemeMode::System) + .toggle_state(value == ThemeMode::System) .on_click(|_, cx| Self::write(ThemeMode::System, cx)) .middle(), ) @@ -161,7 +161,7 @@ impl RenderOnce for ThemeModeControl { ToggleButton::new("dark", "Dark") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(value == ThemeMode::Dark) + .toggle_state(value == ThemeMode::Dark) .on_click(|_, cx| Self::write(ThemeMode::Dark, cx)) .last(), ) @@ -375,8 +375,8 @@ impl RenderOnce for UiFontLigaturesControl { |selection, cx| { Self::write( match selection { - Selection::Selected => true, - Selection::Unselected | Selection::Indeterminate => false, + ToggleState::Selected => true, + ToggleState::Unselected | ToggleState::Indeterminate => false, }, cx, ); diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index c8ab6febdaa07..223dfb7ce9a7e 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -219,7 +219,7 @@ impl PickerDelegate for ScopeSelectorDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(HighlightedLabel::new(label, mat.positions.clone())), ) } diff --git a/crates/storybook/src/stories/picker.rs b/crates/storybook/src/stories/picker.rs index 5336b63e49206..844617ae35b79 100644 --- a/crates/storybook/src/stories/picker.rs +++ b/crates/storybook/src/stories/picker.rs @@ -59,7 +59,7 @@ impl PickerDelegate for Delegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(Label::new(candidate)), ) } diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 8c961dc54d4b6..5d02884472eaa 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -407,7 +407,7 @@ impl PickerDelegate for TabSwitcherDelegate { ListItem::new(ix) .spacing(ListItemSpacing::Sparse) .inset(true) - .selected(selected) + .toggle_state(selected) .child(h_flex().w_full().child(label)) .start_slot::(icon) .map(|el| { diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 3c7b767d5cb50..9cabf555cda75 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -13,7 +13,7 @@ use task::{ResolvedTask, TaskContext, TaskTemplate}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, - KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Selectable, Tooltip, + KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, WindowContext, }; use util::ResultExt; @@ -379,7 +379,7 @@ impl PickerDelegate for TasksModalDelegate { }; item }) - .selected(selected) + .toggle_state(selected) .child(highlighted_location.render(cx)), ) } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ac5895d148bab..a703de467e70d 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -26,7 +26,7 @@ use terminal::{ Terminal, }; use ui::{ - prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Selectable, + prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip, }; use util::{ResultExt, TryFutureExt}; @@ -201,7 +201,7 @@ impl TerminalPanel { let zoomed = pane.is_zoomed(); IconButton::new("toggle_zoom", IconName::Maximize) .icon_size(IconSize::Small) - .selected(zoomed) + .toggle_state(zoomed) .selected_icon(IconName::Minimize) .on_click(cx.listener(|pane, _, cx| { pane.toggle_zoom(&workspace::ToggleZoom, cx); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index e09ad40bf4eaf..45e590a36690d 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -285,7 +285,7 @@ impl PickerDelegate for ThemeSelectorDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(HighlightedLabel::new( theme_match.string.clone(), theme_match.positions.clone(), diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 6e475d9bede93..d1eafe8da752d 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -322,7 +322,7 @@ impl TitleBar { }) .style(ButtonStyle::Subtle) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .selected(is_shared) + .toggle_state(is_shared) .label_size(LabelSize::Small) .on_click(cx.listener(move |this, _, cx| { if is_shared { @@ -380,7 +380,7 @@ impl TitleBar { }) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) - .selected(is_muted) + .toggle_state(is_muted) .selected_style(ButtonStyle::Tinted(TintColor::Negative)) .on_click(move |_, cx| { toggle_mute(&Default::default(), cx); @@ -400,7 +400,7 @@ impl TitleBar { .style(ButtonStyle::Subtle) .selected_style(ButtonStyle::Tinted(TintColor::Negative)) .icon_size(IconSize::Small) - .selected(is_deafened) + .toggle_state(is_deafened) .tooltip(move |cx| { if is_deafened { let label = "Unmute Audio"; @@ -430,7 +430,7 @@ impl TitleBar { IconButton::new("screen-share", ui::IconName::Screen) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) - .selected(is_screen_sharing) + .toggle_state(is_screen_sharing) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) .tooltip(move |cx| { Tooltip::text( diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 4c31d600ba7ee..c277e321543e6 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -345,7 +345,7 @@ impl PickerDelegate for ToolchainSelectorDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(HighlightedLabel::new(label, name_highlights)) .child( HighlightedLabel::new(path, path_highlights) diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 6d13e8a71a560..f6626c745b2f3 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,6 +1,5 @@ mod avatar; mod button; -mod checkbox; mod content_group; mod context_menu; mod disclosure; @@ -28,6 +27,7 @@ mod stack; mod tab; mod tab_bar; mod table; +mod toggle; mod tool_strip; mod tooltip; @@ -36,7 +36,6 @@ mod stories; pub use avatar::*; pub use button::*; -pub use checkbox::*; pub use content_group::*; pub use context_menu::*; pub use disclosure::*; @@ -64,6 +63,7 @@ pub use stack::*; pub use tab::*; pub use tab_bar::*; pub use table::*; +pub use toggle::*; pub use tool_strip::*; pub use tooltip::*; diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 774d9e59e40bf..4a2975bab7284 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -194,7 +194,7 @@ impl Button { } } -impl Selectable for Button { +impl Toggleable for Button { /// Sets the selected state of the button. /// /// This method allows the selection state of the button to be specified. @@ -213,8 +213,8 @@ impl Selectable for Button { /// ``` /// /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected. - fn selected(mut self, selected: bool) -> Self { - self.base = self.base.selected(selected); + fn toggle_state(mut self, selected: bool) -> Self { + self.base = self.base.toggle_state(selected); self } } @@ -405,7 +405,7 @@ impl RenderOnce for Button { this.children(self.icon.map(|icon| { ButtonIcon::new(icon) .disabled(is_disabled) - .selected(is_selected) + .toggle_state(is_selected) .selected_icon(self.selected_icon) .selected_icon_color(self.selected_icon_color) .size(self.icon_size) @@ -429,7 +429,7 @@ impl RenderOnce for Button { this.children(self.icon.map(|icon| { ButtonIcon::new(icon) .disabled(is_disabled) - .selected(is_selected) + .toggle_state(is_selected) .selected_icon(self.selected_icon) .selected_icon_color(self.selected_icon_color) .size(self.icon_size) @@ -500,7 +500,7 @@ impl ComponentPreview for Button { ), single_example( "Selected", - Button::new("selected", "Selected").selected(true), + Button::new("selected", "Selected").toggle_state(true), ), ], ), diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs index f3aebe7f765ff..bb85f7d10a6da 100644 --- a/crates/ui/src/components/button/button_icon.rs +++ b/crates/ui/src/components/button/button_icon.rs @@ -65,8 +65,8 @@ impl Disableable for ButtonIcon { } } -impl Selectable for ButtonIcon { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for ButtonIcon { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 1eff4feb72adf..e2d83486cd9f9 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -6,7 +6,7 @@ use smallvec::SmallVec; use crate::{prelude::*, DynamicSpacing, ElevationIndex}; /// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected. -pub trait SelectableButton: Selectable { +pub trait SelectableButton: Toggleable { fn selected_style(self, style: ButtonStyle) -> Self; } @@ -400,8 +400,8 @@ impl Disableable for ButtonLike { } } -impl Selectable for ButtonLike { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for ButtonLike { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index bad10d6fb43b8..3abe4bb3094bd 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -66,9 +66,9 @@ impl Disableable for IconButton { } } -impl Selectable for IconButton { - fn selected(mut self, selected: bool) -> Self { - self.base = self.base.selected(selected); +impl Toggleable for IconButton { + fn toggle_state(mut self, selected: bool) -> Self { + self.base = self.base.toggle_state(selected); self } } @@ -157,7 +157,7 @@ impl RenderOnce for IconButton { .child( ButtonIcon::new(self.icon) .disabled(is_disabled) - .selected(is_selected) + .toggle_state(is_selected) .selected_icon(self.selected_icon) .when_some(selected_style, |this, style| this.selected_style(style)) .size(self.icon_size) diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 33577fc4e8c16..96bb37670b816 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -57,9 +57,9 @@ impl ToggleButton { } } -impl Selectable for ToggleButton { - fn selected(mut self, selected: bool) -> Self { - self.base = self.base.selected(selected); +impl Toggleable for ToggleButton { + fn toggle_state(mut self, selected: bool) -> Self { + self.base = self.base.toggle_state(selected); self } } diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs deleted file mode 100644 index df2dbe3da2b27..0000000000000 --- a/crates/ui/src/components/checkbox.rs +++ /dev/null @@ -1,248 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; - -use crate::prelude::*; -use crate::{Color, Icon, IconName, Selection}; - -/// # Checkbox -/// -/// Checkboxes are used for multiple choices, not for mutually exclusive choices. -/// Each checkbox works independently from other checkboxes in the list, -/// therefore checking an additional box does not affect any other selections. -#[derive(IntoElement)] -pub struct Checkbox { - id: ElementId, - checked: Selection, - disabled: bool, - on_click: Option>, -} - -impl Checkbox { - pub fn new(id: impl Into, checked: Selection) -> Self { - Self { - id: id.into(), - checked, - disabled: false, - on_click: None, - } - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - pub fn on_click(mut self, handler: impl Fn(&Selection, &mut WindowContext) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); - self - } -} - -impl RenderOnce for Checkbox { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let group_id = format!("checkbox_group_{:?}", self.id); - - let icon = match self.checked { - Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color( - if self.disabled { - Color::Disabled - } else { - Color::Selected - }, - )), - Selection::Indeterminate => Some( - Icon::new(IconName::Dash) - .size(IconSize::Small) - .color(if self.disabled { - Color::Disabled - } else { - Color::Selected - }), - ), - Selection::Unselected => None, - }; - - let selected = - self.checked == Selection::Selected || self.checked == Selection::Indeterminate; - - let (bg_color, border_color) = match (self.disabled, selected) { - (true, _) => ( - cx.theme().colors().ghost_element_disabled, - cx.theme().colors().border_disabled, - ), - (false, true) => ( - cx.theme().colors().element_selected, - cx.theme().colors().border, - ), - (false, false) => ( - cx.theme().colors().element_background, - cx.theme().colors().border, - ), - }; - - h_flex() - .id(self.id) - .justify_center() - .items_center() - .size(DynamicSpacing::Base20.rems(cx)) - .group(group_id.clone()) - .child( - div() - .flex() - .flex_none() - .justify_center() - .items_center() - .m(DynamicSpacing::Base04.px(cx)) - .size(DynamicSpacing::Base16.rems(cx)) - .rounded_sm() - .bg(bg_color) - .border_1() - .border_color(border_color) - .when(!self.disabled, |this| { - this.group_hover(group_id.clone(), |el| { - el.bg(cx.theme().colors().element_hover) - }) - }) - .children(icon), - ) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)), - ) - } -} - -impl ComponentPreview for Checkbox { - fn description() -> impl Into> { - "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state." - } - - fn examples(_: &mut WindowContext) -> Vec> { - vec![ - example_group_with_title( - "Default", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", Selection::Unselected), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", Selection::Indeterminate), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", Selection::Selected), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_disabled_unselected", Selection::Unselected) - .disabled(true), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_disabled_indeterminate", Selection::Indeterminate) - .disabled(true), - ), - single_example( - "Selected", - Checkbox::new("checkbox_disabled_selected", Selection::Selected) - .disabled(true), - ), - ], - ), - ] - } -} - -use std::sync::Arc; - -/// A [`Checkbox`] that has a [`Label`]. -#[derive(IntoElement)] -pub struct CheckboxWithLabel { - id: ElementId, - label: Label, - checked: Selection, - on_click: Arc, -} - -impl CheckboxWithLabel { - pub fn new( - id: impl Into, - label: Label, - checked: Selection, - on_click: impl Fn(&Selection, &mut WindowContext) + 'static, - ) -> Self { - Self { - id: id.into(), - label, - checked, - on_click: Arc::new(on_click), - } - } -} - -impl RenderOnce for CheckboxWithLabel { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - h_flex() - .gap(DynamicSpacing::Base08.rems(cx)) - .child(Checkbox::new(self.id.clone(), self.checked).on_click({ - let on_click = self.on_click.clone(); - move |checked, cx| { - (on_click)(checked, cx); - } - })) - .child( - div() - .id(SharedString::from(format!("{}-label", self.id))) - .on_click(move |_event, cx| { - (self.on_click)(&self.checked.inverse(), cx); - }) - .child(self.label), - ) - } -} - -impl ComponentPreview for CheckboxWithLabel { - fn description() -> impl Into> { - "A checkbox with an associated label, allowing users to select an option while providing a descriptive text." - } - - fn examples(_: &mut WindowContext) -> Vec> { - vec![example_group(vec![ - single_example( - "Unselected", - CheckboxWithLabel::new( - "checkbox_with_label_unselected", - Label::new("Always save on quit"), - Selection::Unselected, - |_, _| {}, - ), - ), - single_example( - "Indeterminate", - CheckboxWithLabel::new( - "checkbox_with_label_indeterminate", - Label::new("Always save on quit"), - Selection::Indeterminate, - |_, _| {}, - ), - ), - single_example( - "Selected", - CheckboxWithLabel::new( - "checkbox_with_label_selected", - Label::new("Always save on quit"), - Selection::Selected, - |_, _| {}, - ), - ), - ])] - } -} diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 2f83953329660..52d28306ac699 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -434,7 +434,7 @@ impl Render for ContextMenu { ListItem::new(ix) .inset(true) .disabled(*disabled) - .selected(Some(ix) == self.selected_index) + .toggle_state(Some(ix) == self.selected_index) .when_some(*toggle, |list_item, (position, toggled)| { let contents = if toggled { v_flex().flex_none().child( @@ -495,7 +495,7 @@ impl Render for ContextMenu { let selectable = *selectable; ListItem::new(ix) .inset(true) - .selected(if selectable { + .toggle_state(if selectable { Some(ix) == self.selected_index } else { false diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 9e8ab48221e17..7f04cc3528efc 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -34,8 +34,8 @@ impl Disclosure { } } -impl Selectable for Disclosure { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for Disclosure { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } @@ -65,7 +65,7 @@ impl RenderOnce for Disclosure { .shape(IconButtonShape::Square) .icon_color(Color::Muted) .icon_size(IconSize::Small) - .selected(self.selected) + .toggle_state(self.selected) .when_some(self.on_toggle, move |this, on_toggle| { this.on_click(move |event, cx| on_toggle(event, cx)) }) diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 8d930a63acd47..2fa32894b4078 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -85,8 +85,8 @@ impl Disableable for DropdownMenuTrigger { } } -impl Selectable for DropdownMenuTrigger { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for DropdownMenuTrigger { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/list/list_header.rs b/crates/ui/src/components/list/list_header.rs index 0d1411e70a129..70af7c2abc3c6 100644 --- a/crates/ui/src/components/list/list_header.rs +++ b/crates/ui/src/components/list/list_header.rs @@ -73,8 +73,8 @@ impl ListHeader { } } -impl Selectable for ListHeader { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for ListHeader { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index bf216649e7192..48d1e9b03d379 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -156,8 +156,8 @@ impl Disableable for ListItem { } } -impl Selectable for ListItem { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for ListItem { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs index 62d2ca00919d7..35e72f4ba7fbb 100644 --- a/crates/ui/src/components/list/list_sub_header.rs +++ b/crates/ui/src/components/list/list_sub_header.rs @@ -32,8 +32,8 @@ impl ListSubHeader { } } -impl Selectable for ListSubHeader { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for ListSubHeader { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 547b8f5a83e04..d9d502efeaab0 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -11,9 +11,9 @@ use gpui::{ use crate::prelude::*; -pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {} +pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {} -impl PopoverTrigger for T {} +impl PopoverTrigger for T {} pub struct PopoverMenuHandle(Rc>>>); @@ -129,7 +129,7 @@ impl PopoverMenu { pub fn trigger(mut self, t: T) -> Self { self.child_builder = Some(Box::new(|menu, builder| { let open = menu.borrow().is_some(); - t.selected(open) + t.toggle_state(open) .when_some(builder, |el, builder| { el.on_click(move |_, cx| show_menu(&builder, &menu, cx)) }) diff --git a/crates/ui/src/components/stories/button.rs b/crates/ui/src/components/stories/button.rs index c3fcdc5ae9139..7701a278c9235 100644 --- a/crates/ui/src/components/stories/button.rs +++ b/crates/ui/src/components/stories/button.rs @@ -13,11 +13,11 @@ impl Render for ButtonStory { .child(Story::label("Default")) .child(Button::new("default_filled", "Click me")) .child(Story::label("Selected")) - .child(Button::new("selected_filled", "Click me").selected(true)) + .child(Button::new("selected_filled", "Click me").toggle_state(true)) .child(Story::label("Selected with `selected_label`")) .child( Button::new("selected_label_filled", "Click me") - .selected(true) + .toggle_state(true) .selected_label("I have been selected"), ) .child(Story::label("With `label_color`")) @@ -27,7 +27,7 @@ impl Render for ButtonStory { .child(Story::label("Selected with `icon`")) .child( Button::new("filled_and_selected_with_icon", "Click me") - .selected(true) + .toggle_state(true) .icon(IconName::FileGit), ) .child(Story::label("Default (Subtle)")) diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index 6d787e80b131a..fb9b33e12bd9b 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -21,7 +21,7 @@ impl Render for IconButtonStory { let selected_button = StoryItem::new( "Selected", - IconButton::new("selected_icon_button", IconName::Hash).selected(true), + IconButton::new("selected_icon_button", IconName::Hash).toggle_state(true), ) .description("Displays an icon button that is selected.") .usage( @@ -33,7 +33,7 @@ impl Render for IconButtonStory { let selected_with_selected_icon = StoryItem::new( "Selected with `selected_icon`", IconButton::new("selected_with_selected_icon_button", IconName::AudioOn) - .selected(true) + .toggle_state(true) .selected_icon(IconName::AudioOff), ) .description( @@ -89,7 +89,7 @@ impl Render for IconButtonStory { let selected_with_tooltip_button = StoryItem::new( "Selected with `tooltip`", IconButton::new("selected_with_tooltip_button", IconName::InlayHint) - .selected(true) + .toggle_state(true) .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)), ) .description("Displays a selected icon button with tooltip.") diff --git a/crates/ui/src/components/stories/tab.rs b/crates/ui/src/components/stories/tab.rs index eb0dd084b9868..4f90268e6e6f4 100644 --- a/crates/ui/src/components/stories/tab.rs +++ b/crates/ui/src/components/stories/tab.rs @@ -48,7 +48,7 @@ impl Render for TabStory { h_flex() .child( Tab::new("tab_1") - .selected(true) + .toggle_state(true) .position(TabPosition::First) .child("Tab 1"), ) @@ -85,7 +85,7 @@ impl Render for TabStory { .child( Tab::new("tab_4") .position(TabPosition::Last) - .selected(true) + .toggle_state(true) .child("Tab 4"), ), ) @@ -100,7 +100,7 @@ impl Render for TabStory { .child( Tab::new("tab_2") .position(TabPosition::Middle(Ordering::Equal)) - .selected(true) + .toggle_state(true) .child("Tab 2"), ) .child( diff --git a/crates/ui/src/components/stories/tab_bar.rs b/crates/ui/src/components/stories/tab_bar.rs index d6d42fa5e0f3c..dbce8936417b6 100644 --- a/crates/ui/src/components/stories/tab_bar.rs +++ b/crates/ui/src/components/stories/tab_bar.rs @@ -13,7 +13,7 @@ impl Render for TabBarStory { let tabs = (0..tab_count) .map(|index| { Tab::new(index) - .selected(index == selected_tab_index) + .toggle_state(index == selected_tab_index) .position(if index == 0 { TabPosition::First } else if index == tab_count - 1 { diff --git a/crates/ui/src/components/stories/toggle_button.rs b/crates/ui/src/components/stories/toggle_button.rs index 86f19d34c4cca..88523d2ce58a0 100644 --- a/crates/ui/src/components/stories/toggle_button.rs +++ b/crates/ui/src/components/stories/toggle_button.rs @@ -68,7 +68,7 @@ impl Render for ToggleButtonStory { ToggleButton::new(2, "Banana") .style(ButtonStyle::Filled) .size(ButtonSize::Large) - .selected(true) + .toggle_state(true) .middle(), ) .child( diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index e33fc732da0bf..17962cdbc5439 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -91,8 +91,8 @@ impl InteractiveElement for Tab { impl StatefulInteractiveElement for Tab {} -impl Selectable for Tab { - fn selected(mut self, selected: bool) -> Self { +impl Toggleable for Tab { + fn toggle_state(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs new file mode 100644 index 0000000000000..4d434b81ca756 --- /dev/null +++ b/crates/ui/src/components/toggle.rs @@ -0,0 +1,409 @@ +#![allow(missing_docs)] + +use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; +use std::sync::Arc; + +use crate::prelude::*; +use crate::utils::is_light; +use crate::{Color, Icon, IconName, ToggleState}; + +/// Creates a new checkbox +pub fn checkbox(id: impl Into, toggle_state: ToggleState) -> Checkbox { + Checkbox::new(id, toggle_state) +} + +/// Creates a new switch +pub fn switch(id: impl Into, toggle_state: ToggleState) -> Switch { + Switch::new(id, toggle_state) +} + +/// # Checkbox +/// +/// Checkboxes are used for multiple choices, not for mutually exclusive choices. +/// Each checkbox works independently from other checkboxes in the list, +/// therefore checking an additional box does not affect any other selections. +#[derive(IntoElement)] +pub struct Checkbox { + id: ElementId, + toggle_state: ToggleState, + disabled: bool, + on_click: Option>, +} + +impl Checkbox { + pub fn new(id: impl Into, checked: ToggleState) -> Self { + Self { + id: id.into(), + toggle_state: checked, + disabled: false, + on_click: None, + } + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ToggleState, &mut WindowContext) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl RenderOnce for Checkbox { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let group_id = format!("checkbox_group_{:?}", self.id); + + let icon = match self.toggle_state { + ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color( + if self.disabled { + Color::Disabled + } else { + Color::Selected + }, + )), + ToggleState::Indeterminate => Some( + Icon::new(IconName::Dash) + .size(IconSize::Small) + .color(if self.disabled { + Color::Disabled + } else { + Color::Selected + }), + ), + ToggleState::Unselected => None, + }; + + let selected = self.toggle_state == ToggleState::Selected + || self.toggle_state == ToggleState::Indeterminate; + + let (bg_color, border_color) = match (self.disabled, selected) { + (true, _) => ( + cx.theme().colors().ghost_element_disabled, + cx.theme().colors().border_disabled, + ), + (false, true) => ( + cx.theme().colors().element_selected, + cx.theme().colors().border, + ), + (false, false) => ( + cx.theme().colors().element_background, + cx.theme().colors().border, + ), + }; + + h_flex() + .id(self.id) + .justify_center() + .items_center() + .size(DynamicSpacing::Base20.rems(cx)) + .group(group_id.clone()) + .child( + div() + .flex() + .flex_none() + .justify_center() + .items_center() + .m(DynamicSpacing::Base04.px(cx)) + .size(DynamicSpacing::Base16.rems(cx)) + .rounded_sm() + .bg(bg_color) + .border_1() + .border_color(border_color) + .when(!self.disabled, |this| { + this.group_hover(group_id.clone(), |el| { + el.bg(cx.theme().colors().element_hover) + }) + }) + .children(icon), + ) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| { + this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx)) + }, + ) + } +} + +/// A [`Checkbox`] that has a [`Label`]. +#[derive(IntoElement)] +pub struct CheckboxWithLabel { + id: ElementId, + label: Label, + checked: ToggleState, + on_click: Arc, +} + +impl CheckboxWithLabel { + pub fn new( + id: impl Into, + label: Label, + checked: ToggleState, + on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static, + ) -> Self { + Self { + id: id.into(), + label, + checked, + on_click: Arc::new(on_click), + } + } +} + +impl RenderOnce for CheckboxWithLabel { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + h_flex() + .gap(DynamicSpacing::Base08.rems(cx)) + .child(Checkbox::new(self.id.clone(), self.checked).on_click({ + let on_click = self.on_click.clone(); + move |checked, cx| { + (on_click)(checked, cx); + } + })) + .child( + div() + .id(SharedString::from(format!("{}-label", self.id))) + .on_click(move |_event, cx| { + (self.on_click)(&self.checked.inverse(), cx); + }) + .child(self.label), + ) + } +} + +/// # Switch +/// +/// Switches are used to represent opposite states, such as enabled or disabled. +#[derive(IntoElement)] +pub struct Switch { + id: ElementId, + toggle_state: ToggleState, + disabled: bool, + on_click: Option>, +} + +impl Switch { + pub fn new(id: impl Into, state: ToggleState) -> Self { + Self { + id: id.into(), + toggle_state: state, + disabled: false, + on_click: None, + } + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ToggleState, &mut WindowContext) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl RenderOnce for Switch { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let is_on = self.toggle_state == ToggleState::Selected; + let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 }; + let base_color = cx.theme().colors().text; + + let bg_color = if is_on { + cx.theme() + .colors() + .element_background + .blend(base_color.opacity(0.08)) + } else { + cx.theme().colors().element_background + }; + let thumb_color = base_color.opacity(0.8); + let thumb_hover_color = base_color; + let border_color = cx.theme().colors().border_variant; + // Lighter themes need higher contrast borders + let border_hover_color = if is_on { + border_color.blend(base_color.opacity(0.16 * adjust_ratio)) + } else { + border_color.blend(base_color.opacity(0.05 * adjust_ratio)) + }; + let thumb_opacity = match (is_on, self.disabled) { + (_, true) => 0.2, + (true, false) => 1.0, + (false, false) => 0.5, + }; + + let group_id = format!("switch_group_{:?}", self.id); + + h_flex() + .id(self.id) + .items_center() + .w(DynamicSpacing::Base32.rems(cx)) + .h(DynamicSpacing::Base20.rems(cx)) + .group(group_id.clone()) + .child( + h_flex() + .when(is_on, |on| on.justify_end()) + .when(!is_on, |off| off.justify_start()) + .items_center() + .size_full() + .rounded_full() + .px(DynamicSpacing::Base02.px(cx)) + .bg(bg_color) + .border_1() + .border_color(border_color) + .when(!self.disabled, |this| { + this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color)) + }) + .child( + div() + .size(DynamicSpacing::Base12.rems(cx)) + .rounded_full() + .bg(thumb_color) + .when(!self.disabled, |this| { + this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color)) + }) + .opacity(thumb_opacity), + ), + ) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| { + this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx)) + }, + ) + } +} + +impl ComponentPreview for Checkbox { + fn description() -> impl Into> { + "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state." + } + + fn examples(_: &mut WindowContext) -> Vec> { + vec![ + example_group_with_title( + "Default", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_unselected", ToggleState::Unselected), + ), + single_example( + "Indeterminate", + Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate), + ), + single_example( + "Selected", + Checkbox::new("checkbox_selected", ToggleState::Selected), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected) + .disabled(true), + ), + single_example( + "Indeterminate", + Checkbox::new( + "checkbox_disabled_indeterminate", + ToggleState::Indeterminate, + ) + .disabled(true), + ), + single_example( + "Selected", + Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) + .disabled(true), + ), + ], + ), + ] + } +} + +impl ComponentPreview for Switch { + fn description() -> impl Into> { + "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting." + } + + fn examples(_cx: &mut WindowContext) -> Vec> { + vec![ + example_group_with_title( + "Default", + vec![ + single_example( + "Off", + Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}), + ), + single_example( + "On", + Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![ + single_example( + "Off", + Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true), + ), + single_example( + "On", + Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true), + ), + ], + ), + ] + } +} + +impl ComponentPreview for CheckboxWithLabel { + fn description() -> impl Into> { + "A checkbox with an associated label, allowing users to select an option while providing a descriptive text." + } + + fn examples(_: &mut WindowContext) -> Vec> { + vec![example_group(vec![ + single_example( + "Unselected", + CheckboxWithLabel::new( + "checkbox_with_label_unselected", + Label::new("Always save on quit"), + ToggleState::Unselected, + |_, _| {}, + ), + ), + single_example( + "Indeterminate", + CheckboxWithLabel::new( + "checkbox_with_label_indeterminate", + Label::new("Always save on quit"), + ToggleState::Indeterminate, + |_, _| {}, + ), + ), + single_example( + "Selected", + CheckboxWithLabel::new( + "checkbox_with_label_selected", + Label::new("Always save on quit"), + ToggleState::Selected, + |_, _| {}, + ), + ), + ])] + } +} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index e763d0b663c92..887070f50a606 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -12,8 +12,8 @@ pub use crate::traits::clickable::*; pub use crate::traits::component_preview::*; pub use crate::traits::disableable::*; pub use crate::traits::fixed::*; -pub use crate::traits::selectable::*; pub use crate::traits::styled_ext::*; +pub use crate::traits::toggleable::*; pub use crate::traits::visible_on_hover::*; pub use crate::DynamicSpacing; pub use crate::{h_flex, h_group, v_flex, v_group}; diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs index 0898375e969f9..1b4d76171100c 100644 --- a/crates/ui/src/traits.rs +++ b/crates/ui/src/traits.rs @@ -2,6 +2,6 @@ pub mod clickable; pub mod component_preview; pub mod disableable; pub mod fixed; -pub mod selectable; pub mod styled_ext; +pub mod toggleable; pub mod visible_on_hover; diff --git a/crates/ui/src/traits/selectable.rs b/crates/ui/src/traits/toggleable.rs similarity index 74% rename from crates/ui/src/traits/selectable.rs rename to crates/ui/src/traits/toggleable.rs index 342a16a89e5b6..2da5a64ad4971 100644 --- a/crates/ui/src/traits/selectable.rs +++ b/crates/ui/src/traits/toggleable.rs @@ -1,14 +1,15 @@ -/// A trait for elements that can be selected. +/// A trait for elements that can be toggled. /// -/// Generally used to enable "toggle" or "active" behavior and styles on an element through the [`Selection`] status. -pub trait Selectable { +/// Implement this for elements that are visually distinct +/// when in two opposing states, like checkboxes or switches. +pub trait Toggleable { /// Sets whether the element is selected. - fn selected(self, selected: bool) -> Self; + fn toggle_state(self, selected: bool) -> Self; } /// Represents the selection status of an element. #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] -pub enum Selection { +pub enum ToggleState { /// The element is not selected. #[default] Unselected, @@ -18,7 +19,7 @@ pub enum Selection { Selected, } -impl Selection { +impl ToggleState { /// Returns the inverse of the current selection status. /// /// Indeterminate states become selected if inverted. @@ -30,7 +31,7 @@ impl Selection { } } -impl From for Selection { +impl From for ToggleState { fn from(selected: bool) -> Self { if selected { Self::Selected @@ -40,7 +41,7 @@ impl From for Selection { } } -impl From> for Selection { +impl From> for ToggleState { fn from(selected: Option) -> Self { match selected { Some(true) => Self::Selected, diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index e5c591a97041e..604ff7f4bf1ac 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -1,5 +1,8 @@ //! UI-related utilities +use gpui::WindowContext; +use theme::ActiveTheme; + mod color_contrast; mod format_distance; mod search_input; @@ -9,3 +12,8 @@ pub use color_contrast::*; pub use format_distance::*; pub use search_input::*; pub use with_rem_size::*; + +/// Returns true if the current theme is light or vibrant light. +pub fn is_light(cx: &WindowContext) -> bool { + cx.theme().appearance.is_light() +} diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index f61bad57fae6a..9a80c1b4dc48b 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -284,7 +284,7 @@ impl PickerDelegate for BranchListDelegate { ListItem::new(SharedString::from(format!("vcs-menu-{ix}"))) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .map(|parent| match hit { BranchEntry::Branch(branch) => { let highlights: Vec<_> = branch diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 96a9df9c3c8cc..4e47fa1351067 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -208,7 +208,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) + .toggle_state(selected) .child(HighlightedLabel::new( keymap_match.string.clone(), keymap_match.positions.clone(), diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index f6953b944c50c..47636bf113927 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -273,9 +273,9 @@ impl Render for WelcomePage { "enable-vim", Label::new("Enable Vim Mode"), if VimModeSetting::get_global(cx).0 { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, cx.listener(move |this, selection, cx| { this.telemetry @@ -298,9 +298,9 @@ impl Render for WelcomePage { "enable-crash", Label::new("Send Crash Reports"), if TelemetrySettings::get_global(cx).diagnostics { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, cx.listener(move |this, selection, cx| { this.telemetry.report_app_event( @@ -324,9 +324,9 @@ impl Render for WelcomePage { "enable-telemetry", Label::new("Send Telemetry"), if TelemetrySettings::get_global(cx).metrics { - ui::Selection::Selected + ui::ToggleState::Selected } else { - ui::Selection::Unselected + ui::ToggleState::Unselected }, cx.listener(move |this, selection, cx| { this.telemetry.report_app_event( @@ -381,7 +381,7 @@ impl WelcomePage { fn update_settings( &mut self, - selection: &Selection, + selection: &ToggleState, cx: &mut ViewContext, callback: impl 'static + Send + Fn(&mut T::FileContent, bool), ) { @@ -390,8 +390,8 @@ impl WelcomePage { let selection = *selection; settings::update_settings_file::(fs, cx, move |settings, _| { let value = match selection { - Selection::Unselected => false, - Selection::Selected => true, + ToggleState::Unselected => false, + ToggleState::Selected => true, _ => return, }; diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index acc47cd11e4b2..73fbbda3897f8 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -781,7 +781,7 @@ impl Render for PanelButtons { .trigger( IconButton::new(name, icon) .icon_size(IconSize::Small) - .selected(is_active_button) + .toggle_state(is_active_button) .on_click({ let action = action.boxed_clone(); move |_, cx| cx.dispatch_action(action.boxed_clone()) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3d844f78720f2..8748c234f8923 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -481,7 +481,7 @@ impl Pane { let zoomed = pane.is_zoomed(); IconButton::new("toggle_zoom", IconName::Maximize) .icon_size(IconSize::Small) - .selected(zoomed) + .toggle_state(zoomed) .selected_icon(IconName::Minimize) .on_click(cx.listener(|pane, _, cx| { pane.toggle_zoom(&crate::ToggleZoom, cx); @@ -2038,7 +2038,7 @@ impl Pane { ClosePosition::Left => ui::TabCloseSide::Start, ClosePosition::Right => ui::TabCloseSide::End, }) - .selected(is_active) + .toggle_state(is_active) .on_click( cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)), ) @@ -3273,7 +3273,7 @@ impl Render for DraggedTab { cx, ); Tab::new("") - .selected(self.is_active) + .toggle_state(self.is_active) .child(label) .render(cx) .font(ui_font) diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 932696f5602a7..8ad8db05f57d3 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -6,7 +6,7 @@ use ui::{ element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile, - IconDecoration, Indicator, Table, TintColor, Tooltip, + IconDecoration, Indicator, Switch, Table, TintColor, Tooltip, }; use crate::{Item, Workspace}; @@ -369,6 +369,7 @@ impl ThemePreview { .overflow_scroll() .size_full() .gap_2() + .child(Switch::render_component_previews(cx)) .child(ContentGroup::render_component_previews(cx)) .child(IconDecoration::render_component_previews(cx)) .child(DecoratedIcon::render_component_previews(cx)) @@ -394,7 +395,7 @@ impl ThemePreview { this.current_page = p; cx.notify(); })) - .selected(p == self.current_page) + .toggle_state(p == self.current_page) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) })) } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index be38502566f30..5f11cfa44aa79 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -162,7 +162,7 @@ impl Render for QuickActionBar { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .selected(self.toggle_selections_handle.is_deployed()) + .toggle_state(self.toggle_selections_handle.is_deployed()) .when(!self.toggle_selections_handle.is_deployed(), |this| { this.tooltip(|cx| Tooltip::text("Selection Controls", cx)) }), @@ -212,7 +212,7 @@ impl Render for QuickActionBar { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .selected(self.toggle_settings_handle.is_deployed()) + .toggle_state(self.toggle_settings_handle.is_deployed()) .when(!self.toggle_settings_handle.is_deployed(), |this| { this.tooltip(|cx| Tooltip::text("Editor Controls", cx)) }), @@ -407,7 +407,7 @@ impl RenderOnce for QuickActionBarButton { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .selected(self.toggled) + .toggle_state(self.toggled) .tooltip(move |cx| { Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx) }) diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index d29e45856ee2f..ba9375b7a3446 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -578,7 +578,7 @@ impl Render for RateCompletionModal { .inset(true) .spacing(ListItemSpacing::Sparse) .focused(index == self.selected_index) - .selected(selected) + .toggle_state(selected) .start_slot(if rated { Icon::new(IconName::Check).color(Color::Success).size(IconSize::Small) } else if completion.edits.is_empty() {