From ae3bbdeb2df000acd8e0c5c75275cbb357e2b223 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 16 Jul 2026 21:26:49 +0200 Subject: [PATCH] gpui: Avoid redundant group hover redraws (#61118) Anonymous elements with group hover styles have no persistent hover state. The prepaint mouse handler treated missing state as unhovered and notified the current view for every pointer movement within the group. This made controls such as switches redraw continuously while the pointer moved over them. Let anonymous elements use the existing paint-time hover transition handler. Only elements with persistent hover state now update that state and notify from prepaint. Add regression coverage for entering, moving within, and leaving an anonymous hover group. Signed-off-by: Daan De Meyer Release Notes: - N/A Signed-off-by: Daan De Meyer --- crates/gpui/src/elements/div.rs | 105 +++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 5633371f5b2..c53c76e7871 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2669,8 +2669,8 @@ impl Interactivity { if phase == DispatchPhase::Capture && group_hovered != was_group_hovered { if let Some(hover_state) = &hover_state { hover_state.borrow_mut().group = group_hovered; + cx.notify(current_view); } - cx.notify(current_view); } }); } @@ -4114,9 +4114,108 @@ mod tests { use super::*; use crate::{ AnyWindowHandle, AppContext as _, Context, InputEvent, Keystroke, MouseMoveEvent, - TestAppContext, util::FluentBuilder as _, + TestAppContext, canvas, util::FluentBuilder as _, }; - use std::rc::Weak; + use std::{cell::Cell, rc::Weak}; + + struct GroupHoverTestView { + render_count: Rc>, + anonymous_paint_count: Rc>, + stateful_width: Rc>, + } + + impl Render for GroupHoverTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.render_count.set(self.render_count.get() + 1); + let anonymous_paint_count = self.anonymous_paint_count.clone(); + let stateful_width = self.stateful_width.clone(); + div().size_full().child( + div() + .ml(px(20.)) + .mt(px(20.)) + .size(px(50.)) + .relative() + .group("hover-group") + .child( + div() + .absolute() + .size_full() + .invisible() + .group_hover("hover-group", |style| style.visible()) + .child(canvas( + |_, _, _| {}, + move |_, _, _, _| { + anonymous_paint_count.set(anonymous_paint_count.get() + 1) + }, + )), + ) + .child( + div() + .id("stateful-group-hover-target") + .absolute() + .top_0() + .left_0() + .size(px(10.)) + .group_hover("hover-group", |style| style.size(px(20.))) + .child(canvas( + move |bounds, _, _| stateful_width.set(bounds.size.width), + |_, _, _, _| {}, + )), + ), + ) + } + } + + #[gpui::test] + fn group_hover_styles_update_only_on_transitions(cx: &mut TestAppContext) { + let render_count = Rc::new(Cell::new(0)); + let anonymous_paint_count = Rc::new(Cell::new(0)); + let stateful_width = Rc::new(Cell::new(px(0.))); + let window = cx.add_window({ + let render_count = render_count.clone(); + let anonymous_paint_count = anonymous_paint_count.clone(); + let stateful_width = stateful_width.clone(); + move |_, _| GroupHoverTestView { + render_count, + anonymous_paint_count, + stateful_width, + } + }); + let window = AnyWindowHandle::from(window); + + cx.update_window(window, |_, window, cx| window.draw(cx).clear()) + .unwrap(); + assert_eq!(anonymous_paint_count.get(), 0); + assert_eq!(stateful_width.get(), px(10.)); + + let move_mouse = |cx: &mut TestAppContext, position| { + cx.update_window(window, |_, window, cx| { + window.simulate_mouse_move(position, cx) + }) + .unwrap(); + }; + + let initial_render_count = render_count.get(); + move_mouse(cx, point(px(25.), px(25.))); + assert_eq!(render_count.get(), initial_render_count + 1); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(20.)); + + move_mouse(cx, point(px(30.), px(30.))); + assert_eq!(render_count.get(), initial_render_count + 1); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(20.)); + + move_mouse(cx, point(px(5.), px(5.))); + assert_eq!(render_count.get(), initial_render_count + 2); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(10.)); + + move_mouse(cx, point(px(10.), px(10.))); + assert_eq!(render_count.get(), initial_render_count + 2); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(10.)); + } struct TestTooltipView;