# 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.cs` - `DonkeysAndDroids.Godot/CardControl.cs` - `DonkeysAndDroids.Godot/card_row.tscn` - `DonkeysAndDroids.Godot/program.tscn` - `DonkeysAndDroids.Godot/ProgramScreen.cs` - `DonkeysAndDroids.Godot/improve.tscn` - `DonkeysAndDroids.Godot/Improve.cs` - `DonkeysAndDroids.Godot/draw_glitch.tscn` - `DonkeysAndDroids.Godot/DrawGlitch.cs` - `DonkeysAndDroids.Godot/gamble.tscn` - `DonkeysAndDroids.Godot/Gamble.cs` - `DonkeysAndDroids.Godot/buffer_overflow.tscn` - `DonkeysAndDroids.Godot/BufferOverflow.cs` ## Component model - CardRow is the container that owns CardControls, maintains OrderedCards, layout, and drag/drop reordering. It exposes signals `SelectionChanged` and `OrderChanged`. - CardControl is the per-card UI. It handles hover/selection/drag visuals and emits `CardDroppedOn` and `CardSelectionChanged` signals. ## 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 via `Tooltip.Instance.Describe`. - Drop target (before/after): during a valid drag over this card, `m_DropBefore` set 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`): 1) Dragging overrides all other visuals. 2) Drop target overrides pivot X and alpha (pivot Y from Selected may remain). 3) Hover overrides scale/modulate. 4) 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 `Selected` if `CanSelect` and 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 `SupportsMultiSelect` is 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. - `SelectionChanged` emits an array of selected CardControls from the row. The array order is not guaranteed (dictionary order). - Tooltip: - Hover uses Godot `TooltipText` set to card name + description. - Selection additionally calls `Tooltip.Instance.Describe(card)`. ## Drag and drop behavior ### Drag start - Valid only when `CanDrag` is 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 - `SupportsMultiDrag` must 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. - `OrderChanged` is 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. - `OrderChanged` is 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 = true` and are not Disabled. - Dropping on a row with `CardsCanDrag = false` is invalid (no acceptance, no drop indicator). - Dropping into a row that would exceed `MaxCards` is 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 row `Size`. - 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 set `CustomMinimumSize` to fit. - `GetSortedCards` sorts by: 1) Rarity 2) Card Id (ECard enum) 3) Modifier count 4) 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. - `MaxCards` is not used to enforce tape/hand capacity; capacity is enforced by selection rules. Behavior: - The row contains all cards from `CoreLoop.Hand` and `CoreLoop.Tape`. - Initial order on entry: `CoreLoop.Tape` (in tape order) followed by `GetSortedCards(CoreLoop.Hand)` (hand pre-sorted). - Initial selection: cards from `CoreLoop.Tape` are Selected; cards from `CoreLoop.Hand` are 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`/`TapeResult` are 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 `OccupiedSpace` is `<= TapeLength`. - Deselecting a card is only allowed if the resulting hand `OccupiedSpace` is `<= 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`): `%Patches` row uses CardsCanDrag false and single selection to pick exactly one card to add. - BufferOverflow (`DonkeysAndDroids.Godot/buffer_overflow.tscn`): `%Hand` row uses CardsCanDrag false and single selection to pick exactly one card to remove.