From 2851a030db0f0bc5892305fb189a41c97c13578d Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 12:05:26 +0800 Subject: fix(desktop): 标题菜单光标与文字分列不再叠印 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:页面标题行(panelY+24)与首行内容(MenuTopY)同为 y=320 造成两串文字叠印。修复:TitleScreen 布局新增 HeadingY/FirstItemY 分层与独立光标列(CursorX/CursorWidth/TextX),selectionRects 纯函数返回光标与文字矩形,回归测试断言逐行矩形不相交且标题行净空≥32px;渲染改为光标、文字分别绘制。 --- src/LivingVillage.Desktop/Game.fs | 18 ++++++++++------- src/LivingVillage.Desktop/TitleScreen.fs | 33 ++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 9 deletions(-) (limited to 'src/LivingVillage.Desktop') 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 } -- cgit v1.2.3