Add hover state to assistant buttons

Antonio Scandurra created

Change summary

crates/ai/src/assistant.rs        | 215 ++++++++++++++++----------------
crates/theme/src/theme.rs         |  14 +-
styles/src/styleTree/assistant.ts | 203 ++++++++++++++++++++----------
3 files changed, 249 insertions(+), 183 deletions(-)

Detailed changes

crates/ai/src/assistant.rs 🔗

@@ -38,7 +38,7 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use theme::{ui::IconStyle, AssistantStyle};
+use theme::AssistantStyle;
 use util::{channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
@@ -343,131 +343,140 @@ impl AssistantPanel {
         self.editors.get(self.active_editor_index?)
     }
 
-    fn render_hamburger_button(style: &IconStyle) -> impl Element<Self> {
+    fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
         enum ListConversations {}
-        Svg::for_style(style.icon.clone())
-            .contained()
-            .with_style(style.container)
-            .mouse::<ListConversations>(0)
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
-                if this.active_editor().is_some() {
-                    this.set_active_editor_index(None, cx);
-                } else {
-                    this.set_active_editor_index(this.prev_active_editor_index, cx);
-                }
-            })
+        let theme = theme::current(cx);
+        MouseEventHandler::<ListConversations, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.hamburger_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if this.active_editor().is_some() {
+                this.set_active_editor_index(None, cx);
+            } else {
+                this.set_active_editor_index(this.prev_active_editor_index, cx);
+            }
+        })
     }
 
-    fn render_editor_tools(
-        &self,
-        style: &AssistantStyle,
-        cx: &mut ViewContext<Self>,
-    ) -> Vec<AnyElement<Self>> {
+    fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement<Self>> {
         if self.active_editor().is_some() {
             vec![
-                Self::render_split_button(&style.split_button, cx).into_any(),
-                Self::render_quote_button(&style.quote_button, cx).into_any(),
-                Self::render_assist_button(&style.assist_button, cx).into_any(),
+                Self::render_split_button(cx).into_any(),
+                Self::render_quote_button(cx).into_any(),
+                Self::render_assist_button(cx).into_any(),
             ]
         } else {
             Default::default()
         }
     }
 
-    fn render_split_button(style: &IconStyle, cx: &mut ViewContext<Self>) -> impl Element<Self> {
+    fn render_split_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
         let tooltip_style = theme::current(cx).tooltip.clone();
-        Svg::for_style(style.icon.clone())
-            .contained()
-            .with_style(style.container)
-            .mouse::<Split>(0)
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
-                if let Some(active_editor) = this.active_editor() {
-                    active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
-                }
-            })
-            .with_tooltip::<Split>(
-                1,
-                "Split Message".into(),
-                Some(Box::new(Split)),
-                tooltip_style,
-                cx,
-            )
+        MouseEventHandler::<Split, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.split_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if let Some(active_editor) = this.active_editor() {
+                active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
+            }
+        })
+        .with_tooltip::<Split>(
+            1,
+            "Split Message".into(),
+            Some(Box::new(Split)),
+            tooltip_style,
+            cx,
+        )
     }
 
-    fn render_assist_button(style: &IconStyle, cx: &mut ViewContext<Self>) -> impl Element<Self> {
+    fn render_assist_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
         let tooltip_style = theme::current(cx).tooltip.clone();
-        Svg::for_style(style.icon.clone())
-            .contained()
-            .with_style(style.container)
-            .mouse::<Assist>(0)
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
-                if let Some(active_editor) = this.active_editor() {
-                    active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
-                }
-            })
-            .with_tooltip::<Assist>(
-                1,
-                "Assist".into(),
-                Some(Box::new(Assist)),
-                tooltip_style,
-                cx,
-            )
+        MouseEventHandler::<Assist, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.assist_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if let Some(active_editor) = this.active_editor() {
+                active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
+            }
+        })
+        .with_tooltip::<Assist>(
+            1,
+            "Assist".into(),
+            Some(Box::new(Assist)),
+            tooltip_style,
+            cx,
+        )
     }
 
-    fn render_quote_button(style: &IconStyle, cx: &mut ViewContext<Self>) -> impl Element<Self> {
+    fn render_quote_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
         let tooltip_style = theme::current(cx).tooltip.clone();
-        Svg::for_style(style.icon.clone())
-            .contained()
-            .with_style(style.container)
-            .mouse::<QuoteSelection>(0)
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    cx.window_context().defer(move |cx| {
-                        workspace.update(cx, |workspace, cx| {
-                            ConversationEditor::quote_selection(workspace, &Default::default(), cx)
-                        });
+        MouseEventHandler::<QuoteSelection, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.quote_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if let Some(workspace) = this.workspace.upgrade(cx) {
+                cx.window_context().defer(move |cx| {
+                    workspace.update(cx, |workspace, cx| {
+                        ConversationEditor::quote_selection(workspace, &Default::default(), cx)
                     });
-                }
-            })
-            .with_tooltip::<QuoteSelection>(
-                1,
-                "Assist".into(),
-                Some(Box::new(QuoteSelection)),
-                tooltip_style,
-                cx,
-            )
+                });
+            }
+        })
+        .with_tooltip::<QuoteSelection>(
+            1,
+            "Assist".into(),
+            Some(Box::new(QuoteSelection)),
+            tooltip_style,
+            cx,
+        )
     }
 
-    fn render_plus_button(style: &IconStyle) -> impl Element<Self> {
+    fn render_plus_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
         enum AddConversation {}
-        Svg::for_style(style.icon.clone())
-            .contained()
-            .with_style(style.container)
-            .mouse::<AddConversation>(0)
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
-                this.new_conversation(cx);
-            })
+        let theme = theme::current(cx);
+        MouseEventHandler::<AddConversation, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.plus_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            this.new_conversation(cx);
+        })
     }
 
-    fn render_zoom_button(
-        &self,
-        style: &AssistantStyle,
-        cx: &mut ViewContext<Self>,
-    ) -> impl Element<Self> {
+    fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
         enum ToggleZoomButton {}
 
+        let theme = theme::current(cx);
         let style = if self.zoomed {
-            &style.zoom_out_button
+            &theme.assistant.zoom_out_button
         } else {
-            &style.zoom_in_button
+            &theme.assistant.zoom_in_button
         };
 
-        MouseEventHandler::<ToggleZoomButton, _>::new(0, cx, |_, _| {
+        MouseEventHandler::<ToggleZoomButton, _>::new(0, cx, |state, _| {
+            let style = style.style_for(state);
             Svg::for_style(style.icon.clone())
                 .contained()
                 .with_style(style.container)
@@ -605,21 +614,15 @@ impl View for AssistantPanel {
             Flex::column()
                 .with_child(
                     Flex::row()
-                        .with_child(
-                            Self::render_hamburger_button(&style.hamburger_button).aligned(),
-                        )
+                        .with_child(Self::render_hamburger_button(cx).aligned())
                         .with_children(title)
                         .with_children(
-                            self.render_editor_tools(&style, cx)
+                            self.render_editor_tools(cx)
                                 .into_iter()
                                 .map(|tool| tool.aligned().flex_float()),
                         )
-                        .with_child(
-                            Self::render_plus_button(&style.plus_button)
-                                .aligned()
-                                .flex_float(),
-                        )
-                        .with_child(self.render_zoom_button(&style, cx).aligned())
+                        .with_child(Self::render_plus_button(cx).aligned().flex_float())
+                        .with_child(self.render_zoom_button(cx).aligned())
                         .contained()
                         .with_style(theme.workspace.tab_bar.container)
                         .expanded()

crates/theme/src/theme.rs 🔗

@@ -993,13 +993,13 @@ pub struct TerminalStyle {
 #[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct AssistantStyle {
     pub container: ContainerStyle,
-    pub hamburger_button: IconStyle,
-    pub split_button: IconStyle,
-    pub assist_button: IconStyle,
-    pub quote_button: IconStyle,
-    pub zoom_in_button: IconStyle,
-    pub zoom_out_button: IconStyle,
-    pub plus_button: IconStyle,
+    pub hamburger_button: Interactive<IconStyle>,
+    pub split_button: Interactive<IconStyle>,
+    pub assist_button: Interactive<IconStyle>,
+    pub quote_button: Interactive<IconStyle>,
+    pub zoom_in_button: Interactive<IconStyle>,
+    pub zoom_out_button: Interactive<IconStyle>,
+    pub plus_button: Interactive<IconStyle>,
     pub title: ContainedText,
     pub message_header: ContainerStyle,
     pub sent_at: ContainedText,

styles/src/styleTree/assistant.ts 🔗

@@ -15,97 +15,160 @@ export default function assistant(colorScheme: ColorScheme) {
             margin: { bottom: 6, top: 6 },
             background: editor(colorScheme).background,
         },
-        hamburgerButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/hamburger_15.svg",
-                dimensions: {
-                    width: 15,
-                    height: 15,
+        hamburgerButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/hamburger_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
                 },
+                container: {
+                    margin: { left: 12 },
+                }
             },
-            container: {
-                margin: { left: 12 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
-        splitButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/split_message_15.svg",
-                dimensions: {
-                    width: 15,
-                    height: 15,
+        }),
+        splitButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/split_message_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
                 },
+                container: {
+                    margin: { left: 12 },
+                }
             },
-            container: {
-                margin: { left: 12 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
-        quoteButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/quote_15.svg",
-                dimensions: {
-                    width: 15,
-                    height: 15,
+        }),
+        quoteButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/quote_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
                 },
+                container: {
+                    margin: { left: 12 },
+                }
             },
-            container: {
-                margin: { left: 12 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
-        assistButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/assist_15.svg",
-                dimensions: {
-                    width: 15,
-                    height: 15,
+        }),
+        assistButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/assist_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
                 },
+                container: {
+                    margin: { left: 12, right: 24 },
+                }
             },
-            container: {
-                margin: { left: 12, right: 24 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
-        zoomInButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/maximize_8.svg",
-                dimensions: {
-                    width: 12,
-                    height: 12,
+        }),
+        zoomInButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/maximize_8.svg",
+                    dimensions: {
+                        width: 12,
+                        height: 12,
+                    },
                 },
+                container: {
+                    margin: { right: 12 },
+                }
             },
-            container: {
-                margin: { right: 12 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
-        zoomOutButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/minimize_8.svg",
-                dimensions: {
-                    width: 12,
-                    height: 12,
+        }),
+        zoomOutButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/minimize_8.svg",
+                    dimensions: {
+                        width: 12,
+                        height: 12,
+                    },
                 },
+                container: {
+                    margin: { right: 12 },
+                }
             },
-            container: {
-                margin: { right: 12 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
-        plusButton: {
-            icon: {
-                color: text(layer, "sans", "default", { size: "sm" }).color,
-                asset: "icons/plus_12.svg",
-                dimensions: {
-                    width: 12,
-                    height: 12,
+        }),
+        plusButton: interactive({
+            base: {
+                icon: {
+                    color: foreground(layer, "variant"),
+                    asset: "icons/plus_12.svg",
+                    dimensions: {
+                        width: 12,
+                        height: 12,
+                    },
                 },
+                container: {
+                    margin: { right: 12 },
+                }
             },
-            container: {
-                margin: { right: 12 },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(layer, "hovered")
+                    }
+                }
             }
-        },
+        }),
         title: {
             margin: { left: 12 },
             ...text(layer, "sans", "default", { size: "sm" })