From 043ab10b3c95e19e36fd86546de0e972257ec1fe Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Jul 2026 06:17:31 +0200 Subject: [PATCH] fix(ui): select retained items on mouse down Port UIElement_ListBox's press-time selection ordering through the shared retained item-list contract. Inventory, loot, paperdoll, and physical shortcuts now update canonical selection before release or drag promotion, while target-mode consumption suppresses drag and release-time activation. Co-authored-by: Codex --- AGENTS.md | 8 +-- CLAUDE.md | 8 +-- docs/ISSUES.md | 9 +++ docs/plans/2026-04-11-roadmap.md | 5 +- docs/plans/2026-05-12-milestones.md | 4 ++ ...2026-07-23-world-interaction-completion.md | 14 +++- ...07-17-retail-world-selection-pseudocode.md | 46 +++++++++++++ .../UI/Layout/ExternalContainerController.cs | 17 +++-- .../UI/Layout/InventoryController.cs | 65 +++++++++++++------ .../UI/Layout/PaperdollController.cs | 21 +++--- .../UI/Layout/ToolbarController.cs | 17 +++++ src/AcDream.App/UI/UiItemList.cs | 14 ++++ src/AcDream.App/UI/UiItemSlot.cs | 27 ++++++-- .../UI/DragDropSpineTests.cs | 55 ++++++++++++++++ .../ExternalContainerControllerTests.cs | 14 ++++ .../UI/Layout/InventoryControllerTests.cs | 13 ++-- .../UI/Layout/PaperdollControllerTests.cs | 5 +- .../UI/Layout/ToolbarControllerTests.cs | 12 +++- 18 files changed, 292 insertions(+), 62 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f7ae3053..1947ec2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,8 +115,8 @@ and the final two-client portal-out/materialization observer flow are user-gated. Deterministic world-lifecycle automation protects fresh login, outdoor/world-edge/dungeon travel, same-location revisit, exact graceful disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts. -Release builds; the appraisal corrective implementation passes 3,496 App -tests / 3 skips and 7,913 complete-solution tests / 5 skips. +Release builds; the appraisal and retained-item input corrections pass 3,548 +App tests / 3 skips and 7,979 complete-solution tests / 5 skips. **Current work order:** the six-slice pre-M4 world-interaction completion program in `docs/plans/2026-07-23-world-interaction-completion.md`: favorite @@ -125,8 +125,8 @@ equipped-child picking, vendor browsing, and authoritative vendor transactions. Slices 1 and 2 plus the adjacent shared-cooldown presentation are user-accepted. Slice 3's first connected gate exposed and corrected the monster-response flag mismatch, non-retail shared-panel mounting, and missing -inscription transaction. Its independent floaty examination window awaits the -corrective connected gate. +inscription transaction. The creature and common-item report corrections are +user-accepted; press-time retained-item selection is the current visual gate. **Structural prerequisite before new M4 subsystem work:** all eight behavior-preserving `GameWindow` decomposition slices and the automated diff --git a/CLAUDE.md b/CLAUDE.md index bc31ae4b..9da6c181 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,8 +113,8 @@ and the final two-client portal-out/materialization observer flow are user-gated. Deterministic world-lifecycle automation protects fresh login, outdoor/world-edge/dungeon travel, same-location revisit, exact graceful disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts. -Release builds; the appraisal corrective implementation passes 3,496 App -tests / 3 skips and 7,913 complete-solution tests / 5 skips. +Release builds; the appraisal and retained-item input corrections pass 3,548 +App tests / 3 skips and 7,979 complete-solution tests / 5 skips. **Current work order:** the six-slice pre-M4 world-interaction completion program in `docs/plans/2026-07-23-world-interaction-completion.md`: favorite @@ -123,8 +123,8 @@ equipped-child picking, vendor browsing, and authoritative vendor transactions. Slices 1 and 2 plus the adjacent shared-cooldown presentation are user-accepted. Slice 3's first connected gate exposed and corrected the monster-response flag mismatch, non-retail shared-panel mounting, and missing -inscription transaction. Its independent floaty examination window awaits the -corrective connected gate. +inscription transaction. The creature and common-item report corrections are +user-accepted; press-time retained-item selection is the current visual gate. **Structural prerequisite before new M4 subsystem work:** all eight behavior-preserving `GameWindow` decomposition slices and the automated diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 1c0d88fd..6ce81815 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -151,6 +151,13 @@ The separate retained ItemList path now does the same for occupied backpack, side-bag, loot-container, paperdoll-slot, and physical shortcut cells; it selects the item before entering the shared appraisal owner, and RMB movement neither appraises nor lifts the item. +The adjacent input-latency correction ports +`UIElement_ListBox::MouseDown @ 0x0046E3A0`: physical retained items now update +canonical selection and their green frame on left-button down, before the +three-pixel drag threshold. Target mode is intercepted at that same press. +Bag opening, item use/equip, and shortcut activation remain completed +click/double-click actions, so a drag or target-consumed press cannot also +activate the item. **Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`; `src/AcDream.App/UI/RetailUiRuntime.cs`; @@ -176,6 +183,8 @@ Right-clicking a visible world object opens the same examination window; right-dragging the camera does not. Right-clicking an occupied retained item cell selects and examines it without using, equipping, looting, or dragging it. +Left-pressing one selects it before the button is released; holding and +dragging preserves that selection without activating the item. The Black Phyntos Hive specifically reads `Value: ???`, `Burden: Unknown`, then its description after one retail paragraph break, without the bogus 255-item/255-container line. diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 1f3ffe11..3da76840 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -35,7 +35,10 @@ the configurable `SelectRight` action's retail release-completed gesture, including drag cancellation, then reuses the canonical picker/selection/appraisal path. The parallel retained ItemList branch now selects and appraises occupied inventory, external-container, paperdoll, and -physical-shortcut cells through the same request owner. Slice 3's first connected gate exposed +physical-shortcut cells through the same request owner. Its shared physical +left-click contract now also matches retail's press-time selection: the +selection frame updates on MouseDown before drag promotion, while completed +click/double-click remains the activation boundary. Slice 3's first connected gate exposed three deeper defects, now corrected: the response enum mislabeled ACE's creature bit `0x0100` and dropped monster packets while retaining the busy cursor; the imported 310 x 400 examination layout was incorrectly hosted as diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 7b18b7a5..47395428 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -104,6 +104,10 @@ instead of appraising the release point. Retail's separate retained ItemList right-click branch now covers physical backpack, loot, paperdoll, and toolbar item cells through the same selection and appraisal owner. +The same shared ItemList contract now ports retail's left-button-down +selection timing across inventory, loot, paperdoll, and physical shortcuts: +selection is visible before release or drag promotion, while use/open/equip +remains release-completed. Slice 3's first connected gate found and corrected the real remaining defects: ACE creature flag `0x0100` was mislabeled as a weapon profile and left the appraisal busy reference held, retail's examination UI had been mounted as a diff --git a/docs/plans/2026-07-23-world-interaction-completion.md b/docs/plans/2026-07-23-world-interaction-completion.md index 5074e262..16719c6c 100644 --- a/docs/plans/2026-07-23-world-interaction-completion.md +++ b/docs/plans/2026-07-23-world-interaction-completion.md @@ -222,9 +222,10 @@ present in the authored base name. Empty `AddItemInfo` calls are retained as real report fragments, restoring retail's intentional blank rows after workmanship, before armor level, around rating/special-property blocks, and at the later use/item-level boundaries. Focused wire, projection, object-table, -title, and boots-layout fixtures protect the full path. The Release solution -build and 3,533 App tests / 3 skips plus 7,956 complete-solution tests / -5 skips pass; the connected boots comparison is the remaining visual gate. +title, and boots-layout fixtures protect the full path. With the subsequent +right-click and press-time retained-item input ports, the Release solution +build and 3,548 App tests / 3 skips plus 7,979 complete-solution tests / +5 skips pass. The world right-click follow-up ports `UIElement_SmartBoxWrapper::MouseUp @ 0x004E5820` and the @@ -241,6 +242,13 @@ The retained follow-up ports the separate backpack, side-bag, loot, paperdoll-slot, or physical toolbar cell now selects its item and enters that same appraisal owner on a completed right-click. Right-button movement cancels the click and can never begin an item drag. +The input-latency follow-up ports `UIElement_ListBox::MouseDown @ 0x0046E3A0` +and the physical left-click branch of +`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`. Canonical selection +and the retained green frame update during left-button down, before the +three-pixel drag threshold. Target mode is offered first. Opening, using, +equipping, looting, and shortcut activation remain completed-click or +double-click actions and are suppressed when target mode consumed the press. ## Slice 1 — spell-bar overflow arrows diff --git a/docs/research/2026-07-17-retail-world-selection-pseudocode.md b/docs/research/2026-07-17-retail-world-selection-pseudocode.md index 873166ba..457a6ecd 100644 --- a/docs/research/2026-07-17-retail-world-selection-pseudocode.md +++ b/docs/research/2026-07-17-retail-world-selection-pseudocode.md @@ -190,6 +190,52 @@ procedural arrow. The indicator is persistent selection presentation. Retail also has a separate, short-lived click confirmation owned by SmartBox; it is not implemented in `CPhysicsPart::Draw` or `ACCWeenieObject::SetSelected`. +## Retained physical-item press, activation, and drag ordering + +`UIElement_ListBox::MouseDown @ 0x0046E3A0` + +```text +on left mouse down over a single-selection ListBox: + item = GetItemUnderMouse() + if item exists: + SetSelectedItem(item, notify=true) + if drag-selection is enabled: + StartDragSelect() +``` + +`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` + +```text +on the physical-item left-button-down notice: + item = UIItem under the mouse + if a target mode is active: + HandleTargetedUseLeftClick(item) + consume the notice + else if item.itemID != 0: + if this list uses single selection: + update the list selection + SetSelectedObject(item.itemID) + +on the physical-item double-click notice: + if item.itemID != 0: + UseObject(item.itemID) +``` + +`gmPaperDollUI::ListenToElementMessage @ 0x004A5C30` uses the same ordering +for a worn item: target mode first, otherwise set the selected object on the +left-button-down notice; right-click selects and examines. + +Therefore selection is not a completed-click action. The green selection frame +appears while the left button is still held, before the three-pixel drag +threshold can promote the press into a drag. Opening a bag, using/equipping an +item, and shortcut activation remain completed click or double-click actions, +so dragging never also activates the source. A target-mode-consumed press +suppresses the rest of that physical-item click gesture. + +Acdream implements that split once in `UiItemList`/`UiItemSlot`. Inventory, +external-container loot, paperdoll equipment, and physical toolbar shortcuts +only supply their selection source and release-time activation command. + ## Right-click selection and examination `UIElement_SmartBoxWrapper::MouseUp @ 0x004E5820` diff --git a/src/AcDream.App/UI/Layout/ExternalContainerController.cs b/src/AcDream.App/UI/Layout/ExternalContainerController.cs index 117bf7cb..d7bf4460 100644 --- a/src/AcDream.App/UI/Layout/ExternalContainerController.cs +++ b/src/AcDream.App/UI/Layout/ExternalContainerController.cs @@ -83,6 +83,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine ConfigureList(_topContainer, ContainerCellSize, horizontalScroll: false, containerEmptySprite); ConfigureList(_containerList, ContainerCellSize, horizontalScroll: true, containerEmptySprite); ConfigureList(_contentsList, ItemCellSize, horizontalScroll: true, contentsEmptySprite); + _topContainer.PrimaryItemPressed = PressItem; + _containerList.PrimaryItemPressed = PressItem; + _contentsList.PrimaryItemPressed = PressItem; _topContainer.ExamineItemRequested = ExamineItem; _containerList.ExamineItemRequested = ExamineItem; _contentsList.ExamineItemRequested = ExamineItem; @@ -325,7 +328,6 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine private void AddRootCell(uint guid) { UiItemSlot cell = CreateCell(_topContainer, guid, ItemDragSource.Ground); - cell.Clicked = () => HandlePrimaryClick(guid, () => Select(guid)); cell.DoubleClicked = RequestClose; cell.IsOpenContainer = _openContainer == guid; SetCapacity(cell, guid); @@ -335,7 +337,7 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine private void AddContainerCell(uint guid) { UiItemSlot cell = CreateCell(_containerList, guid, ItemDragSource.Ground); - cell.Clicked = () => HandlePrimaryClick(guid, () => OpenNestedContainer(guid)); + cell.Clicked = () => OpenNestedContainer(guid); SetCapacity(cell, guid); _containerList.AddItem(cell); } @@ -343,7 +345,6 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine private void AddContentsCell(uint guid) { UiItemSlot cell = CreateCell(_contentsList, guid, ItemDragSource.Ground); - cell.Clicked = () => HandlePrimaryClick(guid, () => Select(guid)); cell.DoubleClicked = () => _itemInteraction.ActivateItem(guid); cell.DragAcceptSprite = 0x060011F9u; cell.DragRejectSprite = 0x060011F8u; @@ -384,11 +385,12 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine _itemInteraction.ExamineSelectedOrEnterMode(guid); } - private void HandlePrimaryClick(uint guid, Action fallback) + private bool PressItem(uint guid) { if (_itemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive) - return; - fallback(); + return true; + Select(guid); + return false; } private void ApplyIndicators() @@ -573,6 +575,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine _objects.Cleared -= OnObjectsCleared; _selection.Changed -= OnSelectionChanged; _itemInteraction.StateChanged -= OnInteractionStateChanged; + _topContainer.PrimaryItemPressed = null; + _containerList.PrimaryItemPressed = null; + _contentsList.PrimaryItemPressed = null; _topContainer.ExamineItemRequested = null; _containerList.ExamineItemRequested = null; _contentsList.ExamineItemRequested = null; diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 774678ee..4030a4b1 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -153,9 +153,21 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo _contentsGrid?.RegisterDragHandler(this); _containerList?.RegisterDragHandler(this); _topContainer?.RegisterDragHandler(this); - if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = ExamineItem; - if (_containerList is not null) _containerList.ExamineItemRequested = ExamineItem; - if (_topContainer is not null) _topContainer.ExamineItemRequested = ExamineItem; + if (_contentsGrid is not null) + { + _contentsGrid.PrimaryItemPressed = PressItem; + _contentsGrid.ExamineItemRequested = ExamineItem; + } + if (_containerList is not null) + { + _containerList.PrimaryItemPressed = PressItem; + _containerList.ExamineItemRequested = ExamineItem; + } + if (_topContainer is not null) + { + _topContainer.PrimaryItemPressed = PressSelfItem; + _topContainer.ExamineItemRequested = ExamineItem; + } // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). _burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter; @@ -432,12 +444,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo dragIconTexture: _dragIconIds?.Invoke( ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u); main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u; - main.Clicked = () => - { - if (_itemInteraction?.OfferSelfPrimaryClick() - is not null and not ItemPrimaryClickResult.NotActive) return; - OpenContainer(p); - }; + main.Clicked = () => OpenContainer(p); main.DoubleClicked = () => _itemInteraction?.ActivateItem(p); SetCapacityBar(main, p); // main-pack fullness (items / ItemsCapacity) _topContainer.AddItem(main); @@ -480,22 +487,28 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo cell.DoubleClicked = () => _itemInteraction?.ActivateItem(guid); if (isContainer) { - cell.Clicked = () => HandlePrimaryClick(guid, () => OpenContainer(guid)); + cell.Clicked = () => OpenContainer(guid); SetCapacityBar(cell, guid); } - else - { - cell.Clicked = () => HandlePrimaryClick(guid, () => SelectItem(guid)); - } list.AddItem(cell); } - private void HandlePrimaryClick(uint guid, Action fallback) + private bool PressItem(uint guid) { if (_itemInteraction?.OfferPrimaryClick(guid) is not null and not ItemPrimaryClickResult.NotActive) - return; - fallback(); + return true; + SelectItem(guid); + return false; + } + + private bool PressSelfItem(uint guid) + { + if (_itemInteraction?.OfferSelfPrimaryClick() + is not null and not ItemPrimaryClickResult.NotActive) + return true; + SelectItem(guid); + return false; } private void AddEmptyCell(UiItemList list) @@ -931,9 +944,21 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo _objects.ObjectUpdated -= OnObjectChanged; _objects.Cleared -= OnObjectsCleared; _selection.Changed -= OnSelectionChanged; - if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = null; - if (_containerList is not null) _containerList.ExamineItemRequested = null; - if (_topContainer is not null) _topContainer.ExamineItemRequested = null; + if (_contentsGrid is not null) + { + _contentsGrid.PrimaryItemPressed = null; + _contentsGrid.ExamineItemRequested = null; + } + if (_containerList is not null) + { + _containerList.PrimaryItemPressed = null; + _containerList.ExamineItemRequested = null; + } + if (_topContainer is not null) + { + _topContainer.PrimaryItemPressed = null; + _topContainer.ExamineItemRequested = null; + } if (_itemInteraction is not null) { _itemInteraction.StateChanged -= OnInteractionStateChanged; diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index f1dac2fd..9d84f10b 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -84,6 +84,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo var (element, mask, _, unlockBit) = PaperdollSlotBackgrounds.Definitions[i]; if (layout.FindElement(element) is not UiItemList list) continue; list.RegisterDragHandler(this); + list.PrimaryItemPressed = PressItem; list.ExamineItemRequested = ExamineItem; list.Cell.SourceKind = ItemDragSource.Equipment; list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot @@ -91,15 +92,6 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo && emptySlotSprites.TryGetValue(element, out uint authoredSprite) ? authoredSprite : emptySlotSprite; - list.Cell.Clicked = () => - { - uint itemId = list.Cell.ItemId; - if (itemId != 0 && _itemInteraction?.OfferPrimaryClick(itemId) - is not null and not ItemPrimaryClickResult.NotActive) - return; - if (itemId != 0) - _selection.Select(itemId, SelectionChangeSource.Paperdoll); - }; list.Cell.DoubleClicked = () => { if (list.Cell.ItemId != 0) @@ -357,6 +349,14 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo _itemInteraction.ExamineSelectedOrEnterMode(itemId); } + private bool PressItem(uint itemId) + { + if (_itemInteraction.OfferPrimaryClick(itemId) != ItemPrimaryClickResult.NotActive) + return true; + _selection.Select(itemId, SelectionChangeSource.Paperdoll); + return false; + } + /// Detach event handlers (idempotent). public void Dispose() { @@ -369,7 +369,10 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo _objects.Cleared -= OnObjectsCleared; _selection.Changed -= OnSelectionChanged; foreach (var (_, list) in _slots) + { + list.PrimaryItemPressed = null; list.ExamineItemRequested = null; + } if (_dollViewport is UiViewport doll) doll.ClickedAt = null; switch (_dollDragMask) diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 4df5a9b3..8418b4dd 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -138,6 +138,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont if (_slots[i] is { } list) { WireClick(list); + list.PrimaryItemPressed = PressItem; list.ExamineItemRequested = ExamineItem; // B.1 drag-drop spine: this controller is the drop handler for every // toolbar slot list; each cell knows its slot index + that it's a @@ -532,6 +533,19 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont }; } + private bool PressItem(uint itemId) + { + if (_itemInteraction?.OfferPrimaryClick(itemId) + is not null and not ItemPrimaryClickResult.NotActive) + return true; + + if (_selectItem is not null) + _selectItem(itemId); + else + _selection?.Select(itemId, SelectionChangeSource.Toolbar); + return false; + } + private void ExamineItem(uint itemId) { if (_selectItem is not null) @@ -777,7 +791,10 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont _selection.Changed -= OnSelectionChanged; foreach (UiItemList? list in _slots) if (list is not null) + { + list.PrimaryItemPressed = null; list.ExamineItemRequested = null; + } _repo.ObjectAdded -= OnRepositoryObjectChanged; _repo.ObjectUpdated -= OnRepositoryObjectChanged; _repo.ObjectRemoved -= OnRepositoryObjectChanged; diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs index 50b38c36..bf48e6d2 100644 --- a/src/AcDream.App/UI/UiItemList.cs +++ b/src/AcDream.App/UI/UiItemList.cs @@ -73,6 +73,20 @@ public sealed class UiItemList : UiElement /// public Action? CatalogDropped { get; set; } + /// + /// Physical-item left-button-down command supplied by the owning panel. + /// Return when a target mode consumed the press so + /// the slot suppresses drag promotion and the completed Click/DoubleClick + /// gesture. + /// + /// Retail references: + /// UIElement_ListBox::MouseDown @ 0x0046E3A0 selects the item under + /// the pointer during MouseDown, before optional drag selection; + /// UIElement_ItemList::ListenToElementMessage @ 0x004E4D50 handles + /// target mode before ordinary physical-item selection. + /// + public Func? PrimaryItemPressed { get; set; } + /// /// Appraisal command supplied by the owning panel controller for physical /// item cells. Retail UIElement_ItemList::ListenToElementMessage diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index 11b38ed0..b77acb86 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -99,6 +99,7 @@ public class UiItemSlot : UiElement private bool _waiting; internal bool WaitingVisual => _waiting; + private bool _primaryPressConsumed; /// RenderSurface id -> (GL texture, w, h). Set by the factory/controller. public Func? SpriteResolve { get; set; } @@ -134,11 +135,14 @@ public class UiItemSlot : UiElement DragIconTexture = 0; Shortcut = null; _waiting = false; + _primaryPressConsumed = false; } /// public override object? GetDragPayload() - => ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut) : null; + => ItemId != 0 && !_primaryPressConsumed + ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut) + : null; /// public override (uint tex, int w, int h)? GetDragGhost() @@ -251,8 +255,10 @@ public class UiItemSlot : UiElement // ── Events / draw ───────────────────────────────────────────────────────── - /// Invoked by when a left-button-down lands on - /// a bound slot. Wired by ToolbarController to the use-item callback. + /// Invoked by when a completed left click lands + /// on a bound slot. Selection already occurred on MouseDown through the owning + /// ; this callback performs release-time activation + /// such as opening a bag or using a shortcut. public Action? Clicked { get; set; } /// Invoked by when a double-click lands on a bound slot. @@ -267,12 +273,21 @@ public class UiItemSlot : UiElement // drag (press + >3px move) does NOT also use the item. UiRoot suppresses the // post-drag Click (UiRoot.cs FinishDrag returns before the Click emit). case UiEventType.MouseDown: - return true; // consume the press; no use here + // UIElement_ListBox::MouseDown @ 0x0046E3A0 selects immediately, + // before StartDragSelect. UIElement_ItemList's notice handler + // intercepts target mode first. Activation remains release-time, + // so a press promoted to drag never also uses the item. + _primaryPressConsumed = ItemId != 0 + && FindList() is { PrimaryItemPressed: { } pressed } + && pressed(ItemId); + return true; case UiEventType.Click: - Clicked?.Invoke(); + if (!_primaryPressConsumed) + Clicked?.Invoke(); return true; case UiEventType.DoubleClick: - DoubleClicked?.Invoke(); + if (!_primaryPressConsumed) + DoubleClicked?.Invoke(); return true; case UiEventType.RightClick: if (ItemId != 0 diff --git a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs index 2fee034a..4dafc157 100644 --- a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs +++ b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs @@ -275,6 +275,61 @@ public class DragDropSpineTests Assert.True(used); } + [Fact] + public void PhysicalItemPress_selectsBeforeRelease_thenClickActivates() + { + var (root, list, cell) = RootWithBoundSlot(0x5001u); + var selected = new List(); + bool used = false; + list.PrimaryItemPressed = item => + { + selected.Add(item); + return false; + }; + cell.Clicked = () => used = true; + + root.OnMouseDown(UiMouseButton.Left, 10, 10); + + Assert.Equal(new uint[] { 0x5001u }, selected); + Assert.False(used); + + root.OnMouseUp(UiMouseButton.Left, 10, 10); + + Assert.True(used); + } + + [Fact] + public void ConsumedPhysicalItemPress_suppressesCompletedActivation() + { + var (root, list, cell) = RootWithBoundSlot(0x5001u); + bool used = false; + bool doubleUsed = false; + list.PrimaryItemPressed = _ => true; + cell.Clicked = () => used = true; + cell.DoubleClicked = () => doubleUsed = true; + + root.OnMouseDown(UiMouseButton.Left, 10, 10); + root.OnMouseUp(UiMouseButton.Left, 10, 10); + root.OnMouseDown(UiMouseButton.Left, 10, 10); + root.OnMouseUp(UiMouseButton.Left, 10, 10); + + Assert.False(used); + Assert.False(doubleUsed); + } + + [Fact] + public void ConsumedPhysicalItemPress_cannotPromoteIntoDrag() + { + var (root, list, _) = RootWithBoundSlot(0x5001u); + list.PrimaryItemPressed = _ => true; + + root.OnMouseDown(UiMouseButton.Left, 10, 10); + root.OnMouseMove(20, 10); + + Assert.Null(root.DragSource); + Assert.Null(root.DragPayload); + } + [Fact] public void RightClick_withoutDrag_requestsItemListAppraisal() { diff --git a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs index 20cb375c..795329aa 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs @@ -199,6 +199,20 @@ public sealed class ExternalContainerControllerTests Assert.Empty(h.Pickups); } + [Fact] + public void MouseDownLoot_selectsBeforeCompletedClick() + { + using var h = new Harness(); + h.Open(Chest, new ContainerContentEntry(Item, 0u)); + + UiItemSlot cell = h.Contents.GetItem(0)!; + cell.OnEvent(new UiEvent(0u, cell, UiEventType.MouseDown)); + + Assert.Equal(Item, h.Selection.SelectedObjectId); + Assert.True(cell.Selected); + Assert.Empty(h.Pickups); + } + [Fact] public void CloseAndRangeExit_SendUseOnceWithoutNoLongerViewing() { diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 1c7e106e..02e2adbd 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -460,7 +460,7 @@ public class InventoryControllerTests } [Fact] - public void ClickGridItem_movesSquareOnly_noWire_keepsOpenContainer() + public void MouseDownGridItem_movesSquareImmediately_noWire_keepsOpenContainer() { var (layout, grid, _, top, _, _, _, _) = BuildLayout(); var objects = new ClientObjectTable(); @@ -468,7 +468,8 @@ public class InventoryControllerTests var uses = new List(); Bind(layout, objects, uses: uses); - grid.GetItem(0)!.Clicked!(); // select the loose item + UiItemSlot item = grid.GetItem(0)!; + item.OnEvent(new UiEvent(0u, item, UiEventType.MouseDown)); Assert.True(grid.GetItem(0)!.Selected); // square on the selected grid item Assert.True(top.GetItem(0)!.IsOpenContainer); // main pack still the open container (unchanged) @@ -476,7 +477,7 @@ public class InventoryControllerTests } [Fact] - public void ClickGridItem_updatesSharedSelection_andExternalSelectionMovesSquare() + public void MouseDownGridItem_updatesSharedSelection_andExternalSelectionMovesSquare() { var (layout, grid, _, _, _, _, _, _) = BuildLayout(); var objects = new ClientObjectTable(); @@ -485,7 +486,8 @@ public class InventoryControllerTests var selection = new SelectionState(); Bind(layout, objects, selection: selection); - grid.GetItem(0)!.Clicked!(); + UiItemSlot item = grid.GetItem(0)!; + item.OnEvent(new UiEvent(0u, item, UiEventType.MouseDown)); Assert.Equal(0xAu, selection.SelectedObjectId); Assert.True(grid.GetItem(0)!.Selected); @@ -555,7 +557,8 @@ public class InventoryControllerTests selection: new SelectionState(), datFont: null, itemInteraction: interaction); - grid.GetItem(0)!.Clicked!(); + UiItemSlot item = grid.GetItem(0)!; + item.OnEvent(new UiEvent(0u, item, UiEventType.MouseDown)); Assert.True(grid.GetItem(0)!.Selected); Assert.True(interaction.ActivateItem(0xAu)); diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs index 34383d1c..3786210a 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs @@ -176,7 +176,7 @@ public class PaperdollControllerTests } [Fact] - public void ClickEquippedItem_updatesSharedSelection() + public void MouseDownEquippedItem_updatesSharedSelection() { var (layout, lists) = BuildLayout(); var objects = new ClientObjectTable(); @@ -184,7 +184,8 @@ public class PaperdollControllerTests var selection = new SelectionState(); Bind(layout, objects, selection: selection); - lists[HeadSlot].Cell.Clicked!(); + UiItemSlot cell = lists[HeadSlot].Cell; + cell.OnEvent(new UiEvent(0u, cell, UiEventType.MouseDown)); Assert.Equal(0xA01u, selection.SelectedObjectId); Assert.True(lists[HeadSlot].Cell.Selected); diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index fbbcd8f6..a1432bbd 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -192,11 +192,19 @@ public class ToolbarControllerTests var shortcuts = new List { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) }; uint used = 0; + var selection = new SelectionState(); ToolbarController.Bind(layout, repo, () => shortcuts, - iconIds: (_,_,_,_,_) => 0x77u, useItem: g => used = g); + iconIds: (_,_,_,_,_) => 0x77u, + useItem: g => used = g, + selection: selection); + UiItemSlot cell = slots[Row1[0]].Cell; + cell.OnEvent(new UiEvent(0u, cell, UiEventType.MouseDown)); + + Assert.Equal(0x5001u, selection.SelectedObjectId); + Assert.Equal(0u, used); // Use now fires on Click (mouse-up), not MouseDown — drag/click disambiguation. - slots[Row1[0]].Cell.OnEvent(new UiEvent(0u, null, UiEventType.Click)); + cell.OnEvent(new UiEvent(0u, cell, UiEventType.Click)); Assert.Equal(0x5001u, used); }