diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 12:07:13 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 12:07:13 +0800 |
| commit | 506afd9e38cad2ab0752734b79ef02da0864db67 (patch) | |
| tree | 6006269927609e53bfd826df4f6307681f994a0a | |
| parent | 2851a030db0f0bc5892305fb189a41c97c13578d (diff) | |
| download | living-village-506afd9e38cad2ab0752734b79ef02da0864db67.tar.gz | |
fix(desktop): 键位页键名列统一宽度不叠印
Controls 页按键名列独立成列(ControlsKeyX/KeyWidth=120/ActionX),controlRowRects 纯函数返回键名与动作矩形,回归测试断言逐行矩形不相交、全部行键名列 x/宽度一致、列宽容纳最长键名 F1/F2/F3;面板上移至 ControlsPanelY 保证 14 行键位加页脚完整落入视口。
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 23 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 6 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/TitleScreen.fs | 21 |
3 files changed, 47 insertions, 3 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index c85287f..77d2530 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -219,6 +219,29 @@ type PrototypeTests () = Assert.AreEqual<SimulationControl>(original, restoredControl) [<TestMethod>] + member _.ControlsPageKeyColumnIsUniformAndNeverOverprintsActions () = + let layout = TitleScreen.layout 1280 720 + let rows = MenuState.controlRows + + // Every row: key-name rectangle and action rectangle must be disjoint. + for rowIndex in 0 .. rows.Length - 1 do + let keyRect, actionRect = TitleScreen.controlRowRects layout rowIndex + Assert.IsTrue(TitleScreen.rectsDisjoint keyRect actionRect, sprintf "row %d key overlaps action" rowIndex) + + // The key column is uniform: every row shares the same x and width. + let keyRects = [ 0 .. rows.Length - 1 ] |> List.map (fun index -> TitleScreen.controlRowRects layout index |> fst) + Assert.AreEqual<int>(1, keyRects |> List.map (fun rect -> rect.X) |> List.distinct |> List.length) + Assert.AreEqual<int>(1, keyRects |> List.map (fun rect -> rect.Width) |> List.distinct |> List.length) + + // The key column fits the longest key name (latin glyph: 6px * scale 2 per char). + let longestKey = rows |> List.maxBy (fun row -> row.Keys.Length) + Assert.IsTrue(layout.ControlsKeyWidth >= longestKey.Keys.Length * 12, sprintf "key width %d too small for %s" layout.ControlsKeyWidth longestKey.Keys) + + // The controls panel holds every row plus the footer inside the viewport. + let panelBottom = layout.ControlsPanelY + 60 + rows.Length * layout.ControlsRowHeight + 48 + Assert.IsTrue(panelBottom <= 720, sprintf "controls panel bottom %d overflows" panelBottom) + + [<TestMethod>] member _.CornerStatusShowsDayMoneyAndEnergyFromRealData () = let world = Sim.initialWorldN 42UL 0 let lines = M5Interaction.cornerStatusLines world diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 7337129..25b8c48 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -547,6 +547,8 @@ type LivingVillageGame() as this = | LoadError -> 4 | Playing -> 0 let rowHeight = if menu.Page = Controls then 25 else layout.MenuLineHeight + let panelY = + if menu.Page = Controls then layout.ControlsPanelY else layout.PanelY let panelHeight = min (height - panelY - 16) (max layout.PanelHeight (120 + contentRows * rowHeight + 48)) @@ -622,7 +624,9 @@ type LivingVillageGame() as this = 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)) + let keyRect, actionRect = TitleScreen.controlRowRects layout index + PixelText.draw spriteBatch pixel cjkAtlas keyRect.X keyRect.Y textScale textColor row.Keys + PixelText.draw spriteBatch pixel cjkAtlas actionRect.X actionRect.Y textScale textColor row.Action) drawText innerX (panelY + panelHeight - 40) mutedColor "ESC 返回" | LoadError -> drawText innerX (layout.HeadingY) (Color(240, 130, 120)) "读取失败" diff --git a/src/LivingVillage.Desktop/TitleScreen.fs b/src/LivingVillage.Desktop/TitleScreen.fs index 00cf5b8..7ec46d2 100644 --- a/src/LivingVillage.Desktop/TitleScreen.fs +++ b/src/LivingVillage.Desktop/TitleScreen.fs @@ -29,7 +29,12 @@ module TitleScreen = FirstItemY: int CursorX: int CursorWidth: int - TextX: int } + TextX: int + ControlsPanelY: int + ControlsRowHeight: int + ControlsKeyX: int + ControlsKeyWidth: int + ControlsActionX: int } type TextRect = { X: int @@ -43,6 +48,13 @@ module TitleScreen = || a.Y + a.Height <= b.Y || b.Y + b.Height <= a.Y + /// Key-name and action rectangles for one controls-page row. The key column has a + /// fixed width shared by every row, so all action texts start on the same x. + let controlRowRects (layout: TitleLayout) (rowIndex: int) : TextRect * TextRect = + let y = layout.FirstItemY + rowIndex * layout.ControlsRowHeight + { X = layout.ControlsKeyX; Y = y; Width = layout.ControlsKeyWidth; Height = 16 }, + { X = layout.ControlsActionX; Y = y; Width = layout.PanelX + layout.PanelWidth - layout.ControlsActionX - 24; Height = 16 } + /// 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 = @@ -111,4 +123,9 @@ module TitleScreen = FirstItemY = height / 2 - 64 + 60 CursorX = (width - panelWidth) / 2 + 24 CursorWidth = 20 - TextX = (width - panelWidth) / 2 + 24 + 20 + 12 } + TextX = (width - panelWidth) / 2 + 24 + 20 + 12 + ControlsPanelY = max 16 (height - 480) + ControlsRowHeight = 25 + ControlsKeyX = (width - panelWidth) / 2 + 24 + ControlsKeyWidth = 120 + ControlsActionX = (width - panelWidth) / 2 + 24 + 120 + 12 } |
