Merge pull request #1762 from zed-industries/less-click-and-hover-invalidation

Reduce unnecessary view invalidations related to mouse events
This commit is contained in:
Nathan Sobo 2022-10-16 10:23:54 -06:00 committed by GitHub
commit 3e23d1f48d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 136 additions and 60 deletions

View file

@ -285,7 +285,7 @@ impl View for ActivityIndicator {
.workspace .workspace
.status_bar .status_bar
.lsp_status; .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) theme.hover.as_ref().unwrap_or(&theme.default)
} else { } else {
&theme.default &theme.default

View file

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

View file

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

View file

@ -653,7 +653,11 @@ impl ContactList {
.constrained() .constrained()
.with_height(theme.row_height) .with_height(theme.row_height)
.contained() .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() .boxed()
} }
@ -768,7 +772,9 @@ impl ContactList {
) -> ElementBox { ) -> ElementBox {
enum Header {} 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 { let text = match section {
Section::ActiveCall => "Collaborators", Section::ActiveCall => "Collaborators",
Section::Requests => "Contact Requests", Section::Requests => "Contact Requests",
@ -890,7 +896,11 @@ impl ContactList {
.constrained() .constrained()
.with_height(theme.row_height) .with_height(theme.row_height)
.contained() .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() .boxed()
}) })
.on_click(MouseButton::Left, move |_, cx| { .on_click(MouseButton::Left, move |_, cx| {
@ -1014,7 +1024,11 @@ impl ContactList {
row.constrained() row.constrained()
.with_height(theme.row_height) .with_height(theme.row_height)
.contained() .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() .boxed()
} }

View file

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

View file

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

View file

@ -759,7 +759,7 @@ impl CompletionsMenu {
|state, _| { |state, _| {
let item_style = if item_ix == selected_item { let item_style = if item_ix == selected_item {
style.autocomplete.selected_item style.autocomplete.selected_item
} else if state.hovered { } else if state.hovered() {
style.autocomplete.hovered_item style.autocomplete.hovered_item
} else { } else {
style.autocomplete.item style.autocomplete.item
@ -914,7 +914,7 @@ impl CodeActionsMenu {
MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| { MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
let item_style = if item_ix == selected_item { let item_style = if item_ix == selected_item {
style.autocomplete.selected_item style.autocomplete.selected_item
} else if state.hovered { } else if state.hovered() {
style.autocomplete.hovered_item style.autocomplete.hovered_item
} else { } else {
style.autocomplete.item style.autocomplete.item
@ -5983,8 +5983,11 @@ impl Editor {
&mut self, &mut self,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<(fn(&Theme) -> Color, Vec<Range<Anchor>>)> { ) -> Option<(fn(&Theme) -> Color, Vec<Range<Anchor>>)> {
let highlights = self.background_highlights.remove(&TypeId::of::<T>());
if highlights.is_some() {
cx.notify(); cx.notify();
self.background_highlights.remove(&TypeId::of::<T>()) }
highlights
} }
#[cfg(feature = "test-support")] #[cfg(feature = "test-support")]
@ -6098,9 +6101,13 @@ impl Editor {
&mut self, &mut self,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> { ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
let highlights = self
.display_map
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
if highlights.is_some() {
cx.notify(); cx.notify();
self.display_map }
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>())) highlights
} }
fn next_blink_epoch(&mut self) -> usize { fn next_blink_epoch(&mut self) -> usize {

View file

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

View file

@ -3774,10 +3774,32 @@ pub struct RenderContext<'a, T: View> {
pub refreshing: bool, pub refreshing: bool,
} }
#[derive(Clone, Copy, Default)] #[derive(Clone, Default)]
pub struct MouseState { pub struct MouseState {
pub hovered: bool, hovered: bool,
pub clicked: Option<MouseButton>, 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> { impl<'a, V: View> RenderContext<'a, V> {
@ -3818,6 +3840,8 @@ impl<'a, V: View> RenderContext<'a, V> {
None None
} }
}), }),
accessed_hovered: false,
accessed_clicked: false,
} }
} }

View file

@ -22,6 +22,8 @@ pub struct MouseEventHandler<Tag: 'static> {
cursor_style: Option<CursorStyle>, cursor_style: Option<CursorStyle>,
handlers: HandlerSet, handlers: HandlerSet,
hoverable: bool, hoverable: bool,
notify_on_hover: bool,
notify_on_click: bool,
padding: Padding, padding: Padding,
_tag: PhantomData<Tag>, _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 pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where where
V: View, 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 { Self {
child: render_child(cx.mouse_state::<Tag>(region_id), cx), child,
region_id, region_id,
cursor_style: None, cursor_style: None,
handlers: Default::default(), handlers: Default::default(),
notify_on_hover,
notify_on_click,
hoverable: true, hoverable: true,
padding: Default::default(), padding: Default::default(),
_tag: PhantomData, _tag: PhantomData,
@ -185,7 +193,9 @@ impl<Tag> Element for MouseEventHandler<Tag> {
hit_bounds, hit_bounds,
self.handlers.clone(), 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); self.child.paint(bounds.origin(), visible_bounds, cx);

View file

@ -231,7 +231,7 @@ impl Presenter {
) -> bool { ) -> bool {
if let Some(root_view_id) = cx.root_view_id(self.window_id) { if let Some(root_view_id) = cx.root_view_id(self.window_id) {
let mut events_to_send = Vec::new(); 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 // 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?] // -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
@ -257,11 +257,6 @@ impl Presenter {
}) })
.collect(); .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); self.clicked_button = Some(e.button);
} }
@ -392,17 +387,31 @@ impl Presenter {
//Ensure that hover entrance events aren't sent twice //Ensure that hover entrance events aren't sent twice
if self.hovered_region_ids.insert(region.id()) { if self.hovered_region_ids.insert(region.id()) {
valid_regions.push(region.clone()); 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 { } else {
// Ensure that hover exit events aren't sent twice // Ensure that hover exit events aren't sent twice
if self.hovered_region_ids.remove(&region.id()) { if self.hovered_region_ids.remove(&region.id()) {
valid_regions.push(region.clone()); 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) => { MouseRegionEvent::Click(e) => {
// Only raise click events if the released button is the same as the one stored // Only raise click events if the released button is the same as the one stored
if self if self
@ -413,11 +422,6 @@ impl Presenter {
// Clear clicked regions and clicked button // Clear clicked regions and clicked button
let clicked_region_ids = let clicked_region_ids =
std::mem::replace(&mut self.clicked_region_ids, Default::default()); 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; self.clicked_button = None;
// Find regions which still overlap with the mouse since the last MouseDown happened // Find regions which still overlap with the mouse since the last MouseDown happened
@ -459,7 +463,7 @@ impl Presenter {
//3. Fire region events //3. Fire region events
let hovered_region_ids = self.hovered_region_ids.clone(); let hovered_region_ids = self.hovered_region_ids.clone();
for valid_region in valid_regions.into_iter() { 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); region_event.set_region(valid_region.bounds);
if let MouseRegionEvent::Hover(e) = &mut region_event { if let MouseRegionEvent::Hover(e) = &mut region_event {
@ -500,11 +504,11 @@ impl Presenter {
} }
if !any_event_handled && !event_reused { 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); 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); cx.notify_view(self.window_id, view_id);
} }
@ -516,7 +520,7 @@ impl Presenter {
pub fn build_event_context<'a>( pub fn build_event_context<'a>(
&'a mut self, &'a mut self,
invalidated_views: &'a mut HashSet<usize>, notified_views: &'a mut HashSet<usize>,
cx: &'a mut MutableAppContext, cx: &'a mut MutableAppContext,
) -> EventContext<'a> { ) -> EventContext<'a> {
EventContext { EventContext {
@ -524,7 +528,7 @@ impl Presenter {
font_cache: &self.font_cache, font_cache: &self.font_cache,
text_layout_cache: &self.text_layout_cache, text_layout_cache: &self.text_layout_cache,
view_stack: Default::default(), view_stack: Default::default(),
invalidated_views, notified_views,
notify_count: 0, notify_count: 0,
handled: false, handled: false,
window_id: self.window_id, window_id: self.window_id,
@ -747,7 +751,7 @@ pub struct EventContext<'a> {
pub notify_count: usize, pub notify_count: usize,
view_stack: Vec<usize>, view_stack: Vec<usize>,
handled: bool, handled: bool,
invalidated_views: &'a mut HashSet<usize>, notified_views: &'a mut HashSet<usize>,
} }
impl<'a> EventContext<'a> { impl<'a> EventContext<'a> {
@ -806,7 +810,7 @@ impl<'a> EventContext<'a> {
pub fn notify(&mut self) { pub fn notify(&mut self) {
self.notify_count += 1; self.notify_count += 1;
if let Some(view_id) = self.view_stack.last() { if let Some(view_id) = self.view_stack.last() {
self.invalidated_views.insert(*view_id); self.notified_views.insert(*view_id);
} }
} }

View file

@ -20,6 +20,8 @@ pub struct MouseRegion {
pub bounds: RectF, pub bounds: RectF,
pub handlers: HandlerSet, pub handlers: HandlerSet,
pub hoverable: bool, pub hoverable: bool,
pub notify_on_hover: bool,
pub notify_on_click: bool,
} }
impl MouseRegion { impl MouseRegion {
@ -52,6 +54,8 @@ impl MouseRegion {
bounds, bounds,
handlers, handlers,
hoverable: true, hoverable: true,
notify_on_hover: false,
notify_on_click: false,
} }
} }
@ -137,6 +141,16 @@ impl MouseRegion {
self.hoverable = is_hoverable; self.hoverable = is_hoverable;
self 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)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]

View file

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

View file

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

View file

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

View file

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

View file

@ -645,12 +645,12 @@ pub struct Interactive<T> {
} }
impl<T> 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 { if active {
self.active.as_ref().unwrap_or(&self.default) 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() self.clicked.as_ref().unwrap()
} else if state.hovered { } else if state.hovered() {
self.hover.as_ref().unwrap_or(&self.default) self.hover.as_ref().unwrap_or(&self.default)
} else { } else {
&self.default &self.default

View file

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

View file

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