Refactor vertical card stacking with clip containers for variable zone sizes (#6774)

* Refactor vertical card stacking with opt-in overflow for variable zone sizes

Introduce a shared vertical stacking layout system in SelectZone that replaces the old divideCardSpaceInZone() free function with structured layout computation (StackLayoutParams, ZoneLayout, computeZoneLayout).

By default, cards are guaranteed to fit within zone bounds (no overflow). Zones can opt-in to bottom overflow via allowBottomOverflow flag, with sqrt-scaled compression for smooth visual transitions. A clip container mechanism is available for future zones that need visual clipping.

  Key changes:
  - SelectZone: new layout engine with allowBottomOverflow opt-in; clip container infrastructure for future zones needing visual clipping
  - StackZone: uses new layout (no overflow); adds setHeight() for dynamic resizing capabilities
  - HandZone: vertical layout delegates to SelectZone's shared stacking
  - AbstractCardItem: preserves hover z-value during layout passes; invalidates scene rect on hover exit for proper sibling repainting
  - CardZone::onCardAdded made virtual for clip container reparenting
  - Zone widths updated to CardDimensions::WIDTH_F * 1.5

* Changed anonymous namespace for static and braced functions

* CI tests re-run
This commit is contained in:
DawnFire42 2026-05-10 22:10:14 -04:00 committed by GitHub
parent 5735a44a9a
commit 1a62f82aee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 402 additions and 111 deletions

View file

@ -1,7 +1,7 @@
/**
* @file select_zone.h
* @ingroup GameGraphicsZones
* @brief TODO: Document this.
* @brief Base class for zones where cards are laid out and individually interactable.
*/
#ifndef SELECTZONE_H
@ -10,6 +10,9 @@
#include "card_zone.h"
#include <QSet>
#include <optional>
class QGraphicsRectItem;
/**
* A CardZone where the cards are laid out, with each card directly interactable by clicking.
@ -17,19 +20,113 @@
class SelectZone : public CardZone
{
Q_OBJECT
public:
/// 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(CardItem *addedCard) 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<CardItem *> cardsInSelectionRect;
/// 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 --
/// 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)
/// 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;
};
/// 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
};
/// 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 &params);
/// Builds StackLayoutParams from the current card list and zone geometry.
StackLayoutParams buildStackParams(qreal minOffset = 0.0) const;
/// 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 &params);
// -- 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.
/// 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();
/// 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<qreal> zValue = std::nullopt);
/// 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;
public:
SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr);
};
qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse = false);
#endif