M6.3: Trait popovers — shared PopoverLayer + TraitChip triggers
Per GODOT_PORTING_GUIDE.md §6 (and §12 build order — popovers before
the easy card-grid steps because traits/skills/bonuses surface them
everywhere). One reusable popover panel; lightweight chip triggers.
Scenes/Widgets/PopoverLayer.cs:
CanvasLayer added once as a child of Wizard.tscn. Owns one
PanelContainer + close Timer; static Instance for chip-side access.
ShowFor(trigger, ...) populates and positions the popover at the
trigger's global rect with viewport clamp + flip-above logic
(mirrors src/trait-hint.jsx). 80 ms grace period when moving from
trigger to popover so the popover stays open across the gap.
Detriment popovers get a red Modulate as a placeholder for the
seal-coloured StyleBox the theming pass will install.
Scenes/Widgets/TraitChip.cs:
Lightweight PanelContainer + Label trigger. On MouseEntered asks
PopoverLayer.Instance to show; on MouseExited schedules close.
Pill styling deferred to theming (default Godot panel for now;
TraitChip / TraitChipDetriment styleboxes will land alongside
the parchment Theme pass).
Wizard.tscn:
PopoverLayer added as a top-level CanvasLayer child so popovers
float above every step's content regardless of where the trigger
is in the tree.
Steps/StepClade.cs:
Replaces the placeholder "{n} traits, {m} detriments" line with an
HFlowContainer of TraitChip per trait + per detriment. Hover any
chip → popover shows name + description (+ DETRIMENT tag for the
detriment chips).
Also: cards switched from Button to PanelContainer for content-
driven height. Button isn't a Container, so its intrinsic min
size didn't aggregate from the inner vbox — at higher trait
counts the chips overflowed into the cards below. PanelContainer
is a Container, so the card grows with its content. GuiInput
handles the click-to-select; selected state shown via Modulate
tint until the proper StyleBox swap lands in theming.
Closes M6.3. Per guide §12, next is M6.4 — easy card-grid steps
(Species / Calling / Subclass / History) variations on the StepClade
pattern, then StepSkills, then StepReview.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Godot;
|
||||
using Theriapolis.Core.Data;
|
||||
using Theriapolis.GodotHost.Scenes.Widgets;
|
||||
using Theriapolis.GodotHost.UI;
|
||||
|
||||
namespace Theriapolis.GodotHost.Scenes.Steps;
|
||||
@@ -69,55 +70,41 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
{
|
||||
bool selected = _draft.CladeId == clade.Id;
|
||||
|
||||
var btn = new Button
|
||||
// PanelContainer (a Container subclass) so the card height is
|
||||
// driven by its inner VBoxContainer's content. Switching from
|
||||
// Button avoids the issue where Button's intrinsic min size
|
||||
// doesn't aggregate from non-Button children, causing chips to
|
||||
// overflow into the cards below at high trait counts.
|
||||
var card = new PanelContainer
|
||||
{
|
||||
Text = "",
|
||||
Flat = false,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = selected,
|
||||
FocusMode = Control.FocusModeEnum.None,
|
||||
// 200 wide so 3 cards + separators (≈ 632) + page margins +
|
||||
// Aside fit inside ≥ 1024-px viewports (the smaller-screen
|
||||
// floor we want to support). Height held at 200 so the inner
|
||||
// labels render at readable size without a content-driven
|
||||
// height collapse (Button isn't a Container, so child vbox
|
||||
// height doesn't bubble up to the button's intrinsic min size).
|
||||
CustomMinimumSize = new Vector2(200, 200),
|
||||
ClipText = false,
|
||||
Alignment = HorizontalAlignment.Left,
|
||||
CustomMinimumSize = new Vector2(200, 0),
|
||||
MouseFilter = MouseFilterEnum.Stop,
|
||||
};
|
||||
btn.Pressed += () =>
|
||||
if (selected) card.Modulate = new Color(1f, 0.95f, 0.85f); // gild tint placeholder until theming
|
||||
|
||||
card.GuiInput += (InputEvent e) =>
|
||||
{
|
||||
// Default species for the new clade — match React app.jsx:
|
||||
// when clade changes, species defaults to first species in clade.
|
||||
string speciesId = "";
|
||||
foreach (var s in CodexContent.SpeciesOfClade(clade.Id))
|
||||
if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
speciesId = s.Id;
|
||||
break;
|
||||
// Default species for the new clade — match React app.jsx:
|
||||
// when clade changes, species defaults to first species in clade.
|
||||
string speciesId = "";
|
||||
foreach (var s in CodexContent.SpeciesOfClade(clade.Id))
|
||||
{
|
||||
speciesId = s.Id;
|
||||
break;
|
||||
}
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ "clade_id", clade.Id },
|
||||
{ "species_id", speciesId },
|
||||
});
|
||||
}
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ "clade_id", clade.Id },
|
||||
{ "species_id", speciesId },
|
||||
});
|
||||
};
|
||||
|
||||
// Label content stacked inside the button via an anchored VBoxContainer
|
||||
// (Button isn't a Container, so we anchor the vbox to fill the button's
|
||||
// rect and let the children flow within it).
|
||||
var box = new VBoxContainer
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
box.AnchorRight = 1f;
|
||||
box.AnchorBottom = 1f;
|
||||
box.OffsetLeft = 12;
|
||||
box.OffsetTop = 12;
|
||||
box.OffsetRight = -12;
|
||||
box.OffsetBottom = -12;
|
||||
var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass };
|
||||
box.AddThemeConstantOverride("separation", 6);
|
||||
btn.AddChild(box);
|
||||
card.AddChild(box);
|
||||
|
||||
box.AddChild(new Label { Text = clade.Name, MouseFilter = MouseFilterEnum.Ignore });
|
||||
box.AddChild(new Label
|
||||
@@ -139,15 +126,34 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
});
|
||||
}
|
||||
|
||||
if (clade.Traits.Length > 0)
|
||||
if (clade.Traits.Length > 0 || clade.Detriments.Length > 0)
|
||||
{
|
||||
box.AddChild(new Label
|
||||
var chips = new HFlowContainer { MouseFilter = MouseFilterEnum.Pass };
|
||||
chips.AddThemeConstantOverride("h_separation", 6);
|
||||
chips.AddThemeConstantOverride("v_separation", 4);
|
||||
box.AddChild(chips);
|
||||
|
||||
foreach (var t in clade.Traits)
|
||||
{
|
||||
Text = $"{clade.Traits.Length} traits, {clade.Detriments.Length} detriments",
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
});
|
||||
var chip = new TraitChip
|
||||
{
|
||||
TraitName = t.Name,
|
||||
Description = t.Description,
|
||||
};
|
||||
chips.AddChild(chip);
|
||||
}
|
||||
foreach (var d in clade.Detriments)
|
||||
{
|
||||
var chip = new TraitChip
|
||||
{
|
||||
TraitName = d.Name,
|
||||
Description = d.Description,
|
||||
Detriment = true,
|
||||
};
|
||||
chips.AddChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
return btn;
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user