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)