diff --git a/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs b/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs index 0a590fc1594..578bed73087 100644 --- a/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs +++ b/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs @@ -175,19 +175,8 @@ internal static Grid MakeLayoutListViewAndEditBox(MainView mainPage, MainViewMod if (Se.Settings.Appearance.GridAlternatingRows) { var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark; - var altColorHex = UiTheme.GetGridAlternatingRowColorHex(isDark); - try - { - if (!string.IsNullOrWhiteSpace(altColorHex)) - { - alternatingRowBrush = new SolidColorBrush(altColorHex.FromHexToColor()); - vm.SubtitleGridAlternatingRowBrush = alternatingRowBrush; - } - } - catch - { - alternatingRowBrush = null; - } + alternatingRowBrush = new SolidColorBrush(UiTheme.GetGridAlternatingRowColor(isDark)); + vm.SubtitleGridAlternatingRowBrush = alternatingRowBrush; } // Collapse hidden rows (style bindings evaluate against the row's item). diff --git a/src/ui/Features/Main/MainViewModel.cs b/src/ui/Features/Main/MainViewModel.cs index 96ad8fec858..0779e03d739 100644 --- a/src/ui/Features/Main/MainViewModel.cs +++ b/src/ui/Features/Main/MainViewModel.cs @@ -15534,15 +15534,7 @@ private void UpdateAlternatingRowBrush() // Color notifies every realized row without rebuilding the grid (and losing its state). if (SubtitleGridAlternatingRowBrush != null) { - var colorHex = UiTheme.GetGridAlternatingRowColorHex(Application.Current?.ActualThemeVariant == ThemeVariant.Dark); - try - { - SubtitleGridAlternatingRowBrush.Color = colorHex.FromHexToColor(); - } - catch - { - // Keep the current brush color when the configured value is invalid. - } + SubtitleGridAlternatingRowBrush.Color = UiTheme.GetGridAlternatingRowColor(Application.Current?.ActualThemeVariant == ThemeVariant.Dark); } } diff --git a/src/ui/Features/Options/Settings/SettingsPage.cs b/src/ui/Features/Options/Settings/SettingsPage.cs index 3d0b56c4641..cb3f4df63e5 100644 --- a/src/ui/Features/Options/Settings/SettingsPage.cs +++ b/src/ui/Features/Options/Settings/SettingsPage.cs @@ -1144,8 +1144,8 @@ private List CreateSections() MakeCheckboxSetting(Se.Language.Options.Settings.ShowTimeCodeModeInStatusBar, nameof(_vm.ShowTimeCodeModeInStatusBar)), MakeCheckboxSetting(Se.Language.Options.Settings.GridCompactMode, nameof(_vm.GridCompactMode)), MakeCheckboxSetting(Se.Language.Options.Settings.GridAlternatingRows, nameof(_vm.GridAlternatingRows)), - new SettingsItem(Se.Language.Options.Settings.GridAlternatingRowColor, () => UiUtil.MakeColorPickerButton(_vm, nameof(_vm.GridAlternatingRowColor))), - new SettingsItem(Se.Language.Options.Settings.GridAlternatingRowColorDark, () => UiUtil.MakeColorPickerButton(_vm, nameof(_vm.GridAlternatingRowColorDark))), + MakeNumericSettingInt(Se.Language.Options.Settings.GridAlternatingRowContrast, nameof(_vm.GridAlternatingRowContrast), + UiTheme.MinGridAlternatingRowContrast, UiTheme.MaxGridAlternatingRowContrast), new SettingsItem(Se.Language.Options.Settings.ShowGridLines, () => UiUtil.MakeComboBox(_vm.GridLinesVisibilities, _vm, nameof(_vm.SelectedGridLinesVisibility))), new SettingsItem(Se.Language.Options.Settings.BookmarkColor, () => UiUtil.MakeColorPickerButton(_vm, nameof(_vm.BookmarkColor))), new SettingsItem(Se.Language.Options.Settings.SpellCheckHighlightColor, () => UiUtil.MakeColorPickerButton(_vm, nameof(_vm.SpellCheckHighlightColor))), diff --git a/src/ui/Features/Options/Settings/SettingsViewModel.cs b/src/ui/Features/Options/Settings/SettingsViewModel.cs index 33ea50be616..3bc7663eb2f 100644 --- a/src/ui/Features/Options/Settings/SettingsViewModel.cs +++ b/src/ui/Features/Options/Settings/SettingsViewModel.cs @@ -462,8 +462,7 @@ private static int ParseMouseWheelStep(string display) [ObservableProperty] private Color _focusedButtonBackgroundColor; [ObservableProperty] private Color _bookmarkColor; [ObservableProperty] private Color _spellCheckHighlightColor; - [ObservableProperty] private Color _gridAlternatingRowColor; - [ObservableProperty] private Color _gridAlternatingRowColorDark; + [ObservableProperty] private int _gridAlternatingRowContrast; [ObservableProperty] private bool _isEditCustomContinuationStyleVisible; [ObservableProperty] private bool _isMpvChosen; @@ -1057,8 +1056,7 @@ private void LoadSettings() FocusedButtonBackgroundColor = appearance.FocusedButtonBackgroundColor.FromHexToColor(); BookmarkColor = appearance.BookmarkColor.FromHexToColor(); SpellCheckHighlightColor = appearance.SpellCheckHighlightColor.FromHexToColor(); - GridAlternatingRowColor = appearance.GridAlternatingRowColor.FromHexToColor(); - GridAlternatingRowColorDark = appearance.GridAlternatingRowColorDark.FromHexToColor(); + GridAlternatingRowContrast = UiTheme.ClampGridAlternatingRowContrast(appearance.GridAlternatingRowContrast); ShowUpDownStartTime = appearance.ShowUpDownStartTime; ShowTimeCodeModeInStatusBar = appearance.ShowTimeCodeModeInStatusBar; ShowUpDownEndTime = appearance.ShowUpDownEndTime; @@ -1972,8 +1970,7 @@ private void SaveSettings() appearance.FocusedButtonBackgroundColor = FocusedButtonBackgroundColor.FromColorToHex(); appearance.BookmarkColor = BookmarkColor.FromColorToHex(); appearance.SpellCheckHighlightColor = SpellCheckHighlightColor.FromColorToHex(); - appearance.GridAlternatingRowColor = GridAlternatingRowColor.FromColorToHex(); - appearance.GridAlternatingRowColorDark = GridAlternatingRowColorDark.FromColorToHex(); + appearance.GridAlternatingRowContrast = UiTheme.ClampGridAlternatingRowContrast(GridAlternatingRowContrast); appearance.ShowUpDownStartTime = ShowUpDownStartTime; appearance.ShowTimeCodeModeInStatusBar = ShowTimeCodeModeInStatusBar; appearance.ShowUpDownEndTime = ShowUpDownEndTime; diff --git a/src/ui/Logic/Config/Language/Options/LanguageSettings.cs b/src/ui/Logic/Config/Language/Options/LanguageSettings.cs index 0be0a0380b2..da29fd118bb 100644 --- a/src/ui/Logic/Config/Language/Options/LanguageSettings.cs +++ b/src/ui/Logic/Config/Language/Options/LanguageSettings.cs @@ -64,8 +64,7 @@ public class LanguageSettings public string ShowTimeCodeModeInStatusBar { get; set; } public string GridCompactMode { get; set; } public string GridAlternatingRows { get; set; } - public string GridAlternatingRowColor { get; set; } - public string GridAlternatingRowColorDark { get; set; } + public string GridAlternatingRowContrast { get; set; } public string UiFont { get; set; } public string IconTheme { get; set; } public string DarkThemeForegroundColor { get; set; } @@ -462,8 +461,7 @@ public LanguageSettings() ShowTimeCodeModeInStatusBar = "Show time code mode in status bar"; GridCompactMode = "Use compact mode for grids"; GridAlternatingRows = "Use alternating row colors in grid"; - GridAlternatingRowColor = "Alternating row color (light theme)"; - GridAlternatingRowColorDark = "Alternating row color (dark theme)"; + GridAlternatingRowContrast = "Alternating row contrast (%)"; UiFont = "UI font"; IconTheme = "Icon theme"; DarkThemeForegroundColor = "Dark theme foreground color"; diff --git a/src/ui/Logic/Config/SeAppearance.cs b/src/ui/Logic/Config/SeAppearance.cs index 2c0204bc389..e09ec8443dd 100644 --- a/src/ui/Logic/Config/SeAppearance.cs +++ b/src/ui/Logic/Config/SeAppearance.cs @@ -63,8 +63,7 @@ public class SeAppearance /// public string WindowsRenderingMode { get; set; } public bool GridAlternatingRows { get; set; } - public string GridAlternatingRowColor { get; set; } - public string GridAlternatingRowColorDark { get; set; } + public int GridAlternatingRowContrast { get; set; } public bool ShowHorizontalLineAboveToolbar { get; set; } /// @@ -150,8 +149,7 @@ public SeAppearance() GridLinesAppearance = SeGridLinesVisibility.None.ToString(); WindowsRenderingMode = nameof(WindowsRenderingModeType.Default); GridAlternatingRows = true; - GridAlternatingRowColor = new Color(255, 245, 245, 245).FromColorToHex(); - GridAlternatingRowColorDark = new Color(255, 45, 45, 45).FromColorToHex(); + GridAlternatingRowContrast = 10; DarkModeBackgroundColor = new Color(255, 33, 33, 33).FromColorToHex(); DarkModeForegroundColor = new Color(255, 220, 220, 220).FromColorToHex(); UseFocusedButtonBackgroundColor = true; diff --git a/src/ui/Logic/UiTheme.cs b/src/ui/Logic/UiTheme.cs index 332707fb4f2..23267747059 100644 --- a/src/ui/Logic/UiTheme.cs +++ b/src/ui/Logic/UiTheme.cs @@ -1837,39 +1837,53 @@ internal static Color GetLightThemeBackgroundColor(string? themeSetting) private static readonly Color PastelBackgroundColor = Color.FromRgb(240, 235, 255); internal static readonly Color NordBackgroundColor = Color.FromRgb(0x2E, 0x34, 0x40); // nord0 internal static readonly Color NordForegroundColor = Color.FromRgb(0xE5, 0xE9, 0xF0); // nord5 - private static readonly Color NordAlternatingRowColor = Color.FromRgb(0x34, 0x3B, 0x48); internal static readonly Color MidnightCinemaBackgroundColor = Color.FromRgb(0x14, 0x12, 0x18); internal static readonly Color MidnightCinemaForegroundColor = Color.FromRgb(0xEC, 0xE6, 0xD9); internal static readonly Color MidnightCinemaHeaderColor = Color.FromRgb(0x1E, 0x1B, 0x24); internal static readonly Color MidnightCinemaAccentColor = Color.FromRgb(0xE8, 0xA3, 0x3D); - private static readonly Color MidnightCinemaAlternatingRowColor = Color.FromRgb(0x1A, 0x17, 0x20); internal static readonly Color SakuraBackgroundColor = Color.FromRgb(0xFF, 0xF7, 0xF9); internal static readonly Color SakuraForegroundColor = Color.FromRgb(0x3A, 0x2A, 0x31); internal static readonly Color SakuraAccentColor = Color.FromRgb(0xD9, 0x46, 0x6F); - private static readonly Color SakuraAlternatingRowColor = Color.FromRgb(0xFD, 0xEF, 0xF3); + + public const int MinGridAlternatingRowContrast = 4; + public const int MaxGridAlternatingRowContrast = 20; + + public static int ClampGridAlternatingRowContrast(int percent) => + Math.Clamp(percent, MinGridAlternatingRowContrast, MaxGridAlternatingRowContrast); /// - /// The subtitle grid's alternating row color. The fixed-palette themes have their own - the - /// gray settings read as stray gray stripes on their tinted rows. + /// The subtitle grid's alternating row color: the theme background moved the configured + /// percentage toward the theme text color. Following both colors keeps the stripe in the + /// theme's own tint (Sakura pink, Nord blue-gray) and makes it darker on light themes and + /// lighter on dark ones without a light/dark color pair to keep in step (#16779). /// - public static string GetGridAlternatingRowColorHex(bool isDark) + public static Color GetGridAlternatingRowColor(bool isDark) { - var theme = Se.Settings.Appearance.Theme; - if (!isDark) + Color background; + Color foreground; + try { - return theme == ThemeNameSakura - ? SakuraAlternatingRowColor.FromColorToHex() - : Se.Settings.Appearance.GridAlternatingRowColor; + background = isDark ? GetDarkThemeBackgroundColor() : GetLightThemeBackgroundColor(Se.Settings.Appearance.Theme); + foreground = isDark ? GetDarkThemeForegroundColor() : GetLightThemeForegroundColor(Se.Settings.Appearance.Theme); } - - if (theme == ThemeNameNord) + catch { - return NordAlternatingRowColor.FromColorToHex(); + // malformed dark-mode colors in the settings file + background = isDark ? Color.FromRgb(0x21, 0x21, 0x21) : Colors.White; + foreground = isDark ? Color.FromRgb(0xDC, 0xDC, 0xDC) : Colors.Black; } - return theme == ThemeNameMidnightCinema - ? MidnightCinemaAlternatingRowColor.FromColorToHex() - : Se.Settings.Appearance.GridAlternatingRowColorDark; + return BlendGridAlternatingRowColor(background, foreground, Se.Settings.Appearance.GridAlternatingRowContrast); + } + + internal static Color BlendGridAlternatingRowColor(Color background, Color foreground, int percent) + { + var amount = ClampGridAlternatingRowContrast(percent) / 100.0; + static byte Mix(byte from, byte to, double t) => (byte)Math.Round(from + (to - from) * t); + return Color.FromRgb( + Mix(background.R, foreground.R, amount), + Mix(background.G, foreground.G, amount), + Mix(background.B, foreground.B, amount)); } private static readonly WaveformColors MidnightCinemaWaveformColors = new( @@ -1996,9 +2010,12 @@ public static Color GetThemeForegroundColor() return GetDarkThemeForegroundColor(); } - return ThemeName == ThemeNameSakura ? SakuraForegroundColor : Colors.Black; + return GetLightThemeForegroundColor(ThemeName); } + internal static Color GetLightThemeForegroundColor(string? themeSetting) => + themeSetting == ThemeNameSakura ? SakuraForegroundColor : Colors.Black; + public static Color GetDarkThemeForegroundColor() { var theme = Se.Settings.Appearance.Theme; diff --git a/tests/UI/Logic/GridAlternatingRowContrastTests.cs b/tests/UI/Logic/GridAlternatingRowContrastTests.cs new file mode 100644 index 00000000000..44027de7d1f --- /dev/null +++ b/tests/UI/Logic/GridAlternatingRowContrastTests.cs @@ -0,0 +1,53 @@ +using Avalonia.Media; +using Nikse.SubtitleEdit.Logic; +using Nikse.SubtitleEdit.Logic.Config; + +namespace UITests.Logic; + +/// +/// The grid's alternating row tint is the theme background moved a percentage (4-20%) toward +/// the theme text color, so it is visible on every theme without hand-picked colors (#16779). +/// +public class GridAlternatingRowContrastTests +{ + [Fact] + public void Light_Theme_Darkens_White_By_The_Percentage() + { + var color = UiTheme.BlendGridAlternatingRowColor(Colors.White, Colors.Black, 10); + + Assert.Equal(Color.FromRgb(230, 230, 230), color); + } + + [Fact] + public void Dark_Theme_Lightens_Toward_Foreground() + { + var background = Color.FromRgb(33, 33, 33); + var foreground = Color.FromRgb(220, 220, 220); + + var color = UiTheme.BlendGridAlternatingRowColor(background, foreground, 20); + + Assert.True(color.R > background.R); + Assert.Equal(Color.FromRgb(70, 70, 70), color); + } + + [Theory] + [InlineData(0, 4)] + [InlineData(4, 4)] + [InlineData(12, 12)] + [InlineData(20, 20)] + [InlineData(99, 20)] + public void Percentage_Is_Clamped(int value, int expected) + { + Assert.Equal(expected, UiTheme.ClampGridAlternatingRowContrast(value)); + } + + [Fact] + public void Default_Is_Visible_On_Light_Theme() + { + Assert.Equal(10, new SeAppearance().GridAlternatingRowContrast); + var color = UiTheme.BlendGridAlternatingRowColor(Colors.White, Colors.Black, 10); + + // The old default was #F5F5F5, too faint to notice on many screens. + Assert.Equal(Color.FromRgb(230, 230, 230), color); + } +}