using System; using Cthangover.Core.Items; using Cthangover.Mods.Interface.Base.Lists; using Cthangover.Core.UI.Theming; using Godot; namespace Cthangover.Core.UI.Inventory { /// /// Interactive cell of the inventory grid. Unlike the old thin cell that /// only drew an icon and a stack counter, this one owns the whole visual /// language of a slot: the frame (normal/hover/selected), the type accent /// dot, the short action tag and the dimmed state for inert items. It is /// built entirely in code because the grid needs pixel-accurate /// frame swapping that theme overrides in the /// tscn pipeline cannot express. /// /// Click handling lives inside the cell: it raises /// after consuming the mouse event, instead of the panel subscribing to /// GuiInput from outside, so event propagation is stopped by the /// control that actually owns the click. /// public partial class InventoryItemBehaviour : ListItem { /// Raised on a left-click after the cell consumed the event. The payload is the cell itself. public event Action Clicked; private static readonly StyleBoxFlat NormalFrame = MakeFrame(UiTokens.LineSoft, UiTokens.Surface1); private static readonly StyleBoxFlat HoverFrame = MakeFrame(UiTokens.LineStrong, UiTokens.SurfaceHover); private static readonly StyleBoxFlat SelectedFrame = MakeFrame(UiTokens.Accent, UiTokens.SurfaceSelected); private static readonly StyleBoxFlat TagStyle = MakeTagStyle(); private static readonly Color IconDimmed = UiTokens.Alpha(UiTokens.White, 0.38f); private Panel _frame; private TextureRect _icon; private Label _count; private Panel _dot; private StyleBoxFlat _dotStyle; private PanelContainer _tag; private Label _tagLabel; private bool _built; private bool _selected; private bool _hovered; /// Stops mouse propagation and wires hover/click handling before any model is bound. public InventoryItemBehaviour() { MouseFilter = MouseFilterEnum.Stop; GuiInput += OnGuiInput; MouseEntered += OnMouseEntered; MouseExited += OnMouseExited; } /// /// Binds the item stack and lazily builds the visual tree on first /// use; repeated binds only refresh the rendered data. /// public override void Construct(IItemContainer model) { base.Construct(model); EnsureBuilt(); UpdateInfo(); } /// /// Pushes the bound stack into the visuals: icon, stack counter /// (hidden for singletons), type accent dot, action tag and the /// dimmed state for items without a usable action. /// public void UpdateInfo() { if (!_built || Model?.Item == null) return; var item = Model.Item; _icon.Texture = item.Sprite; _count.Text = Model.Count > 1 ? "×" + Model.Count : string.Empty; _count.Visible = Model.Count > 1; _dotStyle.BgColor = InventoryItemUi.AccentColor(item.ItemType); var tagKey = InventoryItemUi.TagKey(item); _tag.Visible = tagKey != null; if (tagKey != null) _tagLabel.Text = InventoryItemUi.Translate(tagKey); var dimmed = !InventoryItemUi.IsUsable(item); _icon.Modulate = dimmed ? IconDimmed : Colors.White; _count.Modulate = dimmed ? IconDimmed : Colors.White; } /// /// Overrides what derived from the item's action, /// for grids where the action is not the point — the trade window shows a /// price in the corner tag (, null hides it) and /// dims what cannot be traded. Call after . /// public void Decorate(string tag, bool dimmed) { if (!_built) return; _tag.Visible = tag != null; if (tag != null) _tagLabel.Text = tag; _icon.Modulate = dimmed ? IconDimmed : Colors.White; _count.Modulate = dimmed ? IconDimmed : Colors.White; } /// Switches the frame to the gold selected state; the panel owns this flag. public void SetSelected(bool selected) { _selected = selected; ApplyFrame(); } /// Removes the cell from the scene tree; the grid rebuilds cells instead of recycling them. public override void Destruct() { QueueFree(); } private void EnsureBuilt() { if (_built) return; _built = true; _frame = new Panel(); _frame.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _frame.AddThemeStyleboxOverride("panel", NormalFrame); _frame.MouseFilter = MouseFilterEnum.Ignore; AddChild(_frame); _dot = new Panel(); _dot.SetAnchorsAndOffsetsPreset(LayoutPreset.TopLeft); _dot.OffsetLeft = 6; _dot.OffsetTop = 6; _dot.OffsetRight = 13; _dot.OffsetBottom = 13; _dot.MouseFilter = MouseFilterEnum.Ignore; _dotStyle = new StyleBoxFlat(); _dotStyle.CornerRadiusTopLeft = 2; _dotStyle.CornerRadiusTopRight = 2; _dotStyle.CornerRadiusBottomLeft = 2; _dotStyle.CornerRadiusBottomRight = 2; _dot.AddThemeStyleboxOverride("panel", _dotStyle); AddChild(_dot); _icon = new TextureRect(); _icon.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _icon.OffsetLeft = 12; _icon.OffsetTop = 12; _icon.OffsetRight = -12; _icon.OffsetBottom = -12; _icon.ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize; _icon.StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered; _icon.MouseFilter = MouseFilterEnum.Ignore; AddChild(_icon); _count = new Label(); _count.SetAnchorsAndOffsetsPreset(LayoutPreset.BottomRight); _count.OffsetLeft = -70; _count.OffsetTop = -26; _count.OffsetRight = -6; _count.OffsetBottom = -3; _count.HorizontalAlignment = HorizontalAlignment.Right; _count.VerticalAlignment = VerticalAlignment.Center; _count.AddThemeFontSizeOverride("font_size", 13); _count.AddThemeColorOverride("font_color", UiTokens.Stack); _count.AddThemeColorOverride("font_outline_color", new Color(0f, 0f, 0f, 0.9f)); _count.AddThemeConstantOverride("outline_size", 4); _count.MouseFilter = MouseFilterEnum.Ignore; AddChild(_count); var tagRow = new HBoxContainer(); tagRow.SetAnchorsAndOffsetsPreset(LayoutPreset.TopWide); tagRow.OffsetLeft = 5; tagRow.OffsetTop = 4; tagRow.OffsetRight = -5; tagRow.OffsetBottom = 24; tagRow.Alignment = BoxContainer.AlignmentMode.End; tagRow.MouseFilter = MouseFilterEnum.Ignore; AddChild(tagRow); _tag = new PanelContainer(); _tag.AddThemeStyleboxOverride("panel", TagStyle); _tag.MouseFilter = MouseFilterEnum.Ignore; tagRow.AddChild(_tag); _tagLabel = new Label(); _tagLabel.AddThemeFontSizeOverride("font_size", 9); _tagLabel.AddThemeColorOverride("font_color", UiTokens.AccentBright); _tagLabel.MouseFilter = MouseFilterEnum.Ignore; _tag.AddChild(_tagLabel); } private void OnGuiInput(InputEvent @event) { if (@event is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) { Clicked?.Invoke(this); AcceptEvent(); } } private void OnMouseEntered() { _hovered = true; ApplyFrame(); } private void OnMouseExited() { _hovered = false; ApplyFrame(); } private void ApplyFrame() { if (!_built || _frame == null) return; _frame.AddThemeStyleboxOverride("panel", _selected ? SelectedFrame : (_hovered ? HoverFrame : NormalFrame)); } private static StyleBoxFlat MakeFrame(Color border, Color background) { var style = new StyleBoxFlat(); style.BgColor = background; style.BorderColor = border; style.BorderWidthLeft = 1; style.BorderWidthRight = 1; style.BorderWidthTop = 1; style.BorderWidthBottom = 1; style.CornerRadiusTopLeft = 6; style.CornerRadiusTopRight = 6; style.CornerRadiusBottomLeft = 6; style.CornerRadiusBottomRight = 6; return style; } private static StyleBoxFlat MakeTagStyle() { var style = new StyleBoxFlat(); style.BgColor = UiTokens.SlotTagBg; style.BorderColor = UiTokens.AccentLine; style.BorderWidthLeft = 1; style.BorderWidthRight = 1; style.BorderWidthTop = 1; style.BorderWidthBottom = 1; style.CornerRadiusTopLeft = 3; style.CornerRadiusTopRight = 3; style.CornerRadiusBottomLeft = 3; style.CornerRadiusBottomRight = 3; style.ContentMarginLeft = 5; style.ContentMarginRight = 5; style.ContentMarginTop = 1; style.ContentMarginBottom = 1; return style; } } }