Merge branch 'feature/workshop' into development

This commit is contained in:
Robert committed 2023-07-27 20:17:06 +02:00
commit 3030312df9
108 files changed
+3309 -365

No files matched your search

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using ReactiveUI;
namespace Artemis.UI.Shared.Pagination;
/// <summary>
/// Represents a pagination control that can be used to switch between pages.
/// </summary>
[TemplatePart("PART_PreviousButton", typeof(Button))]
[TemplatePart("PART_NextButton", typeof(Button))]
[TemplatePart("PART_PagesView", typeof(StackPanel))]
public partial class Pagination : TemplatedControl
{
/// <inheritdoc />
public Pagination()
{
PropertyChanged += OnPropertyChanged;
}
public Button? PreviousButton { get; set; }
public Button? NextButton { get; set; }
public StackPanel? PagesView { get; set; }
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (PreviousButton != null)
PreviousButton.Click -= PreviousButtonOnClick;
if (NextButton != null)
NextButton.Click -= NextButtonOnClick;
PreviousButton = e.NameScope.Find<Button>("PART_PreviousButton");
NextButton = e.NameScope.Find<Button>("PART_NextButton");
PagesView = e.NameScope.Find<StackPanel>("PART_PagesView");
if (PreviousButton != null)
PreviousButton.Click += PreviousButtonOnClick;
if (NextButton != null)
NextButton.Click += NextButtonOnClick;
Update();
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ValueProperty || e.Property == MaximumProperty)
Update();
}
private void NextButtonOnClick(object? sender, RoutedEventArgs e)
{
if (Value < Maximum)
Value++;
}
private void PreviousButtonOnClick(object? sender, RoutedEventArgs e)
{
if (Value > 1)
Value--;
}
private void Update()
{
if (PagesView == null)
return;
List<int> pages = GetPages(Value, Maximum);
// Remove extra children
while (PagesView.Children.Count > pages.Count)
{
PagesView.Children.RemoveAt(PagesView.Children.Count - 1);
}
if (PagesView.Children.Count > pages.Count)
PagesView.Children.RemoveRange(0, PagesView.Children.Count - pages.Count);
// Add/modify children
for (int i = 0; i < pages.Count; i++)
{
int page = pages[i];
// -1 indicates an ellipsis (...)
if (page == -1)
{
if (PagesView.Children.ElementAtOrDefault(i) is not PaginationEllipsis)
{
if (PagesView.Children.Count - 1 >= i)
PagesView.Children[i] = new PaginationEllipsis();
else
PagesView.Children.Add(new PaginationEllipsis());
}
}
// Anything else indicates a regular page
else
{
if (PagesView.Children.ElementAtOrDefault(i) is PaginationPage paginationPage)
{
paginationPage.Page = page;
paginationPage.Command = ReactiveCommand.Create(() => Value = page);
continue;
}
paginationPage = new PaginationPage {Page = page, Command = ReactiveCommand.Create(() => Value = page)};
if (PagesView.Children.Count - 1 >= i)
PagesView.Children[i] = paginationPage;
else
PagesView.Children.Add(paginationPage);
}
}
foreach (Control child in PagesView.Children)
{
if (child is PaginationPage paginationPage)
((IPseudoClasses) paginationPage.Classes).Set(":selected", paginationPage.Page == Value);
}
}
private void PaginationPageOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender is PaginationPage paginationPage)
Value = paginationPage.Page;
}
private static List<int> GetPages(int currentPage, int pageCount)
{
// Determine the delta based on how close to the edge the current page is
int delta;
if (pageCount <= 7)
delta = 7;
else
delta = currentPage > 4 && currentPage < pageCount - 3 ? 2 : 4;
int start = currentPage - delta / 2;
int end = currentPage + delta / 2;
if (start - 1 == 1 || end + 1 == pageCount)
{
start += 1;
end += 1;
}
// Determine start and end numbers based on how close to the edge the current page is
start = currentPage > delta ? Math.Min(start, pageCount - delta) : 1;
end = currentPage > delta ? Math.Min(end, pageCount) : Math.Min(pageCount, delta + 1);
// Start with the pages neighbouring the current page
List<int> paginationItems = Enumerable.Range(start, end - start + 1).ToList();
// If not starting at the first page, add the first page and an ellipsis (-1)
if (paginationItems.First() != 1)
{
paginationItems.Insert(0, 1);
paginationItems.Insert(1, -1);
}
// If not ending at the last page, add an ellipsis (-1) and the last page
if (paginationItems.Last() < pageCount)
{
paginationItems.Add(-1);
paginationItems.Add(pageCount);
}
return paginationItems;
}
}
@@ -0,0 +1,39 @@
using System;
using Avalonia;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
namespace Artemis.UI.Shared.Pagination;
public partial class Pagination : TemplatedControl
{
/// <summary>
/// Defines the <see cref="Value" /> property
/// </summary>
public static readonly StyledProperty<int> ValueProperty =
AvaloniaProperty.Register<Pagination, int>(nameof(Value), 1, defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Maximum" /> property
/// </summary>
public static readonly StyledProperty<int> MaximumProperty =
AvaloniaProperty.Register<Pagination, int>(nameof(Maximum), 10, coerce: (_, v) => Math.Max(1, v));
/// <summary>
/// Gets or sets the numeric value of a NumberBox.
/// </summary>
public int Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
/// <summary>
/// Gets or sets the numerical maximum for Value.
/// </summary>
public int Maximum
{
get => GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
}
@@ -0,0 +1,10 @@
using Avalonia.Controls.Primitives;
namespace Artemis.UI.Shared.Pagination;
/// <summary>
/// Represents a pagination ellipsis control that indicates a gap in pagination.
/// </summary>
public class PaginationEllipsis : TemplatedControl
{
}
@@ -0,0 +1,39 @@
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls.Primitives;
namespace Artemis.UI.Shared.Pagination;
/// <summary>
/// Represents a pagination page control that indicates a page in pagination.
/// </summary>
public class PaginationPage : TemplatedControl
{
/// <summary>
/// Defines the <see cref="Page" /> property.
/// </summary>
public static readonly StyledProperty<int> PageProperty = AvaloniaProperty.Register<PaginationPage, int>(nameof(Page));
/// <summary>
/// Gets or sets the page that is being represented.
/// </summary>
public int Page
{
get => GetValue(PageProperty);
set => SetValue(PageProperty, value);
}
/// <summary>
/// Defines the <see cref="Command" /> property.
/// </summary>
public static readonly StyledProperty<ICommand> CommandProperty = AvaloniaProperty.Register<PaginationPage, ICommand>(nameof(Command));
/// <summary>
/// Gets or sets the command to invoke when the page is clicked.
/// </summary>
public ICommand Command
{
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
}
@@ -0,0 +1,80 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="30" Width="400">
<StackPanel Spacing="20">
<pagination:Pagination Value="{CompiledBinding Value, ElementName=Numeric, Mode=TwoWay}" HorizontalAlignment="Center"/>
<pagination:Pagination Value="{CompiledBinding Value, ElementName=Numeric, Mode=TwoWay}" Maximum="999" HorizontalAlignment="Center"/>
<NumericUpDown Name="Numeric" Value="1" Width="120" HorizontalAlignment="Center"></NumericUpDown>
</StackPanel>
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type pagination:Pagination}" TargetType="pagination:Pagination">
<Setter Property="Template">
<ControlTemplate>
<StackPanel Orientation="Horizontal" Spacing="2">
<Button Name="PART_PreviousButton" Theme="{StaticResource TransparentButton}" Width="32" Height="30">
<ui:SymbolIcon Symbol="ChevronLeft" />
</Button>
<StackPanel Name="PART_PagesView" Orientation="Horizontal" Spacing="2"></StackPanel>
<Button Name="PART_NextButton" Theme="{StaticResource TransparentButton}" Width="32" Height="30">
<ui:SymbolIcon Symbol="ChevronRight" />
</Button>
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type pagination:PaginationPage}" TargetType="pagination:PaginationPage">
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Button Theme="{StaticResource TransparentButton}"
MinWidth="32"
Padding="6 5"
Content="{TemplateBinding Page}"
Command="{TemplateBinding Command}"/>
<Rectangle Name="SelectionIndicator"
Width="16"
Height="3"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
RadiusX="2"
RadiusY="2"
IsVisible="False"
RenderTransform="scaleX(0)"
Fill="{DynamicResource TreeViewItemSelectionIndicatorForeground}">
<Rectangle.Transitions>
<Transitions>
<TransformOperationsTransition Duration="00:00:00.167"
Property="RenderTransform"
Easing="0,0 0,1" />
</Transitions>
</Rectangle.Transitions>
</Rectangle>
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^:selected">
<Style Selector="^ /template/ Rectangle#SelectionIndicator">
<Setter Property="IsVisible" Value="True" />
<Setter Property="RenderTransform" Value="scaleX(1)" />
</Style>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type pagination:PaginationEllipsis}" TargetType="pagination:PaginationEllipsis">
<Setter Property="Template">
<ControlTemplate>
<TextBlock VerticalAlignment="Bottom" Margin="11 5">...</TextBlock>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>
+23 -36
View File
@@ -1,4 +1,5 @@
using System;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
@@ -26,6 +27,8 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
.Register<ReactiveAppWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
private bool _micaEnabled;
/// <summary>
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}" /> class.
/// </summary>
@@ -33,23 +36,33 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
{
// This WhenActivated block calls ViewModel's WhenActivated
// block if the ViewModel implements IActivatableViewModel.
this.WhenActivated(disposables => { });
this.WhenActivated(disposables => UI.MicaEnabled.Subscribe(ToggleMica).DisposeWith(disposables));
this.GetObservable(DataContextProperty).Subscribe(OnDataContextChanged);
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
}
/// <inheritdoc />
protected override void OnOpened(EventArgs e)
private void ToggleMica(bool enable)
{
// TODO: Move to a style and remove opacity on focus loss
base.OnOpened(e);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
if (enable == _micaEnabled)
return;
TransparencyBackgroundFallback = Brushes.Transparent;
TransparencyLevelHint = new[] {WindowTransparencyLevel.Mica};
TryEnableMicaEffect();
if (enable)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
return;
// TransparencyBackgroundFallback = Brushes.Transparent;
TransparencyLevelHint = new[] {WindowTransparencyLevel.Mica};
Background = new SolidColorBrush(new Color(80, 0,0,0));
}
else
{
ClearValue(TransparencyLevelHintProperty);
ClearValue(BackgroundProperty);
}
_micaEnabled = enable;
}
private void OnDataContextChanged(object? value)
@@ -67,32 +80,6 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
else if (DataContext != value) DataContext = value;
}
private void TryEnableMicaEffect()
{
// The background colors for the Mica brush are still based around SolidBackgroundFillColorBase resource
// BUT since we can't control the actual Mica brush color, we have to use the window background to create
// the same effect. However, we can't use SolidBackgroundFillColorBase directly since its opaque, and if
// we set the opacity the color become lighter than we want. So we take the normal color, darken it and
// apply the opacity until we get the roughly the correct color
// NOTE that the effect still doesn't look right, but it suffices. Ideally we need access to the Mica
// CompositionBrush to properly change the color but I don't know if we can do that or not
if (ActualThemeVariant == ThemeVariant.Dark)
{
Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Dark, out object? value) ? (Color) value : new Color2(32, 32, 32);
color = color.LightenPercent(-0.5f);
Background = new ImmutableSolidColorBrush(color, 0.78);
}
else if (ActualThemeVariant == ThemeVariant.Light)
{
// Similar effect here
Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Light, out object? value) ? (Color) value : new Color2(243, 243, 243);
color = color.LightenPercent(0.5f);
Background = new ImmutableSolidColorBrush(color, 0.9);
}
}
/// <summary>
/// The ViewModel.
/// </summary>
@@ -62,7 +62,15 @@ public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRouta
void IRoutableScreen.InternalChangeScreen(object? screen)
{
Screen = screen as TScreen;
if (screen == null)
{
Screen = null;
return;
}
if (screen is not TScreen typedScreen)
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
Screen = typedScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
@@ -4,6 +4,7 @@ using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform;
namespace Artemis.UI.Shared.Routing;
@@ -72,7 +73,15 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
void IRoutableScreen.InternalChangeScreen(object? screen)
{
Screen = screen as TScreen;
if (screen == null)
{
Screen = null;
return;
}
if (screen is not TScreen typedScreen)
throw new ArtemisRoutingException($"Provided screen is not assignable to {typeof(TScreen).FullName}");
Screen = typedScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a registration for a route.
/// </summary>
public interface IRouterRegistration
{
/// <summary>
/// Gets the route associated with this registration.
/// </summary>
Route Route { get; }
/// <summary>
/// Gets the type of the view model associated with the route.
/// </summary>
Type ViewModel { get; }
/// <summary>
/// Gets or sets the child registrations of this route.
/// </summary>
List<IRouterRegistration> Children { get; set; }
}
@@ -1,7 +1,23 @@
namespace Artemis.UI.Shared.Routing.ParameterParsers;
/// <summary>
/// Represents a contract for parsing route parameters.
/// </summary>
public interface IRouteParameterParser
{
/// <summary>
/// Checks if the given segment matches the provided source.
/// </summary>
/// <param name="segment">The route segment to match.</param>
/// <param name="source">The source value to match against the route segment.</param>
/// <returns><see langword="true"/> if the segment matches the source; otherwise, <see langword="false"/>.</returns>
bool IsMatch(RouteSegment segment, string source);
/// <summary>
/// Gets the parameter value from the provided source value.
/// </summary>
/// <param name="segment">The route segment containing the parameter information.</param>
/// <param name="source">The source value from which to extract the parameter value.</param>
/// <returns>The extracted parameter value.</returns>
object GetValue(RouteSegment segment, string source);
}
@@ -0,0 +1,17 @@
namespace Artemis.UI.Shared.Routing.ParameterParsers;
internal class IntParameterParser : IRouteParameterParser
{
/// <inheritdoc />
public bool IsMatch(RouteSegment segment, string source)
{
return int.TryParse(source, out _);
}
/// <inheritdoc />
public object GetValue(RouteSegment segment, string source)
{
return int.Parse(source);
}
}
@@ -3,15 +3,29 @@ using System.Linq;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a route at a certain path.
/// </summary>
public class Route
{
/// <summary>
/// Initializes a new instance of the <see cref="Route" /> class.
/// </summary>
/// <param name="path">The path of the route.</param>
public Route(string path)
{
Path = path;
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
}
/// <summary>
/// Gets the path of the route.
/// </summary>
public string Path { get; }
/// <summary>
/// Gets the list of segments that makes up the path.
/// </summary>
public List<RouteSegment> Segments { get; }
/// <inheritdoc />
@@ -1,12 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a registration for a route and its associated view model.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model associated with the route.</typeparam>
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.
/// </summary>
/// <param name="path">The path of the route.</param>
public RouteRegistration(string path)
{
Route = new Route(path);
@@ -18,6 +24,9 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
}
/// <summary>
/// Gets the route associated with this registration.
/// </summary>
public Route Route { get; }
/// <inheritdoc />
@@ -25,12 +34,4 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
/// <inheritdoc />
public List<IRouterRegistration> Children { get; set; } = new();
}
public interface IRouterRegistration
{
Route Route { get; }
Type ViewModel { get; }
List<IRouterRegistration> Children { get; set; }
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DryIoc;
namespace Artemis.UI.Shared.Routing;
@@ -4,10 +4,17 @@ using Artemis.UI.Shared.Routing.ParameterParsers;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a segment of a route.
/// </summary>
public partial class RouteSegment
{
private readonly IRouteParameterParser? _parameterParser;
/// <summary>
/// Initializes a new instance of the <see cref="RouteSegment"/> class.
/// </summary>
/// <param name="segment">The segment value.</param>
public RouteSegment(string segment)
{
Segment = segment;
@@ -21,10 +28,26 @@ public partial class RouteSegment
}
}
/// <summary>
/// Gets the segment value.
/// </summary>
public string Segment { get; }
/// <summary>
/// Gets the parameter name if the segment is a parameterized segment; otherwise <see langword="null"/>.
/// </summary>
public string? Parameter { get; }
/// <summary>
/// Gets the type of the parameter if the segment is a parameterized segment; otherwise <see langword="null"/>.
/// </summary>
public string? ParameterType { get; }
/// <summary>
/// Checks if the segment matches the provided value.
/// </summary>
/// <param name="value">The value to compare with the segment.</param>
/// <returns><see langword="true"/> if the segment matches the value; otherwise, <see langword="false"/>.</returns>
public bool IsMatch(string value)
{
if (_parameterParser == null)
@@ -32,6 +55,11 @@ public partial class RouteSegment
return _parameterParser.IsMatch(this, value);
}
/// <summary>
/// Gets the parameter value from the provided value.
/// </summary>
/// <param name="value">The value from which to extract the parameter value.</param>
/// <returns>The extracted parameter value.</returns>
public object? GetParameter(string value)
{
if (_parameterParser == null)
@@ -48,13 +76,20 @@ public partial class RouteSegment
private IRouteParameterParser GetParameterParser(string parameterType)
{
if (parameterType == "guid")
return new GuidParameterParser();
return parameterType switch
{
"guid" => new GuidParameterParser(),
"int" => new IntParameterParser(),
_ => new StringParameterParser()
};
// Default to a string parser which just returns the segment as is
return new StringParameterParser();
}
/// <summary>
/// Gets the regular expression used to identify parameterized segments in the route.
/// </summary>
/// <returns>The regular expression pattern.</returns>
[GeneratedRegex(@"\{(\w+):(\w+)\}")]
private static partial Regex ParameterRegex();
}
@@ -70,7 +70,18 @@ internal class Navigation
// Only change the screen if it wasn't reused
if (!ReferenceEquals(host.InternalScreen, screen))
host.InternalChangeScreen(screen);
{
try
{
host.InternalChangeScreen(screen);
}
catch (Exception e)
{
Cancel();
if (e is not TaskCanceledException)
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
}
if (CancelIfRequested(args, "ChangeScreen", screen))
return;
@@ -86,15 +97,24 @@ internal class Navigation
catch (Exception e)
{
Cancel();
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
if (e is not TaskCanceledException)
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
if (CancelIfRequested(args, "OnNavigating", screen))
return;
}
if (resolution.Child != null && screen is IRoutableScreen childScreen)
await NavigateResolution(resolution.Child, args, childScreen);
if (screen is IRoutableScreen childScreen)
{
// Navigate the child too
if (resolution.Child != null)
await NavigateResolution(resolution.Child, args, childScreen);
// Make sure there is no child
else if (childScreen.InternalScreen != null)
childScreen.InternalChangeScreen(null);
}
Completed = true;
}
+17 -10
View File
@@ -1,18 +1,25 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,12,12">
<VisualBrush.Visual>
<Canvas Width="12" Height="12">
<Rectangle Width="6" Height="6" Fill="Black" Opacity="0.15" />
<Rectangle Width="6" Height="6" Canvas.Left="6" />
<Rectangle Width="6" Height="6" Canvas.Top="6" />
<Rectangle Width="6" Height="6" Canvas.Left="6" Canvas.Top="6" Fill="Black" Opacity="0.15" />
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
<ResourceDictionary>
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,12,12">
<VisualBrush.Visual>
<Canvas Width="12" Height="12">
<Rectangle Width="6" Height="6" Fill="Black" Opacity="0.15" />
<Rectangle Width="6" Height="6" Canvas.Left="6" />
<Rectangle Width="6" Height="6" Canvas.Top="6" />
<Rectangle Width="6" Height="6" Canvas.Left="6" Canvas.Top="6" Fill="Black" Opacity="0.15" />
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="/Controls/Pagination/PaginationStyles.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>
<!-- Custom controls -->
<StyleInclude Source="/Styles/Controls/GradientPicker.axaml" />
<StyleInclude Source="/Styles/Controls/GradientPickerButton.axaml" />
@@ -106,7 +106,7 @@
<gradientPicker:GradientPickerColorStop ColorStop="{CompiledBinding}"
PositionReference="{CompiledBinding $parent[Border]}"
Classes="gradient-handle"
GradientPicker="{CompiledBinding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
GradientPicker="{CompiledBinding $parent[gradientPicker:GradientPicker]}">
</gradientPicker:GradientPickerColorStop>
</DataTemplate>
</ItemsControl.ItemTemplate>
@@ -207,7 +207,7 @@
<Button Name="DeleteButton"
Grid.Column="3"
Classes="icon-button"
Command="{CompiledBinding DeleteStop, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}"
Command="{CompiledBinding $parent[gradientPicker:GradientPicker].DeleteStop}"
CommandParameter="{CompiledBinding}">
<avalonia:MaterialIcon Kind="Close" />
</Button>
+9 -3
View File
@@ -13,9 +13,15 @@
<!-- Add Styles Here -->
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
<Setter Property="MinHeight" Value="35" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="MinHeight" Value="{DynamicResource NavigationViewItemOnLeftMinHeight}" />
</Style>
<Style Selector="ListBox.sidebar-listbox ContentPresenter">
<Setter Property="Margin" Value="0" />
<!-- <Style Selector="ListBox.sidebar-listbox ContentPresenter"> -->
<!-- <Setter Property="Margin" Value="0" /> -->
<!-- </Style> -->
<Style Selector="ListBox.sidebar-listbox ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="MinHeight" Value="{DynamicResource NavigationViewItemOnLeftMinHeight}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
</Styles>
@@ -10,6 +10,15 @@
<TextBlock Classes="h5">This is heading 5</TextBlock>
<TextBlock Classes="h6">This is heading 6</TextBlock>
<TextBlock Classes="subtitle">This is a subtitle</TextBlock>
<TextBlock>
<Run Classes="h1">This is heading 1</Run>
<Run Classes="h2">This is heading 2</Run>
<Run Classes="h3">This is heading 3</Run>
<Run Classes="h4">This is heading 4</Run>
<Run Classes="h5">This is heading 5</Run>
<Run Classes="h6">This is heading 6</Run>
<Run Classes="subtitle">This is a subtitle</Run>
</TextBlock>
</StackPanel>
</Border>
</Design.PreviewWith>
@@ -50,4 +59,26 @@
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Margin" Value="0 25 0 5" />
</Style>
<Style Selector="Run.h1">
<Setter Property="FontSize" Value="64" />
</Style>
<Style Selector="Run.h2">
<Setter Property="FontSize" Value="48" />
</Style>
<Style Selector="Run.h3">
<Setter Property="FontSize" Value="32" />
</Style>
<Style Selector="Run.h4">
<Setter Property="FontSize" Value="24" />
</Style>
<Style Selector="Run.h5">
<Setter Property="FontSize" Value="18" />
</Style>
<Style Selector="Run.h6">
<Setter Property="FontSize" Value="14" />
</Style>
<Style Selector="Run.subtitle">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}" />
</Style>
</Styles>
+29 -9
View File
@@ -1,29 +1,29 @@
using System;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using IContainer = DryIoc.IContainer;
using Avalonia.Threading;
using DryIoc;
namespace Artemis.UI.Shared;
/// <summary>
/// Static UI helpers.
/// Static UI helpers.
/// </summary>
public static class UI
{
private static readonly BehaviorSubject<bool> MicaEnabledSubject = new(false);
public static EventLoopScheduler BackgroundScheduler = new(ts => new Thread(ts));
static UI()
{
KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true);
}
public static EventLoopScheduler BackgroundScheduler = new EventLoopScheduler(ts => new Thread(ts));
internal static void ClearCache()
{
DeviceVisualizer.BitmapCache.Clear();
MicaEnabled = MicaEnabledSubject.AsObservable();
}
/// <summary>
@@ -40,4 +40,24 @@ public static class UI
/// Gets a boolean indicating whether hotkeys are to be disabled.
/// </summary>
public static IObservable<bool> KeyBindingsEnabled { get; }
/// <summary>
/// Gets a boolean indicating whether the Mica effect should be enabled.
/// </summary>
public static IObservable<bool> MicaEnabled { get; }
/// <summary>
/// Changes whether Mica should be enabled.
/// </summary>
/// <param name="enabled"></param>
public static void SetMicaEnabled(bool enabled)
{
if (MicaEnabledSubject.Value != enabled)
Dispatcher.UIThread.Invoke(() => MicaEnabledSubject.OnNext(enabled));
}
internal static void ClearCache()
{
DeviceVisualizer.BitmapCache.Clear();
}
}