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