From cd54ce1bb0f4465cc4535558458f7b283f2847ad Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 00:32:04 +0000 Subject: [PATCH] feat(engine): pointer cursor on hover over interactive buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the cursor stayed the default arrow over every clickable UI element (modal buttons, HUD action bar, mode-launcher cards, settings toggles). Adds the standard "this is clickable" hand affordance: while not dragging a card, hovering any entity with Interaction::Hovered (or Pressed — keeps the pointer through a click-and-hold) sets the window cursor to SystemCursorIcon::Pointer. The new branch sits between the existing drag handlers in update_cursor_icon: Grabbing wins when actively dragging, then Pointer when a button is hovered, then Grab when a draggable card is hovered, then Default. Card-drag affordance unchanged. A pure pick_cursor_icon(is_dragging, any_button_hovered, any_card_hovered) helper makes the priority logic unit-testable without standing up a full Window + Camera fixture; four new tests pin every branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- solitaire_engine/src/cursor_plugin.rs | 127 ++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 19 deletions(-) diff --git a/solitaire_engine/src/cursor_plugin.rs b/solitaire_engine/src/cursor_plugin.rs index e554982..0e367ce 100644 --- a/solitaire_engine/src/cursor_plugin.rs +++ b/solitaire_engine/src/cursor_plugin.rs @@ -2,9 +2,19 @@ //! //! **Cursor icons** (`update_cursor_icon`) //! - Cards are being dragged → `Grabbing` (closed hand) +//! - A UI `Button` entity is hovered (and no drag in progress) → `Pointer` +//! (the hand-with-extended-index-finger icon). This telegraphs +//! clickability for every modal button, HUD action, mode-launcher +//! card, settings toggle, etc. //! - Cursor hovers over a face-up draggable card → `Grab` (open hand) //! - Otherwise → `Default` (arrow) //! +//! Priority order: dragging > button-hover > card-hover > default. A +//! button-overlapping-a-card edge case favours `Pointer` because UI +//! elements take precedence over world-space cards; in practice +//! buttons are always on UI nodes and cards are sprites, so they +//! cannot occupy the same hit region simultaneously. +//! //! **Drop-target highlights** (`update_drop_highlights`) //! While a drag is in progress every `PileMarker` sprite is tinted: //! - **Green** if the dragged stack can legally land there. @@ -70,6 +80,31 @@ impl Plugin for CursorPlugin { // #31 — Cursor icon // --------------------------------------------------------------------------- +/// Pure decision function for the cursor icon, separated from the Bevy +/// system so it can be unit-tested without `PrimaryWindow` / +/// `Camera` / `Time` plumbing. +/// +/// Priority order (highest first): +/// 1. `is_dragging` → `Grabbing` +/// 2. `any_button_hovered` → `Pointer` +/// 3. `any_card_hovered` → `Grab` +/// 4. otherwise → `Default` +fn pick_cursor_icon( + is_dragging: bool, + any_button_hovered: bool, + any_card_hovered: bool, +) -> SystemCursorIcon { + if is_dragging { + SystemCursorIcon::Grabbing + } else if any_button_hovered { + SystemCursorIcon::Pointer + } else if any_card_hovered { + SystemCursorIcon::Grab + } else { + SystemCursorIcon::Default + } +} + /// Updates the primary-window cursor icon based on drag state and hover. fn update_cursor_icon( drag: Res, @@ -77,32 +112,39 @@ fn update_cursor_icon( cameras: Query<(&Camera, &GlobalTransform)>, layout: Option>, game: Option>, + button_q: Query<&Interaction, With