diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8f6f4bf627..596e9ba995 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/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 diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 60eda235ac..0c22c21c71 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -311,7 +311,7 @@ impl ChatPanel { MouseEventHandler::::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() diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index 8835dc0b0e..a4ec02d2f0 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/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 { diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index e1d96cd1da..cf8a8f8223 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/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() } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 125a4eeb02..7702aaaf2a 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/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 { diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index b79e931257..c284375966 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/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, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 07429a600d..7f84772a45 100644 --- a/crates/editor/src/editor.rs +++ b/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::::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 diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index fe0fa3ae77..e787e3c1a6 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/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 { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 76aae930a1..a9529f3f9f 100644 --- a/crates/gpui/src/app.rs +++ b/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, + hovered: bool, + clicked: Option, + 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 { + 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, } } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index e809c0080f..ab5aeb562b 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -22,6 +22,8 @@ pub struct MouseEventHandler { cursor_style: Option, handlers: HandlerSet, hoverable: bool, + notify_on_hover: bool, + notify_on_click: bool, padding: Padding, _tag: PhantomData, } @@ -30,13 +32,19 @@ impl MouseEventHandler { pub fn new(region_id: usize, cx: &mut RenderContext, render_child: F) -> Self where V: View, - F: FnOnce(MouseState, &mut RenderContext) -> ElementBox, + F: FnOnce(&mut MouseState, &mut RenderContext) -> ElementBox, { + let mut mouse_state = cx.mouse_state::(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::(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 Element for MouseEventHandler { 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); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index d0d70aae18..d082ebd095 100644 --- a/crates/gpui/src/presenter.rs +++ b/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 = Default::default(); + let mut notified_views: HashSet = 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,17 +387,31 @@ 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(®ion.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()); + } + } + } MouseRegionEvent::Click(e) => { // Only raise click events if the released button is the same as the one stored if self @@ -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, + notified_views: &'a mut HashSet, 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, handled: bool, - invalidated_views: &'a mut HashSet, + notified_views: &'a mut HashSet, } 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); } } diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index 9bd1ab008b..e84508622b 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/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)] diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index 113fbf5828..216491db0f 100644 --- a/crates/gpui/src/views/select.rs +++ b/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, ) }) diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index afcfa263a8..a677ab5b67 100644 --- a/crates/outline/src/outline.rs +++ b/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 { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index a093740e25..30ad7827ef 100644 --- a/crates/picker/src/picker.rs +++ b/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; diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 6aa866342a..a81231b98f 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/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 { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6e411c010f..51aa3232d4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -645,12 +645,12 @@ pub struct Interactive { } impl Interactive { - 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 diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index a014fc8e92..3236120857 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/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 { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c8019e1123..b7ae7f2ba0 100644 --- a/crates/workspace/src/pane.rs +++ b/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::::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()