10 KiB
Card Row UX (target design)
This document describes the intended behavior for the next CardRow/CardControl UX iteration (presorted hand, external-drag box select, unified program row).
Sources
DonkeysAndDroids.Godot/CardRow.csDonkeysAndDroids.Godot/CardControl.csDonkeysAndDroids.Godot/card_row.tscnDonkeysAndDroids.Godot/program.tscnDonkeysAndDroids.Godot/ProgramScreen.csDonkeysAndDroids.Godot/improve.tscnDonkeysAndDroids.Godot/Improve.csDonkeysAndDroids.Godot/draw_glitch.tscnDonkeysAndDroids.Godot/DrawGlitch.csDonkeysAndDroids.Godot/gamble.tscnDonkeysAndDroids.Godot/Gamble.csDonkeysAndDroids.Godot/buffer_overflow.tscnDonkeysAndDroids.Godot/BufferOverflow.cs
Component model
- CardRow is the container that owns CardControls, maintains OrderedCards, layout, and drag/drop reordering. It exposes signals
SelectionChangedandOrderChanged. - CardControl is the per-card UI. It handles hover/selection/drag visuals and emits
CardDroppedOnandCardSelectionChangedsignals.
Card states and visuals
State triggers and visuals (applies when not Disabled unless noted):
- Idle: default; scale (1,1), pivot center, modulate (1,1,1), z-index 0.
- Hovered: mouse enters card while no GUI drag is active and (CanSelect or CanDrag) is true; scale (1.25,1.25), modulate (1.25,1.25,1.25), z-index 1.
- Selected:
Selected == true; pivot Y moves to bottom edge, scale (1.25,1.25). Tooltip panel is updated viaTooltip.Instance.Describe. - Drop target (before/after): during a valid drag over this card,
m_DropBeforeset based on cursor X; pivot X snaps to left or right edge, scale (1.25,1.25), alpha set to 0.5. - Dragging (source card): after drag starts, card is hidden by setting scale X to 0; hover and drop indicators do not apply.
State precedence (in UpdateState):
- Dragging overrides all other visuals.
- Drop target overrides pivot X and alpha (pivot Y from Selected may remain).
- Hover overrides scale/modulate.
- Selected sets pivot Y and scale.
All pivot/scale/modulate changes are tweened with short spring transitions.
Selection behavior
- Select toggle: left mouse press + release on a card toggles
SelectedifCanSelectand not Disabled. - External-drag box select: if the left mouse button is pressed outside any CardRow (not on a card/row) and the mouse is dragged, any hovered card in the first CardRow entered is force-selected (no toggle off) until release. This is additive unless
SupportsMultiSelectis false (in which case the most recently hovered card remains selected). - Clicking a selected card again deselects it (subject to capacity rules when deselecting; see Program screen).
- SupportsMultiSelect:
- If false, selecting a card automatically deselects any other selected cards in that row.
- If true, multiple cards can be toggled independently.
- SelectionContext is global: selecting a card in any row clears selection in the previously active row. Multi-row selection is not possible.
SelectionChangedemits an array of selected CardControls from the row. The array order is not guaranteed (dictionary order).- Tooltip:
- Hover uses Godot
TooltipTextset to card name + description. - Selection additionally calls
Tooltip.Instance.Describe(card).
- Hover uses Godot
Drag and drop behavior
Drag start
- Valid only when
CanDragis true and the card is not Disabled. - Drag payload is the original CardControl; drag preview is a duplicated CardControl configured with the same card and row.
- The source card becomes visually hidden (scale X = 0) until drag ends.
Multi-drag selection
SupportsMultiDragmust be true.- If multiple cards are selected and the dragged card is among them, all selected cards are dragged together.
- If multiple cards are selected but the dragged card is not selected, only the dragged card moves.
- If only one card is selected, only that card moves.
- Dragged card order is preserved as the order they appear in
OrderedCards.
Drop targets
- Row background (
CardRow._DropData) inserts at an index computed from cursor X relative to existing card positions. - Card target (
CardControl._DropData) inserts before/after the target card based on cursor X < card width/2.
Valid drop checks
A drop is valid only if all are true:
- Target row is not Disabled.
- Target row
CardsCanDrag == true. - Drag payload is a CardControl currently in a CardRow.
- If source row != target row, capacity check passes:
targetRow.OccupiedSpace + sum(dragged.OccupiedSpace) <= targetRow.MaxCards.
Additional invalid cases:
- Dropping a card onto itself (source == target) is ignored.
- Dropping onto a card that is part of the dragged set is ignored.
- If the dragged card cannot be resolved in its source row (no card id match), the drop is ignored.
Move results
- Reorder within the same row:
- Cards are removed and reinserted at the adjusted index (accounts for removed cards before the insert point).
- Selection state is preserved because existing CardControls are reused.
OrderChangedis emitted on that row.
- Move across rows:
- Cards are removed from the source list and inserted into the target list.
- New CardControls are created in the target row; selection state resets for moved cards.
OrderChangedis emitted on the target row only.
- All moves animate via tweened layout. During drag-triggered moves, new cards start at their target positions (no center-entry animation).
Row-to-row interaction rules (valid and invalid)
- Selecting a card in Row B always clears selection in Row A (global SelectionContext).
- External-drag box selection locks onto the first row entered; hovered cards in other rows are ignored until mouse release.
- Dragging from Row A to Row B is only possible if both rows have
CardsCanDrag = trueand are not Disabled. - Dropping on a row with
CardsCanDrag = falseis invalid (no acceptance, no drop indicator). - Dropping into a row that would exceed
MaxCardsis invalid (drop rejected). - Dragging within a row is always allowed when the row is draggable, regardless of capacity.
- Dragging while any GUI drag is active disables hover visuals on other cards (no hover scaling).
Layout and ordering
- Card positions are computed from
CardSize,CardSpacing, and the rowSize. - Non-wrap rows center the line of cards; spacing shrinks (can be negative) when there is insufficient width, causing overlap.
- Wrap rows (
WrapCards = true) break onto additional rows when the next card would overflow width and setCustomMinimumSizeto fit. GetSortedCardssorts by:- Rarity
- Card Id (ECard enum)
- Modifier count
- CardId (unique Guid)
Use cases by screen
Program row (Program screen, unified hand/tape)
Files: DonkeysAndDroids.Godot/program.tscn, DonkeysAndDroids.Godot/ProgramScreen.cs.
Row node: single CardRow replacing the split %Hand/%Tape.
Configuration:
- CardSize 80x100 (from
card_row.tscn). - CardSpacing 2.
- AnimationDuration 0.5.
- CardsCanDrag true, CardsCanSelect true.
- SupportsMultiSelect true, SupportsMultiDrag true.
MaxCardsis not used to enforce tape/hand capacity; capacity is enforced by selection rules.
Behavior:
- The row contains all cards from
CoreLoop.HandandCoreLoop.Tape. - Initial order on entry:
CoreLoop.Tape(in tape order) followed byGetSortedCards(CoreLoop.Hand)(hand pre-sorted). - Initial selection: cards from
CoreLoop.Tapeare Selected; cards fromCoreLoop.Handare Deselected. - Selection defines location:
- Selected cards count as Tape.
- Deselected cards count as Hand.
- Tape order is the order of selected cards in the row; hand order is the order of deselected cards in the row.
- Program row order is authoritative; results must not reorder existing cards.
HandResult/TapeResultare treated as membership sets (selection + add/remove only). - New cards not already in the row are appended after existing cards (tape cards in result order, then hand cards in result order).
- Dragging reorders the row only (no row-to-row drag). Multi-drag still reorders within the row.
- Selecting a card is only allowed if the resulting tape
OccupiedSpaceis<= TapeLength. - Deselecting a card is only allowed if the resulting hand
OccupiedSpaceis<= HandSize. - Discard operates on the selected set (selected cards can be discarded).
- Sort button re-sorts only the deselected (hand) cards and keeps selected (tape) cards fixed in place.
Shop (Improve screen)
Files: DonkeysAndDroids.Godot/improve.tscn, DonkeysAndDroids.Godot/Improve.cs.
Row node: %Shop (CardRow instance).
Configuration:
- CardSize 90x112.
- CardSpacing 5.
- CardsCanDrag false.
- CardsCanSelect true.
- SupportsMultiSelect true.
- SupportsMultiDrag false (default).
Behavior:
- Multiple cards can be selected for purchase; Buy button cost is the sum of selected cards.
- Selection is the only interaction; drag and drop are disabled.
- Selection persists within the row until cleared by clicking again or selecting another row.
Invalid interactions:
- Dragging from or dropping onto the shop row is not possible (CardsCanDrag false).
- Selecting cards in the deck preview clears shop selection.
Deck preview (Improve + DrawGlitch screens)
Files: DonkeysAndDroids.Godot/improve.tscn, DonkeysAndDroids.Godot/draw_glitch.tscn,
DonkeysAndDroids.Godot/Improve.cs, DonkeysAndDroids.Godot/DrawGlitch.cs.
Row nodes: %Deck in both scenes.
Configuration (Improve deck):
- CardsCanDrag false.
- WrapCards true.
- CardSpacing 15.
- CardsCanSelect true (default), SupportsMultiSelect false.
Configuration (DrawGlitch deck):
- CardsCanDrag false.
- WrapCards true.
- CardSpacing 10.
- CardsCanSelect true (default), SupportsMultiSelect false.
Behavior:
- Deck order is always the sorted order from
GetSortedCards. - Cards can be hovered and selected (single selection only), primarily for tooltip/inspect.
- No drag/drop or reordering.
Invalid interactions:
- Dragging from or dropping onto deck preview rows is not possible.
- Selecting a card here clears any selection in other rows.
Variants using the same CardRow mechanics (for completeness)
- Gamble (
DonkeysAndDroids.Godot/gamble.tscn):%Patchesrow uses CardsCanDrag false and single selection to pick exactly one card to add. - BufferOverflow (
DonkeysAndDroids.Godot/buffer_overflow.tscn):%Handrow uses CardsCanDrag false and single selection to pick exactly one card to remove.