UI: themed dark menu — cards, accent rule, hover/press buttons, backdrop

The menus were default gray Myra buttons on the raw clear color. Give the shared
Ui helpers a cohesive dark theme so every menu screen benefits at once:
- Button: dark fill, accent border, hover (OverBackground) and press (PressedBackground)
  highlights, padded, light text — real feedback instead of flat gray boxes.
- Card: bordered, translucent-dark, padded container; AccentRule: thin accent divider
  under the title.
- MenuScreen: full-screen dark backdrop behind menu content (separate from Screen,
  which stays background-free so the in-game HUD over the world is unaffected).

Main menu, New World, Credits and Load are now framed cards with a title + accent
rule; Settings/Boot/Loading get the backdrop. WorldScene/MultiplayerScene keep the
background-free Screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-15 01:07:30 +03:00
co-authored by Claude Opus 4.8
parent 02dcd01c0f
commit 36b6d576a1
8 changed files with 102 additions and 27 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ public sealed class BootScene : Scene
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
}; };
desktop.Root = Ui.Screen(_label); desktop.Root = Ui.MenuScreen(_label);
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока. // Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
_load = Task.Run(() => GameContent.Load()); _load = Task.Run(() => GameContent.Load());
+4 -4
View File
@@ -17,15 +17,15 @@ public sealed class CreditsScene : Scene
var input = this.UseInput(); var input = this.UseInput();
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12); var column = Ui.Card(10);
column.Widgets.Add(Ui.Title(lang.Get("credits.title"))); column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(Ui.AccentRule());
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author"))); column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role"))); column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 12 });
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back)); column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
desktop.Root = Ui.Screen(column); desktop.Root = Ui.MenuScreen(column);
UpdateSystems.Add( UpdateSystems.Add(
new CallbackSystem(() => new CallbackSystem(() =>
{ {
+3 -3
View File
@@ -41,9 +41,9 @@ public sealed class LoadGameScene : Scene
private void Rebuild() private void Rebuild()
{ {
var lang = _content.Languages; var lang = _content.Languages;
var column = Ui.Column(8); var column = Ui.Card(8);
column.Widgets.Add(Ui.Title(lang.Get("load.title"))); column.Widgets.Add(Ui.Title(lang.Get("load.title")));
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.AccentRule());
var saves = _store.List(); var saves = _store.List();
if (saves.Count == 0) if (saves.Count == 0)
@@ -98,7 +98,7 @@ public sealed class LoadGameScene : Scene
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back)); column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
_desktop.Root = Ui.Screen(column); _desktop.Root = Ui.MenuScreen(column);
} }
private void Back() private void Back()
+10 -10
View File
@@ -19,19 +19,19 @@ public sealed class MainMenuScene : Scene
var lang = content.Languages; var lang = content.Languages;
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12); var card = Ui.Card(8);
column.Widgets.Add(Ui.Title(lang.Get("menu.title"))); card.Widgets.Add(Ui.Title(lang.Get("menu.title")));
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle"))); card.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); card.Widgets.Add(Ui.AccentRule());
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
column.Widgets.Add( card.Widgets.Add(
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit()) Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
); );
desktop.Root = Ui.Screen(column); desktop.Root = Ui.MenuScreen(card);
} }
private void Go(Scene scene) private void Go(Scene scene)
+3 -3
View File
@@ -38,9 +38,9 @@ public sealed class NewWorldScene : Scene
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList(); var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
_preset = presets[0]; _preset = presets[0];
var column = Ui.Column(10); var column = Ui.Card(10);
column.Widgets.Add(Ui.Title(lang.Get("newworld.title"))); column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.AccentRule());
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 }; _name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name)); column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
@@ -106,7 +106,7 @@ public sealed class NewWorldScene : Scene
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150)); buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
column.Widgets.Add(buttons); column.Widgets.Add(buttons);
desktop.Root = Ui.Screen(column); desktop.Root = Ui.MenuScreen(column);
Refresh(); Refresh();
UpdateSystems.Add( UpdateSystems.Add(
+3 -1
View File
@@ -47,7 +47,9 @@ public sealed class SettingsScene : Scene
} }
private void Rebuild() => private void Rebuild() =>
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back)); _desktop.Root = Ui.MenuScreen(
SettingsPanel.Build(_settings, _content.Languages, Apply, Back)
);
private void Apply() private void Apply()
{ {
+1 -1
View File
@@ -39,7 +39,7 @@ public sealed class WorldLoadingScene : Scene
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
}; };
desktop.Root = Ui.Screen(_label); desktop.Root = Ui.MenuScreen(_label);
UpdateSystems.Add(new CallbackSystem(Tick)); UpdateSystems.Add(new CallbackSystem(Tick));
} }
+77 -4
View File
@@ -1,13 +1,18 @@
using System; using System;
using Friflo.Engine.ECS.Systems; using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
namespace LittleSim.UI; namespace LittleSim.UI;
/// <summary> /// <summary>
/// Общие фабрики виджетов Myra для экранов меню — единый стиль и отсутствие дублирования /// Общие фабрики виджетов Myra для экранов меню — единый стиль и отсутствие дублирования
/// разметки между сценами. Все подписи приходят готовыми строками (локализованными вызывающим). /// разметки между сценами. Тёмная тема с холодным акцентом: затемнённый фон-подложка на весь
/// экран (<see cref="Screen"/>), карточки с рамкой (<see cref="Card"/>), кнопки с подсветкой
/// при наведении и нажатии (<see cref="Button"/>). Все подписи приходят готовыми строками
/// (локализованными вызывающим).
/// </summary> /// </summary>
internal static class Ui internal static class Ui
{ {
@@ -17,7 +22,19 @@ internal static class Ui
/// <summary>Приглушённый цвет вторичного текста.</summary> /// <summary>Приглушённый цвет вторичного текста.</summary>
public static readonly Color Muted = new(150, 160, 175); public static readonly Color Muted = new(150, 160, 175);
private const int ButtonWidth = 280; /// <summary>Основной цвет текста на кнопках/карточках.</summary>
public static readonly Color Text = new(214, 222, 232);
// Палитра тёмной темы (фон → карточка → кнопка и её состояния).
private static readonly Color Backdrop = new(12, 15, 21);
private static readonly Color CardBg = new(22, 28, 38, 235);
private static readonly Color CardBorder = new(70, 100, 134);
private static readonly Color ButtonBg = new(30, 39, 52);
private static readonly Color ButtonOver = new(46, 64, 88);
private static readonly Color ButtonPress = new(62, 88, 120);
private static readonly Color ButtonBorder = new(64, 88, 118);
private const int ButtonWidth = 300;
/// <summary>Крупный заголовок по центру.</summary> /// <summary>Крупный заголовок по центру.</summary>
public static Label Title(string text) => public static Label Title(string text) =>
@@ -37,7 +54,18 @@ internal static class Ui
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
}; };
/// <summary>Кнопка фиксированной ширины с обработчиком клика.</summary> /// <summary>Тонкая акцентная черта-разделитель по центру (под заголовком).</summary>
public static Panel AccentRule(int width = 130) =>
new()
{
Width = width,
Height = 2,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 4, 0, 10),
Background = new SolidBrush(Accent),
};
/// <summary>Кнопка фиксированной ширины: тёмная заливка, акцентная рамка, подсветка hover/press.</summary>
public static TextButton Button(string text, Action onClick, int width = ButtonWidth) public static TextButton Button(string text, Action onClick, int width = ButtonWidth)
{ {
var button = new TextButton var button = new TextButton
@@ -45,6 +73,13 @@ internal static class Ui
Text = text, Text = text,
Width = width, Width = width,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
Padding = new Thickness(12, 9),
TextColor = Text,
Background = new SolidBrush(ButtonBg),
OverBackground = new SolidBrush(ButtonOver),
PressedBackground = new SolidBrush(ButtonPress),
Border = new SolidBrush(ButtonBorder),
BorderThickness = new Thickness(1),
}; };
button.Click += (_, _) => onClick(); button.Click += (_, _) => onClick();
return button; return button;
@@ -59,11 +94,25 @@ internal static class Ui
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
}; };
/// <summary>Карточка по центру: вертикальная колонка с тёмным фоном, акцентной рамкой и отступами.</summary>
public static VerticalStackPanel Card(int spacing = 8) =>
new()
{
Spacing = spacing,
Padding = new Thickness(30, 26),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Background = new SolidBrush(CardBg),
Border = new SolidBrush(CardBorder),
BorderThickness = new Thickness(1),
};
/// <summary>Горизонтальный ряд.</summary> /// <summary>Горизонтальный ряд.</summary>
public static HorizontalStackPanel Row(int spacing = 8) => public static HorizontalStackPanel Row(int spacing = 8) =>
new() { Spacing = spacing, HorizontalAlignment = HorizontalAlignment.Center }; new() { Spacing = spacing, HorizontalAlignment = HorizontalAlignment.Center };
/// <summary>Оборачивает содержимое в панель на весь экран (абсолютное позиционирование детей).</summary> /// <summary>Оборачивает содержимое в панель на весь экран (абсолютное позиционирование детей).
/// Без фона — годится и для HUD поверх игрового мира.</summary>
public static Panel Screen(params Widget[] children) public static Panel Screen(params Widget[] children)
{ {
var panel = new Panel(); var panel = new Panel();
@@ -74,6 +123,30 @@ internal static class Ui
return panel; return panel;
} }
/// <summary>
/// Как <see cref="Screen"/>, но с тёмной подложкой на весь экран под содержимым — для экранов меню,
/// чтобы фон был единообразно тёмным независимо от цвета очистки кадра. НЕ использовать для HUD над
/// игровым миром (подложка непрозрачна и скрыла бы мир).
/// </summary>
public static Panel MenuScreen(params Widget[] children)
{
var panel = new Panel();
panel.Widgets.Add(
new Panel
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Background = new SolidBrush(Backdrop),
}
);
foreach (var child in children)
{
panel.Widgets.Add(child);
}
return panel;
}
} }
/// <summary> /// <summary>