This skill should be used when the user asks to "create a menu layout", "fix UI spacing", "adjust positioning", "design a menu", or discusses UI alignment, element placement, or visual layout...
Design and validate Stardew Valley menu layouts with confidence. This skill provides a systematic approach to UI development that reduces trial-and-error iteration.
Before writing any positioning code, create a text diagram of the intended layout:
+------------------------------------------+
| [Tab1] [Tab2] [Tab3] [X Close] | <- Tab bar (64px height)
+------------------------------------------+
| |
| +------------+ +------------------+ |
| | | | Hat: [<] [>] | | <- Item selectors
| | Portrait | | Shirt: [<] [>] | |
| | Box | | Pants: [<] [>] | |
| | (256x384) | | Boots: [<] [>] | |
| | | +------------------+ |
| +------------+ |
| |
| [Spring][Summer][Fall][Winter][Special] | <- Categories
| |
| [Save Outfit] | <- Action buttons
+------------------------------------------+
Replace magic numbers with named relationships:
// BAD: Magic numbers
int buttonX = 324;
int buttonY = yPositionOnScreen + 88;
// GOOD: Semantic relationships
int buttonX = _portraitBox.Right + Spacing.ElementGap;
int buttonY = _portraitBox.Y;
Create named constants for consistent spacing:
private static class Layout
{
// Standard SDV spacing
public const int BorderWidth = IClickableMenu.borderWidth; // 16
public const int SpaceToClearSideBorder = IClickableMenu.spaceToClearSideBorder; // 16
// Custom spacing for this menu
public const int ElementGap = 16; // Between sibling elements
public const int SectionGap = 32; // Between major sections
public const int ButtonSize = 64; // Standard button dimensions
public const int ArrowButtonSize = 48; // Arrow/selector buttons
public const int TabHeight = 64; // Tab bar height
public const int PortraitWidth = 256; // 4x farmer sprite
public const int PortraitHeight = 384; // 4x farmer sprite
}
Position elements relative to anchors, not absolute coordinates:
// Portrait anchored to menu left
_portraitBox = new Rectangle(
xPositionOnScreen + Layout.BorderWidth + Layout.SpaceToClearSideBorder,
yPositionOnScreen + Layout.TabHeight + Layout.SectionGap,
Layout.PortraitWidth,
Layout.PortraitHeight
);
// Selector buttons anchored to portrait right
int selectorX = _portraitBox.Right + Layout.SectionGap;
int selectorY = _portraitBox.Y;
// Categories anchored to portrait bottom
int categoryY = _portraitBox.Bottom + Layout.ElementGap;
Run these static checks before manual testing:
IClickableMenu.borderWidth = 16 pixelsIClickableMenu.spaceToClearSideBorder = 16 pixels// Arrows (from Game1.getSourceRectForStandardTileSheet)
Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 44) // Left arrow
Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 33) // Right arrow
// Icons
new Rectangle(352, 495, 12, 11) // Small left arrow
new Rectangle(365, 495, 12, 11) // Small right arrow
new Rectangle(128, 256, 64, 64) // OK/checkmark button
new Rectangle(322, 498, 12, 12) // X/close icon
new Rectangle(310, 392, 16, 16) // Star icon
new Rectangle(323, 433, 9, 10) // Trash can
Vector2 topLeft = Utility.getTopLeftPositionForCenteringOnScreen(width, height);
xPositionOnScreen = (int)topLeft.X;
yPositionOnScreen = (int)topLeft.Y;
public override void gameWindowSizeChanged(Rectangle oldBounds, Rectangle newBounds)
{
base.gameWindowSizeChanged(oldBounds, newBounds);
// Recenter menu
Vector2 topLeft = Utility.getTopLeftPositionForCenteringOnScreen(width, height);
xPositionOnScreen = (int)topLeft.X;
yPositionOnScreen = (int)topLeft.Y;
// Recalculate all element positions...
RecalculateLayout();
// CRITICAL: Propagate to child menus
GetChildMenu()?.gameWindowSizeChanged(oldBounds, newBounds);
}
// Every clickable element needs:
component.myID = uniqueId;
component.leftNeighborID = leftId; // or -99999 for none
component.rightNeighborID = rightId;
component.upNeighborID = upId;
component.downNeighborID = downId;
// Call after adding all components:
populateClickableComponentList();
snapToDefaultClickableComponent();
When SMAPI debug mode is enabled, consider rendering element bounds for verification:
#if DEBUG
private void DrawDebugOverlay(SpriteBatch b)
{
// Draw clickable component bounds
foreach (var component in allClickableComponents)
{
DrawBorder(b, component.bounds, Color.Red * 0.5f);
// Draw component name/ID
b.DrawString(Game1.tinyFont, $"{component.name}:{component.myID}",
new Vector2(component.bounds.X, component.bounds.Y - 12), Color.White);
}
}
private void DrawBorder(SpriteBatch b, Rectangle rect, Color color)
{
b.Draw(Game1.staminaRect, new Rectangle(rect.X, rect.Y, rect.Width, 1), color);
b.Draw(Game1.staminaRect, new Rectangle(rect.X, rect.Y + rect.Height - 1, rect.Width, 1), color);
b.Draw(Game1.staminaRect, new Rectangle(rect.X, rect.Y, 1, rect.Height), color);
b.Draw(Game1.staminaRect, new Rectangle(rect.X + rect.Width - 1, rect.Y, 1, rect.Height), color);
}
#endif
Before requesting manual testing, verify:
See references/ for detailed patterns from this codebase and SDV conventions.