mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
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
This commit is contained in:
parent
338c56678a
commit
4c93ebf14d
12 changed files with 402 additions and 111 deletions
|
|
@ -85,7 +85,11 @@ const CardInfo &AbstractCardItem::getCardInfo() const
|
|||
void AbstractCardItem::setRealZValue(qreal _zValue)
|
||||
{
|
||||
realZValue = _zValue;
|
||||
setZValue(_zValue);
|
||||
// During hover, zValue is overridden to HOVERED_CARD. Layout operations
|
||||
// like reorganizeCards() call setRealZValue() on all cards including the
|
||||
// hovered one — skip setZValue() here to avoid clobbering the override.
|
||||
if (!isHovered)
|
||||
setZValue(_zValue);
|
||||
}
|
||||
|
||||
QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
||||
|
|
@ -213,8 +217,16 @@ void AbstractCardItem::setHovered(bool _hovered)
|
|||
if (isHovered == _hovered)
|
||||
return;
|
||||
|
||||
if (_hovered)
|
||||
if (_hovered) {
|
||||
processHoverEvent();
|
||||
} else {
|
||||
// Mark the hovered card's current scene footprint dirty so overlapped
|
||||
// sibling zones (e.g. StackZone) repaint after the card moves away.
|
||||
if (scene()) {
|
||||
scene()->update(sceneBoundingRect());
|
||||
}
|
||||
}
|
||||
|
||||
isHovered = _hovered;
|
||||
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
|
||||
setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file abstract_card_item.h
|
||||
* @ingroup GameGraphicsCards
|
||||
* @brief TODO: Document this.
|
||||
* @brief Base class for graphical card items, providing shared rendering, identity, and interaction logic.
|
||||
*/
|
||||
|
||||
#ifndef ABSTRACTCARDITEM_H
|
||||
|
|
@ -96,6 +96,10 @@ public:
|
|||
}
|
||||
void setRealZValue(qreal _zValue);
|
||||
void setHovered(bool _hovered);
|
||||
bool getIsHovered() const
|
||||
{
|
||||
return isHovered;
|
||||
}
|
||||
QString getColor() const
|
||||
{
|
||||
return color;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "phases_toolbar.h"
|
||||
#include "player/player.h"
|
||||
#include "player/player_graphics_item.h"
|
||||
#include "zones/select_zone.h"
|
||||
#include "zones/view_zone.h"
|
||||
#include "zones/view_zone_widget.h"
|
||||
|
||||
|
|
@ -344,12 +345,26 @@ void GameScene::updateHover(const QPointF &scenePos)
|
|||
void GameScene::updateHoveredCard(CardItem *newCard)
|
||||
{
|
||||
if (hoveredCard && (newCard != hoveredCard))
|
||||
hoveredCard->setHovered(false);
|
||||
endCardHover(hoveredCard);
|
||||
if (newCard && (newCard != hoveredCard))
|
||||
newCard->setHovered(true);
|
||||
beginCardHover(newCard);
|
||||
hoveredCard = newCard;
|
||||
}
|
||||
|
||||
void GameScene::beginCardHover(CardItem *card)
|
||||
{
|
||||
card->setHovered(true);
|
||||
if (auto *zone = SelectZone::findOwningSelectZone(card))
|
||||
zone->escapeClipForHover(card);
|
||||
}
|
||||
|
||||
void GameScene::endCardHover(CardItem *card)
|
||||
{
|
||||
if (auto *zone = SelectZone::findOwningSelectZone(card))
|
||||
zone->restoreClipAfterHover(card);
|
||||
card->setHovered(false);
|
||||
}
|
||||
|
||||
CardZone *GameScene::findTopmostZone(const QList<QGraphicsItem *> &items)
|
||||
{
|
||||
for (QGraphicsItem *item : items)
|
||||
|
|
@ -484,6 +499,8 @@ bool GameScene::event(QEvent *event)
|
|||
{
|
||||
if (event->type() == QEvent::GraphicsSceneMouseMove)
|
||||
updateHover(static_cast<QGraphicsSceneMouseEvent *>(event)->scenePos());
|
||||
else if (event->type() == QEvent::Leave)
|
||||
updateHoveredCard(nullptr);
|
||||
|
||||
return QGraphicsScene::event(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ private:
|
|||
*/
|
||||
void updateHover(const QPointF &scenePos);
|
||||
|
||||
/// Activates hover state and escapes the card from its clip container so hover scaling is visible beyond zone
|
||||
/// bounds.
|
||||
void beginCardHover(CardItem *card);
|
||||
/// Deactivates hover state and restores the card to its clip container.
|
||||
void endCardHover(CardItem *card);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the GameScene.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent)
|
|||
void CardZone::onCardAdded(CardItem *addedCard)
|
||||
{
|
||||
addedCard->setParentItem(this);
|
||||
addedCard->setVisible(true);
|
||||
addedCard->update();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file card_zone.h
|
||||
* @ingroup GameGraphicsZones
|
||||
* @brief TODO: Document this.
|
||||
* @brief Base graphics item for zones that contain cards.
|
||||
*/
|
||||
|
||||
#ifndef CARDZONE_H
|
||||
|
|
@ -40,7 +40,10 @@ protected:
|
|||
}
|
||||
public slots:
|
||||
bool showContextMenu(const QPoint &screenPos);
|
||||
void onCardAdded(CardItem *addedCard);
|
||||
/// @brief Called when a card is added to this zone. Default: reparents card to this item.
|
||||
/// Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal
|
||||
/// connection in CardZone's constructor dispatches through the vtable.
|
||||
virtual void onCardAdded(CardItem *addedCard);
|
||||
|
||||
public:
|
||||
enum
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
CardZoneLogic *startZone,
|
||||
const QPoint &dropPoint)
|
||||
{
|
||||
if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPoint point = dropPoint + scenePos().toPoint();
|
||||
int x = -1;
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
|
|
@ -34,9 +38,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x())
|
||||
break;
|
||||
} else {
|
||||
for (x = 0; x < getLogic()->getCards().size(); x++)
|
||||
if (point.y() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().y())
|
||||
break;
|
||||
x = calcDropIndexFromY(dropPoint.y());
|
||||
}
|
||||
|
||||
Command_MoveCard cmd;
|
||||
|
|
@ -58,7 +60,7 @@ QRectF HandZone::boundingRect() const
|
|||
if (SettingsCache::instance().getHorizontalHand())
|
||||
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
|
||||
else
|
||||
return QRectF(0, 0, 100, zoneHeight);
|
||||
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
|
||||
}
|
||||
|
||||
void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
|
|
@ -78,35 +80,31 @@ void HandZone::reorganizeCards()
|
|||
qreal totalWidth =
|
||||
leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding;
|
||||
|
||||
for (int i = 0; i < cardCount; i++) {
|
||||
CardItem *c = getLogic()->getCards().at(i);
|
||||
// If the total width of the cards is smaller than the available width,
|
||||
// the cards do not need to overlap and are displayed in the center of the area.
|
||||
if (cardWidth * cardCount > totalWidth)
|
||||
c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5);
|
||||
else {
|
||||
qreal xPosition =
|
||||
leftJustified ? xPadding + ((qreal)i) * cardWidth
|
||||
: xPadding + ((qreal)i) * cardWidth + (totalWidth - cardCount * cardWidth) / 2;
|
||||
c->setPos(xPosition, 5);
|
||||
if (cardCount == 1) {
|
||||
CardItem *c = getLogic()->getCards().at(0);
|
||||
qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2;
|
||||
c->setPos(xPosition, 5);
|
||||
c->setRealZValue(0);
|
||||
} else {
|
||||
for (int i = 0; i < cardCount; i++) {
|
||||
CardItem *c = getLogic()->getCards().at(i);
|
||||
// If the total width of the cards is smaller than the available width,
|
||||
// the cards do not need to overlap and are displayed in the center of the area.
|
||||
if (cardWidth * cardCount > totalWidth)
|
||||
c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5);
|
||||
else {
|
||||
qreal xPosition = leftJustified ? xPadding + ((qreal)i) * cardWidth
|
||||
: xPadding + ((qreal)i) * cardWidth +
|
||||
(totalWidth - cardCount * cardWidth) / 2;
|
||||
c->setPos(xPosition, 5);
|
||||
}
|
||||
c->setRealZValue(i);
|
||||
}
|
||||
c->setRealZValue(i);
|
||||
}
|
||||
} else {
|
||||
qreal totalWidth = boundingRect().width();
|
||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
||||
qreal xspace = 5;
|
||||
qreal x1 = xspace;
|
||||
qreal x2 = totalWidth - xspace - cardWidth;
|
||||
|
||||
for (int i = 0; i < cardCount; i++) {
|
||||
CardItem *card = getLogic()->getCards().at(i);
|
||||
qreal x = (i % 2) ? x2 : x1;
|
||||
qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(),
|
||||
getLogic()->getCards().at(0)->boundingRect().height());
|
||||
card->setPos(x, y);
|
||||
card->setRealZValue(i);
|
||||
}
|
||||
// No clip container: hand cards should always be visible to the player.
|
||||
const auto params = buildStackParams();
|
||||
layoutCardsVertically(params);
|
||||
}
|
||||
}
|
||||
update();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file hand_zone.h
|
||||
* @ingroup GameGraphicsZones
|
||||
* @brief TODO: Document this.
|
||||
* @brief Graphical zone for the player's hand, supporting horizontal and vertical layouts.
|
||||
*/
|
||||
|
||||
#ifndef HANDZONE_H
|
||||
|
|
@ -14,7 +14,8 @@ class HandZone : public SelectZone
|
|||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
qreal width, zoneHeight;
|
||||
qreal width = 0.0;
|
||||
qreal zoneHeight;
|
||||
private slots:
|
||||
void updateBg();
|
||||
public slots:
|
||||
|
|
|
|||
|
|
@ -4,38 +4,207 @@
|
|||
#include "../board/card_item.h"
|
||||
#include "../game_scene.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGraphicsRectItem>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QtMath>
|
||||
|
||||
qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse)
|
||||
namespace
|
||||
{
|
||||
qreal cardMinOverlap = cardHeight * SettingsCache::instance().getStackCardOverlapPercent() / 100;
|
||||
qreal desiredHeight = cardHeight * cardCount - cardMinOverlap * (cardCount - 1);
|
||||
qreal y;
|
||||
if (desiredHeight > totalHeight) {
|
||||
if (reverse) {
|
||||
y = index / ((totalHeight - cardHeight) / (cardCount - 1));
|
||||
qreal stackingOffset(qreal cardHeight)
|
||||
{
|
||||
const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent();
|
||||
return cardHeight * (100.0 - overlapPercent) / 100.0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams ¶ms)
|
||||
{
|
||||
if (params.cardCount <= 0) {
|
||||
return {0.0, 0.0};
|
||||
}
|
||||
qreal effectiveOffset = params.desiredOffset;
|
||||
if (params.cardCount > 1) {
|
||||
qreal fitOffset;
|
||||
if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) {
|
||||
// Zone is shorter than a card (e.g. minimized). Compress offsets so
|
||||
// every card has at least minOffset pixels of its top visible.
|
||||
fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1);
|
||||
effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset));
|
||||
} else {
|
||||
y = index * (totalHeight - cardHeight) / (cardCount - 1);
|
||||
}
|
||||
} else {
|
||||
qreal start = (totalHeight - desiredHeight) / 2;
|
||||
if (reverse) {
|
||||
if (index <= start) {
|
||||
return 0;
|
||||
qreal reservedForBottomCard;
|
||||
if (params.allowBottomOverflow) {
|
||||
// Allow the bottom card to partially overflow in tight zones, scaling the
|
||||
// overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
|
||||
// as cards are added rather than dropping by 1/(n-1) each time.
|
||||
// The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
|
||||
constexpr qreal bottomCardZoneRatio = 0.75;
|
||||
const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast<qreal>(params.cardCount - 1));
|
||||
reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
|
||||
} else {
|
||||
// No overflow: reserve full card height for the bottom card
|
||||
reservedForBottomCard = params.cardHeight;
|
||||
}
|
||||
y = (index - start) / (cardHeight - cardMinOverlap);
|
||||
} else {
|
||||
y = index * (cardHeight - cardMinOverlap) + start;
|
||||
fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
|
||||
effectiveOffset = qMax(params.minOffset, qMin(params.desiredOffset, fitOffset));
|
||||
}
|
||||
}
|
||||
return y;
|
||||
qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;
|
||||
qreal start = (stackHeight <= params.totalHeight) ? (params.totalHeight - stackHeight) / 2.0 : 0.0;
|
||||
return {effectiveOffset, start};
|
||||
}
|
||||
|
||||
SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card)
|
||||
{
|
||||
QGraphicsItem *parent = card ? card->parentItem() : nullptr;
|
||||
if (!parent)
|
||||
return nullptr;
|
||||
// Card may be direct child of zone (escaped for hover) or child of clip container.
|
||||
if (auto *zone = dynamic_cast<SelectZone *>(parent))
|
||||
return zone;
|
||||
if (auto *zone = dynamic_cast<SelectZone *>(parent->parentItem()))
|
||||
return zone;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const
|
||||
{
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (cards.isEmpty())
|
||||
return {0, boundingRect().height(), 0.0, 0.0, minOffset};
|
||||
const auto cardCount = static_cast<int>(cards.size());
|
||||
const qreal cardHeight = cards.at(0)->boundingRect().height();
|
||||
const qreal offset = stackingOffset(cardHeight);
|
||||
return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
|
||||
}
|
||||
|
||||
int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
||||
{
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (cards.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
const auto params = buildStackParams(minOffset);
|
||||
auto [effectiveOffset, start] = computeZoneLayout(params);
|
||||
if (effectiveOffset <= 0.0) {
|
||||
return 0;
|
||||
}
|
||||
return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1);
|
||||
}
|
||||
|
||||
void SelectZone::restoreStaleEscapedCards()
|
||||
{
|
||||
if (!cardClipContainer)
|
||||
return;
|
||||
for (auto *card : getLogic()->getCards()) {
|
||||
// A card parented to the zone (instead of the clip container) should
|
||||
// only occur while it is actively hovered. If hover cleanup was
|
||||
// missed, reparent it back so clipping resumes.
|
||||
if (card && card->parentItem() == this && !card->getIsHovered()) {
|
||||
card->setParentItem(cardClipContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms)
|
||||
{
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (cards.isEmpty() || params.cardCount <= 0)
|
||||
return;
|
||||
if (params.cardCount > cards.size())
|
||||
return;
|
||||
|
||||
constexpr qreal xspace = 5;
|
||||
const qreal cardWidth = cards.at(0)->boundingRect().width();
|
||||
const qreal totalWidth = boundingRect().width();
|
||||
const qreal x1 = xspace;
|
||||
const qreal x2 = totalWidth - xspace - cardWidth;
|
||||
const qreal xCentered = (totalWidth - cardWidth) / 2.0;
|
||||
|
||||
auto [effectiveOffset, start] = computeZoneLayout(params);
|
||||
for (int i = 0; i < params.cardCount; i++) {
|
||||
CardItem *card = cards.at(i);
|
||||
qreal y = start + i * effectiveOffset;
|
||||
// Center single card; alternate left/right for multiple cards
|
||||
qreal x = (params.cardCount == 1) ? xCentered : ((i % 2) ? x2 : x1);
|
||||
card->setPos(x, y);
|
||||
card->setRealZValue(i);
|
||||
}
|
||||
}
|
||||
|
||||
SelectZone::SelectZone(CardZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
|
||||
{
|
||||
}
|
||||
|
||||
SelectZone::~SelectZone()
|
||||
{
|
||||
if (cardClipContainer) {
|
||||
// Reparent any hover-escaped cards back to the clip container so Qt's
|
||||
// parent-child tree is consistent for destruction. setParentItem() does
|
||||
// not invalidate getLogic()->getCards() (it modifies the graphics tree,
|
||||
// not the zone's logical card list).
|
||||
for (auto *card : getLogic()->getCards()) {
|
||||
if (card && card->parentItem() == this) {
|
||||
card->setParentItem(cardClipContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::onCardAdded(CardItem *addedCard)
|
||||
{
|
||||
if (cardClipContainer && addedCard) {
|
||||
addedCard->setParentItem(cardClipContainer);
|
||||
addedCard->setVisible(true);
|
||||
addedCard->update();
|
||||
} else {
|
||||
CardZone::onCardAdded(addedCard);
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::setupClipContainer(std::optional<qreal> zValue)
|
||||
{
|
||||
if (cardClipContainer)
|
||||
return;
|
||||
|
||||
setFlag(QGraphicsItem::ItemClipsChildrenToShape, false);
|
||||
|
||||
cardClipContainer = new QGraphicsRectItem(this); // Owned by Qt parent-child tree; deleted with this zone.
|
||||
cardClipContainer->setFlag(QGraphicsItem::ItemClipsChildrenToShape, true);
|
||||
cardClipContainer->setPen(Qt::NoPen);
|
||||
cardClipContainer->setBrush(Qt::NoBrush);
|
||||
cardClipContainer->setRect(boundingRect());
|
||||
if (zValue.has_value()) {
|
||||
cardClipContainer->setZValue(*zValue);
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::escapeClipForHover(QGraphicsItem *card)
|
||||
{
|
||||
// Reparent from clip container to zone so the hover-scaled card is visible
|
||||
// beyond clip bounds. Coordinates are identical because the clip container
|
||||
// is at (0,0) with no transform relative to this zone.
|
||||
if (cardClipContainer && card && card->parentItem() == cardClipContainer) {
|
||||
card->setParentItem(this);
|
||||
cardClipContainer->update();
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::restoreClipAfterHover(QGraphicsItem *card)
|
||||
{
|
||||
// Restore card to clip container. If card's parent is not this zone,
|
||||
// a zone transition already reparented it via onCardAdded — skip.
|
||||
if (cardClipContainer && card && card->parentItem() == this) {
|
||||
card->setParentItem(cardClipContainer);
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::updateClipRect()
|
||||
{
|
||||
if (cardClipContainer) {
|
||||
cardClipContainer->setRect(boundingRect());
|
||||
}
|
||||
}
|
||||
|
||||
void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (event->buttons().testFlag(Qt::LeftButton)) {
|
||||
|
|
@ -56,7 +225,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
|||
continue;
|
||||
}
|
||||
|
||||
bool inRect = selectionRect.intersects(card->mapRectToParent(card->boundingRect()));
|
||||
bool inRect = selectionRect.intersects(card->mapRectToItem(this, card->boundingRect()));
|
||||
if (inRect && !cardsInSelectionRect.contains(card)) {
|
||||
// selection has just expanded to cover the card
|
||||
cardsInSelectionRect.insert(card);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
qreal totalHeight;
|
||||
qreal cardHeight;
|
||||
qreal desiredOffset;
|
||||
qreal minOffset = 0.0;
|
||||
/// 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 ¶ms);
|
||||
|
||||
/// 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 ¶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.
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
#include "stack_zone.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../interface/theme_manager.h"
|
||||
#include "../board/arrow_item.h"
|
||||
#include "../board/card_drag_item.h"
|
||||
#include "../board/card_item.h"
|
||||
#include "../card_dimensions.h"
|
||||
#include "../player/player.h"
|
||||
#include "../player/player_actions.h"
|
||||
#include "logic/stack_zone_logic.h"
|
||||
|
|
@ -27,7 +26,7 @@ void StackZone::updateBg()
|
|||
|
||||
QRectF StackZone::boundingRect() const
|
||||
{
|
||||
return {0, 0, 100, zoneHeight};
|
||||
return {0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight};
|
||||
}
|
||||
|
||||
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
|
|
@ -40,7 +39,15 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
CardZoneLogic *startZone,
|
||||
const QPoint &dropPoint)
|
||||
{
|
||||
if (startZone == nullptr || startZone->getPlayer() == nullptr) {
|
||||
if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||
|
||||
// Same-zone no-op: don't move a card onto itself
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -49,36 +56,12 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
cmd.set_start_zone(startZone->getName().toStdString());
|
||||
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
|
||||
cmd.set_target_zone(getLogic()->getName().toStdString());
|
||||
|
||||
int index = 0;
|
||||
|
||||
if (!getLogic()->getCards().isEmpty()) {
|
||||
const auto cardCount = static_cast<int>(getLogic()->getCards().size());
|
||||
const auto &card = getLogic()->getCards().at(0);
|
||||
|
||||
index = qRound(divideCardSpaceInZone(dropPoint.y(), cardCount, boundingRect().height(),
|
||||
card->boundingRect().height(), true));
|
||||
|
||||
// divideCardSpaceInZone is not guaranteed to return a valid index
|
||||
// currently, so clamp it to avoid crashes.
|
||||
index = qBound(0, index, cardCount - 1);
|
||||
|
||||
if (startZone == getLogic()) {
|
||||
const auto &dragItem = dragItems.at(0);
|
||||
const auto &card = getLogic()->getCards().at(index);
|
||||
|
||||
if (card->getId() == dragItem->getId()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.set_x(index);
|
||||
cmd.set_y(0);
|
||||
|
||||
for (CardDragItem *item : dragItems) {
|
||||
for (const CardDragItem *item : dragItems) {
|
||||
if (item) {
|
||||
auto cardToMove = cmd.mutable_cards_to_move()->add_card();
|
||||
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
|
||||
cardToMove->set_card_id(item->getId());
|
||||
if (item->isForceFaceDown()) {
|
||||
cardToMove->set_face_down(true);
|
||||
|
|
@ -89,24 +72,22 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
|
||||
}
|
||||
|
||||
void StackZone::setHeight(qreal newHeight)
|
||||
{
|
||||
if (qFuzzyCompare(1.0 + zoneHeight, 1.0 + newHeight)) {
|
||||
return;
|
||||
}
|
||||
prepareGeometryChange();
|
||||
zoneHeight = newHeight;
|
||||
reorganizeCards();
|
||||
update();
|
||||
}
|
||||
|
||||
void StackZone::reorganizeCards()
|
||||
{
|
||||
if (!getLogic()->getCards().isEmpty()) {
|
||||
const auto cardCount = static_cast<int>(getLogic()->getCards().size());
|
||||
qreal totalWidth = boundingRect().width();
|
||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
||||
qreal xspace = 5;
|
||||
qreal x1 = xspace;
|
||||
qreal x2 = totalWidth - xspace - cardWidth;
|
||||
|
||||
for (int i = 0; i < cardCount; i++) {
|
||||
CardItem *card = getLogic()->getCards().at(i);
|
||||
qreal x = (i % 2) ? x2 : x1;
|
||||
qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(),
|
||||
getLogic()->getCards().at(0)->boundingRect().height());
|
||||
card->setPos(x, y);
|
||||
card->setRealZValue(i);
|
||||
}
|
||||
const auto params = buildStackParams(MIN_CARD_VISIBLE);
|
||||
layoutCardsVertically(params);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file stack_zone.h
|
||||
* @ingroup GameGraphicsZones
|
||||
* @brief TODO: Document this.
|
||||
* @brief Graphical zone for the stack, displaying cards in a vertical pile.
|
||||
*/
|
||||
|
||||
#ifndef STACKZONE_H
|
||||
|
|
@ -20,6 +20,8 @@ private slots:
|
|||
|
||||
public:
|
||||
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
|
||||
/// @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone.
|
||||
void setHeight(qreal newHeight);
|
||||
void
|
||||
handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;
|
||||
QRectF boundingRect() const override;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue