/** * @file select_zone.h * @ingroup GameGraphicsZones * @brief Base class for zones where cards are laid out and individually interactable. */ #ifndef SELECTZONE_H #define SELECTZONE_H #include "card_zone.h" #include #include class QGraphicsRectItem; /** * A CardZone where the cards are laid out, with each card directly interactable by clicking. */ class SelectZone : public CardZone { Q_OBJECT public: /** * @brief Finds the SelectZone that owns a card, regardless of whether the card is parented * to the zone directly or to its clip container. Returns nullptr if not in a SelectZone. */ static SelectZone *findOwningSelectZone(const QGraphicsItem *card); SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr); ~SelectZone() override; void onCardAdded(CardState *addedCard, int x, int y) override; /** * @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip * bounds. Safe no-op if no clip container exists. Coordinates are preserved (clip container is at (0,0) with no * transform). */ void escapeClipForHover(QGraphicsItem *card); /** * @brief Restores a hover-escaped card back to the clip container. Guards against zone transitions that already * reparented the card. */ void restoreClipAfterHover(QGraphicsItem *card); private: QPointF selectionOrigin; QSet cardsInSelectionRect; /** * @brief Invisible clipping parent for cards; owned by Qt parent-child tree (parented to this zone). * Created by setupClipContainer(); null when no clip container is active. */ QGraphicsRectItem *cardClipContainer = nullptr; protected: // -- Layout computation -- /** @brief Parameters describing a vertical card stack's geometry. */ struct StackLayoutParams { int cardCount; ///< Number of cards in the stack qreal totalHeight; ///< Available height for the stack (zone height) qreal cardHeight; ///< Height of a single card qreal desiredOffset; ///< Preferred vertical offset between card tops qreal minOffset = 0.0; ///< Minimum offset to preserve (0 allows full compression) /** * @brief When false (default), reserves full cardHeight for the bottom card, ensuring * all cards remain within zone bounds. When true, allows the bottom card to * partially overflow using sqrt-scaled allowance. Use with setupClipContainer() * for zones too short to fit a full card. */ bool allowBottomOverflow = false; }; /** @brief Result of computing a vertical stack layout. */ struct ZoneLayout { qreal effectiveOffset; ///< Actual offset between card tops (may be compressed) qreal start; ///< Y coordinate of the first card's top edge }; /** @brief Minimum visible pixels of each card's top edge when stacking compresses offsets in tight zones. */ static constexpr qreal MIN_CARD_VISIBLE = 10.0; /** * @brief Computes layout for a vertical card stack (effective offset and start position). * * Three regimes: * 1. Minimized zone (totalHeight < card height with minOffset > 0): offsets compress * so each card retains at least minOffset visible pixels of its top edge. * 2. Normal zone with allowBottomOverflow=false (default): the bottom card is * guaranteed to fit within the zone boundary. Offsets compress as needed. * 3. Normal zone with allowBottomOverflow=true: the bottom card may partially * overflow. The overflow allowance is scaled by sqrt(cardCount-1) so that * adding one card shifts existing cards smoothly. * * When the stack fits with room to spare, it is centered vertically. */ static ZoneLayout computeZoneLayout(const StackLayoutParams ¶ms); /** @brief Builds StackLayoutParams from the current card list and zone geometry. */ StackLayoutParams buildStackParams(qreal minOffset = 0.0) const; /** * @brief Computes the card index at a given y-coordinate within the zone's vertical layout. * Returns 0 if the zone has no cards or the offset is zero. */ int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const; /** * @brief Positions cards vertically with alternating left/right x-offsets. * * Cards alternate between left and right margins (5px padding from zone edges): * even-indexed cards at left, odd-indexed at right. * Cards are assigned ascending z-values. * * @param params Stack layout geometry parameters (use allowBottomOverflow to control overflow) */ void layoutCardsVertically(const StackLayoutParams ¶ms); // -- Clip container -- // The clip container mechanism is available for future zones that need visual clipping // (e.g., zones too short to fit a full card). To enable: call setupClipContainer() in the // zone's constructor, and set allowBottomOverflow=true in layout params. /** * @brief Restores any cards that were hover-escaped but whose hover state was not properly cleaned up. * Call at the start of reorganizeCards() in zones that use a clip container. */ void restoreStaleEscapedCards(); /** * @brief Creates a clip container child item that clips card overflow to zone bounds. * Cards entering this zone are reparented to this container by the onCardAdded override. * Disables zone-level child clipping; clipping is delegated to the container. * @param zValue Optional z-value for the clip container (e.g. ZValues::CARD_BASE) */ void setupClipContainer(std::optional zValue = std::nullopt); /** @brief Updates the clip container rect to match this zone's current boundingRect(). */ void updateClipRect(); void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; }; #endif