diff --git a/assets/icons/chevron_up_down.svg b/assets/icons/chevron_up_down.svg new file mode 100644 index 0000000000..a7414ec8a0 --- /dev/null +++ b/assets/icons/chevron_up_down.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/font.svg b/assets/icons/font.svg new file mode 100644 index 0000000000..861ab1a415 --- /dev/null +++ b/assets/icons/font.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/font_size.svg b/assets/icons/font_size.svg new file mode 100644 index 0000000000..cfba2deb6c --- /dev/null +++ b/assets/icons/font_size.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/font_weight.svg b/assets/icons/font_weight.svg new file mode 100644 index 0000000000..3ebbfa77bc --- /dev/null +++ b/assets/icons/font_weight.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/line_height.svg b/assets/icons/line_height.svg new file mode 100644 index 0000000000..904cfad8a8 --- /dev/null +++ b/assets/icons/line_height.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/visible.svg b/assets/icons/visible.svg new file mode 100644 index 0000000000..0a7e65d60d --- /dev/null +++ b/assets/icons/visible.svg @@ -0,0 +1 @@ + diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index c7013a11f8..46a79ccc6a 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -31,6 +31,7 @@ pub enum ComponentStory { OverflowScroll, Picker, Scroll, + Setting, Tab, TabBar, Text, @@ -64,6 +65,7 @@ impl ComponentStory { Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(), Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(), Self::Scroll => ScrollStory::view(cx).into(), + Self::Setting => cx.new_view(|cx| ui::SettingStory::init(cx)).into(), Self::Text => TextStory::view(cx).into(), Self::Tab => cx.new_view(|_| ui::TabStory).into(), Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(), diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index e1f6e3cabd..1b33ec420c 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -4,6 +4,7 @@ mod checkbox; mod context_menu; mod disclosure; mod divider; +mod dropdown_menu; mod icon; mod indicator; mod keybinding; @@ -14,6 +15,7 @@ mod popover; mod popover_menu; mod radio; mod right_click_menu; +mod setting; mod stack; mod tab; mod tab_bar; @@ -30,6 +32,7 @@ pub use checkbox::*; pub use context_menu::*; pub use disclosure::*; pub use divider::*; +use dropdown_menu::*; pub use icon::*; pub use indicator::*; pub use keybinding::*; @@ -40,6 +43,7 @@ pub use popover::*; pub use popover_menu::*; pub use radio::*; pub use right_click_menu::*; +pub use setting::*; pub use stack::*; pub use tab::*; pub use tab_bar::*; diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs new file mode 100644 index 0000000000..7480bd139d --- /dev/null +++ b/crates/ui/src/components/dropdown_menu.rs @@ -0,0 +1,85 @@ +use crate::prelude::*; + +/// !!don't use this yet – it's not functional!! +/// +/// pub crate until this is functional +/// +/// just a placeholder for now for filling out the settings menu stories. +#[derive(Debug, Clone, IntoElement)] +pub(crate) struct DropdownMenu { + pub id: ElementId, + current_item: Option, + // items: Vec, + full_width: bool, + disabled: bool, +} + +impl DropdownMenu { + pub fn new(id: impl Into, _cx: &WindowContext) -> Self { + Self { + id: id.into(), + current_item: None, + // items: Vec::new(), + full_width: false, + disabled: false, + } + } + + pub fn current_item(mut self, current_item: Option) -> Self { + self.current_item = current_item; + self + } + + pub fn full_width(mut self, full_width: bool) -> Self { + self.full_width = full_width; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl RenderOnce for DropdownMenu { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let disabled = self.disabled; + + h_flex() + .id(self.id) + .justify_between() + .rounded_md() + .bg(cx.theme().colors().editor_background) + .pl_2() + .pr_1p5() + .py_0p5() + .gap_2() + .min_w_20() + .when_else( + self.full_width, + |full_width| full_width.w_full(), + |auto_width| auto_width.flex_none().w_auto(), + ) + .when_else( + disabled, + |disabled| disabled.cursor_not_allowed(), + |enabled| enabled.cursor_pointer(), + ) + .child( + Label::new(self.current_item.unwrap_or("".into())).color(if disabled { + Color::Disabled + } else { + Color::Default + }), + ) + .child( + Icon::new(IconName::ChevronUpDown) + .size(IconSize::XSmall) + .color(if disabled { + Color::Disabled + } else { + Color::Muted + }), + ) + } +} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 72c7a85bbe..d1fe7e8979 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -106,6 +106,7 @@ pub enum IconName { ChevronLeft, ChevronRight, ChevronUp, + ChevronUpDown, Close, Code, Collab, @@ -141,6 +142,9 @@ pub enum IconName { Folder, FolderOpen, FolderX, + Font, + FontSize, + FontWeight, Github, Hash, HistoryRerun, @@ -148,6 +152,7 @@ pub enum IconName { IndicatorX, InlayHint, Library, + LineHeight, Link, ListTree, MagicWand, @@ -181,8 +186,8 @@ pub enum IconName { RotateCw, Save, Screen, - SelectAll, SearchSelection, + SelectAll, Server, Settings, Shift, @@ -212,6 +217,7 @@ pub enum IconName { ZedAssistant, ZedAssistantFilled, ZedXCopilot, + Visible, } impl IconName { @@ -224,6 +230,7 @@ impl IconName { IconName::ArrowLeft => "icons/arrow_left.svg", IconName::ArrowRight => "icons/arrow_right.svg", IconName::ArrowUp => "icons/arrow_up.svg", + IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg", IconName::ArrowUpRight => "icons/arrow_up_right.svg", IconName::AtSign => "icons/at_sign.svg", IconName::AudioOff => "icons/speaker_off.svg", @@ -243,6 +250,7 @@ impl IconName { IconName::ChevronLeft => "icons/chevron_left.svg", IconName::ChevronRight => "icons/chevron_right.svg", IconName::ChevronUp => "icons/chevron_up.svg", + IconName::ChevronUpDown => "icons/chevron_up_down.svg", IconName::Close => "icons/x.svg", IconName::Code => "icons/code.svg", IconName::Collab => "icons/user_group_16.svg", @@ -278,6 +286,9 @@ impl IconName { IconName::Folder => "icons/file_icons/folder.svg", IconName::FolderOpen => "icons/file_icons/folder_open.svg", IconName::FolderX => "icons/stop_sharing.svg", + IconName::Font => "icons/font.svg", + IconName::FontSize => "icons/font_size.svg", + IconName::FontWeight => "icons/font_weight.svg", IconName::Github => "icons/github.svg", IconName::Hash => "icons/hash.svg", IconName::HistoryRerun => "icons/history_rerun.svg", @@ -285,6 +296,7 @@ impl IconName { IconName::IndicatorX => "icons/indicator_x.svg", IconName::InlayHint => "icons/inlay_hint.svg", IconName::Library => "icons/library.svg", + IconName::LineHeight => "icons/line_height.svg", IconName::Link => "icons/link.svg", IconName::ListTree => "icons/list_tree.svg", IconName::MagicWand => "icons/magic_wand.svg", @@ -308,18 +320,18 @@ impl IconName { IconName::Quote => "icons/quote.svg", IconName::Regex => "icons/regex.svg", IconName::Replace => "icons/replace.svg", - IconName::Reveal => "icons/reveal.svg", IconName::ReplaceAll => "icons/replace_all.svg", IconName::ReplaceNext => "icons/replace_next.svg", IconName::ReplyArrowRight => "icons/reply_arrow_right.svg", IconName::Rerun => "icons/rerun.svg", IconName::Return => "icons/return.svg", - IconName::RotateCw => "icons/rotate_cw.svg", + IconName::Reveal => "icons/reveal.svg", IconName::RotateCcw => "icons/rotate_ccw.svg", + IconName::RotateCw => "icons/rotate_cw.svg", IconName::Save => "icons/save.svg", IconName::Screen => "icons/desktop.svg", - IconName::SelectAll => "icons/select_all.svg", IconName::SearchSelection => "icons/search_selection.svg", + IconName::SelectAll => "icons/select_all.svg", IconName::Server => "icons/server.svg", IconName::Settings => "icons/file_icons/settings.svg", IconName::Shift => "icons/shift.svg", @@ -349,7 +361,7 @@ impl IconName { IconName::ZedAssistant => "icons/zed_assistant.svg", IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg", IconName::ZedXCopilot => "icons/zed_x_copilot.svg", - IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg", + IconName::Visible => "icons/visible.svg", } } } diff --git a/crates/ui/src/components/setting.rs b/crates/ui/src/components/setting.rs new file mode 100644 index 0000000000..df4b4bc1be --- /dev/null +++ b/crates/ui/src/components/setting.rs @@ -0,0 +1,351 @@ +use crate::{prelude::*, Checkbox, ListHeader}; + +use super::DropdownMenu; + +#[derive(PartialEq, Clone, Eq, Debug)] +pub enum ToggleType { + Checkbox, + // Switch, +} + +impl From for SettingType { + fn from(toggle_type: ToggleType) -> Self { + SettingType::Toggle(toggle_type) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputType { + Text, + Number, +} + +impl From for SettingType { + fn from(input_type: InputType) -> Self { + SettingType::Input(input_type) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SecondarySettingType { + Dropdown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SettingType { + Toggle(ToggleType), + ToggleAnd(SecondarySettingType), + Input(InputType), + Dropdown, + Range, + Unsupported, +} + +#[derive(Debug, Clone, IntoElement)] +pub struct SettingsGroup { + pub name: String, + settings: Vec, +} + +impl SettingsGroup { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + settings: Vec::new(), + } + } + + pub fn add_setting(mut self, setting: SettingsItem) -> Self { + self.settings.push(setting); + self + } +} + +impl RenderOnce for SettingsGroup { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + let empty_message = format!("No settings available for {}", self.name); + + let header = ListHeader::new(self.name); + + let settings = self.settings.clone().into_iter(); + + v_flex() + .p_1() + .gap_2() + .child(header) + .when(self.settings.len() == 0, |this| { + this.child(Label::new(empty_message)) + }) + .children(settings) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SettingLayout { + Stacked, + AutoWidth, + FullLine, + FullLineJustified, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SettingId(pub SharedString); + +impl From for ElementId { + fn from(id: SettingId) -> Self { + ElementId::Name(id.0) + } +} + +impl From<&str> for SettingId { + fn from(id: &str) -> Self { + Self(id.to_string().into()) + } +} + +impl From for SettingId { + fn from(id: SharedString) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SettingValue(pub SharedString); + +impl From for SettingValue { + fn from(value: SharedString) -> Self { + Self(value) + } +} + +impl From for SettingValue { + fn from(value: String) -> Self { + Self(value.into()) + } +} + +impl From for SettingValue { + fn from(value: bool) -> Self { + Self(value.to_string().into()) + } +} + +impl From for bool { + fn from(value: SettingValue) -> Self { + value.0 == "true" + } +} + +#[derive(Debug, Clone, IntoElement)] +pub struct SettingsItem { + pub id: SettingId, + current_value: Option, + disabled: bool, + hide_label: bool, + icon: Option, + layout: SettingLayout, + name: SharedString, + // possible_values: Option>, + setting_type: SettingType, + toggled: Option, +} + +impl SettingsItem { + pub fn new( + id: impl Into, + name: SharedString, + setting_type: SettingType, + current_value: Option, + ) -> Self { + let toggled = match setting_type { + SettingType::Toggle(_) | SettingType::ToggleAnd(_) => Some(false), + _ => None, + }; + + Self { + id: id.into(), + current_value, + disabled: false, + hide_label: false, + icon: None, + layout: SettingLayout::FullLine, + name, + // possible_values: None, + setting_type, + toggled, + } + } + + pub fn layout(mut self, layout: SettingLayout) -> Self { + self.layout = layout; + self + } + + pub fn toggled(mut self, toggled: bool) -> Self { + self.toggled = Some(toggled); + self + } + + // pub fn hide_label(mut self, hide_label: bool) -> Self { + // self.hide_label = hide_label; + // self + // } + + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = Some(icon); + self + } + + // pub fn disabled(mut self, disabled: bool) -> Self { + // self.disabled = disabled; + // self + // } +} + +impl RenderOnce for SettingsItem { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let id: ElementId = self.id.clone().into(); + + // When the setting is disabled or toggled off, we don't want any secondary elements to be interactable + let secondary_element_disabled = self.disabled || self.toggled == Some(false); + + let full_width = match self.layout { + SettingLayout::FullLine | SettingLayout::FullLineJustified => true, + _ => false, + }; + + let hide_label = self.hide_label || self.icon.is_some(); + + let justified = match (self.layout.clone(), self.setting_type.clone()) { + (_, SettingType::ToggleAnd(_)) => true, + (SettingLayout::FullLineJustified, _) => true, + _ => false, + }; + + let (setting_type, current_value) = (self.setting_type.clone(), self.current_value.clone()); + let current_string = if let Some(current_value) = current_value.clone() { + Some(current_value.0) + } else { + None + }; + + let toggleable = match setting_type { + SettingType::Toggle(_) => true, + SettingType::ToggleAnd(_) => true, + _ => false, + }; + + let setting_element = match setting_type { + SettingType::Toggle(_) => None, + SettingType::ToggleAnd(secondary_setting_type) => match secondary_setting_type { + SecondarySettingType::Dropdown => Some( + DropdownMenu::new(id.clone(), &cx) + .current_item(current_string) + .disabled(secondary_element_disabled) + .into_any_element(), + ), + }, + SettingType::Input(input_type) => match input_type { + InputType::Text => Some(div().child("text").into_any_element()), + InputType::Number => Some(div().child("number").into_any_element()), + }, + SettingType::Dropdown => Some( + DropdownMenu::new(id.clone(), &cx) + .current_item(current_string) + .full_width(true) + .into_any_element(), + ), + SettingType::Range => Some(div().child("range").into_any_element()), + SettingType::Unsupported => None, + }; + + let checkbox = Checkbox::new( + ElementId::Name(format!("toggle-{}", self.id.0).to_string().into()), + self.toggled.into(), + ) + .disabled(self.disabled); + + let toggle_element = match (toggleable, self.setting_type.clone()) { + (true, SettingType::Toggle(toggle_type)) => match toggle_type { + ToggleType::Checkbox => Some(checkbox.into_any_element()), + }, + (true, SettingType::ToggleAnd(_)) => Some(checkbox.into_any_element()), + (_, _) => None, + }; + + let item = if self.layout == SettingLayout::Stacked { + v_flex() + } else { + h_flex() + }; + + item.id(id) + .gap_2() + .w_full() + .when_some(self.icon, |this, icon| { + this.child(div().px_0p5().child(Icon::new(icon).color(Color::Muted))) + }) + .children(toggle_element) + .children(if hide_label { + None + } else { + Some(Label::new(self.name.clone())) + }) + .when(justified, |this| this.child(div().flex_1().size_full())) + .child( + h_flex() + .when(full_width, |this| this.w_full()) + .when(self.layout == SettingLayout::FullLineJustified, |this| { + this.justify_end() + }) + .children(setting_element), + ) + // help flex along when full width is disabled + // + // this probably isn't needed, but fighting with flex to + // get this right without inspection tools will be a pain + .when(!full_width, |this| this.child(div().size_full().flex_1())) + } +} + +pub struct SettingsMenu { + name: SharedString, + groups: Vec, +} + +impl SettingsMenu { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + groups: Vec::new(), + } + } + + pub fn add_group(mut self, group: SettingsGroup) -> Self { + self.groups.push(group); + self + } +} + +impl Render for SettingsMenu { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let is_empty = self.groups.is_empty(); + v_flex() + .id(ElementId::Name(self.name.clone())) + .elevation_2(cx) + .min_w_56() + .max_w_96() + .max_h_2_3() + .px_2() + .when_else( + is_empty, + |empty| empty.py_1(), + |not_empty| not_empty.pt_0().pb_1(), + ) + .gap_1() + .when(is_empty, |this| { + this.child(Label::new("No settings found").color(Color::Muted)) + }) + .children(self.groups.clone()) + } +} diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs index 302b740c7f..81b08762eb 100644 --- a/crates/ui/src/components/stories.rs +++ b/crates/ui/src/components/stories.rs @@ -10,6 +10,7 @@ mod label; mod list; mod list_header; mod list_item; +mod setting; mod tab; mod tab_bar; mod title_bar; @@ -28,6 +29,7 @@ pub use label::*; pub use list::*; pub use list_header::*; pub use list_item::*; +pub use setting::*; pub use tab::*; pub use tab_bar::*; pub use title_bar::*; diff --git a/crates/ui/src/components/stories/setting.rs b/crates/ui/src/components/stories/setting.rs new file mode 100644 index 0000000000..1a456ea391 --- /dev/null +++ b/crates/ui/src/components/stories/setting.rs @@ -0,0 +1,225 @@ +use gpui::View; + +use crate::prelude::*; + +use crate::{ + SecondarySettingType, SettingLayout, SettingType, SettingsGroup, SettingsItem, SettingsMenu, + ToggleType, +}; + +pub struct SettingStory { + menus: Vec<(SharedString, View)>, +} + +impl SettingStory { + pub fn new() -> Self { + Self { menus: Vec::new() } + } + + pub fn init(cx: &mut ViewContext) -> Self { + let mut story = Self::new(); + story.empty_menu(cx); + story.editor_example(cx); + story.menu_single_group(cx); + story + } +} + +impl SettingStory { + pub fn empty_menu(&mut self, cx: &mut ViewContext) { + let menu = cx.new_view(|_cx| SettingsMenu::new("Empty Menu")); + + self.menus.push(("Empty Menu".into(), menu)); + } + + pub fn menu_single_group(&mut self, cx: &mut ViewContext) { + let theme_setting = SettingsItem::new( + "theme-setting", + "Theme".into(), + SettingType::Dropdown, + Some(cx.theme().name.clone().into()), + ) + .layout(SettingLayout::Stacked); + let high_contrast_setting = SettingsItem::new( + "theme-contrast", + "Use high contrast theme".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(false); + let appearance_setting = SettingsItem::new( + "switch-appearance", + "Match system appearance".into(), + SettingType::ToggleAnd(SecondarySettingType::Dropdown), + Some("When Dark".to_string().into()), + ) + .layout(SettingLayout::FullLineJustified); + + let group = SettingsGroup::new("Appearance") + .add_setting(theme_setting) + .add_setting(appearance_setting) + .add_setting(high_contrast_setting); + + let menu = cx.new_view(|_cx| SettingsMenu::new("Appearance").add_group(group)); + + self.menus.push(("Single Group".into(), menu)); + } + + pub fn editor_example(&mut self, cx: &mut ViewContext) { + let font_group = SettingsGroup::new("Font") + .add_setting( + SettingsItem::new( + "font-family", + "Font".into(), + SettingType::Dropdown, + Some("Berkeley Mono".to_string().into()), + ) + .icon(IconName::Font) + .layout(SettingLayout::AutoWidth), + ) + .add_setting( + SettingsItem::new( + "font-weifht", + "Font Weight".into(), + SettingType::Dropdown, + Some("400".to_string().into()), + ) + .icon(IconName::FontWeight) + .layout(SettingLayout::AutoWidth), + ) + .add_setting( + SettingsItem::new( + "font-size", + "Font Size".into(), + SettingType::Dropdown, + Some("14".to_string().into()), + ) + .icon(IconName::FontSize) + .layout(SettingLayout::AutoWidth), + ) + .add_setting( + SettingsItem::new( + "line-height", + "Line Height".into(), + SettingType::Dropdown, + Some("1.35".to_string().into()), + ) + .icon(IconName::LineHeight) + .layout(SettingLayout::AutoWidth), + ) + .add_setting( + SettingsItem::new( + "enable-ligatures", + "Enable Ligatures".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(true), + ); + + let editor_group = SettingsGroup::new("Editor") + .add_setting( + SettingsItem::new( + "show-indent-guides", + "Indent Guides".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(true), + ) + .add_setting( + SettingsItem::new( + "show-git-blame", + "Git Blame".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(false), + ); + + let gutter_group = SettingsGroup::new("Gutter") + .add_setting( + SettingsItem::new( + "enable-git-hunks", + "Show Git Hunks".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(true), + ) + .add_setting( + SettingsItem::new( + "show-line-numbers", + "Line Numbers".into(), + SettingType::ToggleAnd(SecondarySettingType::Dropdown), + Some("Ascending".to_string().into()), + ) + .toggled(true) + .layout(SettingLayout::FullLineJustified), + ); + + let scrollbar_group = SettingsGroup::new("Scrollbar") + .add_setting( + SettingsItem::new( + "scrollbar-visibility", + "Show scrollbar when:".into(), + SettingType::Dropdown, + Some("Always Visible".to_string().into()), + ) + .layout(SettingLayout::AutoWidth) + .icon(IconName::Visible), + ) + .add_setting( + SettingsItem::new( + "show-diagnostic-markers", + "Diagnostic Markers".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(true), + ) + .add_setting( + SettingsItem::new( + "show-git-markers", + "Git Status Markers".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(false), + ) + .add_setting( + SettingsItem::new( + "show-selection-markers", + "Selection & Match Markers".into(), + SettingType::Toggle(ToggleType::Checkbox), + None, + ) + .toggled(true), + ); + + let menu = cx.new_view(|_cx| { + SettingsMenu::new("Editor") + .add_group(font_group) + .add_group(editor_group) + .add_group(gutter_group) + .add_group(scrollbar_group) + }); + + self.menus.push(("Editor Example".into(), menu)); + } +} + +impl Render for SettingStory { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .bg(cx.theme().colors().background) + .text_color(cx.theme().colors().text) + .children(self.menus.iter().map(|(name, menu)| { + v_flex() + .p_2() + .gap_2() + .child(Headline::new(name.clone()).size(HeadlineSize::Medium)) + .child(menu.clone()) + })) + } +} diff --git a/crates/ui/src/selectable.rs b/crates/ui/src/selectable.rs index 54da86d094..342a16a89e 100644 --- a/crates/ui/src/selectable.rs +++ b/crates/ui/src/selectable.rs @@ -29,3 +29,23 @@ impl Selection { } } } + +impl From for Selection { + fn from(selected: bool) -> Self { + if selected { + Self::Selected + } else { + Self::Unselected + } + } +} + +impl From> for Selection { + fn from(selected: Option) -> Self { + match selected { + Some(true) => Self::Selected, + Some(false) => Self::Unselected, + None => Self::Unselected, + } + } +}