| name | sdv-ui-layout |
| description | 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. Also use when working on files in the Menus/ directory or when the user mentions layout, positioning, spacing, or menu design. |
| version | 1.0.0 |
SDV UI Layout Designer
Design and validate Stardew Valley menu layouts with confidence. This skill provides a systematic approach to UI development that reduces trial-and-error iteration.
Design Workflow
1. Conceptual Design First
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
+------------------------------------------+
2. Use Semantic Positioning
Replace magic numbers with named relationships:
int buttonX = 324;
int buttonY = yPositionOnScreen + 88;
int buttonX = _portraitBox.Right + Spacing.ElementGap;
int buttonY = _portraitBox.Y;
3. Define Layout Constants
Create named constants for consistent spacing:
private static class Layout
{
public const int BorderWidth = IClickableMenu.borderWidth;
public const int SpaceToClearSideBorder = IClickableMenu.spaceToClearSideBorder;
public const int ElementGap = 16;
public const int SectionGap = 32;
public const int ButtonSize = 64;
public const int ArrowButtonSize = 48;
public const int TabHeight = 64;
public const int PortraitWidth = 256;
public const int PortraitHeight = 384;
}
4. Use Anchor-Based Positioning
Position elements relative to anchors, not absolute coordinates:
_portraitBox = new Rectangle(
xPositionOnScreen + Layout.BorderWidth + Layout.SpaceToClearSideBorder,
yPositionOnScreen + Layout.TabHeight + Layout.SectionGap,
Layout.PortraitWidth,
Layout.PortraitHeight
);
int selectorX = _portraitBox.Right + Layout.SectionGap;
int selectorY = _portraitBox.Y;
int categoryY = _portraitBox.Bottom + Layout.ElementGap;
5. Validate Before Testing
Run these static checks before manual testing:
- Bounds Check: Verify all elements fit within menu bounds
- Overlap Check: Detect potentially overlapping clickable regions
- Gamepad Navigation: Verify all ClickableComponents have myID and neighborIDs
- Resolution Check: Test positions at 1280x720, 1920x1080, 2560x1440
SDV UI Conventions
Standard Dimensions
IClickableMenu.borderWidth = 16 pixels
IClickableMenu.spaceToClearSideBorder = 16 pixels
- Standard button: 64x64 pixels
- Arrow buttons: 48x48 pixels (scale 1f) or 60x60 (scale 1.25f)
- Tab buttons: varies, typically 64px height
Common Source Rectangles (Game1.mouseCursors)
Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 44)
Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 33)
new Rectangle(352, 495, 12, 11)
new Rectangle(365, 495, 12, 11)
new Rectangle(128, 256, 64, 64)
new Rectangle(322, 498, 12, 12)
new Rectangle(310, 392, 16, 16)
new Rectangle(323, 433, 9, 10)
Centering Pattern
Vector2 topLeft = Utility.getTopLeftPositionForCenteringOnScreen(width, height);
xPositionOnScreen = (int)topLeft.X;
yPositionOnScreen = (int)topLeft.Y;
Window Resize Handling
public override void gameWindowSizeChanged(Rectangle oldBounds, Rectangle newBounds)
{
base.gameWindowSizeChanged(oldBounds, newBounds);
Vector2 topLeft = Utility.getTopLeftPositionForCenteringOnScreen(width, height);
xPositionOnScreen = (int)topLeft.X;
yPositionOnScreen = (int)topLeft.Y;
RecalculateLayout();
GetChildMenu()?.gameWindowSizeChanged(oldBounds, newBounds);
}
Gamepad Navigation Requirements
component.myID = uniqueId;
component.leftNeighborID = leftId;
component.rightNeighborID = rightId;
component.upNeighborID = upId;
component.downNeighborID = downId;
populateClickableComponentList();
snapToDefaultClickableComponent();
Debug Overlay
When SMAPI debug mode is enabled, consider rendering element bounds for verification:
#if DEBUG
private void DrawDebugOverlay(SpriteBatch b)
{
foreach (var component in allClickableComponents)
{
DrawBorder(b, component.bounds, Color.Red * 0.5f);
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
Static Analysis Checklist
Before requesting manual testing, verify:
References
See references/ for detailed patterns from this codebase and SDV conventions.