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); } }