Port the retail horizontal ItemList empty-slot padding and share UIItem shortcut-number graphics with the status toolbar. Preserve all authored face children on compound DAT buttons so the three-piece Cast control reflows and renders as one complete button. Co-authored-by: OpenAI Codex <codex@openai.com>
163 lines
6.5 KiB
C#
163 lines
6.5 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Icon-list cell for non-weenie catalog entries such as spells and components.
|
|
/// It reuses the retail UIItemList geometry without pretending a spell id is an
|
|
/// object GUID; catalog drags stay distinct from physical item drag/drop.
|
|
/// </summary>
|
|
public sealed class UiCatalogSlot : UiItemSlot
|
|
{
|
|
public uint EntryId { get; set; }
|
|
public uint CatalogIconTexture { get; set; }
|
|
/// <summary>Optional second 32x32 layer (retail spell endowment item icon).</summary>
|
|
public uint CatalogOverlayTexture { get; set; }
|
|
public string Label { get; set; } = string.Empty;
|
|
public string? Detail { get; set; }
|
|
public bool ShowLabel { get; init; }
|
|
public uint BackgroundSprite { get; init; }
|
|
public UiDatFont? LabelFont { get; init; }
|
|
public Vector4 LabelColor { get; init; } = new(0.92f, 0.88f, 0.70f, 1f);
|
|
public float IconLeft { get; init; }
|
|
public float IconTop { get; init; }
|
|
public float IconWidth { get; init; }
|
|
public float IconHeight { get; init; }
|
|
public float LabelLeft { get; init; }
|
|
public float LabelWidth { get; init; }
|
|
public bool SelectionBehindContent { get; init; }
|
|
|
|
public new Action? Clicked { get; set; }
|
|
public new Action? DoubleClicked { get; set; }
|
|
public object? CatalogDragPayload { get; init; }
|
|
public Action<object>? DragBegan { get; init; }
|
|
public Action<object>? DragEnded { get; init; }
|
|
public Action<object>? Dropped { get; init; }
|
|
|
|
protected override bool IsShortcutOccupied => EntryId != 0u;
|
|
public override bool IsEmptySlot => EntryId == 0u;
|
|
|
|
public override string? GetTooltipText() => string.IsNullOrWhiteSpace(Label) ? null : Label;
|
|
|
|
public override bool IsDragSource => CatalogDragPayload is not null;
|
|
public override object? GetDragPayload() => CatalogDragPayload;
|
|
public override (uint tex, int w, int h)? GetDragGhost() =>
|
|
CatalogDragPayload is not null && CatalogIconTexture != 0u
|
|
? (CatalogIconTexture, 32, 32)
|
|
: null;
|
|
|
|
internal override void SetDragSourceActive(bool active, object? payload)
|
|
{
|
|
if (!active && payload is not null) DragEnded?.Invoke(payload);
|
|
}
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
switch (e.Type)
|
|
{
|
|
case UiEventType.MouseDown:
|
|
return true;
|
|
case UiEventType.Click:
|
|
Clicked?.Invoke();
|
|
return true;
|
|
case UiEventType.DoubleClick:
|
|
DoubleClicked?.Invoke();
|
|
return true;
|
|
case UiEventType.DragBegin:
|
|
if (e.Payload is not null) DragBegan?.Invoke(e.Payload);
|
|
return true;
|
|
case UiEventType.DragEnter:
|
|
case UiEventType.DragOver:
|
|
return true;
|
|
case UiEventType.DropReleased:
|
|
if (e.Payload is not null) Dropped?.Invoke(e.Payload);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
if (BackgroundSprite != 0u && SpriteResolve is not null)
|
|
{
|
|
var (texture, _, _) = SpriteResolve(BackgroundSprite);
|
|
if (texture != 0u)
|
|
ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
|
}
|
|
|
|
if (SelectionBehindContent)
|
|
DrawSelection(ctx);
|
|
|
|
float iconWidth = IconWidth > 0f ? IconWidth : ShowLabel ? MathF.Min(32f, Height) : Width;
|
|
float iconHeight = IconHeight > 0f ? IconHeight : ShowLabel ? MathF.Min(32f, Height) : Height;
|
|
if (CatalogIconTexture != 0)
|
|
ctx.DrawSprite(CatalogIconTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One);
|
|
else if (SpriteResolve is not null && EmptySprite != 0)
|
|
{
|
|
var (texture, _, _) = SpriteResolve(EmptySprite);
|
|
if (texture != 0)
|
|
ctx.DrawSprite(texture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One);
|
|
}
|
|
|
|
if (CatalogOverlayTexture != 0)
|
|
ctx.DrawSprite(CatalogOverlayTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One);
|
|
|
|
// Spell favorites are UIElement_UIItems too. Retail's
|
|
// SpellCastSubMenu::UpdateShortcutOverlays @ 0x004C5BE0 calls the same
|
|
// SetShortcutNum path used by physical toolbar shortcuts.
|
|
DrawShortcutOverlay(ctx);
|
|
|
|
if (!SelectionBehindContent)
|
|
DrawSelection(ctx);
|
|
|
|
if (ShowLabel)
|
|
{
|
|
float labelLeft = LabelLeft > 0f ? LabelLeft : IconLeft + iconWidth + 4f;
|
|
float labelWidth = LabelWidth > 0f ? LabelWidth : MathF.Max(0f, Width - labelLeft);
|
|
if (LabelFont is not null)
|
|
{
|
|
string text = FitText(Label, labelWidth, LabelFont.MeasureWidth);
|
|
float y = MathF.Max(0f, (Height - LabelFont.LineHeight) * 0.5f);
|
|
ctx.DrawStringDat(LabelFont, text, labelLeft, y, LabelColor);
|
|
}
|
|
else
|
|
{
|
|
BitmapFont? font = ctx.DefaultFont;
|
|
string text = font is null ? Label : FitText(Label, labelWidth, font.MeasureWidth);
|
|
float y = font is null ? 3f : MathF.Max(0f, (Height - font.LineHeight) * 0.5f);
|
|
ctx.DrawString(text, labelLeft, y, LabelColor, font);
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(Detail))
|
|
ctx.DrawString(Detail!, MathF.Max(labelLeft, Width - 48f), 3f, Vector4.One);
|
|
}
|
|
}
|
|
|
|
private void DrawSelection(UiRenderContext ctx)
|
|
{
|
|
if (!Selected || SpriteResolve is null || SelectedSprite == 0u) return;
|
|
var (texture, _, _) = SpriteResolve(SelectedSprite);
|
|
if (texture != 0u)
|
|
ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
|
}
|
|
|
|
private static string FitText(string text, float width, Func<string, float> measure)
|
|
{
|
|
if (width <= 0f || string.IsNullOrEmpty(text) || measure(text) <= width)
|
|
return text;
|
|
|
|
const string ellipsis = "...";
|
|
float ellipsisWidth = measure(ellipsis);
|
|
if (ellipsisWidth >= width) return string.Empty;
|
|
int low = 0, high = text.Length;
|
|
while (low < high)
|
|
{
|
|
int mid = (low + high + 1) / 2;
|
|
if (measure(text[..mid]) + ellipsisWidth <= width) low = mid;
|
|
else high = mid - 1;
|
|
}
|
|
return text[..low] + ellipsis;
|
|
}
|
|
}
|