summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 12:05:26 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 12:05:26 +0800
commit2851a030db0f0bc5892305fb189a41c97c13578d (patch)
treedbb834c31ad43777fd43f39c00f58449cb753388 /src
parent3812e78ba39764b3844d52ee7382b1350960ecb9 (diff)
downloadliving-village-2851a030db0f0bc5892305fb189a41c97c13578d.tar.gz
fix(desktop): 标题菜单光标与文字分列不再叠印
根因:页面标题行(panelY+24)与首行内容(MenuTopY)同为 y=320 造成两串文字叠印。修复:TitleScreen 布局新增 HeadingY/FirstItemY 分层与独立光标列(CursorX/CursorWidth/TextX),selectionRects 纯函数返回光标与文字矩形,回归测试断言逐行矩形不相交且标题行净空≥32px;渲染改为光标、文字分别绘制。
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs16
-rw-r--r--src/LivingVillage.Desktop/Game.fs18
-rw-r--r--src/LivingVillage.Desktop/TitleScreen.fs33
3 files changed, 58 insertions, 9 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 524573f..c85287f 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -246,6 +246,22 @@ type PrototypeTests () =
Assert.IsTrue(ChineseText.requiredUiLabels |> List.contains "精力")
[<TestMethod>]
+ member _.TitleScreenSelectionCursorAndLabelNeverOverprint () =
+ let layout = TitleScreen.layout 1280 720
+
+ // Heading must sit fully above the first menu row (no same-line overprint).
+ Assert.IsTrue(layout.FirstItemY - layout.HeadingY >= 32, sprintf "heading clearance %d" (layout.FirstItemY - layout.HeadingY))
+
+ // Every menu row: cursor rectangle and label rectangle must be disjoint.
+ for rowIndex in 0 .. 5 do
+ let cursor, label = TitleScreen.selectionRects layout rowIndex
+ Assert.IsTrue(TitleScreen.rectsDisjoint cursor label, sprintf "row %d cursor overlaps label" rowIndex)
+ Assert.AreEqual<int>(cursor.Y, label.Y)
+
+ // Cursor column leaves real clearance: label starts beyond cursor + margin.
+ Assert.IsTrue(layout.TextX >= layout.CursorX + layout.CursorWidth)
+
+ [<TestMethod>]
member _.TitleScreenLayoutIsDeterministicAndLayered () =
let first = TitleScreen.layout 1280 720
let second = TitleScreen.layout 1280 720
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index c8d0044..7337129 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -560,8 +560,12 @@ type LivingVillageGame() as this =
PixelText.draw spriteBatch pixel cjkAtlas x y textScale color text
let drawSelection (index: int) (label: string) (y: int) =
let selected = index = menu.Selected
- let prefix = if selected then ">" else " "
- drawText innerX y (if selected then selectedColor else textColor) (sprintf "%s %s" prefix label)
+ let cursor, labelRect = TitleScreen.selectionRects layout index
+ if selected then
+ PixelText.draw spriteBatch pixel cjkAtlas cursor.X cursor.Y textScale selectedColor ">"
+ else
+ PixelText.draw spriteBatch pixel cjkAtlas cursor.X cursor.Y textScale mutedColor " "
+ PixelText.draw spriteBatch pixel cjkAtlas labelRect.X labelRect.Y textScale (if selected then selectedColor else textColor) label
spriteBatch.Begin()
// Layered dusk gradient: sky bands darken toward the horizon, never a flat block.
@@ -601,27 +605,27 @@ type LivingVillageGame() as this =
// Menu panel floats over the lower half; translucent so the scene layers stay visible.
spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, panelHeight), Color(11, 19, 27, 200))
spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, 4), Color(92, 180, 190))
- let contentY = layout.MenuTopY
+ let contentY = layout.FirstItemY
match menu.Page with
| MainMenu ->
- drawText innerX (panelY + 24) mutedColor (ChineseText.menuHeading MainMenuHeading)
+ drawText innerX (layout.HeadingY) mutedColor (ChineseText.menuHeading MainMenuHeading)
MenuState.items menu
|> List.iteri (fun index item -> drawSelection index (MenuState.itemLabel item) (contentY + index * lineHeight))
drawText innerX (panelY + panelHeight - 40) mutedColor "UP DOWN 选择 ENTER 确认"
| Settings ->
- drawText innerX (panelY + 24) mutedColor (ChineseText.menuHeading SettingsHeading)
+ drawText innerX (layout.HeadingY) mutedColor (ChineseText.menuHeading SettingsHeading)
MenuState.settings
|> List.iteri (fun index setting -> drawSelection index (MenuState.settingsLabel setting) (contentY + index * lineHeight))
drawText innerX (panelY + panelHeight - 72) mutedColor (sprintf "当前 %s" (M6Presentation.clockLabel menu.Settings))
drawText innerX (panelY + panelHeight - 40) mutedColor "ENTER 应用 ESC 返回"
| Controls ->
- drawText innerX (panelY + 24) mutedColor (ChineseText.menuHeading ControlsHeading)
+ drawText innerX (layout.HeadingY) mutedColor (ChineseText.menuHeading ControlsHeading)
MenuState.controlRows
|> List.iteri (fun index row ->
drawText innerX (contentY + index * 25) textColor (sprintf "%s %s" row.Keys row.Action))
drawText innerX (panelY + panelHeight - 40) mutedColor "ESC 返回"
| LoadError ->
- drawText innerX (panelY + 24) (Color(240, 130, 120)) "读取失败"
+ drawText innerX (layout.HeadingY) (Color(240, 130, 120)) "读取失败"
let message = menu.Error |> Option.defaultValue "暂无存档"
let visible = if message.Length > 24 then message.Substring(0, 24) else message
drawText innerX contentY textColor visible
diff --git a/src/LivingVillage.Desktop/TitleScreen.fs b/src/LivingVillage.Desktop/TitleScreen.fs
index 8b1c5d5..00cf5b8 100644
--- a/src/LivingVillage.Desktop/TitleScreen.fs
+++ b/src/LivingVillage.Desktop/TitleScreen.fs
@@ -24,7 +24,31 @@ module TitleScreen =
PanelX: int
PanelY: int
PanelWidth: int
- PanelHeight: int }
+ PanelHeight: int
+ HeadingY: int
+ FirstItemY: int
+ CursorX: int
+ CursorWidth: int
+ TextX: int }
+
+ type TextRect =
+ { X: int
+ Y: int
+ Width: int
+ Height: int }
+
+ let rectsDisjoint (a: TextRect) (b: TextRect) : bool =
+ a.X + a.Width <= b.X
+ || b.X + b.Width <= a.X
+ || a.Y + a.Height <= b.Y
+ || b.Y + b.Height <= a.Y
+
+ /// Cursor and label rectangles for one menu row. The cursor is drawn in its own
+ /// column; the label always starts to the right of it, so they can never overprint.
+ let selectionRects (layout: TitleLayout) (rowIndex: int) : TextRect * TextRect =
+ let y = layout.FirstItemY + rowIndex * layout.MenuLineHeight
+ { X = layout.CursorX; Y = y; Width = layout.CursorWidth; Height = 16 },
+ { X = layout.TextX; Y = y; Width = layout.PanelX + layout.PanelWidth - layout.TextX - 24; Height = 16 }
// Palette mirrors the VillageArt atlas: roof (45,49,57)/(30,33,38), water (54,116,145),
// lantern paper (212,92,74). Purely decorative reuse of existing art colors.
@@ -82,4 +106,9 @@ module TitleScreen =
PanelX = (width - panelWidth) / 2
PanelY = height / 2 - 64
PanelWidth = panelWidth
- PanelHeight = min 360 (height - height / 2 - 40) }
+ PanelHeight = min 360 (height - height / 2 - 40)
+ HeadingY = height / 2 - 64 + 20
+ FirstItemY = height / 2 - 64 + 60
+ CursorX = (width - panelWidth) / 2 + 24
+ CursorWidth = 20
+ TextX = (width - panelWidth) / 2 + 24 + 20 + 12 }