Skip to content
Merged
6 changes: 6 additions & 0 deletions src/Files.App/Data/Contracts/IAppThemeModeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.UI;

namespace Files.App.Data.Contracts
{
Expand All @@ -18,6 +19,11 @@ public interface IAppThemeModeService
/// </summary>
public ElementTheme AppThemeMode { get; set; }

/// <summary>
/// Gets the default accent fill color for the current theme mode.
/// </summary>
public Color DefaultAccentColor { get; }

/// <summary>
/// Refreshes the application theme mode only for the main window.
/// </summary>
Expand Down
5 changes: 2 additions & 3 deletions src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<DefineConstants>TRACE;RELEASE;NETFX_CORE;DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<Optimize>true</Optimize>
</PropertyGroup>

<ItemGroup>
<Manifest Include="app.manifest" />
<Content Include="7z.dll">
Expand Down Expand Up @@ -70,7 +70,6 @@
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
<PackageReference Include="LiteDB" Version="5.0.19" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc1.2" />
<PackageReference Include="MessageFormat" Version="7.1.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
Expand Down Expand Up @@ -135,5 +134,5 @@
<TrimmerRootAssembly Include="CommunityToolkit.WinUI.UI.Controls.Media" />
<TrimmerRootAssembly Include="CommunityToolkit.WinUI.UI.Controls.Primitives" />
</ItemGroup>

</Project>
16 changes: 15 additions & 1 deletion src/Files.App/Services/App/AppThemeModeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +35,19 @@ public ElementTheme AppThemeMode
}
}

/// <inheritdoc/>
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
};
}

/// <inheritdoc/>
public event EventHandler? AppThemeModeChanged;

Expand Down Expand Up @@ -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.");
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Files.App/UserControls/AddressToolbar.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -481,7 +482,7 @@
</Style>
</Flyout.FlyoutPresenterStyle>

<uc:StatusCenter
<ucs:StatusCenter
x:Name="OngoingTasksControl"
Width="400"
MinHeight="300"
Expand Down
254 changes: 254 additions & 0 deletions src/Files.App/UserControls/StatusCenter/SpeedGraph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using System.Collections.Specialized;
using System.Numerics;
using Windows.UI;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace Files.App.UserControls.StatusCenter
{
public sealed partial class SpeedGraph : Control
{
public ObservableCollection<Vector2> Points
{
get => (ObservableCollection<Vector2>)GetValue(PointsProperty);
set
{
highestValue = 0;
SetValue(PointsProperty, value);
}
}

public static readonly DependencyProperty PointsProperty =
DependencyProperty.Register(nameof(Points), typeof(ObservableCollection<Vector2>), 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<IAppThemeModeService>();

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;
}
}
}
Loading