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:
co-authored by
Claude Opus 4.8
parent
02dcd01c0f
commit
36b6d576a1
@@ -53,7 +53,7 @@ public sealed class BootScene : Scene
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
desktop.Root = Ui.Screen(_label);
|
||||
desktop.Root = Ui.MenuScreen(_label);
|
||||
|
||||
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
|
||||
_load = Task.Run(() => GameContent.Load());
|
||||
|
||||
@@ -17,15 +17,15 @@ public sealed class CreditsScene : Scene
|
||||
var input = this.UseInput();
|
||||
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(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.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));
|
||||
|
||||
desktop.Root = Ui.Screen(column);
|
||||
desktop.Root = Ui.MenuScreen(column);
|
||||
UpdateSystems.Add(
|
||||
new CallbackSystem(() =>
|
||||
{
|
||||
|
||||
@@ -41,9 +41,9 @@ public sealed class LoadGameScene : Scene
|
||||
private void Rebuild()
|
||||
{
|
||||
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(new Label { Height = 8 });
|
||||
column.Widgets.Add(Ui.AccentRule());
|
||||
|
||||
var saves = _store.List();
|
||||
if (saves.Count == 0)
|
||||
@@ -98,7 +98,7 @@ public sealed class LoadGameScene : Scene
|
||||
column.Widgets.Add(new Label { Height = 8 });
|
||||
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
|
||||
|
||||
_desktop.Root = Ui.Screen(column);
|
||||
_desktop.Root = Ui.MenuScreen(column);
|
||||
}
|
||||
|
||||
private void Back()
|
||||
|
||||
@@ -19,19 +19,19 @@ public sealed class MainMenuScene : Scene
|
||||
var lang = content.Languages;
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
|
||||
var column = Ui.Column(12);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
||||
column.Widgets.Add(
|
||||
var card = Ui.Card(8);
|
||||
card.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||
card.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
||||
card.Widgets.Add(Ui.AccentRule());
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
||||
card.Widgets.Add(
|
||||
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)
|
||||
|
||||
@@ -38,9 +38,9 @@ public sealed class NewWorldScene : Scene
|
||||
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
||||
_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(new Label { Height = 8 });
|
||||
column.Widgets.Add(Ui.AccentRule());
|
||||
|
||||
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
|
||||
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));
|
||||
column.Widgets.Add(buttons);
|
||||
|
||||
desktop.Root = Ui.Screen(column);
|
||||
desktop.Root = Ui.MenuScreen(column);
|
||||
Refresh();
|
||||
|
||||
UpdateSystems.Add(
|
||||
|
||||
@@ -47,7 +47,9 @@ public sealed class SettingsScene : Scene
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class WorldLoadingScene : Scene
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
desktop.Root = Ui.Screen(_label);
|
||||
desktop.Root = Ui.MenuScreen(_label);
|
||||
UpdateSystems.Add(new CallbackSystem(Tick));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
using System;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Myra.Graphics2D;
|
||||
using Myra.Graphics2D.Brushes;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Общие фабрики виджетов Myra для экранов меню — единый стиль и отсутствие дублирования
|
||||
/// разметки между сценами. Все подписи приходят готовыми строками (локализованными вызывающим).
|
||||
/// разметки между сценами. Тёмная тема с холодным акцентом: затемнённый фон-подложка на весь
|
||||
/// экран (<see cref="Screen"/>), карточки с рамкой (<see cref="Card"/>), кнопки с подсветкой
|
||||
/// при наведении и нажатии (<see cref="Button"/>). Все подписи приходят готовыми строками
|
||||
/// (локализованными вызывающим).
|
||||
/// </summary>
|
||||
internal static class Ui
|
||||
{
|
||||
@@ -17,7 +22,19 @@ internal static class Ui
|
||||
/// <summary>Приглушённый цвет вторичного текста.</summary>
|
||||
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>
|
||||
public static Label Title(string text) =>
|
||||
@@ -37,7 +54,18 @@ internal static class Ui
|
||||
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)
|
||||
{
|
||||
var button = new TextButton
|
||||
@@ -45,6 +73,13 @@ internal static class Ui
|
||||
Text = text,
|
||||
Width = width,
|
||||
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();
|
||||
return button;
|
||||
@@ -59,11 +94,25 @@ internal static class Ui
|
||||
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>
|
||||
public static HorizontalStackPanel Row(int spacing = 8) =>
|
||||
new() { Spacing = spacing, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
|
||||
/// <summary>Оборачивает содержимое в панель на весь экран (абсолютное позиционирование детей).</summary>
|
||||
/// <summary>Оборачивает содержимое в панель на весь экран (абсолютное позиционирование детей).
|
||||
/// Без фона — годится и для HUD поверх игрового мира.</summary>
|
||||
public static Panel Screen(params Widget[] children)
|
||||
{
|
||||
var panel = new Panel();
|
||||
@@ -74,6 +123,30 @@ internal static class Ui
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user