Detailed changes
@@ -4,7 +4,7 @@ use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt;
use std::num::ParseIntError;
-pub fn rgb(hex: u32) -> Rgba {
+pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
let b = (hex & 0xFF) as f32 / 255.0;
@@ -6,11 +6,11 @@ use crate::ui::{Label, Panel};
use crate::story::Story;
#[derive(Element)]
-pub struct PanelStory<S: 'static + Send + Sync> {
+pub struct PanelStory<S: 'static + Send + Sync + Clone> {
state_type: PhantomData<S>,
}
-impl<S: 'static + Send + Sync> PanelStory<S> {
+impl<S: 'static + Send + Sync + Clone> PanelStory<S> {
pub fn new() -> Self {
Self {
state_type: PhantomData,
@@ -6,11 +6,11 @@ use crate::ui::Label;
use crate::story::Story;
#[derive(Element)]
-pub struct LabelStory<S: 'static + Send + Sync> {
+pub struct LabelStory<S: 'static + Send + Sync + Clone> {
state_type: PhantomData<S>,
}
-impl<S: 'static + Send + Sync> LabelStory<S> {
+impl<S: 'static + Send + Sync + Clone> LabelStory<S> {
pub fn new() -> Self {
Self {
state_type: PhantomData,
@@ -7,11 +7,11 @@ use crate::story_selector::{ComponentStory, ElementStory};
use crate::ui::prelude::*;
#[derive(Element)]
-pub struct KitchenSinkStory<S: 'static + Send + Sync> {
+pub struct KitchenSinkStory<S: 'static + Send + Sync + Clone> {
state_type: PhantomData<S>,
}
-impl<S: 'static + Send + Sync> KitchenSinkStory<S> {
+impl<S: 'static + Send + Sync + Clone> KitchenSinkStory<S> {
pub fn new() -> Self {
Self {
state_type: PhantomData,
@@ -18,7 +18,7 @@ pub enum ElementStory {
}
impl ElementStory {
- pub fn story<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
+ pub fn story<S: 'static + Send + Sync + Clone>(&self) -> AnyElement<S> {
use crate::stories::elements;
match self {
@@ -36,7 +36,7 @@ pub enum ComponentStory {
}
impl ComponentStory {
- pub fn story<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
+ pub fn story<S: 'static + Send + Sync + Clone>(&self) -> AnyElement<S> {
use crate::stories::components;
match self {
@@ -81,7 +81,7 @@ impl FromStr for StorySelector {
}
impl StorySelector {
- pub fn story<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
+ pub fn story<S: 'static + Send + Sync + Clone>(&self) -> AnyElement<S> {
match self {
Self::Element(element_story) => element_story.story(),
Self::Component(component_story) => component_story.story(),
@@ -91,6 +91,7 @@ fn main() {
});
}
+#[derive(Clone)]
pub struct StoryWrapper {
selector: StorySelector,
}
@@ -1,3 +1,5 @@
+mod list;
mod panel;
+pub use list::*;
pub use panel::*;
@@ -0,0 +1,519 @@
+use std::marker::PhantomData;
+
+use gpui3::{div, Div, Hsla, WindowContext};
+
+use crate::theme::theme;
+use crate::ui::prelude::*;
+use crate::ui::{
+ h_stack, token, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor,
+ LabelSize,
+};
+
+#[derive(Clone, Copy, Default, Debug, PartialEq)]
+pub enum ListItemVariant {
+ /// The list item extends to the far left and right of the list.
+ #[default]
+ FullWidth,
+ Inset,
+}
+
+#[derive(Element, Clone)]
+pub struct ListHeader<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+ label: &'static str,
+ left_icon: Option<Icon>,
+ variant: ListItemVariant,
+ state: InteractionState,
+ toggleable: Toggleable,
+}
+
+impl<S: 'static + Send + Sync + Clone> ListHeader<S> {
+ pub fn new(label: &'static str) -> Self {
+ Self {
+ state_type: PhantomData,
+ label,
+ left_icon: None,
+ variant: ListItemVariant::default(),
+ state: InteractionState::default(),
+ toggleable: Toggleable::default(),
+ }
+ }
+
+ pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+ self.toggleable = toggle.into();
+ self
+ }
+
+ pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
+ self.toggleable = toggleable;
+ self
+ }
+
+ pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+ self.left_icon = left_icon;
+ self
+ }
+
+ pub fn state(mut self, state: InteractionState) -> Self {
+ self.state = state;
+ self
+ }
+
+ fn disclosure_control(&self) -> Div<S> {
+ let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+ let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+ match (is_toggleable, is_toggled) {
+ (false, _) => div(),
+ (_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
+ (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
+ }
+ }
+
+ fn background_color(&self, cx: &WindowContext) -> Hsla {
+ let theme = theme(cx);
+ let system_color = SystemColor::new();
+
+ match self.state {
+ InteractionState::Hovered => theme.lowest.base.hovered.background,
+ InteractionState::Active => theme.lowest.base.pressed.background,
+ InteractionState::Enabled => theme.lowest.on.default.background,
+ _ => system_color.transparent,
+ }
+ }
+
+ fn label_color(&self) -> LabelColor {
+ match self.state {
+ InteractionState::Disabled => LabelColor::Disabled,
+ _ => Default::default(),
+ }
+ }
+
+ fn icon_color(&self) -> IconColor {
+ match self.state {
+ InteractionState::Disabled => IconColor::Disabled,
+ _ => Default::default(),
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+ let token = token();
+ let system_color = SystemColor::new();
+ let background_color = self.background_color(cx);
+
+ let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+ let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+ let disclosure_control = self.disclosure_control();
+
+ h_stack()
+ .flex_1()
+ .w_full()
+ .fill(background_color)
+ // .when(self.state == InteractionState::Focused, |this| {
+ // this.border()
+ // .border_color(theme.lowest.accent.default.border)
+ // })
+ .relative()
+ .py_1()
+ .child(
+ div()
+ .h_6()
+ // .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+ .flex()
+ .flex_1()
+ .w_full()
+ .gap_1()
+ .items_center()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .gap_1()
+ .items_center()
+ .children(self.left_icon.map(|i| {
+ IconElement::new(i)
+ .color(IconColor::Muted)
+ .size(IconSize::Small)
+ }))
+ .child(
+ Label::new(self.label.clone())
+ .color(LabelColor::Muted)
+ .size(LabelSize::Small),
+ ),
+ )
+ .child(disclosure_control),
+ )
+ }
+}
+
+#[derive(Element, Clone)]
+pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+ label: &'static str,
+ left_icon: Option<Icon>,
+ variant: ListItemVariant,
+}
+
+impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
+ pub fn new(label: &'static str) -> Self {
+ Self {
+ state_type: PhantomData,
+ label,
+ left_icon: None,
+ variant: ListItemVariant::default(),
+ }
+ }
+
+ pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+ self.left_icon = left_icon;
+ self
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+ let token = token();
+
+ h_stack().flex_1().w_full().relative().py_1().child(
+ div()
+ .h_6()
+ // .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+ .flex()
+ .flex_1()
+ .w_full()
+ .gap_1()
+ .items_center()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .gap_1()
+ .items_center()
+ .children(self.left_icon.map(|i| {
+ IconElement::new(i)
+ .color(IconColor::Muted)
+ .size(IconSize::Small)
+ }))
+ .child(
+ Label::new(self.label.clone())
+ .color(LabelColor::Muted)
+ .size(LabelSize::Small),
+ ),
+ ),
+ )
+ }
+}
+
+#[derive(Clone)]
+pub enum LeftContent {
+ Icon(Icon),
+ Avatar(&'static str),
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum ListEntrySize {
+ #[default]
+ Small,
+ Medium,
+}
+
+#[derive(Clone, Element)]
+pub enum ListItem<S: 'static + Send + Sync + Clone> {
+ Entry(ListEntry<S>),
+ Separator(ListSeparator<S>),
+ Header(ListSubHeader<S>),
+}
+
+impl<S: 'static + Send + Sync + Clone> From<ListEntry<S>> for ListItem<S> {
+ fn from(entry: ListEntry<S>) -> Self {
+ Self::Entry(entry)
+ }
+}
+
+impl<S: 'static + Send + Sync + Clone> From<ListSeparator<S>> for ListItem<S> {
+ fn from(entry: ListSeparator<S>) -> Self {
+ Self::Separator(entry)
+ }
+}
+
+impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
+ fn from(entry: ListSubHeader<S>) -> Self {
+ Self::Header(entry)
+ }
+}
+
+impl<S: 'static + Send + Sync + Clone> ListItem<S> {
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ match self {
+ ListItem::Entry(entry) => div().child(entry.render(cx)),
+ ListItem::Separator(separator) => div().child(separator.render(cx)),
+ ListItem::Header(header) => div().child(header.render(cx)),
+ }
+ }
+
+ pub fn new(label: Label<S>) -> Self {
+ Self::Entry(ListEntry::new(label))
+ }
+
+ pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
+ if let Self::Entry(entry) = self {
+ Some(entry)
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Element, Clone)]
+pub struct ListEntry<S: 'static + Send + Sync + Clone> {
+ disclosure_control_style: DisclosureControlVisibility,
+ indent_level: u32,
+ label: Label<S>,
+ left_content: Option<LeftContent>,
+ variant: ListItemVariant,
+ size: ListEntrySize,
+ state: InteractionState,
+ toggle: Option<ToggleState>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
+ pub fn new(label: Label<S>) -> Self {
+ Self {
+ disclosure_control_style: DisclosureControlVisibility::default(),
+ indent_level: 0,
+ label,
+ variant: ListItemVariant::default(),
+ left_content: None,
+ size: ListEntrySize::default(),
+ state: InteractionState::default(),
+ toggle: None,
+ }
+ }
+ pub fn variant(mut self, variant: ListItemVariant) -> Self {
+ self.variant = variant;
+ self
+ }
+ pub fn indent_level(mut self, indent_level: u32) -> Self {
+ self.indent_level = indent_level;
+ self
+ }
+
+ pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+ self.toggle = Some(toggle);
+ self
+ }
+
+ pub fn left_content(mut self, left_content: LeftContent) -> Self {
+ self.left_content = Some(left_content);
+ self
+ }
+
+ pub fn left_icon(mut self, left_icon: Icon) -> Self {
+ self.left_content = Some(LeftContent::Icon(left_icon));
+ self
+ }
+
+ pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
+ self.left_content = Some(LeftContent::Avatar(left_avatar));
+ self
+ }
+
+ pub fn state(mut self, state: InteractionState) -> Self {
+ self.state = state;
+ self
+ }
+
+ pub fn size(mut self, size: ListEntrySize) -> Self {
+ self.size = size;
+ self
+ }
+
+ pub fn disclosure_control_style(
+ mut self,
+ disclosure_control_style: DisclosureControlVisibility,
+ ) -> Self {
+ self.disclosure_control_style = disclosure_control_style;
+ self
+ }
+
+ fn background_color(&self, cx: &WindowContext) -> Hsla {
+ let theme = theme(cx);
+ let system_color = SystemColor::new();
+
+ match self.state {
+ InteractionState::Hovered => theme.lowest.base.hovered.background,
+ InteractionState::Active => theme.lowest.base.pressed.background,
+ InteractionState::Enabled => theme.lowest.on.default.background,
+ _ => system_color.transparent,
+ }
+ }
+
+ fn label_color(&self) -> LabelColor {
+ match self.state {
+ InteractionState::Disabled => LabelColor::Disabled,
+ _ => Default::default(),
+ }
+ }
+
+ fn icon_color(&self) -> IconColor {
+ match self.state {
+ InteractionState::Disabled => IconColor::Disabled,
+ _ => Default::default(),
+ }
+ }
+
+ fn disclosure_control(&mut self, cx: &mut ViewContext<S>) -> Option<impl Element<State = S>> {
+ let theme = theme(cx);
+ let token = token();
+
+ let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
+ IconElement::new(Icon::ChevronDown)
+ } else {
+ IconElement::new(Icon::ChevronRight)
+ }
+ .color(IconColor::Muted)
+ .size(IconSize::Small);
+
+ match (self.toggle, self.disclosure_control_style) {
+ (Some(_), DisclosureControlVisibility::OnHover) => {
+ Some(
+ div()
+ .absolute()
+ // .neg_left_5()
+ .child(disclosure_control_icon),
+ )
+ }
+ (Some(_), DisclosureControlVisibility::Always) => {
+ Some(div().child(disclosure_control_icon))
+ }
+ (None, _) => None,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+ let token = token();
+ let system_color = SystemColor::new();
+ let background_color = self.background_color(cx);
+
+ let left_content = match self.left_content {
+ Some(LeftContent::Icon(i)) => {
+ Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
+ }
+ Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
+ None => None,
+ };
+
+ let sized_item = match self.size {
+ ListEntrySize::Small => div().h_6(),
+ ListEntrySize::Medium => div().h_7(),
+ };
+
+ div()
+ .fill(background_color)
+ // .when(self.state == InteractionState::Focused, |this| {
+ // this.border()
+ // .border_color(theme.lowest.accent.default.border)
+ // })
+ .relative()
+ .py_1()
+ .child(
+ sized_item
+ // .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+ // .ml(rems(0.75 * self.indent_level as f32))
+ .children((0..self.indent_level).map(|_| {
+ div()
+ // .w(token.list_indent_depth)
+ .h_full()
+ .flex()
+ .justify_center()
+ .child(h_stack().child(div().w_px().h_full()).child(
+ div().w_px().h_full().fill(theme.middle.base.default.border),
+ ))
+ }))
+ .flex()
+ .gap_1()
+ .items_center()
+ .relative()
+ .children(self.disclosure_control(cx))
+ .children(left_content)
+ .child(self.label.clone()),
+ )
+ }
+}
+
+#[derive(Clone, Element)]
+pub struct ListSeparator<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync> ListSeparator<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+
+ div().h_px().w_full().fill(theme.lowest.base.default.border)
+ }
+}
+
+#[derive(Element)]
+pub struct List<S: 'static + Send + Sync + Clone> {
+ items: Vec<ListItem<S>>,
+ empty_message: &'static str,
+ header: Option<ListHeader<S>>,
+ toggleable: Toggleable,
+}
+
+impl<S: 'static + Send + Sync + Clone> List<S> {
+ pub fn new(items: Vec<ListItem<S>>) -> Self {
+ Self {
+ items,
+ empty_message: "No items",
+ header: None,
+ toggleable: Toggleable::default(),
+ }
+ }
+
+ pub fn empty_message(mut self, empty_message: &'static str) -> Self {
+ self.empty_message = empty_message;
+ self
+ }
+
+ pub fn header(mut self, header: ListHeader<S>) -> Self {
+ self.header = Some(header);
+ self
+ }
+
+ pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+ self.toggleable = toggle.into();
+ self
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+ let token = token();
+ let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+ let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+ let list_content = match (self.items.is_empty(), is_toggled) {
+ (_, false) => div(),
+ (false, _) => div().children(self.items.iter().cloned()),
+ (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
+ };
+
+ v_stack()
+ .py_1()
+ .children(
+ self.header
+ .clone()
+ .map(|header| header.set_toggleable(self.toggleable)),
+ )
+ .child(list_content)
+ }
+}
@@ -46,7 +46,7 @@ pub enum LabelSize {
}
#[derive(Element, Clone)]
-pub struct Label<S: 'static + Send + Sync> {
+pub struct Label<S: 'static + Send + Sync + Clone> {
state_type: PhantomData<S>,
label: String,
color: LabelColor,
@@ -55,7 +55,7 @@ pub struct Label<S: 'static + Send + Sync> {
strikethrough: bool,
}
-impl<S: 'static + Send + Sync> Label<S> {
+impl<S: 'static + Send + Sync + Clone> Label<S> {
pub fn new<L>(label: L) -> Self
where
L: Into<String>,
@@ -1,14 +1,257 @@
pub use gpui3::{
div, Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext,
+ WindowContext,
};
pub use crate::ui::{HackyChildren, HackyChildrenPayload};
+use gpui3::{hsla, rgb, Hsla};
use strum::EnumIter;
+use crate::theme::{theme, Theme};
+
+#[derive(Default)]
+pub struct SystemColor {
+ pub transparent: Hsla,
+ pub mac_os_traffic_light_red: Hsla,
+ pub mac_os_traffic_light_yellow: Hsla,
+ pub mac_os_traffic_light_green: Hsla,
+}
+
+impl SystemColor {
+ pub fn new() -> SystemColor {
+ SystemColor {
+ transparent: hsla(0.0, 0.0, 0.0, 0.0),
+ mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
+ mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
+ mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
+ }
+ }
+ pub fn color(&self) -> Hsla {
+ self.transparent
+ }
+}
+
+#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+pub enum HighlightColor {
+ #[default]
+ Default,
+ Comment,
+ String,
+ Function,
+ Keyword,
+}
+
+impl HighlightColor {
+ pub fn hsla(&self, theme: &Theme) -> Hsla {
+ let system_color = SystemColor::new();
+
+ match self {
+ Self::Default => theme
+ .syntax
+ .get("primary")
+ .expect("no theme.syntax.primary")
+ .clone(),
+ Self::Comment => theme
+ .syntax
+ .get("comment")
+ .expect("no theme.syntax.comment")
+ .clone(),
+ Self::String => theme
+ .syntax
+ .get("string")
+ .expect("no theme.syntax.string")
+ .clone(),
+ Self::Function => theme
+ .syntax
+ .get("function")
+ .expect("no theme.syntax.function")
+ .clone(),
+ Self::Keyword => theme
+ .syntax
+ .get("keyword")
+ .expect("no theme.syntax.keyword")
+ .clone(),
+ }
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum FileSystemStatus {
+ #[default]
+ None,
+ Conflict,
+ Deleted,
+}
+
+impl FileSystemStatus {
+ pub fn to_string(&self) -> String {
+ match self {
+ Self::None => "None".to_string(),
+ Self::Conflict => "Conflict".to_string(),
+ Self::Deleted => "Deleted".to_string(),
+ }
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum GitStatus {
+ #[default]
+ None,
+ Created,
+ Modified,
+ Deleted,
+ Conflict,
+ Renamed,
+}
+
+impl GitStatus {
+ pub fn to_string(&self) -> String {
+ match self {
+ Self::None => "None".to_string(),
+ Self::Created => "Created".to_string(),
+ Self::Modified => "Modified".to_string(),
+ Self::Deleted => "Deleted".to_string(),
+ Self::Conflict => "Conflict".to_string(),
+ Self::Renamed => "Renamed".to_string(),
+ }
+ }
+
+ pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+ let theme = theme(cx);
+ let system_color = SystemColor::new();
+
+ match self {
+ Self::None => system_color.transparent,
+ Self::Created => theme.lowest.positive.default.foreground,
+ Self::Modified => theme.lowest.warning.default.foreground,
+ Self::Deleted => theme.lowest.negative.default.foreground,
+ Self::Conflict => theme.lowest.warning.default.foreground,
+ Self::Renamed => theme.lowest.accent.default.foreground,
+ }
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DiagnosticStatus {
+ #[default]
+ None,
+ Error,
+ Warning,
+ Info,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum IconSide {
+ #[default]
+ Left,
+ Right,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum OrderMethod {
+ #[default]
+ Ascending,
+ Descending,
+ MostRecent,
+}
+
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum Shape {
#[default]
Circle,
RoundedRectangle,
}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DisclosureControlVisibility {
+ #[default]
+ OnHover,
+ Always,
+}
+
+#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
+pub enum InteractionState {
+ #[default]
+ Enabled,
+ Hovered,
+ Active,
+ Focused,
+ Disabled,
+}
+
+impl InteractionState {
+ pub fn if_enabled(&self, enabled: bool) -> Self {
+ if enabled {
+ *self
+ } else {
+ InteractionState::Disabled
+ }
+ }
+}
+
+#[derive(Default, PartialEq)]
+pub enum SelectedState {
+ #[default]
+ Unselected,
+ PartiallySelected,
+ Selected,
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Toggleable {
+ Toggleable(ToggleState),
+ #[default]
+ NotToggleable,
+}
+
+impl Toggleable {
+ pub fn is_toggled(&self) -> bool {
+ match self {
+ Self::Toggleable(ToggleState::Toggled) => true,
+ _ => false,
+ }
+ }
+}
+
+impl From<ToggleState> for Toggleable {
+ fn from(state: ToggleState) -> Self {
+ Self::Toggleable(state)
+ }
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ToggleState {
+ /// The "on" state of a toggleable element.
+ ///
+ /// Example:
+ /// - A collasable list that is currently expanded
+ /// - A toggle button that is currently on.
+ Toggled,
+ /// The "off" state of a toggleable element.
+ ///
+ /// Example:
+ /// - A collasable list that is currently collapsed
+ /// - A toggle button that is currently off.
+ #[default]
+ NotToggled,
+}
+
+impl From<Toggleable> for ToggleState {
+ fn from(toggleable: Toggleable) -> Self {
+ match toggleable {
+ Toggleable::Toggleable(state) => state,
+ Toggleable::NotToggleable => ToggleState::NotToggled,
+ }
+ }
+}
+
+impl From<bool> for ToggleState {
+ fn from(toggled: bool) -> Self {
+ if toggled {
+ ToggleState::Toggled
+ } else {
+ ToggleState::NotToggled
+ }
+ }
+}