diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index 78c56c5..7b3013b 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -120,6 +120,8 @@
"settings.volume": "Volume",
"settings.uiscale": "UI scale",
"settings.devmode": "Developer mode",
+ "settings.on": "On",
+ "settings.off": "Off",
"dev.title": "Dev spawner",
"dev.hint": "Pick an entry · F9 to hide",
"dev.armed": "In hand: {0} · LMB to spawn, RMB to clear",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index 16e308f..4f62168 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -120,6 +120,8 @@
"settings.volume": "Громкость",
"settings.uiscale": "Масштаб UI",
"settings.devmode": "Режим разработчика",
+ "settings.on": "Вкл",
+ "settings.off": "Откл",
"dev.title": "Дев-спавнер",
"dev.hint": "Выбери пункт · F9 — скрыть",
"dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
diff --git a/src/LittleSim/UI/Menu.cs b/src/LittleSim/UI/Menu.cs
index 0d4de85..258da48 100644
--- a/src/LittleSim/UI/Menu.cs
+++ b/src/LittleSim/UI/Menu.cs
@@ -33,6 +33,7 @@ internal static class Ui
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 static readonly Color ChoiceActiveBg = new(48, 84, 120);
private const int ButtonWidth = 300;
@@ -85,6 +86,32 @@ internal static class Ui
return button;
}
+ /// Небольшая кнопка-выбор (сегмент/тумблер): тёмная заливка, рамка, подсветка hover/press.
+ /// Активное состояние задаётся отдельно через .
+ public static TextButton ChoiceButton(string text)
+ {
+ var button = new TextButton
+ {
+ Text = text,
+ Padding = new Thickness(10, 6),
+ TextColor = Text,
+ Background = new SolidBrush(ButtonBg),
+ OverBackground = new SolidBrush(ButtonOver),
+ PressedBackground = new SolidBrush(ButtonPress),
+ Border = new SolidBrush(ButtonBorder),
+ BorderThickness = new Thickness(1),
+ };
+ return button;
+ }
+
+ /// Подсвечивает кнопку-выбор как выбранную (акцентная заливка/рамка) или обычную.
+ public static void SetChoiceActive(TextButton button, bool active)
+ {
+ button.Background = new SolidBrush(active ? ChoiceActiveBg : ButtonBg);
+ button.Border = new SolidBrush(active ? Accent : ButtonBorder);
+ button.TextColor = active ? Color.White : Muted;
+ }
+
/// Вертикальная колонка по центру экрана.
public static VerticalStackPanel Column(int spacing = 10) =>
new()
diff --git a/src/LittleSim/UI/SettingsPanel.cs b/src/LittleSim/UI/SettingsPanel.cs
index 192fde6..586a179 100644
--- a/src/LittleSim/UI/SettingsPanel.cs
+++ b/src/LittleSim/UI/SettingsPanel.cs
@@ -7,13 +7,17 @@ using Myra.Graphics2D.UI;
namespace LittleSim.UI;
///
-/// Конструктор панели настроек (язык, полный экран, VSync, разрешение, громкость).
-/// Возвращает виджет Myra, работающий с переданной копией ; общий
-/// для сцены настроек и оверлея паузы. Вместо неоднозначных комбо/чекбоксов — кнопки-сегменты
-/// и тумблеры (состояние показывает цвет текста), что переносимо между версиями Myra.
+/// Конструктор панели настроек (язык, полный экран, VSync, разрешение, масштаб UI, громкость,
+/// режим разработчика). Возвращает виджет Myra, работающий с переданной копией ;
+/// общий для сцены настроек и оверлея паузы. Каждая настройка — ряд «подпись + контрол» с единым
+/// выравниванием: сегменты-кнопки (активная подсвечена акцентом) и тумблеры Вкл/Откл вместо
+/// неоднозначных комбо/чекбоксов, что переносимо между версиями Myra. Любой клик мгновенно
+/// перерисовывает подсветку (все рефрешеры), а не только после «Применить».
///
internal static class SettingsPanel
{
+ private const int LabelWidth = 200;
+
private static readonly (int W, int H)[] Resolutions =
{
(1280, 720),
@@ -30,47 +34,58 @@ internal static class SettingsPanel
)
{
var refreshers = new List();
- var column = Ui.Column(10);
+ void RefreshAll()
+ {
+ foreach (var refresh in refreshers)
+ {
+ refresh();
+ }
+ }
- column.Widgets.Add(Ui.Title(lang.Get("settings.title")));
- column.Widgets.Add(new Label { Height = 8 });
+ var card = Ui.Card(8);
+ card.Widgets.Add(Ui.Title(lang.Get("settings.title")));
+ card.Widgets.Add(Ui.AccentRule());
// Язык — по одному сегменту на доступный язык.
- column.Widgets.Add(
+ card.Widgets.Add(
Segment(
lang.Get("settings.language"),
lang.AvailableLanguages,
code => code.ToUpperInvariant(),
() => settings.Language,
code => settings.Language = code,
- refreshers
+ refreshers,
+ RefreshAll
)
);
- // Полный экран и VSync — тумблеры (цвет = состояние).
- column.Widgets.Add(
+ // Полный экран и VSync — тумблеры Вкл/Откл.
+ card.Widgets.Add(
Toggle(
+ lang,
lang.Get("settings.fullscreen"),
() => settings.Fullscreen,
v => settings.Fullscreen = v,
- refreshers
+ refreshers,
+ RefreshAll
)
);
- column.Widgets.Add(
+ card.Widgets.Add(
Toggle(
+ lang,
lang.Get("settings.vsync"),
() => settings.VSync,
v => settings.VSync = v,
- refreshers
+ refreshers,
+ RefreshAll
)
);
// Разрешение — сегменты пресетов.
- var resolutions = new List<(int, int)>(Resolutions);
- column.Widgets.Add(
+ card.Widgets.Add(
Segment(
lang.Get("settings.resolution"),
- resolutions,
+ new List<(int, int)>(Resolutions),
r => $"{r.Item1}×{r.Item2}",
() => (settings.Width, settings.Height),
r =>
@@ -78,48 +93,69 @@ internal static class SettingsPanel
settings.Width = r.Item1;
settings.Height = r.Item2;
},
- refreshers
+ refreshers,
+ RefreshAll
)
);
// Масштаб интерфейса — сегменты пресетов.
- var scales = new List { 0.75f, 1f, 1.25f, 1.5f };
- column.Widgets.Add(
+ card.Widgets.Add(
Segment(
lang.Get("settings.uiscale"),
- scales,
+ new List { 0.75f, 1f, 1.25f, 1.5f },
s => $"{(int)MathF.Round(s * 100)}%",
() => settings.UiScale,
s => settings.UiScale = s,
- refreshers
+ refreshers,
+ RefreshAll
)
);
// Громкость — кнопки −/+ с подписью процента.
- column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
+ card.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers, RefreshAll));
// Режим разработчика — тумблер (дев-консоль и спавнер; по умолчанию включён).
- column.Widgets.Add(
+ card.Widgets.Add(
Toggle(
+ lang,
lang.Get("settings.devmode"),
() => settings.DeveloperMode,
v => settings.DeveloperMode = v,
- refreshers
+ refreshers,
+ RefreshAll
)
);
- column.Widgets.Add(new Label { Height = 8 });
+ card.Widgets.Add(new Label { Height = 8 });
var buttons = Ui.Row(10);
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));
buttons.Widgets.Add(Ui.Button(lang.Get("settings.back"), onBack, width: 150));
- column.Widgets.Add(buttons);
+ card.Widgets.Add(buttons);
- foreach (var refresh in refreshers)
+ RefreshAll();
+ return card;
+ }
+
+ // Ряд «подпись слева фиксированной ширины + контрол» — даёт ровные колонки между настройками.
+ private static HorizontalStackPanel Field(string label, Widget control)
+ {
+ var row = new HorizontalStackPanel
{
- refresh();
- }
-
- return column;
+ Spacing = 12,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ };
+ row.Widgets.Add(
+ new Label
+ {
+ Text = label,
+ Width = LabelWidth,
+ Wrap = true,
+ TextColor = Ui.Muted,
+ VerticalAlignment = VerticalAlignment.Center,
+ }
+ );
+ row.Widgets.Add(control);
+ return row;
}
private static Widget Segment(
@@ -128,67 +164,94 @@ internal static class SettingsPanel
Func text,
Func get,
Action set,
- List refreshers
+ List refreshers,
+ Action onChange
)
{
- var row = Ui.Row(6);
- row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted });
+ var group = new HorizontalStackPanel
+ {
+ Spacing = 4,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
foreach (var option in options)
{
var value = option;
- var button = new TextButton { Text = text(value) };
- button.Click += (_, _) => set(value);
- row.Widgets.Add(button);
+ var button = Ui.ChoiceButton(text(value));
+ button.Click += (_, _) =>
+ {
+ set(value);
+ onChange();
+ };
+ group.Widgets.Add(button);
refreshers.Add(() =>
- button.TextColor = EqualityComparer.Default.Equals(get(), value)
- ? Ui.Accent
- : Ui.Muted
+ Ui.SetChoiceActive(button, EqualityComparer.Default.Equals(get(), value))
);
}
- return row;
+ return Field(label, group);
}
private static Widget Toggle(
+ LanguageManager lang,
string label,
Func get,
Action set,
- List refreshers
+ List refreshers,
+ Action onChange
)
{
- var button = new TextButton();
- button.Click += (_, _) => set(!get());
+ var button = Ui.ChoiceButton("");
+ button.Width = 90;
+ button.Click += (_, _) =>
+ {
+ set(!get());
+ onChange();
+ };
refreshers.Add(() =>
{
- button.Text = (get() ? "● " : "○ ") + label;
- button.TextColor = get() ? Ui.Accent : Ui.Muted;
+ button.Text = lang.Get(get() ? "settings.on" : "settings.off");
+ Ui.SetChoiceActive(button, get());
});
- return button;
+ return Field(label, button);
}
- private static Widget Volume(string label, GameSettings settings, List refreshers)
+ private static Widget Volume(
+ string label,
+ GameSettings settings,
+ List refreshers,
+ Action onChange
+ )
{
- var row = Ui.Row(6);
- var caption = new Label { TextColor = Ui.Muted };
- refreshers.Add(() => caption.Text = $"{label}: {(int)MathF.Round(settings.Volume * 100)}%");
+ var value = new Label
+ {
+ TextColor = Ui.Text,
+ Width = 56,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ refreshers.Add(() => value.Text = $"{(int)MathF.Round(settings.Volume * 100)}%");
void Adjust(float delta)
{
settings.Volume = Math.Clamp(settings.Volume + delta, 0f, 1f);
- foreach (var refresh in refreshers)
- {
- refresh();
- }
+ onChange();
}
- var minus = new TextButton { Text = "−" };
+ var minus = Ui.ChoiceButton("−");
+ minus.Width = 42;
minus.Click += (_, _) => Adjust(-0.1f);
- var plus = new TextButton { Text = "+" };
+ var plus = Ui.ChoiceButton("+");
+ plus.Width = 42;
plus.Click += (_, _) => Adjust(+0.1f);
- row.Widgets.Add(caption);
- row.Widgets.Add(minus);
- row.Widgets.Add(plus);
- return row;
+ var group = new HorizontalStackPanel
+ {
+ Spacing = 4,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ group.Widgets.Add(minus);
+ group.Widgets.Add(value);
+ group.Widgets.Add(plus);
+ return Field(label, group);
}
}