Only invalidate parent view on click/hover if we read that state when rendering

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

crates/activity_indicator/src/activity_indicator.rs |  2 
crates/chat_panel/src/chat_panel.rs                 |  2 
crates/collab_ui/src/contact_finder.rs              |  2 
crates/collab_ui/src/contact_list.rs                | 22 ++++++-
crates/command_palette/src/command_palette.rs       |  2 
crates/context_menu/src/context_menu.rs             | 14 ++--
crates/editor/src/editor.rs                         |  4 
crates/file_finder/src/file_finder.rs               |  2 
crates/gpui/src/app.rs                              | 30 +++++++++-
crates/gpui/src/elements/mouse_event_handler.rs     | 16 ++++-
crates/gpui/src/presenter.rs                        | 44 ++++++++------
crates/gpui/src/scene/mouse_region.rs               | 14 ++++
crates/gpui/src/views/select.rs                     |  4 
crates/outline/src/outline.rs                       |  2 
crates/picker/src/picker.rs                         |  2 
crates/project_symbols/src/project_symbols.rs       |  2 
crates/theme/src/theme.rs                           |  6 +-
crates/theme_selector/src/theme_selector.rs         |  2 
crates/workspace/src/pane.rs                        |  7 +-
19 files changed, 124 insertions(+), 55 deletions(-)

Detailed changes

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -285,7 +285,7 @@ impl View for ActivityIndicator {
                 .workspace
                 .status_bar
                 .lsp_status;
-            let style = if state.hovered && action.is_some() {
+            let style = if state.hovered() && action.is_some() {
                 theme.hover.as_ref().unwrap_or(&theme.default)
             } else {
                 &theme.default

crates/chat_panel/src/chat_panel.rs 🔗

@@ -311,7 +311,7 @@ impl ChatPanel {
             MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
                 Label::new(
                     "Sign in to use chat".to_string(),
-                    if mouse_state.hovered {
+                    if mouse_state.hovered() {
                         theme.chat_panel.hovered_sign_in_prompt.clone()
                     } else {
                         theme.chat_panel.sign_in_prompt.clone()

crates/collab_ui/src/contact_finder.rs 🔗

@@ -101,7 +101,7 @@ impl PickerDelegate for ContactFinder {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: MouseState,
+        mouse_state: &mut MouseState,
         selected: bool,
         cx: &gpui::AppContext,
     ) -> ElementBox {

crates/collab_ui/src/contact_list.rs 🔗

@@ -653,7 +653,11 @@ impl ContactList {
             .constrained()
             .with_height(theme.row_height)
             .contained()
-            .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
+            .with_style(
+                *theme
+                    .contact_row
+                    .style_for(&mut Default::default(), is_selected),
+            )
             .boxed()
     }
 
@@ -768,7 +772,9 @@ impl ContactList {
     ) -> ElementBox {
         enum Header {}
 
-        let header_style = theme.header_row.style_for(Default::default(), is_selected);
+        let header_style = theme
+            .header_row
+            .style_for(&mut Default::default(), is_selected);
         let text = match section {
             Section::ActiveCall => "Collaborators",
             Section::Requests => "Contact Requests",
@@ -890,7 +896,11 @@ impl ContactList {
                     .constrained()
                     .with_height(theme.row_height)
                     .contained()
-                    .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
+                    .with_style(
+                        *theme
+                            .contact_row
+                            .style_for(&mut Default::default(), is_selected),
+                    )
                     .boxed()
             })
             .on_click(MouseButton::Left, move |_, cx| {
@@ -1014,7 +1024,11 @@ impl ContactList {
         row.constrained()
             .with_height(theme.row_height)
             .contained()
-            .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
+            .with_style(
+                *theme
+                    .contact_row
+                    .style_for(&mut Default::default(), is_selected),
+            )
             .boxed()
     }
 

crates/command_palette/src/command_palette.rs 🔗

@@ -224,7 +224,7 @@ impl PickerDelegate for CommandPalette {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: MouseState,
+        mouse_state: &mut MouseState,
         selected: bool,
         cx: &gpui::AppContext,
     ) -> gpui::ElementBox {

crates/context_menu/src/context_menu.rs 🔗

@@ -258,9 +258,10 @@ impl ContextMenu {
                     .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                         match item {
                             ContextMenuItem::Item { label, .. } => {
-                                let style = style
-                                    .item
-                                    .style_for(Default::default(), Some(ix) == self.selected_index);
+                                let style = style.item.style_for(
+                                    &mut Default::default(),
+                                    Some(ix) == self.selected_index,
+                                );
 
                                 Label::new(label.to_string(), style.label.clone())
                                     .contained()
@@ -283,9 +284,10 @@ impl ContextMenu {
                     .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                         match item {
                             ContextMenuItem::Item { action, .. } => {
-                                let style = style
-                                    .item
-                                    .style_for(Default::default(), Some(ix) == self.selected_index);
+                                let style = style.item.style_for(
+                                    &mut Default::default(),
+                                    Some(ix) == self.selected_index,
+                                );
                                 KeystrokeLabel::new(
                                     action.boxed_clone(),
                                     style.keystroke.container,

crates/editor/src/editor.rs 🔗

@@ -705,7 +705,7 @@ impl CompletionsMenu {
                             |state, _| {
                                 let item_style = if item_ix == selected_item {
                                     style.autocomplete.selected_item
-                                } else if state.hovered {
+                                } else if state.hovered() {
                                     style.autocomplete.hovered_item
                                 } else {
                                     style.autocomplete.item
@@ -850,7 +850,7 @@ impl CodeActionsMenu {
                         MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
                             let item_style = if item_ix == selected_item {
                                 style.autocomplete.selected_item
-                            } else if state.hovered {
+                            } else if state.hovered() {
                                 style.autocomplete.hovered_item
                             } else {
                                 style.autocomplete.item

crates/file_finder/src/file_finder.rs 🔗

@@ -251,7 +251,7 @@ impl PickerDelegate for FileFinder {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: MouseState,
+        mouse_state: &mut MouseState,
         selected: bool,
         cx: &AppContext,
     ) -> ElementBox {

crates/gpui/src/app.rs 🔗

@@ -3774,10 +3774,32 @@ pub struct RenderContext<'a, T: View> {
     pub refreshing: bool,
 }
 
-#[derive(Clone, Copy, Default)]
+#[derive(Clone, Default)]
 pub struct MouseState {
-    pub hovered: bool,
-    pub clicked: Option<MouseButton>,
+    hovered: bool,
+    clicked: Option<MouseButton>,
+    accessed_hovered: bool,
+    accessed_clicked: bool,
+}
+
+impl MouseState {
+    pub fn hovered(&mut self) -> bool {
+        self.accessed_hovered = true;
+        self.hovered
+    }
+
+    pub fn clicked(&mut self) -> Option<MouseButton> {
+        self.accessed_clicked = true;
+        self.clicked
+    }
+
+    pub fn accessed_hovered(&self) -> bool {
+        self.accessed_hovered
+    }
+
+    pub fn accessed_clicked(&self) -> bool {
+        self.accessed_clicked
+    }
 }
 
 impl<'a, V: View> RenderContext<'a, V> {
@@ -3818,6 +3840,8 @@ impl<'a, V: View> RenderContext<'a, V> {
                     None
                 }
             }),
+            accessed_hovered: false,
+            accessed_clicked: false,
         }
     }
 

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -22,6 +22,8 @@ pub struct MouseEventHandler<Tag: 'static> {
     cursor_style: Option<CursorStyle>,
     handlers: HandlerSet,
     hoverable: bool,
+    notify_on_hover: bool,
+    notify_on_click: bool,
     padding: Padding,
     _tag: PhantomData<Tag>,
 }
@@ -30,13 +32,19 @@ impl<Tag> MouseEventHandler<Tag> {
     pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
     where
         V: View,
-        F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
+        F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
     {
+        let mut mouse_state = cx.mouse_state::<Tag>(region_id);
+        let child = render_child(&mut mouse_state, cx);
+        let notify_on_hover = mouse_state.accessed_hovered();
+        let notify_on_click = mouse_state.accessed_clicked();
         Self {
-            child: render_child(cx.mouse_state::<Tag>(region_id), cx),
+            child,
             region_id,
             cursor_style: None,
             handlers: Default::default(),
+            notify_on_hover,
+            notify_on_click,
             hoverable: true,
             padding: Default::default(),
             _tag: PhantomData,
@@ -185,7 +193,9 @@ impl<Tag> Element for MouseEventHandler<Tag> {
                 hit_bounds,
                 self.handlers.clone(),
             )
-            .with_hoverable(self.hoverable),
+            .with_hoverable(self.hoverable)
+            .with_notify_on_hover(self.notify_on_hover)
+            .with_notify_on_click(self.notify_on_click),
         );
 
         self.child.paint(bounds.origin(), visible_bounds, cx);

crates/gpui/src/presenter.rs 🔗

@@ -231,7 +231,7 @@ impl Presenter {
     ) -> bool {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
             let mut events_to_send = Vec::new();
-            let mut invalidated_views: HashSet<usize> = Default::default();
+            let mut notified_views: HashSet<usize> = Default::default();
 
             // 1. Allocate the correct set of GPUI events generated from the platform events
             //  -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
@@ -257,11 +257,6 @@ impl Presenter {
                             })
                             .collect();
 
-                        // Clicked status is used when rendering views via the RenderContext.
-                        // So when it changes, these views need to be rerendered
-                        for clicked_region_id in self.clicked_region_ids.iter() {
-                            invalidated_views.insert(clicked_region_id.view_id());
-                        }
                         self.clicked_button = Some(e.button);
                     }
 
@@ -392,14 +387,28 @@ impl Presenter {
                                 //Ensure that hover entrance events aren't sent twice
                                 if self.hovered_region_ids.insert(region.id()) {
                                     valid_regions.push(region.clone());
-                                    invalidated_views.insert(region.id().view_id());
+                                    if region.notify_on_hover {
+                                        notified_views.insert(region.id().view_id());
+                                    }
                                 }
                             } else {
                                 // Ensure that hover exit events aren't sent twice
                                 if self.hovered_region_ids.remove(&region.id()) {
                                     valid_regions.push(region.clone());
-                                    invalidated_views.insert(region.id().view_id());
+                                    if region.notify_on_hover {
+                                        notified_views.insert(region.id().view_id());
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    MouseRegionEvent::Down(_) | MouseRegionEvent::Up(_) => {
+                        for (region, _) in self.mouse_regions.iter().rev() {
+                            if region.bounds.contains_point(self.mouse_position) {
+                                if region.notify_on_click {
+                                    notified_views.insert(region.id().view_id());
                                 }
+                                valid_regions.push(region.clone());
                             }
                         }
                     }
@@ -413,11 +422,6 @@ impl Presenter {
                             // Clear clicked regions and clicked button
                             let clicked_region_ids =
                                 std::mem::replace(&mut self.clicked_region_ids, Default::default());
-                            // Clicked status is used when rendering views via the RenderContext.
-                            // So when it changes, these views need to be rerendered
-                            for clicked_region_id in clicked_region_ids.iter() {
-                                invalidated_views.insert(clicked_region_id.view_id());
-                            }
                             self.clicked_button = None;
 
                             // Find regions which still overlap with the mouse since the last MouseDown happened
@@ -459,7 +463,7 @@ impl Presenter {
                 //3. Fire region events
                 let hovered_region_ids = self.hovered_region_ids.clone();
                 for valid_region in valid_regions.into_iter() {
-                    let mut event_cx = self.build_event_context(&mut invalidated_views, cx);
+                    let mut event_cx = self.build_event_context(&mut notified_views, cx);
 
                     region_event.set_region(valid_region.bounds);
                     if let MouseRegionEvent::Hover(e) = &mut region_event {
@@ -500,11 +504,11 @@ impl Presenter {
             }
 
             if !any_event_handled && !event_reused {
-                let mut event_cx = self.build_event_context(&mut invalidated_views, cx);
+                let mut event_cx = self.build_event_context(&mut notified_views, cx);
                 any_event_handled = event_cx.dispatch_event(root_view_id, &event);
             }
 
-            for view_id in invalidated_views {
+            for view_id in notified_views {
                 cx.notify_view(self.window_id, view_id);
             }
 
@@ -516,7 +520,7 @@ impl Presenter {
 
     pub fn build_event_context<'a>(
         &'a mut self,
-        invalidated_views: &'a mut HashSet<usize>,
+        notified_views: &'a mut HashSet<usize>,
         cx: &'a mut MutableAppContext,
     ) -> EventContext<'a> {
         EventContext {
@@ -524,7 +528,7 @@ impl Presenter {
             font_cache: &self.font_cache,
             text_layout_cache: &self.text_layout_cache,
             view_stack: Default::default(),
-            invalidated_views,
+            notified_views,
             notify_count: 0,
             handled: false,
             window_id: self.window_id,
@@ -747,7 +751,7 @@ pub struct EventContext<'a> {
     pub notify_count: usize,
     view_stack: Vec<usize>,
     handled: bool,
-    invalidated_views: &'a mut HashSet<usize>,
+    notified_views: &'a mut HashSet<usize>,
 }
 
 impl<'a> EventContext<'a> {
@@ -806,7 +810,7 @@ impl<'a> EventContext<'a> {
     pub fn notify(&mut self) {
         self.notify_count += 1;
         if let Some(view_id) = self.view_stack.last() {
-            self.invalidated_views.insert(*view_id);
+            self.notified_views.insert(*view_id);
         }
     }
 

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -20,6 +20,8 @@ pub struct MouseRegion {
     pub bounds: RectF,
     pub handlers: HandlerSet,
     pub hoverable: bool,
+    pub notify_on_hover: bool,
+    pub notify_on_click: bool,
 }
 
 impl MouseRegion {
@@ -52,6 +54,8 @@ impl MouseRegion {
             bounds,
             handlers,
             hoverable: true,
+            notify_on_hover: false,
+            notify_on_click: false,
         }
     }
 
@@ -137,6 +141,16 @@ impl MouseRegion {
         self.hoverable = is_hoverable;
         self
     }
+
+    pub fn with_notify_on_hover(mut self, notify: bool) -> Self {
+        self.notify_on_hover = notify;
+        self
+    }
+
+    pub fn with_notify_on_click(mut self, notify: bool) -> Self {
+        self.notify_on_click = notify;
+        self
+    }
 }
 
 #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]

crates/gpui/src/views/select.rs 🔗

@@ -113,7 +113,7 @@ impl View for Select {
                 Container::new((self.render_item)(
                     self.selected_item_ix,
                     ItemType::Header,
-                    mouse_state.hovered,
+                    mouse_state.hovered(),
                     cx,
                 ))
                 .with_style(style.header)
@@ -145,7 +145,7 @@ impl View for Select {
                                                 } else {
                                                     ItemType::Unselected
                                                 },
-                                                mouse_state.hovered,
+                                                mouse_state.hovered(),
                                                 cx,
                                             )
                                         })

crates/outline/src/outline.rs 🔗

@@ -233,7 +233,7 @@ impl PickerDelegate for OutlineView {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: MouseState,
+        mouse_state: &mut MouseState,
         selected: bool,
         cx: &AppContext,
     ) -> ElementBox {

crates/picker/src/picker.rs 🔗

@@ -33,7 +33,7 @@ pub trait PickerDelegate: View {
     fn render_match(
         &self,
         ix: usize,
-        state: MouseState,
+        state: &mut MouseState,
         selected: bool,
         cx: &AppContext,
     ) -> ElementBox;

crates/project_symbols/src/project_symbols.rs 🔗

@@ -234,7 +234,7 @@ impl PickerDelegate for ProjectSymbolsView {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: MouseState,
+        mouse_state: &mut MouseState,
         selected: bool,
         cx: &AppContext,
     ) -> ElementBox {

crates/theme/src/theme.rs 🔗

@@ -645,12 +645,12 @@ pub struct Interactive<T> {
 }
 
 impl<T> Interactive<T> {
-    pub fn style_for(&self, state: MouseState, active: bool) -> &T {
+    pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
         if active {
             self.active.as_ref().unwrap_or(&self.default)
-        } else if state.clicked == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
+        } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
             self.clicked.as_ref().unwrap()
-        } else if state.hovered {
+        } else if state.hovered() {
             self.hover.as_ref().unwrap_or(&self.default)
         } else {
             &self.default

crates/theme_selector/src/theme_selector.rs 🔗

@@ -230,7 +230,7 @@ impl PickerDelegate for ThemeSelector {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: MouseState,
+        mouse_state: &mut MouseState,
         selected: bool,
         cx: &AppContext,
     ) -> ElementBox {

crates/workspace/src/pane.rs 🔗

@@ -1088,7 +1088,7 @@ impl Pane {
                         move |mouse_state, cx| {
                             let tab_style =
                                 theme.workspace.tab_bar.tab_style(pane_active, tab_active);
-                            let hovered = mouse_state.hovered;
+                            let hovered = mouse_state.hovered();
                             Self::render_tab(
                                 &item,
                                 pane,
@@ -1161,7 +1161,8 @@ impl Pane {
                         .with_style(filler_style.container)
                         .with_border(filler_style.container.border);
 
-                    if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx)
+                    if let Some(overlay) =
+                        Self::tab_overlay_color(mouse_state.hovered(), &theme, cx)
                     {
                         filler = filler.with_overlay_color(overlay);
                     }
@@ -1283,7 +1284,7 @@ impl Pane {
                         enum TabCloseButton {}
                         let icon = Svg::new("icons/x_mark_thin_8.svg");
                         MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
-                            if mouse_state.hovered {
+                            if mouse_state.hovered() {
                                 icon.with_color(tab_style.icon_close_active).boxed()
                             } else {
                                 icon.with_color(tab_style.icon_close).boxed()