From 692edb30ddaa327a360df37a3d7728141c259440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 1 Nov 2026 03:58:59 +0100 Subject: [PATCH] [Game] Repaint cached zones when the hover zoom moves `TableZone` cuts the hovered card out of its own paint so zone washes, indicators, dimming and the land divider never draw over the zoom. It is a `DeviceCoordinateCache` item, so the card changing geometry never dirtied it and the stale hole stayed transparent, uncovering the black viewport as soon as the zoom crossed a zone edge. - Emit `hoverFootprintChanged(old, new)` from `setCardScale()` and once from `setHovered(false)`. - Repaint every overlapping `TableZone` on that signal. - Clip with the card's painted outline instead of its bounding rectangle, and treat a zoom covering the whole zone as "paint nothing". --- .../board/abstract_card_item.cpp | 13 +++++- .../game_graphics/board/abstract_card_item.h | 9 ++++ cockatrice/src/game_graphics/game_scene.cpp | 42 +++++++++++++----- cockatrice/src/game_graphics/game_scene.h | 9 ++++ .../src/game_graphics/zones/table_zone.cpp | 43 ++++++++++++------- .../src/game_graphics/zones/table_zone.h | 10 ++++- 6 files changed, 97 insertions(+), 29 deletions(-) diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 064b74932..9cd0371c9 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -290,7 +290,11 @@ void AbstractCardItem::setHovered(bool _hovered) // 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()); + const QRectF footprint = sceneBoundingRect(); + scene()->update(footprint); + // The hole disappears the moment the card is no longer hovered, even though the + // card has not changed size yet: tell the zones so they repaint it shut. + emit hoverFootprintChanged(footprint, footprint); } } @@ -354,8 +358,13 @@ void AbstractCardItem::setCardScale(qreal _scale) const QRectF oldFootprint = sceneBoundingRect(); prepareGeometryChange(); setScale(_scale); + const QRectF newFootprint = sceneBoundingRect(); if (scene()) { - scene()->update(oldFootprint.united(sceneBoundingRect())); + scene()->update(oldFootprint.united(newFootprint)); + // Marking the scene region dirty is not enough for cached items: a zone that cuts a + // hover hole out of itself keeps the hole from its last repaint, which then shows as + // an uncovered black rectangle under the zoom. + emit hoverFootprintChanged(oldFootprint, newFootprint); } else { update(); } diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h index 9f506fb52..ddedbb163 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -66,6 +66,15 @@ signals: void faceDownChanged(bool faceDown); /** @brief Emitted when the hover shrink-out animation runs to completion while unhovered. */ void hoverShrinkFinished(AbstractCardItem *card); + /** + * @brief Emitted when the hover zoom changes this card's scene footprint. + * + * Zones (and anything else that dodges the zoomed card) draw from the live footprint at + * paint time but are cached: they need this to invalidate, or a stale hole is left behind. + * Also emitted once when the hover ends, with both rects equal, because the hole + * disappears without the card changing size. + */ + void hoverFootprintChanged(const QRectF &oldFootprint, const QRectF &newFootprint); public: enum diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 65299fb83..e87411bb0 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -15,6 +15,7 @@ #include "z_values.h" #include "zones/card_zone.h" #include "zones/select_zone.h" +#include "zones/table_zone.h" #include "zones/view_zone.h" #include "zones/view_zone_widget.h" @@ -860,23 +861,20 @@ void GameScene::updateHoveredCard(CardItem *newCard) } return; } - if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->getCardItem() == newCard) { + if (cardHoverWidget && cardHoverWidget->isVisible() && !cardHoverWidget->isClosing() && + cardHoverWidget->getCardItem() == newCard) { // Panel open but zoom dropped (e.g. survived a Leave) — zoom back and refresh. beginCardHover(newCard); hoveredCard = newCard; cardHoverWidget->refreshDisplay(); return; } - if (cardHoverWidget && cardHoverWidget->isVisible()) { - // Panel open elsewhere — move the zoom at once so it never strands, but let - // the panel follow after the configured delay instead of strobing open on - // every card swept across. - beginCardHover(newCard); - hoveredCard = newCard; + if (cardHoverWidget && cardHoverWidget->isVisible() && !cardHoverWidget->isClosing()) { + // A different card is hovered while the panel is up: close the old panel + // first, then fall through to first contact so the new card follows the + // normal dwell instead of the old panel trailing the cursor. + hideCardHoverPopup(); confirmArmedCard.clear(); - hoverPanelTarget = newCard; - hoverPanelTimer->start(display.getHoverPanelDelayMs()); - return; } if (zoomTrigger != CardsDisplaySettings::HoverPanelTriggerHover && panelTrigger != CardsDisplaySettings::HoverPanelTriggerHover) { @@ -944,6 +942,8 @@ void GameScene::beginCardHover(CardItem *card) liftOwner = card; connect(card, &AbstractCardItem::hoverShrinkFinished, this, &GameScene::onHoverShrinkFinished, Qt::UniqueConnection); + connect(card, &AbstractCardItem::hoverFootprintChanged, this, &GameScene::onHoverFootprintChanged, + Qt::UniqueConnection); } void GameScene::endCardHover(CardItem *card) @@ -971,6 +971,25 @@ void GameScene::onHoverShrinkFinished(AbstractCardItem *card) restoreHoverLift(); } +void GameScene::onHoverFootprintChanged(const QRectF &oldFootprint, const QRectF &newFootprint) +{ + const QRectF dirty = oldFootprint.united(newFootprint); + if (dirty.isEmpty()) { + return; + } + // TableZone cuts the hovered card out of its own paint, but it is cached: the card + // changing size does not dirty the zone, so the previous hole would stay transparent in + // the cache — an uncovered black rectangle under the zoom whenever the card grows across + // a zone boundary. Nudge only the zones the footprint touches; the scene repaint itself + // is already scheduled by the card. + const auto overlapping = items(dirty, Qt::IntersectsItemBoundingRect); + for (QGraphicsItem *item : overlapping) { + if (auto *zone = qobject_cast(item->toGraphicsObject())) { + zone->update(); + } + } +} + void GameScene::restoreHoverLift() { for (const auto &[lifted, zValue] : hoverLiftedAncestors) { @@ -1170,7 +1189,8 @@ void GameScene::fireTriggers(CardItem *card, bool zoomFired, bool panelFired) return; } - if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->getCardItem() == card) { + if (cardHoverWidget && cardHoverWidget->isVisible() && !cardHoverWidget->isClosing() && + cardHoverWidget->getCardItem() == card) { cardHoverWidget->refreshDisplay(); return; } diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h index 54185e4b3..addff79c0 100644 --- a/cockatrice/src/game_graphics/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -126,6 +127,14 @@ private: void restoreHoverLift(); /** @brief Settles a finished shrink-out: clip container and ancestor lift restore. */ void onHoverShrinkFinished(AbstractCardItem *card); + /** + * @brief Repaints the zones that dodge the zoomed card when its footprint moves. + * + * Those zones cut the hovered card out of their paint, but they are cached: another + * item's geometry change does not dirty them, so without this the previous hole stays + * transparent in the cache and a black rectangle shows under the zoom. + */ + void onHoverFootprintChanged(const QRectF &oldFootprint, const QRectF &newFootprint); // Card hover popup methods void showCardHoverPopup(CardItem *card); diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index c16d7ba87..dd2b3e38e 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #include namespace google @@ -121,10 +123,14 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti // The hovered (zoomed) card may spill over this zone: cut it out of every layer // below so washes, indicators, dimming and the land divider never paint over it, // whatever the stacking order is. - const QRect hoverHole = hoveredCardHole(); - const bool hasHole = !hoverHole.isNull(); - if (hasHole) { - painter->setClipRegion(QRegion(boundingRect().toRect()) - QRegion(hoverHole)); + const std::optional hoverHole = hoveredCardHole(); + const bool clipped = hoverHole.has_value(); + if (clipped && hoverHole->isEmpty()) { + // The zoom covers this zone entirely: there is nothing left of it to paint. + return; + } + if (clipped) { + painter->setClipPath(*hoverHole); } if (playmatActive) { @@ -152,31 +158,38 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti paintLandDivider(painter); - if (hasHole) { + if (clipped) { painter->setClipping(false); } } /** - * @brief The hovered card's footprint in zone coordinates, or null when it does not - * overlap this zone. Lets every paint layer dodge the zoomed card instead of painting - * over it. + * @brief This zone's paint area minus the hovered card's painted outline, in zone coordinates. + * + * @c std::nullopt when no card is hovered or it does not overlap this zone; an *empty* path when + * the zoom covers this zone entirely, which means nothing of it is left to paint. Lets every + * paint layer dodge the zoomed card instead of painting over it. */ -QRect TableZone::hoveredCardHole() const +std::optional TableZone::hoveredCardHole() const { auto *scene = qobject_cast(this->scene()); if (!scene) { - return {}; + return std::nullopt; } CardItem *hovered = scene->getHoveredCard(); if (!hovered) { - return {}; + return std::nullopt; } - QRect hole = mapRectFromScene(hovered->sceneBoundingRect()).toRect(); - if (!hole.intersects(boundingRect().toRect())) { - return {}; + // Cut out the card's painted outline, not its bounding rectangle: the rectangle's corners + // are never covered by the artwork (rounded corners, antialiased edges), so they would stay + // black around the zoom. + const QPainterPath cardOutline = sceneTransform().inverted().map(hovered->sceneTransform().map(hovered->shape())); + if (!cardOutline.boundingRect().intersects(boundingRect())) { + return std::nullopt; } - return hole & boundingRect().toRect(); + QPainterPath zoneArea; + zoneArea.addRect(boundingRect()); + return zoneArea.subtracted(cardOutline); } void TableZone::onPlaymatChanged(bool active) diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index 7fbe57236..6beb54d30 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -18,6 +18,7 @@ #include #include #include +#include #include class CardDragItem; @@ -26,6 +27,7 @@ class CardZoneLogic; class QColor; class QGraphicsItem; class QPainter; +class QPainterPath; template class QList; /** @@ -221,7 +223,13 @@ private: void paintZoneOutline(QPainter *painter); void paintLandDivider(QPainter *painter); - [[nodiscard]] QRect hoveredCardHole() const; + /** + * @brief This zone's paint area minus the hovered card's painted outline, in zone coordinates. + * + * @c std::nullopt when no card is hovered or it does not overlap this zone; an *empty* path when + * the hovered card covers this zone entirely, in which case nothing of the zone is visible. + */ + [[nodiscard]] std::optional hoveredCardHole() const; /* Calculates card stack widths so mapping functions work properly