feat(D.2b): UiMenu (Type 6) — generic dropdown; channel knowledge moves to controller (widget-generalization Task 4)
UiChannelMenu → UiMenu: removed ChatChannelKind, the 14-item array, the button-text map, and the availability default. Generic surface: MenuItem (label + object? Payload), Selected (object?), OnSelect, EnabledProvider, ButtonLabelProvider, RowsPerColumn/RowHeight/ColumnWidth (all settable). All draw/event mechanics unchanged — same popup geometry, same click coordinates, same 8-piece bevel, same 3-slice button face. ChatWindowController gains ChannelItems[], ChannelButtonLabel(), and ChannelAvailable() (verbatim from old widget), and populates the factory-built Type-6 UiMenu via find-by-id rather than constructing a replacement widget. The Menu property type is now UiMenu. OnChannelChanged wrap replaced with the generic OnSelect wrap for the ReflowInputRow hook. DatWidgetFactory registers Type 6 → new UiMenu(). Tests: UiChannelMenuTests → UiMenuTests (10 tests, all green); factory Type6 test added; ChatWindowControllerTests updated to use OnSelect. Divergence register: AP-42 added (flat item model vs retail nested-submenu MakePopup @0x46d310 — latent, unreachable through the chat menu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
805ab5f40b
commit
955f7a69a8
8 changed files with 302 additions and 252 deletions
|
|
@ -41,7 +41,7 @@ public class ChatWindowControllerTests
|
|||
/// transcript (Type-12, no media) [0x10000011] ← skipped by factory
|
||||
/// track (Type-3) [0x10000012]
|
||||
/// inputBar (Type-3) [0x10000013]
|
||||
/// menu (Type-3) [0x10000014]
|
||||
/// menu (Type-6) [0x10000014]
|
||||
/// input (Type-12, no media) [0x10000016] ← skipped by factory
|
||||
/// send (Type-3) [0x10000019]
|
||||
/// maxmin (Type-3) [0x1000046F]
|
||||
|
|
@ -67,7 +67,7 @@ public class ChatWindowControllerTests
|
|||
|
||||
var menuNode = new ElementInfo
|
||||
{
|
||||
Id = 0x10000014u, Type = 3, X = 0, Y = 0, Width = 46, Height = 17,
|
||||
Id = 0x10000014u, Type = 6, X = 0, Y = 0, Width = 46, Height = 17,
|
||||
};
|
||||
var inputNode = new ElementInfo
|
||||
{
|
||||
|
|
@ -180,8 +180,8 @@ public class ChatWindowControllerTests
|
|||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
// Switch channel to General.
|
||||
ctrl!.Menu.OnChannelChanged!.Invoke(ChatChannelKind.General);
|
||||
// Switch channel to General via the generic OnSelect (payload is ChatChannelKind).
|
||||
ctrl!.Menu.OnSelect!.Invoke((object?)ChatChannelKind.General);
|
||||
ctrl.Input.OnSubmit!.Invoke("hey all");
|
||||
|
||||
Assert.Single(bus.Published);
|
||||
|
|
|
|||
|
|
@ -115,6 +115,15 @@ public class DatWidgetFactoryTests
|
|||
Assert.IsType<UiScrollbar>(e);
|
||||
}
|
||||
|
||||
// ── Test 5d: Type 6 → UiMenu ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Type6_Menu_MakesUiMenu()
|
||||
{
|
||||
var e = DatWidgetFactory.Create(new ElementInfo { Type = 6, Width = 46, Height = 18 }, NoTex, null);
|
||||
Assert.IsType<UiMenu>(e);
|
||||
}
|
||||
|
||||
// ── Test 6: Meter slice extraction (the important one) ───────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
using System.Linq;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public class UiChannelMenuTests
|
||||
{
|
||||
// PopupH = Rows(7) * ItemH(17) = 119; popup opens upward so top = -119.
|
||||
// Item idx -> col = idx/7, row = idx%7; row band y in [top+row*17, top+(row+1)*17).
|
||||
// Right column needs lx >= ColW(191).
|
||||
|
||||
[Fact]
|
||||
public void Items_HasExpected14Entries()
|
||||
{
|
||||
Assert.Equal(14, UiChannelMenu.Items.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Items_FirstEntry_IsSquelch_Special()
|
||||
{
|
||||
Assert.Equal("Squelch (ignore)", UiChannelMenu.Items[0].Label);
|
||||
Assert.Null(UiChannelMenu.Items[0].Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Items_LastEntry_IsOlthoi()
|
||||
{
|
||||
var last = UiChannelMenu.Items[^1];
|
||||
Assert.Equal("Tell to Olthoi Chat", last.Label);
|
||||
Assert.Equal(ChatChannelKind.Olthoi, last.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Items_ContainAll12ChannelKinds()
|
||||
{
|
||||
var kinds = new HashSet<ChatChannelKind>(
|
||||
UiChannelMenu.Items.Where(i => i.Channel is not null).Select(i => i.Channel!.Value));
|
||||
foreach (var k in new[]
|
||||
{
|
||||
ChatChannelKind.Say, ChatChannelKind.General, ChatChannelKind.Trade, ChatChannelKind.Lfg,
|
||||
ChatChannelKind.Fellowship, ChatChannelKind.Allegiance, ChatChannelKind.Patron,
|
||||
ChatChannelKind.Vassals, ChatChannelKind.Monarch, ChatChannelKind.Roleplay,
|
||||
ChatChannelKind.Society, ChatChannelKind.Olthoi,
|
||||
})
|
||||
Assert.Contains(k, kinds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSelected_IsSay()
|
||||
{
|
||||
Assert.Equal(ChatChannelKind.Say, new UiChannelMenu().Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_AvailableLeftColumnItem_FiresChannel()
|
||||
{
|
||||
var menu = new UiChannelMenu { Width = 80f, Height = 18f };
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
|
||||
ChatChannelKind? fired = null;
|
||||
menu.OnChannelChanged = k => fired = k;
|
||||
|
||||
// "Chat to All" (Say) is index 2 = left col, row 2: y in [-85,-68). Say is available.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -76)));
|
||||
Assert.Equal(ChatChannelKind.Say, fired);
|
||||
Assert.Equal(ChatChannelKind.Say, menu.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_AvailableRightColumnItem_FiresChannel()
|
||||
{
|
||||
var menu = new UiChannelMenu { Width = 80f, Height = 18f };
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
|
||||
ChatChannelKind? fired = null;
|
||||
menu.OnChannelChanged = k => fired = k;
|
||||
|
||||
// "Tell to Trade Chat" (Trade) is index 11 = right col (lx>=191), row 4: y in [-51,-34).
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 200, -42)));
|
||||
Assert.Equal(ChatChannelKind.Trade, fired);
|
||||
Assert.Equal(ChatChannelKind.Trade, menu.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_SpecialItem_DoesNotFire()
|
||||
{
|
||||
var menu = new UiChannelMenu { Width = 80f, Height = 18f };
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
int fired = 0;
|
||||
menu.OnChannelChanged = _ => fired++;
|
||||
|
||||
// "Squelch (ignore)" is index 0 = left col, row 0 (null channel): y in [-119,-102).
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -110)));
|
||||
Assert.Equal(0, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_UnavailableChannel_DoesNotFire()
|
||||
{
|
||||
var menu = new UiChannelMenu { Width = 80f, Height = 18f };
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
int fired = 0;
|
||||
menu.OnChannelChanged = _ => fired++;
|
||||
|
||||
// "Tell to Fellows" (Fellowship) is index 3 = left col, row 3: y in [-68,-51).
|
||||
// Fellowship is unavailable by the default static gate, so the click is inert.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
|
||||
Assert.Equal(0, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvailabilityProvider_Overrides_DefaultGate()
|
||||
{
|
||||
var menu = new UiChannelMenu { Width = 80f, Height = 18f, AvailabilityProvider = _ => true };
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
|
||||
ChatChannelKind? fired = null;
|
||||
menu.OnChannelChanged = k => fired = k;
|
||||
|
||||
// With every channel available, "Tell to Fellows" (idx 3, row 3) now fires.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
|
||||
Assert.Equal(ChatChannelKind.Fellowship, fired);
|
||||
}
|
||||
}
|
||||
170
tests/AcDream.App.Tests/UI/UiMenuTests.cs
Normal file
170
tests/AcDream.App.Tests/UI/UiMenuTests.cs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public class UiMenuTests
|
||||
{
|
||||
// PopupH = RowsPerColumn(7) * RowHeight(17) = 119; popup opens upward so top = -119.
|
||||
// Item idx -> col = idx/7, row = idx%7; row band y in [top+row*17, top+(row+1)*17).
|
||||
// Right column needs lx >= ColumnWidth(191) + Border(5) = lx >= 196 after bevel offset,
|
||||
// but the original tests used lx=200 which maps ix=195 -> col=(int)(195/191)=1. OK.
|
||||
|
||||
// The 14 channel items verbatim (matches ChannelItems in ChatWindowController).
|
||||
private static readonly UiMenu.MenuItem[] ChannelItems =
|
||||
{
|
||||
new("Squelch (ignore)", (object?)null),
|
||||
new("Tell to Selected", (object?)null),
|
||||
new("Chat to All", (object?)ChatChannelKind.Say),
|
||||
new("Tell to Fellows", (object?)ChatChannelKind.Fellowship),
|
||||
new("Tell to General Chat", (object?)ChatChannelKind.General),
|
||||
new("Tell to LFG Chat", (object?)ChatChannelKind.Lfg),
|
||||
new("Tell to Society Chat", (object?)ChatChannelKind.Society),
|
||||
new("Tell to Monarch", (object?)ChatChannelKind.Monarch),
|
||||
new("Tell to Patron", (object?)ChatChannelKind.Patron),
|
||||
new("Tell to Vassals", (object?)ChatChannelKind.Vassals),
|
||||
new("Tell to Allegiance", (object?)ChatChannelKind.Allegiance),
|
||||
new("Tell to Trade Chat", (object?)ChatChannelKind.Trade),
|
||||
new("Tell to Roleplay Chat", (object?)ChatChannelKind.Roleplay),
|
||||
new("Tell to Olthoi Chat", (object?)ChatChannelKind.Olthoi),
|
||||
};
|
||||
|
||||
// Availability gate identical to ChatWindowController.ChannelAvailable.
|
||||
private static bool ChannelAvailable(object? p)
|
||||
=> p is ChatChannelKind ch
|
||||
? ch is ChatChannelKind.Say or ChatChannelKind.General
|
||||
or ChatChannelKind.Trade or ChatChannelKind.Lfg
|
||||
: false; // null-payload (Squelch/Tell-to-Selected) = inert
|
||||
|
||||
private UiMenu MakeMenu() => new UiMenu
|
||||
{
|
||||
Width = 80f, Height = 18f,
|
||||
Items = ChannelItems,
|
||||
Selected = (object?)ChatChannelKind.Say,
|
||||
EnabledProvider = ChannelAvailable,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Items_HasExpected14Entries()
|
||||
{
|
||||
Assert.Equal(14, ChannelItems.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Items_FirstEntry_IsSquelch_Special()
|
||||
{
|
||||
Assert.Equal("Squelch (ignore)", ChannelItems[0].Label);
|
||||
Assert.Null(ChannelItems[0].Payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Items_LastEntry_IsOlthoi()
|
||||
{
|
||||
var last = ChannelItems[^1];
|
||||
Assert.Equal("Tell to Olthoi Chat", last.Label);
|
||||
Assert.Equal(ChatChannelKind.Olthoi, last.Payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Items_ContainAll12ChannelKinds()
|
||||
{
|
||||
var kinds = new HashSet<ChatChannelKind>(
|
||||
ChannelItems.Where(i => i.Payload is ChatChannelKind).Select(i => (ChatChannelKind)i.Payload!));
|
||||
foreach (var k in new[]
|
||||
{
|
||||
ChatChannelKind.Say, ChatChannelKind.General, ChatChannelKind.Trade, ChatChannelKind.Lfg,
|
||||
ChatChannelKind.Fellowship, ChatChannelKind.Allegiance, ChatChannelKind.Patron,
|
||||
ChatChannelKind.Vassals, ChatChannelKind.Monarch, ChatChannelKind.Roleplay,
|
||||
ChatChannelKind.Society, ChatChannelKind.Olthoi,
|
||||
})
|
||||
Assert.Contains(k, kinds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSelected_IsNull_OnBlankMenu()
|
||||
{
|
||||
// A freshly constructed UiMenu has no Selected by default (controller sets it).
|
||||
Assert.Null(new UiMenu().Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_AvailableLeftColumnItem_FiresOnSelect()
|
||||
{
|
||||
var menu = MakeMenu();
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
|
||||
// "Chat to All" (Say) is index 2 = left col, row 2: y in [-85,-68). Say is available.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -76)));
|
||||
Assert.Equal(ChatChannelKind.Say, fired);
|
||||
Assert.Equal(ChatChannelKind.Say, menu.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_AvailableRightColumnItem_FiresOnSelect()
|
||||
{
|
||||
var menu = MakeMenu();
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
|
||||
// "Tell to Trade Chat" (Trade) is index 11 = right col (lx>=191), row 4: y in [-51,-34).
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 200, -42)));
|
||||
Assert.Equal(ChatChannelKind.Trade, fired);
|
||||
Assert.Equal(ChatChannelKind.Trade, menu.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_SpecialItem_DoesNotFire()
|
||||
{
|
||||
var menu = MakeMenu();
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
int fired = 0;
|
||||
menu.OnSelect = _ => fired++;
|
||||
|
||||
// "Squelch (ignore)" is index 0 = left col, row 0 (null payload): y in [-119,-102).
|
||||
// null payload → ChannelAvailable returns false → inert.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -110)));
|
||||
Assert.Equal(0, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_UnavailableChannel_DoesNotFire()
|
||||
{
|
||||
var menu = MakeMenu();
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
int fired = 0;
|
||||
menu.OnSelect = _ => fired++;
|
||||
|
||||
// "Tell to Fellows" (Fellowship) is index 3 = left col, row 3: y in [-68,-51).
|
||||
// Fellowship is unavailable by the default static gate, so the click is inert.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
|
||||
Assert.Equal(0, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnabledProvider_Overrides_DefaultGate()
|
||||
{
|
||||
// Override: all items enabled (even Fellowship which is normally greyed).
|
||||
var menu = new UiMenu
|
||||
{
|
||||
Width = 80f, Height = 18f,
|
||||
Items = ChannelItems,
|
||||
Selected = (object?)ChatChannelKind.Say,
|
||||
EnabledProvider = _ => true,
|
||||
};
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
|
||||
// With every item enabled, "Tell to Fellows" (idx 3, row 3) now fires.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
|
||||
Assert.Equal(ChatChannelKind.Fellowship, fired);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue