//! Cursor-icon feedback (#31) and drag drop-target highlighting (#32). //! //! **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. //! - **Default** (nearly transparent white) otherwise. //! The tint is cleared to default the frame the drag ends. //! //! **Drop-target overlays** (`update_drop_target_overlays`) //! Pile markers sit *behind* the card stack, so on a tableau column with //! any cards on it the green tint applied above is fully occluded. To //! make legal targets unmistakable mid-drag, this system spawns a //! translucent green rectangle plus four outline edges over every legal //! destination pile. For tableau columns the overlay covers the full //! visible fan (matching `input_plugin::pile_drop_rect`); for //! foundations and empty tableaux it is card-sized. Overlays are //! despawned the frame the drag ends or whenever the legal-target set //! changes. use bevy::prelude::*; use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon}; use solitaire_core::game_state::{DrawMode, GameState}; use solitaire_core::pile::PileType; use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau}; use crate::card_plugin::RightClickHighlight; use crate::layout::{Layout, LayoutResource}; use crate::resources::{DragState, GameStateResource}; use crate::table_plugin::{PileMarker, PILE_MARKER_DEFAULT_COLOUR}; use crate::ui_theme::{ DROP_TARGET_FILL, DROP_TARGET_OUTLINE, DROP_TARGET_OUTLINE_PX, Z_DROP_OVERLAY, }; /// Idle pile-marker tint — re-exported from `table_plugin` so the /// "valid drop" toggle in this plugin and the marker spawn in /// `table_plugin` cannot drift apart. Was previously a duplicated /// literal kept in sync via doc comment. const MARKER_DEFAULT: Color = PILE_MARKER_DEFAULT_COLOUR; /// Lime tint applied to pile markers that are valid drop targets during /// a drag. Same RGB as the design-system [`STATE_SUCCESS`] token at 55% /// alpha, so the in-game "this is a legal target" colour stays /// consistent with foundation-completion flourishes and other /// valid-move signals. Spelled as a literal because `Alpha::with_alpha` /// is not yet a `const` trait method on stable; the tracking test /// below pins the RGB to `STATE_SUCCESS` so a palette swap can't drift /// the two apart silently. Distinct from [`DROP_TARGET_FILL`] (10% /// alpha) because the marker sprite is thin and would otherwise wash /// out at a similar opacity. const MARKER_VALID: Color = Color::srgba(0.675, 0.761, 0.404, 0.55); /// Marker component on a parent entity that owns one drop-target overlay /// (a translucent fill plus four outline edges as children). The wrapped /// `PileType` identifies which pile this overlay highlights, so test /// queries and the despawn-on-target-change logic can filter by pile. #[derive(Component, Debug, Clone, PartialEq, Eq)] pub struct DropTargetOverlay(pub PileType); /// Renders a custom cursor sprite that follows the pointer and swaps to a grab-hand icon while a card drag is in progress. pub struct CursorPlugin; impl Plugin for CursorPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, ( update_cursor_icon, update_drop_highlights.run_if(resource_changed::), update_drop_target_overlays, ), ); } } // --------------------------------------------------------------------------- // #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, windows: Query<(Entity, &Window), With>, cameras: Query<(&Camera, &GlobalTransform)>, layout: Option>, game: Option>, button_q: Query<&Interaction, With