UI: rework settings panel — card, aligned rows, themed segmented controls
The settings screen was ragged: bare gray TextButtons (off-theme), each row center-aligned on its own so labels/controls never lined up, and a latent bug — clicking a segment or toggle only changed the value, the highlight didn''t refresh until Apply rebuilt the panel. - Wrap in a Card with title + accent rule, matching the other menus. - Every setting is a "label (fixed width) + control" row, left-aligned, so the controls form a clean column. - Segments and toggles use the themed ChoiceButton with an active state (Ui.SetChoiceActive: accent fill/border) instead of text-color-only. - Toggles read On/Off (new settings.on/off loc) instead of ●/○ glyphs. - Any click now runs all refreshers immediately, so highlights track the value live, not just after Apply. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
36b6d576a1
commit
92d0302138
@@ -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",
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"settings.volume": "Громкость",
|
||||
"settings.uiscale": "Масштаб UI",
|
||||
"settings.devmode": "Режим разработчика",
|
||||
"settings.on": "Вкл",
|
||||
"settings.off": "Откл",
|
||||
"dev.title": "Дев-спавнер",
|
||||
"dev.hint": "Выбери пункт · F9 — скрыть",
|
||||
"dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>Небольшая кнопка-выбор (сегмент/тумблер): тёмная заливка, рамка, подсветка hover/press.
|
||||
/// Активное состояние задаётся отдельно через <see cref="SetChoiceActive"/>.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Подсвечивает кнопку-выбор как выбранную (акцентная заливка/рамка) или обычную.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Вертикальная колонка по центру экрана.</summary>
|
||||
public static VerticalStackPanel Column(int spacing = 10) =>
|
||||
new()
|
||||
|
||||
@@ -7,13 +7,17 @@ using Myra.Graphics2D.UI;
|
||||
namespace LittleSim.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор панели настроек (язык, полный экран, VSync, разрешение, громкость).
|
||||
/// Возвращает виджет Myra, работающий с переданной копией <see cref="GameSettings"/>; общий
|
||||
/// для сцены настроек и оверлея паузы. Вместо неоднозначных комбо/чекбоксов — кнопки-сегменты
|
||||
/// и тумблеры (состояние показывает цвет текста), что переносимо между версиями Myra.
|
||||
/// Конструктор панели настроек (язык, полный экран, VSync, разрешение, масштаб UI, громкость,
|
||||
/// режим разработчика). Возвращает виджет Myra, работающий с переданной копией <see cref="GameSettings"/>;
|
||||
/// общий для сцены настроек и оверлея паузы. Каждая настройка — ряд «подпись + контрол» с единым
|
||||
/// выравниванием: сегменты-кнопки (активная подсвечена акцентом) и тумблеры Вкл/Откл вместо
|
||||
/// неоднозначных комбо/чекбоксов, что переносимо между версиями Myra. Любой клик мгновенно
|
||||
/// перерисовывает подсветку (все рефрешеры), а не только после «Применить».
|
||||
/// </summary>
|
||||
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<Action>();
|
||||
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<float> { 0.75f, 1f, 1.25f, 1.5f };
|
||||
column.Widgets.Add(
|
||||
card.Widgets.Add(
|
||||
Segment(
|
||||
lang.Get("settings.uiscale"),
|
||||
scales,
|
||||
new List<float> { 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)
|
||||
{
|
||||
refresh();
|
||||
RefreshAll();
|
||||
return card;
|
||||
}
|
||||
|
||||
return column;
|
||||
// Ряд «подпись слева фиксированной ширины + контрол» — даёт ровные колонки между настройками.
|
||||
private static HorizontalStackPanel Field(string label, Widget control)
|
||||
{
|
||||
var row = new HorizontalStackPanel
|
||||
{
|
||||
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<T>(
|
||||
@@ -128,67 +164,94 @@ internal static class SettingsPanel
|
||||
Func<T, string> text,
|
||||
Func<T> get,
|
||||
Action<T> set,
|
||||
List<Action> refreshers
|
||||
List<Action> 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<T>.Default.Equals(get(), value)
|
||||
? Ui.Accent
|
||||
: Ui.Muted
|
||||
Ui.SetChoiceActive(button, EqualityComparer<T>.Default.Equals(get(), value))
|
||||
);
|
||||
}
|
||||
|
||||
return row;
|
||||
return Field(label, group);
|
||||
}
|
||||
|
||||
private static Widget Toggle(
|
||||
LanguageManager lang,
|
||||
string label,
|
||||
Func<bool> get,
|
||||
Action<bool> set,
|
||||
List<Action> refreshers
|
||||
List<Action> 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<Action> refreshers)
|
||||
private static Widget Volume(
|
||||
string label,
|
||||
GameSettings settings,
|
||||
List<Action> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user