diff --git a/src/Files.App/Data/Contracts/IAppThemeModeService.cs b/src/Files.App/Data/Contracts/IAppThemeModeService.cs index 28311d8950b4..bb9dbfefbb13 100644 --- a/src/Files.App/Data/Contracts/IAppThemeModeService.cs +++ b/src/Files.App/Data/Contracts/IAppThemeModeService.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Windows.UI; namespace Files.App.Data.Contracts { @@ -18,6 +19,11 @@ public interface IAppThemeModeService /// public ElementTheme AppThemeMode { get; set; } + /// + /// Gets the default accent fill color for the current theme mode. + /// + public Color DefaultAccentColor { get; } + /// /// Refreshes the application theme mode only for the main window. /// diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj index be483fef27f4..cb6dfb7844fc 100644 --- a/src/Files.App/Files.App.csproj +++ b/src/Files.App/Files.App.csproj @@ -39,7 +39,7 @@ TRACE;RELEASE;NETFX_CORE;DISABLE_XAML_GENERATED_MAIN true - + @@ -70,7 +70,6 @@ - @@ -135,5 +134,5 @@ - + diff --git a/src/Files.App/Services/App/AppThemeModeService.cs b/src/Files.App/Services/App/AppThemeModeService.cs index b9e9c4c51358..c15085881d8d 100644 --- a/src/Files.App/Services/App/AppThemeModeService.cs +++ b/src/Files.App/Services/App/AppThemeModeService.cs @@ -5,6 +5,7 @@ using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; using Windows.Storage; using Windows.UI; using Windows.UI.ViewManagement; @@ -34,6 +35,19 @@ public ElementTheme AppThemeMode } } + /// + public Color DefaultAccentColor + { + get => AppThemeMode switch + { + // these values are from the definition of AccentFillColorDefaultBrush in generic.xaml + // will have to be updated if WinUI changes in the future + ElementTheme.Light => (Color)(App.Current.Resources["SystemAccentColorDark1"]), + ElementTheme.Dark => (Color)(App.Current.Resources["SystemAccentColorLight2"]), + ElementTheme.Default or _ => (App.Current.Resources["AccentFillColorDefaultBrush"] as SolidColorBrush)!.Color + }; + } + /// public event EventHandler? AppThemeModeChanged; @@ -99,7 +113,7 @@ public void SetAppThemeMode(Window? window = null, AppWindowTitleBar? titleBar = } catch (Exception ex) { - App.Logger.LogWarning(ex, "Failed to change theme mode of the app."); + App.Logger.LogWarning(ex, "Failed to change theme mode of the app."); } } diff --git a/src/Files.App/UserControls/AddressToolbar.xaml b/src/Files.App/UserControls/AddressToolbar.xaml index c7abdff6b60d..c7cfd048a082 100644 --- a/src/Files.App/UserControls/AddressToolbar.xaml +++ b/src/Files.App/UserControls/AddressToolbar.xaml @@ -16,6 +16,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:triggers="using:CommunityToolkit.WinUI.UI.Triggers" xmlns:uc="using:Files.App.UserControls" + xmlns:ucs="using:Files.App.UserControls.StatusCenter" x:Name="NavToolbar" Height="50" d:DesignHeight="50" @@ -481,7 +482,7 @@ - Points + { + get => (ObservableCollection)GetValue(PointsProperty); + set + { + highestValue = 0; + SetValue(PointsProperty, value); + } + } + + public static readonly DependencyProperty PointsProperty = + DependencyProperty.Register(nameof(Points), typeof(ObservableCollection), typeof(SpeedGraph), null); + + Compositor compositor; + + ContainerVisual rootVisual; + + CompositionPathGeometry graphGeometry; + InsetClip graphClip; + + SpriteVisual line; + + CompositionColorBrush backgroundBrush; + CompositionColorGradientStop graphFillBottom; + CompositionColorGradientStop graphFillTop; + CompositionColorBrush graphStrokeBrush; + + LinearEasingFunction linearEasing; + + bool initialized; + + float width; + float height; + + float highestValue; + + IAppThemeModeService themeModeService; + + public SpeedGraph() + { + compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; + + themeModeService = Ioc.Default.GetRequiredService(); + + this.SizeChanged += OnSizeChanged; + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if (initialized) + return; + + InitGraph(); + + this.SizeChanged -= OnSizeChanged; + + // added *after* first load + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + Points.CollectionChanged += OnPointsChanged; + this.ActualThemeChanged += OnThemeChanged; + themeModeService.AppThemeModeChanged += OnAppThemeModeChanged; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + if (Points.Count > 0) + { + SetGraphColors(); + UpdateGraph(); + } + + this.Unloaded += OnUnloaded; + Points.CollectionChanged += OnPointsChanged; + this.ActualThemeChanged += OnThemeChanged; + themeModeService.AppThemeModeChanged += OnAppThemeModeChanged; + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + this.Unloaded -= OnUnloaded; + Points.CollectionChanged -= OnPointsChanged; + this.ActualThemeChanged -= OnThemeChanged; + themeModeService.AppThemeModeChanged -= OnAppThemeModeChanged; + } + + private void OnPointsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateGraph(); + } + + private void OnAppThemeModeChanged(object? sender, EventArgs e) + { + // this seemingly doesn't fire? leaving it here in case it does in the future (if it's ever used outside of the flyout) + if (initialized) + SetGraphColors(); + } + + private void OnThemeChanged(FrameworkElement sender, object args) + { + if (initialized) + SetGraphColors(); + } + + private void InitGraph() + { + rootVisual = compositor.CreateContainerVisual(); + rootVisual.Size = this.ActualSize; + ElementCompositionPreview.SetElementChildVisual(this, rootVisual); + + var size = rootVisual.Size; + width = size.X; + height = size.Y; + + var rootClip = compositor.CreateRectangleClip(); + rootClip.Top = 1.5f; + rootClip.Left = 1.5f; + rootClip.Bottom = height - 1.5f; + rootClip.Right = width - 2f; + rootClip.TopLeftRadius = new(4f); + rootClip.TopRightRadius = new(4f); + rootClip.BottomLeftRadius = new(4f); + rootClip.BottomRightRadius = new(4f); + rootVisual.Clip = rootClip; + + backgroundBrush = compositor.CreateColorBrush(); + + var graphFillBrush = compositor.CreateLinearGradientBrush(); + graphFillBrush.StartPoint = new(0.5f, 0f); + graphFillBrush.EndPoint = new(0.5f, 1f); + graphFillTop = compositor.CreateColorGradientStop(); + graphFillTop.Offset = 0f; + graphFillBottom = compositor.CreateColorGradientStop(); + graphFillBottom.Offset = 1f; + graphFillBrush.ColorStops.Add(graphFillBottom); + graphFillBrush.ColorStops.Add(graphFillTop); + + graphStrokeBrush = compositor.CreateColorBrush(); + + SetGraphColors(); + + var container = compositor.CreateSpriteVisual(); + container.Size = rootVisual.Size; + // container is also the graph background + container.Brush = backgroundBrush; + + var graphVisual = compositor.CreateShapeVisual(); + graphVisual.Size = rootVisual.Size; + var graphShape = compositor.CreateSpriteShape(); + graphShape.FillBrush = graphFillBrush; + graphShape.StrokeBrush = graphStrokeBrush; + graphShape.StrokeThickness = 1f; + + graphGeometry = compositor.CreatePathGeometry(); + graphShape.Geometry = graphGeometry; + + graphVisual.Shapes.Add(graphShape); + + container.Children.InsertAtBottom(graphVisual); + + graphClip = compositor.CreateInsetClip(); + graphClip.RightInset = width; + container.Clip = graphClip; + + rootVisual.Children.InsertAtBottom(container); + + line = compositor.CreateSpriteVisual(); + line.Size = new(width, 1.5f); + line.Brush = graphStrokeBrush; + line.Offset = new(0f, height - 4f, 0); + rootVisual.Children.InsertAtTop(line); + + highestValue = 0; + + linearEasing = compositor.CreateLinearEasingFunction(); + + initialized = true; + } + + float YValue(float y) => height - (y / highestValue) * (height - 48f) - 4; + + void UpdateGraph() + { + var path = CreatePathFromPoints(); + graphGeometry.Path = path; + + using var lineAnim = compositor.CreateScalarKeyFrameAnimation(); + lineAnim.InsertKeyFrame(1f, YValue(Points[^1].Y), linearEasing); + lineAnim.Duration = TimeSpan.FromMilliseconds(72); + line.StartAnimation("Offset.Y", lineAnim); + + using var clipAnim = compositor.CreateScalarKeyFrameAnimation(); + clipAnim.InsertKeyFrame(1f, width - (width * Points[^1].X / 100f) - 1, linearEasing); + clipAnim.Duration = TimeSpan.FromMilliseconds(72); + graphClip.StartAnimation("RightInset", clipAnim); + } + + CompositionPath CreatePathFromPoints() + { + using var pathBuilder = new CanvasPathBuilder(null); + pathBuilder.BeginFigure(0f, height); + for (int i = 0; i < Points.Count; i++) + { + if (Points[i].Y > highestValue) + highestValue = Points[i].Y; + // no smooth curve for now. a little ugly but maybe for the best performance-wise, we'll see before this gets merged + pathBuilder.AddLine(width * Points[i].X / 100f, YValue(Points[i].Y)); + } + // little extra part so that steep lines don't get cut off + pathBuilder.AddLine(width * Points[^1].X / 100f + 2, YValue(Points[^1].Y)); + pathBuilder.AddLine(width * Points[^1].X / 100f + 2, height); + pathBuilder.EndFigure(CanvasFigureLoop.Closed); + var path = new CompositionPath(CanvasGeometry.CreatePath(pathBuilder)); + return path; + } + + void SetGraphColors() + { + var accentColor = themeModeService.DefaultAccentColor; + + var veryLightColor = accentColor with { A = 0x0f }; + + var slightlyDarkerColor = this.ActualTheme switch + { + ElementTheme.Light => accentColor with { A = 0x55 }, + _ => accentColor with { A = 0x7f } + }; + + backgroundBrush.Color = veryLightColor; + + graphFillTop.Color = slightlyDarkerColor; + graphFillBottom.Color = veryLightColor; + + graphStrokeBrush.Color = accentColor; + } + } +} diff --git a/src/Files.App/UserControls/StatusCenter.xaml b/src/Files.App/UserControls/StatusCenter/StatusCenter.xaml similarity index 92% rename from src/Files.App/UserControls/StatusCenter.xaml rename to src/Files.App/UserControls/StatusCenter/StatusCenter.xaml index be5bb95e16a8..22f968754810 100644 --- a/src/Files.App/UserControls/StatusCenter.xaml +++ b/src/Files.App/UserControls/StatusCenter/StatusCenter.xaml @@ -1,12 +1,12 @@  @@ -227,7 +227,7 @@ - - - - - - - + diff --git a/src/Files.App/UserControls/StatusCenter.xaml.cs b/src/Files.App/UserControls/StatusCenter/StatusCenter.xaml.cs similarity index 96% rename from src/Files.App/UserControls/StatusCenter.xaml.cs rename to src/Files.App/UserControls/StatusCenter/StatusCenter.xaml.cs index c54df0caaad1..c8904dad330f 100644 --- a/src/Files.App/UserControls/StatusCenter.xaml.cs +++ b/src/Files.App/UserControls/StatusCenter/StatusCenter.xaml.cs @@ -5,7 +5,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -namespace Files.App.UserControls +namespace Files.App.UserControls.StatusCenter { public sealed partial class StatusCenter : UserControl { diff --git a/src/Files.App/Utils/StatusCenter/StatusCenterItem.cs b/src/Files.App/Utils/StatusCenter/StatusCenterItem.cs index ee5c871942a3..7a259f10793d 100644 --- a/src/Files.App/Utils/StatusCenter/StatusCenterItem.cs +++ b/src/Files.App/Utils/StatusCenter/StatusCenterItem.cs @@ -2,14 +2,8 @@ // Licensed under the MIT License. See the LICENSE. using System.Windows.Input; -using SkiaSharp; -using LiveChartsCore; -using LiveChartsCore.Drawing; -using LiveChartsCore.Kernel.Sketches; -using LiveChartsCore.SkiaSharpView; -using LiveChartsCore.SkiaSharpView.Painting; -using LiveChartsCore.Defaults; using Microsoft.UI.Xaml.Media; +using System.Numerics; namespace Files.App.Utils.StatusCenter { @@ -161,22 +155,6 @@ public StatusCenterItemProgressModel Progress public string? SubHeaderStringResource { get; private set; } - public ObservableCollection? SpeedGraphValues { get; private set; } - - public ObservableCollection? SpeedGraphBackgroundValues { get; private set; } - - public ObservableCollection? SpeedGraphSeries { get; private set; } - - public ObservableCollection? SpeedGraphBackgroundSeries { get; private set; } - - public ObservableCollection? SpeedGraphXAxes { get; private set; } - - public ObservableCollection? SpeedGraphYAxes { get; private set; } - - public ObservableCollection? SpeedGraphBackgroundXAxes { get; private set; } - - public ObservableCollection? SpeedGraphBackgroundYAxes { get; private set; } - public double IconBackgroundCircleBorderOpacity { get; private set; } public CancellationToken CancellationToken @@ -189,6 +167,8 @@ public string? HeaderTooltip private readonly CancellationTokenSource? _operationCancellationToken; + public readonly ObservableCollection SpeedGraphValues; + public ICommand CancelCommand { get; } public StatusCenterItem( @@ -218,7 +198,6 @@ public StatusCenterItem( IconBackgroundCircleBorderOpacity = 1; AnimatedIconState = "NormalOff"; SpeedGraphValues = []; - SpeedGraphBackgroundValues = []; CancelCommand = new RelayCommand(ExecuteCancelCommand); Message = "ProcessingItems".GetLocalizedResource(); Source = source; @@ -228,108 +207,6 @@ public StatusCenterItem( if (App.Current.Resources["App.Theme.FillColorAttentionBrush"] is not SolidColorBrush accentBrush) return; - // Initialize graph background fill series - SpeedGraphBackgroundSeries = - [ - new LineSeries - { - Values = SpeedGraphBackgroundValues, - GeometrySize = 0d, - DataPadding = new(0, 0), - IsHoverable = false, - - // Stroke - Stroke = new SolidColorPaint( - new(accentBrush.Color.R, accentBrush.Color.G, accentBrush.Color.B, 40), - 0.1f), - - // Fill under the stroke - Fill = new LinearGradientPaint( - [ - new(accentBrush.Color.R, accentBrush.Color.G, accentBrush.Color.B, 40) - ], - new(0f, 0f), - new(0f, 0f)), - } - ]; - - // Initialize graph series - SpeedGraphSeries = - [ - new LineSeries - { - Values = SpeedGraphValues, - GeometrySize = 0d, - DataPadding = new(0, 0), - IsHoverable = false, - - // Stroke - Stroke = new SolidColorPaint( - new(accentBrush.Color.R, accentBrush.Color.G, accentBrush.Color.B), - 1f), - - // Fill under the stroke - Fill = new LinearGradientPaint( - [ - new(accentBrush.Color.R, accentBrush.Color.G, accentBrush.Color.B, 50), - new(accentBrush.Color.R, accentBrush.Color.G, accentBrush.Color.B, 10) - ], - new(0f, 0f), - new(0f, 0f), - [0.1f, 1.0f]), - }, - ]; - - // Initialize X axes of the graph background fill - SpeedGraphBackgroundXAxes = - [ - new Axis - { - Padding = new Padding(0, 0), - Labels = new List(), - MaxLimit = 100, - ShowSeparatorLines = false, - } - ]; - - // Initialize X axes of the graph - SpeedGraphXAxes = - [ - new Axis - { - Padding = new Padding(0, 0), - Labels = new List(), - MaxLimit = 100, - ShowSeparatorLines = false, - } - ]; - - // Initialize Y axes of the graph background fill - SpeedGraphBackgroundYAxes = - [ - new Axis - { - Padding = new Padding(0, 0), - Labels = new List(), - ShowSeparatorLines = false, - MaxLimit = 100, - } - ]; - - // Initialize Y axes of the graph - SpeedGraphYAxes = - [ - new Axis - { - Padding = new Padding(0, 0), - Labels = new List(), - ShowSeparatorLines = false, - } - ]; - - SpeedGraphXAxes[0].SharedWith = SpeedGraphBackgroundXAxes; - SpeedGraphBackgroundXAxes[0].SharedWith = SpeedGraphXAxes; - // Set icon and initialize string resources switch (FileSystemOperationReturnResult) { @@ -450,7 +327,7 @@ private void ReportProgress(StatusCenterItemProgressModel value) OnPropertyChanged(nameof(HeaderTooltip)); // Graph item point - ObservablePoint point; + Vector2 point; // Set speed text and percentage switch (value.TotalSize, value.ItemsCount) @@ -461,7 +338,7 @@ private void ReportProgress(StatusCenterItemProgressModel value) SpeedText = $"{value.ProcessingSizeSpeed.ToSizeString()}/s"; - point = new(value.ProcessedSize * 100.0 / value.TotalSize, value.ProcessingSizeSpeed); + point = new((float)(value.ProcessedSize * 100.0 / value.TotalSize), (float)value.ProcessingSizeSpeed); break; // In progress, displaying processed size @@ -470,7 +347,7 @@ private void ReportProgress(StatusCenterItemProgressModel value) SpeedText = $"{value.ProcessingSizeSpeed.ToSizeString()}/s"; - point = new(value.ProcessedSize * 100.0 / value.TotalSize, value.ProcessingSizeSpeed); + point = new((float)(value.ProcessedSize * 100.0 / value.TotalSize), (float)value.ProcessingSizeSpeed); break; // In progress, displaying items count @@ -479,11 +356,11 @@ private void ReportProgress(StatusCenterItemProgressModel value) SpeedText = $"{value.ProcessingItemsCountSpeed:0} items/s"; - point = new(value.ProcessedItemsCount * 100.0 / value.ItemsCount, value.ProcessingItemsCountSpeed); + point = new((float)(value.ProcessedItemsCount * 100.0 / value.ItemsCount), (float)value.ProcessingItemsCountSpeed); break; default: - point = new(ProgressPercentage, value.ProcessingItemsCountSpeed); + point = new(ProgressPercentage, (float)value.ProcessingItemsCountSpeed); SpeedText = (value.ProcessedSize, value.ProcessedItemsCount) switch { @@ -495,24 +372,9 @@ private void ReportProgress(StatusCenterItemProgressModel value) break; } - bool isSamePoint = false; - - // Remove the point that has the same X position - if (SpeedGraphValues?.FirstOrDefault(v => v.X == point.X) is ObservablePoint existingPoint) - { - SpeedGraphValues.Remove(existingPoint); - isSamePoint = true; - } - - // Add a new background fill point - if (!isSamePoint) - { - ObservablePoint newPoint = new(point.X, 100); - SpeedGraphBackgroundValues?.Add(newPoint); - } - - // Add a new point - SpeedGraphValues?.Add(point); + // 'debounce' updates a bit so the graph isn't too noisy + if (SpeedGraphValues.Count == 0 || (point.X - SpeedGraphValues[^1].X) > 0.5) + SpeedGraphValues?.Add(point); // Add percentage to the header if (!IsIndeterminateProgress)