From ed30b1cafe09e6032ef89f4a0ca165f173212602 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:34:13 -0400 Subject: [PATCH 1/3] Refine charcoal UI after runtime feedback --- CompactGUI/MainWindow.xaml.vb | 18 +++ CompactGUI/Services/CharcoalTheme.vb | 111 ++++++++++++++++++ .../Components/CompressionMode_Radio.xaml | 24 ++-- .../Views/Pages/PendingCompression.xaml.vb | 5 + 4 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 CompactGUI/Services/CharcoalTheme.vb diff --git a/CompactGUI/MainWindow.xaml.vb b/CompactGUI/MainWindow.xaml.vb index c0d4beca..13d63ee1 100644 --- a/CompactGUI/MainWindow.xaml.vb +++ b/CompactGUI/MainWindow.xaml.vb @@ -1,4 +1,5 @@ Imports System.ComponentModel +Imports System.Windows.Threading Imports CompactGUI.Core.Settings @@ -20,6 +21,8 @@ Class MainWindow : Implements INavigationWindow, INotifyPropertyChanged Public Sub New(settingsService As ISettingsService, navigationService As INavigationService, serviceProvider As IServiceProvider, snackbarService As CustomSnackBarService, viewmodel As MainWindowViewModel) + CharcoalTheme.ApplyApplicationResources() + ' This call is required by the designer. InitializeComponent() ' Add any initialization after the InitializeComponent() call. @@ -34,9 +37,11 @@ Class MainWindow : Implements INavigationWindow, INotifyPropertyChanged DataContext = viewmodel ConfigureCompressNavigationItem() + CharcoalTheme.ApplyRootBackground(RootContentDialog) NotifyIconTrayMenu.DataContext = viewmodel + AddHandler Loaded, AddressOf MainWindow_Loaded AddHandler Application.GetService(Of HomeViewModel)().PropertyChanged, AddressOf HVPropertyChanged AddHandler navigationService.GetNavigationControl.Navigated, AddressOf OnNavigated @@ -56,6 +61,17 @@ Class MainWindow : Implements INavigationWindow, INotifyPropertyChanged End Sub + Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) + ApplyCharcoalPaletteAfterLayout() + End Sub + + Private Sub ApplyCharcoalPaletteAfterLayout() + Dispatcher.BeginInvoke( + DispatcherPriority.Loaded, + New Action(Sub() CharcoalTheme.ApplyToVisualTree(Me))) + End Sub + + Private Sub ConfigureCompressNavigationItem() If NavigationView.MenuItems.Count = 0 Then Return @@ -82,6 +98,8 @@ Class MainWindow : Implements INavigationWindow, INotifyPropertyChanged _MainWindowViewModel.IsActive = False ProgTitle.Visibility = Visibility.Visible End If + + ApplyCharcoalPaletteAfterLayout() End Sub Private Sub HVPropertyChanged(sender As Object, e As PropertyChangedEventArgs) diff --git a/CompactGUI/Services/CharcoalTheme.vb b/CompactGUI/Services/CharcoalTheme.vb new file mode 100644 index 00000000..8475506f --- /dev/null +++ b/CompactGUI/Services/CharcoalTheme.vb @@ -0,0 +1,111 @@ +Imports System.Windows.Controls +Imports System.Windows.Media +Imports System.Windows.Shapes + +Public Module CharcoalTheme + + Private ReadOnly MutedColorMap As New Dictionary(Of Color, Color) From { + {Color.FromRgb(&H98, &HA9, &HB9), Color.FromRgb(&HB8, &HB8, &HB8)}, + {Color.FromRgb(&H4E, &H63, &H79), Color.FromRgb(&H50, &H50, &H50)}, + {Color.FromRgb(&H59, &H71, &H86), Color.FromRgb(&H68, &H68, &H68)}, + {Color.FromRgb(&H4B, &H61, &H75), Color.FromRgb(&H5A, &H5A, &H5A)}, + {Color.FromRgb(&H1F, &H34, &H48), Color.FromRgb(&H20, &H20, &H20)}, + {Color.FromRgb(&HD, &H2A, &H41), Color.FromRgb(&H18, &H18, &H18)} + } + + Public Sub ApplyApplicationResources() + If Application.Current Is Nothing Then Return + + 'Secondary/status text stays neutral gray, while interactive selection accents use + 'the same crisp blue already present elsewhere in the CompactGUI navigation. + Application.Current.Resources("AccentFillColorDisabledBrush") = + New SolidColorBrush(Color.FromRgb(&HAA, &HAA, &HAA)) + + Dim selectionBlue As New SolidColorBrush(Color.FromRgb(&H4C, &HA5, &HFF)) + Application.Current.Resources("CheckBoxCheckGlyphForeground") = selectionBlue + Application.Current.Resources("CheckBoxCheckBorderBrush") = selectionBlue + End Sub + + Public Sub ApplyRootBackground(root As Grid) + If root Is Nothing Then Return + + Dim backgroundLayers = root.Children.OfType(Of Border)().Take(3).ToArray() + If backgroundLayers.Length < 3 Then Return + + 'Use a dark neutral base and shape the light with broad radial falloff instead of + 'a full-height linear ramp. This avoids visible gradient bands while still keeping + 'the surface from looking flat. + backgroundLayers(0).Background = New SolidColorBrush(Color.FromRgb(&H18, &H18, &H18)) + + Dim topGlow As New RadialGradientBrush With { + .Center = New Point(0.5, 0.08), + .GradientOrigin = New Point(0.5, -0.02), + .RadiusX = 0.95, + .RadiusY = 1.08 + } + topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&HFF, &H28, &H28, &H28), 0)) + topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&HE8, &H22, &H22, &H22), 0.34)) + topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&H78, &H1D, &H1D, &H1D), 0.7)) + topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&H0, &H18, &H18, &H18), 1)) + backgroundLayers(1).Background = topGlow + backgroundLayers(1).Opacity = 1 + + 'A very light vignette gives the panel depth without turning the bottom half black. + Dim vignette As New RadialGradientBrush With { + .Center = New Point(0.5, 0.42), + .GradientOrigin = New Point(0.5, 0.42), + .RadiusX = 0.82, + .RadiusY = 0.94 + } + vignette.GradientStops.Add(New GradientStop(Color.FromArgb(&H0, 0, 0, 0), 0)) + vignette.GradientStops.Add(New GradientStop(Color.FromArgb(&H0, 0, 0, 0), 0.62)) + vignette.GradientStops.Add(New GradientStop(Color.FromArgb(&H2A, 0, 0, 0), 1)) + backgroundLayers(2).Background = vignette + backgroundLayers(2).Opacity = 1 + End Sub + + Public Sub ApplyToVisualTree(root As DependencyObject) + If root Is Nothing Then Return + + ApplyMutedPalette(root) + + For index = 0 To VisualTreeHelper.GetChildrenCount(root) - 1 + ApplyToVisualTree(VisualTreeHelper.GetChild(root, index)) + Next + End Sub + + Private Sub ApplyMutedPalette(element As DependencyObject) + Dim textBlock = TryCast(element, TextBlock) + If textBlock IsNot Nothing Then textBlock.Foreground = MapBrush(textBlock.Foreground) + + Dim control = TryCast(element, Control) + If control IsNot Nothing Then + control.Foreground = MapBrush(control.Foreground) + control.Background = MapBrush(control.Background) + control.BorderBrush = MapBrush(control.BorderBrush) + End If + + Dim border = TryCast(element, Border) + If border IsNot Nothing Then + border.Background = MapBrush(border.Background) + border.BorderBrush = MapBrush(border.BorderBrush) + End If + + Dim shape = TryCast(element, Shape) + If shape IsNot Nothing Then + shape.Fill = MapBrush(shape.Fill) + shape.Stroke = MapBrush(shape.Stroke) + End If + End Sub + + Private Function MapBrush(brush As Brush) As Brush + Dim solid = TryCast(brush, SolidColorBrush) + If solid Is Nothing Then Return brush + + Dim replacement As Color + If Not MutedColorMap.TryGetValue(solid.Color, replacement) Then Return brush + + Return New SolidColorBrush(Color.FromArgb(solid.Color.A, replacement.R, replacement.G, replacement.B)) + End Function + +End Module diff --git a/CompactGUI/Views/Components/CompressionMode_Radio.xaml b/CompactGUI/Views/Components/CompressionMode_Radio.xaml index 85dfc359..a4a0dae3 100644 --- a/CompactGUI/Views/Components/CompressionMode_Radio.xaml +++ b/CompactGUI/Views/Components/CompressionMode_Radio.xaml @@ -156,17 +156,6 @@ - - - - - - - - - @@ -189,8 +178,6 @@ Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" To="0.3" Duration="0:0:0.2" /> - - @@ -240,7 +227,6 @@ Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" To="1" Duration="0:0:0.2" /> - @@ -276,6 +262,16 @@ + + + + + + + + + + diff --git a/CompactGUI/Views/Pages/PendingCompression.xaml.vb b/CompactGUI/Views/Pages/PendingCompression.xaml.vb index 7893f8bd..5d799e0d 100644 --- a/CompactGUI/Views/Pages/PendingCompression.xaml.vb +++ b/CompactGUI/Views/Pages/PendingCompression.xaml.vb @@ -7,6 +7,11 @@ Public Class PendingCompression Sub New() InitializeComponent() _settingsService = Application.GetService(Of ISettingsService) + AddHandler Loaded, AddressOf PendingCompression_Loaded + End Sub + + Private Sub PendingCompression_Loaded(sender As Object, e As RoutedEventArgs) + CharcoalTheme.ApplyToVisualTree(Me) End Sub Private Sub CompressionMode_Radio_Checked(sender As Object, e As RoutedEventArgs) From f5d3ba06e9278bb6253d4281af51bcda2abf0b90 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:48:25 -0400 Subject: [PATCH 2/3] Smooth charcoal background blending --- CompactGUI/Services/CharcoalTheme.vb | 70 +++++++++++++++++++--------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/CompactGUI/Services/CharcoalTheme.vb b/CompactGUI/Services/CharcoalTheme.vb index 8475506f..7e8d8b02 100644 --- a/CompactGUI/Services/CharcoalTheme.vb +++ b/CompactGUI/Services/CharcoalTheme.vb @@ -1,5 +1,6 @@ Imports System.Windows.Controls Imports System.Windows.Media +Imports System.Windows.Media.Imaging Imports System.Windows.Shapes Public Module CharcoalTheme @@ -32,38 +33,61 @@ Public Module CharcoalTheme Dim backgroundLayers = root.Children.OfType(Of Border)().Take(3).ToArray() If backgroundLayers.Length < 3 Then Return - 'Use a dark neutral base and shape the light with broad radial falloff instead of - 'a full-height linear ramp. This avoids visible gradient bands while still keeping - 'the surface from looking flat. - backgroundLayers(0).Background = New SolidColorBrush(Color.FromRgb(&H18, &H18, &H18)) + backgroundLayers(0).Background = New SolidColorBrush(Color.FromRgb(&H1A, &H1A, &H1A)) + 'Keep the softly-lit charcoal look from the previous pass, but make the falloff much + 'broader and lower-contrast. ScRGB interpolation plus a one-level dither layer below + 'breaks up the visible WPF gradient contouring that showed up as large streaks. Dim topGlow As New RadialGradientBrush With { - .Center = New Point(0.5, 0.08), - .GradientOrigin = New Point(0.5, -0.02), - .RadiusX = 0.95, - .RadiusY = 1.08 + .Center = New Point(0.48, 0.02), + .GradientOrigin = New Point(0.48, -0.12), + .RadiusX = 1.35, + .RadiusY = 1.7, + .ColorInterpolationMode = ColorInterpolationMode.ScRgbLinearInterpolation } - topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&HFF, &H28, &H28, &H28), 0)) - topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&HE8, &H22, &H22, &H22), 0.34)) - topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&H78, &H1D, &H1D, &H1D), 0.7)) - topGlow.GradientStops.Add(New GradientStop(Color.FromArgb(&H0, &H18, &H18, &H18), 1)) + topGlow.GradientStops.Add(New GradientStop(Color.FromRgb(&H20, &H20, &H20), 0)) + topGlow.GradientStops.Add(New GradientStop(Color.FromRgb(&H1E, &H1E, &H1E), 0.46)) + topGlow.GradientStops.Add(New GradientStop(Color.FromRgb(&H1C, &H1C, &H1C), 0.78)) + topGlow.GradientStops.Add(New GradientStop(Color.FromRgb(&H1A, &H1A, &H1A), 1)) backgroundLayers(1).Background = topGlow backgroundLayers(1).Opacity = 1 - 'A very light vignette gives the panel depth without turning the bottom half black. - Dim vignette As New RadialGradientBrush With { - .Center = New Point(0.5, 0.42), - .GradientOrigin = New Point(0.5, 0.42), - .RadiusX = 0.82, - .RadiusY = 0.94 - } - vignette.GradientStops.Add(New GradientStop(Color.FromArgb(&H0, 0, 0, 0), 0)) - vignette.GradientStops.Add(New GradientStop(Color.FromArgb(&H0, 0, 0, 0), 0.62)) - vignette.GradientStops.Add(New GradientStop(Color.FromArgb(&H2A, 0, 0, 0), 1)) - backgroundLayers(2).Background = vignette + backgroundLayers(2).Background = CreateDitherBrush() backgroundLayers(2).Opacity = 1 End Sub + Private Function CreateDitherBrush() As Brush + Const tileSize As Integer = 64 + Const bytesPerPixel As Integer = 4 + Dim pixels(tileSize * tileSize * bytesPerPixel - 1) As Byte + Dim random As New Random(&H434755) + + For pixelIndex = 0 To tileSize * tileSize - 1 + Dim offset = pixelIndex * bytesPerPixel + Dim isLight = random.Next(0, 2) = 0 + + pixels(offset) = If(isLight, CByte(255), CByte(0)) + pixels(offset + 1) = pixels(offset) + pixels(offset + 2) = pixels(offset) + pixels(offset + 3) = 1 + Next + + Dim bitmap As New WriteableBitmap(tileSize, tileSize, 96, 96, PixelFormats.Bgra32, Nothing) + bitmap.WritePixels(New Int32Rect(0, 0, tileSize, tileSize), pixels, tileSize * bytesPerPixel, 0) + bitmap.Freeze() + + Dim brush As New ImageBrush(bitmap) With { + .TileMode = TileMode.Tile, + .ViewportUnits = BrushMappingMode.Absolute, + .ViewboxUnits = BrushMappingMode.Absolute, + .Viewport = New Rect(0, 0, tileSize, tileSize), + .Viewbox = New Rect(0, 0, tileSize, tileSize), + .Stretch = Stretch.None + } + brush.Freeze() + Return brush + End Function + Public Sub ApplyToVisualTree(root As DependencyObject) If root Is Nothing Then Return From 6db32f85e0f22225be0b6b497df0cf91d207e5c9 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:48:53 -0400 Subject: [PATCH 3/3] Enforce selected compression mode outline --- .../Components/CompressionMode_Radio.xaml.vb | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CompactGUI/Views/Components/CompressionMode_Radio.xaml.vb b/CompactGUI/Views/Components/CompressionMode_Radio.xaml.vb index be00820e..1eefa03d 100644 --- a/CompactGUI/Views/Components/CompressionMode_Radio.xaml.vb +++ b/CompactGUI/Views/Components/CompressionMode_Radio.xaml.vb @@ -1,6 +1,8 @@ Public Class CompressionMode_Radio Inherits RadioButton + Private Shared ReadOnly SelectionBlueBorder As Brush = New SolidColorBrush(Color.FromRgb(&H4C, &HA5, &HFF)) + Private Shared ReadOnly UnselectedModeBorder As Brush = New SolidColorBrush(Color.FromArgb(&H38, &HFF, &HFF, &HFF)) Public Shared ReadOnly CompressionModeProperty As DependencyProperty = DependencyProperty.RegisterAttached( NameOf(CompressionMode), @@ -170,17 +172,49 @@ InitializeComponent() AddHandler Me.Loaded, AddressOf OnLoaded AddHandler Me.Unloaded, AddressOf OnUnloaded + AddHandler Me.Checked, AddressOf OnSelectionStateChanged + AddHandler Me.Unchecked, AddressOf OnSelectionStateChanged End Sub Private Sub OnLoaded(sender As Object, e As RoutedEventArgs) RemoveHandler GlobalHoverChanged, AddressOf OnGlobalHoverChanged AddHandler GlobalHoverChanged, AddressOf OnGlobalHoverChanged + ApplySelectionBorder() End Sub Private Sub OnUnloaded(sender As Object, e As RoutedEventArgs) RemoveHandler GlobalHoverChanged, AddressOf OnGlobalHoverChanged End Sub + Private Sub OnSelectionStateChanged(sender As Object, e As RoutedEventArgs) + ApplySelectionBorder() + End Sub + + Private Sub ApplySelectionBorder() + If Template Is Nothing Then Return + + Dim card = TryCast(Template.FindName("CheckMark", Me), Wpf.Ui.Controls.Card) + If card Is Nothing Then Return + + Dim selected = IsChecked.GetValueOrDefault(False) + Dim borderBrush = If(selected, SelectionBlueBorder, UnselectedModeBorder) + Dim borderThickness = If(selected, New Thickness(2), New Thickness(1)) + + 'Set the Card itself, then also set its rendered ContentBorder directly. The latter + 'avoids WPF-UI template/style precedence swallowing the selected outline on some states. + card.BorderBrush = borderBrush + card.BorderThickness = borderThickness + card.ApplyTemplate() + + If card.Template IsNot Nothing Then + Dim renderedBorder = TryCast(card.Template.FindName("ContentBorder", card), Border) + If renderedBorder IsNot Nothing Then + renderedBorder.BorderBrush = borderBrush + renderedBorder.BorderThickness = borderThickness + End If + End If + End Sub + Private Sub OnGlobalHoverChanged(sender As Object, e As EventArgs) ' Update the local dependency property to trigger visual state SetValue(IsGloballyHoveredProperty, IsAnyHovered) @@ -226,6 +260,7 @@ Public Overrides Sub OnApplyTemplate() MyBase.OnApplyTemplate() + ApplySelectionBorder() If IsForcedDetailed Then VisualStateManager.GoToState(Me, "MouseOver", False) End If