committed by
GitHub
279 changed files with 14343 additions and 4642 deletions
@ -0,0 +1,76 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.IO.Compression; |
|||
using System.Linq; |
|||
using ILRepacking; |
|||
using Mono.Cecil; |
|||
|
|||
public class BuildTasksPatcher |
|||
{ |
|||
public static void PatchBuildTasksInPackage(string packagePath) |
|||
{ |
|||
using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), |
|||
ZipArchiveMode.Update)) |
|||
{ |
|||
|
|||
foreach (var entry in archive.Entries.ToList()) |
|||
{ |
|||
if (entry.Name == "Avalonia.Build.Tasks.dll") |
|||
{ |
|||
var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll"); |
|||
var output = temp + ".output"; |
|||
var patched = new MemoryStream(); |
|||
try |
|||
{ |
|||
entry.ExtractToFile(temp, true); |
|||
var repack = new ILRepacking.ILRepack(new RepackOptions() |
|||
{ |
|||
Internalize = true, |
|||
InputAssemblies = new[] |
|||
{ |
|||
temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0] |
|||
.FullyQualifiedName |
|||
}, |
|||
SearchDirectories = new string[0], |
|||
OutputFile = output |
|||
}); |
|||
repack.Repack(); |
|||
|
|||
|
|||
// 'hurr-durr assembly with the same name is already loaded' prevention
|
|||
using (var asm = AssemblyDefinition.ReadAssembly(output, |
|||
new ReaderParameters { ReadWrite = true, InMemory = true, })) |
|||
{ |
|||
asm.Name = new AssemblyNameDefinition( |
|||
"Avalonia.Build.Tasks." |
|||
+ Guid.NewGuid().ToString().Replace("-", ""), |
|||
new Version(0, 0, 0)); |
|||
asm.Write(patched); |
|||
patched.Position = 0; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
try |
|||
{ |
|||
if (File.Exists(temp)) |
|||
File.Delete(temp); |
|||
if (File.Exists(output)) |
|||
File.Delete(output); |
|||
} |
|||
catch |
|||
{ |
|||
//ignore
|
|||
} |
|||
} |
|||
|
|||
var fn = entry.FullName; |
|||
entry.Delete(); |
|||
var newEntry = archive.CreateEntry(fn, CompressionLevel.Optimal); |
|||
using (var s = newEntry.Open()) |
|||
patched.CopyTo(s); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ControlCatalog.Pages.AcrylicPage"> |
|||
<Border Padding="20" HorizontalAlignment="Center"> |
|||
<StackPanel Spacing="20"> |
|||
|
|||
<ExperimentalAcrylicBorder Width="660" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="White" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
<StackPanel Spacing="5" Margin="40 10"> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<TextBlock Text="TintOpacity" Foreground="Black" /> |
|||
<Slider Name="TintOpacitySlider" Minimum="0" Maximum="1" Value="0.9" Width="400" /> |
|||
<TextBlock Text="{Binding #TintOpacitySlider.Value}" Foreground="Black" /> |
|||
</StackPanel> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<TextBlock Text="MaterialOpacity" Foreground="Black" /> |
|||
<Slider Name="MaterialOpacitySlider" Minimum="0" Maximum="1" Value="0.8" Width="400" /> |
|||
<TextBlock Text="{Binding #MaterialOpacitySlider.Value}" Foreground="Black" /> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
|
|||
<StackPanel Orientation="Horizontal" Spacing="20"> |
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#FF0000" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#00FF00" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#000000" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
</StackPanel> |
|||
|
|||
|
|||
<StackPanel Orientation="Horizontal" Spacing="20"> |
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#0000FF" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#FFFF00" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#000000" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Orientation="Horizontal" Spacing="20"> |
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="White" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="#3c3c3c" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="White" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
</StackPanel> |
|||
|
|||
<ExperimentalAcrylicBorder Height="200" Width="660" CornerRadius="5"> |
|||
<ExperimentalAcrylicBorder.Material> |
|||
<ExperimentalAcrylicMaterial |
|||
TintColor="Red" |
|||
TintOpacity="{Binding #TintOpacitySlider.Value}" |
|||
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}" |
|||
BackgroundSource="Digger" /> |
|||
</ExperimentalAcrylicBorder.Material> |
|||
</ExperimentalAcrylicBorder> |
|||
</StackPanel> |
|||
</Border> |
|||
</UserControl> |
|||
@ -0,0 +1,19 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class AcrylicPage : UserControl |
|||
{ |
|||
public AcrylicPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ControlCatalog.Pages.WindowCustomizationsPage"> |
|||
<StackPanel Spacing="10" Margin="25"> |
|||
<CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" /> |
|||
<CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" /> |
|||
<CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" /> |
|||
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" /> |
|||
<ComboBox x:Name="TransparencyLevels" SelectedIndex="{Binding TransparencyLevel}"> |
|||
<ComboBoxItem>None</ComboBoxItem> |
|||
<ComboBoxItem>Transparent</ComboBoxItem> |
|||
<ComboBoxItem>Blur</ComboBoxItem> |
|||
<ComboBoxItem>AcrylicBlur</ComboBoxItem> |
|||
</ComboBox> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,19 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class WindowCustomizationsPage : UserControl |
|||
{ |
|||
public WindowCustomizationsPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
|
|||
namespace Avalonia.Data.Core |
|||
{ |
|||
public class ClrPropertyInfo : IPropertyInfo |
|||
{ |
|||
private readonly Func<object, object> _getter; |
|||
private readonly Action<object, object> _setter; |
|||
|
|||
public ClrPropertyInfo(string name, Func<object, object> getter, Action<object, object> setter, Type propertyType) |
|||
{ |
|||
_getter = getter; |
|||
_setter = setter; |
|||
PropertyType = propertyType; |
|||
Name = name; |
|||
} |
|||
|
|||
public string Name { get; } |
|||
public Type PropertyType { get; } |
|||
|
|||
public object Get(object target) |
|||
{ |
|||
if (_getter == null) |
|||
throw new NotSupportedException("Property " + Name + " doesn't have a getter"); |
|||
return _getter(target); |
|||
} |
|||
|
|||
public void Set(object target, object value) |
|||
{ |
|||
if (_setter == null) |
|||
throw new NotSupportedException("Property " + Name + " doesn't have a setter"); |
|||
_setter(target, value); |
|||
} |
|||
|
|||
public bool CanSet => _setter != null; |
|||
public bool CanGet => _getter != null; |
|||
} |
|||
|
|||
public class ReflectionClrPropertyInfo : ClrPropertyInfo |
|||
{ |
|||
static Action<object, object> CreateSetter(PropertyInfo info) |
|||
{ |
|||
if (info.SetMethod == null) |
|||
return null; |
|||
var target = Expression.Parameter(typeof(object), "target"); |
|||
var value = Expression.Parameter(typeof(object), "value"); |
|||
return Expression.Lambda<Action<object, object>>( |
|||
Expression.Call(Expression.Convert(target, info.DeclaringType), info.SetMethod, |
|||
Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)), |
|||
target, value) |
|||
.Compile(); |
|||
} |
|||
|
|||
static Func<object, object> CreateGetter(PropertyInfo info) |
|||
{ |
|||
if (info.GetMethod == null) |
|||
return null; |
|||
var target = Expression.Parameter(typeof(object), "target"); |
|||
return Expression.Lambda<Func<object, object>>( |
|||
Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType), info.GetMethod), |
|||
typeof(object))) |
|||
.Compile(); |
|||
} |
|||
|
|||
public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, |
|||
CreateGetter(info), CreateSetter(info), info.PropertyType) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Data.Core |
|||
{ |
|||
public interface IPropertyInfo |
|||
{ |
|||
string Name { get; } |
|||
object Get(object target); |
|||
void Set(object target, object value); |
|||
bool CanSet { get; } |
|||
bool CanGet { get; } |
|||
Type PropertyType { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Data.Core |
|||
{ |
|||
public class PropertyPath |
|||
{ |
|||
public IReadOnlyList<IPropertyPathElement> Elements { get; } |
|||
|
|||
public PropertyPath(IEnumerable<IPropertyPathElement> elements) |
|||
{ |
|||
Elements = elements.ToList(); |
|||
} |
|||
} |
|||
|
|||
public class PropertyPathBuilder |
|||
{ |
|||
readonly List<IPropertyPathElement> _elements = new List<IPropertyPathElement>(); |
|||
|
|||
public PropertyPathBuilder Property(IPropertyInfo property) |
|||
{ |
|||
_elements.Add(new PropertyPropertyPathElement(property)); |
|||
return this; |
|||
} |
|||
|
|||
|
|||
public PropertyPathBuilder ChildTraversal() |
|||
{ |
|||
_elements.Add(new ChildTraversalPropertyPathElement()); |
|||
return this; |
|||
} |
|||
|
|||
public PropertyPathBuilder EnsureType(Type type) |
|||
{ |
|||
_elements.Add(new EnsureTypePropertyPathElement(type)); |
|||
return this; |
|||
} |
|||
|
|||
public PropertyPathBuilder Cast(Type type) |
|||
{ |
|||
_elements.Add(new CastTypePropertyPathElement(type)); |
|||
return this; |
|||
} |
|||
|
|||
public PropertyPath Build() |
|||
{ |
|||
return new PropertyPath(_elements); |
|||
} |
|||
} |
|||
|
|||
public interface IPropertyPathElement |
|||
{ |
|||
|
|||
} |
|||
|
|||
public class PropertyPropertyPathElement : IPropertyPathElement |
|||
{ |
|||
public IPropertyInfo Property { get; } |
|||
|
|||
public PropertyPropertyPathElement(IPropertyInfo property) |
|||
{ |
|||
Property = property; |
|||
} |
|||
} |
|||
|
|||
public class ChildTraversalPropertyPathElement : IPropertyPathElement |
|||
{ |
|||
|
|||
} |
|||
|
|||
public class EnsureTypePropertyPathElement : IPropertyPathElement |
|||
{ |
|||
public Type Type { get; } |
|||
|
|||
public EnsureTypePropertyPathElement(Type type) |
|||
{ |
|||
Type = type; |
|||
} |
|||
} |
|||
|
|||
public class CastTypePropertyPathElement : IPropertyPathElement |
|||
{ |
|||
public CastTypePropertyPathElement(Type type) |
|||
{ |
|||
Type = type; |
|||
} |
|||
|
|||
public Type Type { get; } |
|||
} |
|||
} |
|||
@ -1,35 +1,54 @@ |
|||
using System; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Data.Core.Plugins; |
|||
|
|||
namespace Avalonia.Data.Core |
|||
{ |
|||
public class StreamNode : ExpressionNode |
|||
{ |
|||
private IStreamPlugin _customPlugin = null; |
|||
private IDisposable _subscription; |
|||
|
|||
public override string Description => "^"; |
|||
|
|||
public StreamNode() { } |
|||
|
|||
public StreamNode(IStreamPlugin customPlugin) |
|||
{ |
|||
_customPlugin = customPlugin; |
|||
} |
|||
|
|||
protected override void StartListeningCore(WeakReference<object> reference) |
|||
{ |
|||
GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged); |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
} |
|||
|
|||
private IStreamPlugin GetPlugin(WeakReference<object> reference) |
|||
{ |
|||
if (_customPlugin != null) |
|||
{ |
|||
return _customPlugin; |
|||
} |
|||
|
|||
foreach (var plugin in ExpressionObserver.StreamHandlers) |
|||
{ |
|||
if (plugin.Match(reference)) |
|||
{ |
|||
_subscription = plugin.Start(reference).Subscribe(ValueChanged); |
|||
return; |
|||
return plugin; |
|||
} |
|||
} |
|||
|
|||
// TODO: Improve error.
|
|||
// TODO: Improve error
|
|||
ValueChanged(new BindingNotification( |
|||
new MarkupBindingChainException("Stream operator applied to unsupported type", Description), |
|||
BindingErrorType.Error)); |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
#if !BUILDTASK
|
|||
public |
|||
#endif
|
|||
static class KeywordParser |
|||
{ |
|||
public static bool CheckKeyword(this ref CharacterReader r, string keyword) |
|||
{ |
|||
return (CheckKeywordInternal(ref r, keyword) >= 0); |
|||
} |
|||
|
|||
static int CheckKeywordInternal(this ref CharacterReader r, string keyword) |
|||
{ |
|||
var ws = r.PeekWhitespace(); |
|||
|
|||
var chars = r.TryPeek(ws.Length + keyword.Length); |
|||
if (chars.IsEmpty) |
|||
return -1; |
|||
if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) |
|||
return chars.Length; |
|||
return -1; |
|||
} |
|||
|
|||
static bool SpanEquals(ReadOnlySpan<char> left, ReadOnlySpan<char> right) |
|||
{ |
|||
if (left.Length != right.Length) |
|||
return false; |
|||
for(var c=0; c<left.Length;c++) |
|||
if (left[c] != right[c]) |
|||
return false; |
|||
return true; |
|||
} |
|||
|
|||
public static bool TakeIfKeyword(this ref CharacterReader r, string keyword) |
|||
{ |
|||
var l = CheckKeywordInternal(ref r, keyword); |
|||
if (l < 0) |
|||
return false; |
|||
r.Skip(l); |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines compensation levels for the platform depending on the transparency level.
|
|||
/// It controls the base opacity level of the 'tracing paper' layer that compensates
|
|||
/// for low blur radius.
|
|||
/// </summary>
|
|||
public struct AcrylicPlatformCompensationLevels |
|||
{ |
|||
public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic) |
|||
{ |
|||
TransparentLevel = transparent; |
|||
BlurLevel = blurred; |
|||
AcrylicBlurLevel = acrylic; |
|||
} |
|||
|
|||
public double TransparentLevel { get; } |
|||
|
|||
public double BlurLevel { get; } |
|||
|
|||
public double AcrylicBlurLevel { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls.Primitives; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Chrome |
|||
{ |
|||
/// <summary>
|
|||
/// Draws window minimize / maximize / close buttons in a <see cref="TitleBar"/> when managed client decorations are enabled.
|
|||
/// </summary>
|
|||
public class CaptionButtons : TemplatedControl |
|||
{ |
|||
private CompositeDisposable? _disposables; |
|||
private Window? _hostWindow; |
|||
|
|||
public void Attach(Window hostWindow) |
|||
{ |
|||
if (_disposables == null) |
|||
{ |
|||
_hostWindow = hostWindow; |
|||
|
|||
_disposables = new CompositeDisposable |
|||
{ |
|||
_hostWindow.GetObservable(Window.WindowStateProperty) |
|||
.Subscribe(x => |
|||
{ |
|||
PseudoClasses.Set(":minimized", x == WindowState.Minimized); |
|||
PseudoClasses.Set(":normal", x == WindowState.Normal); |
|||
PseudoClasses.Set(":maximized", x == WindowState.Maximized); |
|||
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); |
|||
}) |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public void Detach() |
|||
{ |
|||
if (_disposables != null) |
|||
{ |
|||
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); |
|||
|
|||
layer?.Children.Remove(this); |
|||
|
|||
_disposables.Dispose(); |
|||
_disposables = null; |
|||
} |
|||
} |
|||
|
|||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
|||
{ |
|||
base.OnApplyTemplate(e); |
|||
|
|||
var closeButton = e.NameScope.Get<Panel>("PART_CloseButton"); |
|||
var restoreButton = e.NameScope.Get<Panel>("PART_RestoreButton"); |
|||
var minimiseButton = e.NameScope.Get<Panel>("PART_MinimiseButton"); |
|||
var fullScreenButton = e.NameScope.Get<Panel>("PART_FullScreenButton"); |
|||
|
|||
closeButton.PointerReleased += (sender, e) => _hostWindow?.Close(); |
|||
|
|||
restoreButton.PointerReleased += (sender, e) => |
|||
{ |
|||
if (_hostWindow != null) |
|||
{ |
|||
_hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; |
|||
} |
|||
}; |
|||
|
|||
minimiseButton.PointerReleased += (sender, e) => |
|||
{ |
|||
if (_hostWindow != null) |
|||
{ |
|||
_hostWindow.WindowState = WindowState.Minimized; |
|||
} |
|||
}; |
|||
|
|||
fullScreenButton.PointerReleased += (sender, e) => |
|||
{ |
|||
if (_hostWindow != null) |
|||
{ |
|||
_hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls.Primitives; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Chrome |
|||
{ |
|||
/// <summary>
|
|||
/// Draws a titlebar when managed client decorations are enabled.
|
|||
/// </summary>
|
|||
public class TitleBar : TemplatedControl |
|||
{ |
|||
private CompositeDisposable? _disposables; |
|||
private readonly Window? _hostWindow; |
|||
private CaptionButtons? _captionButtons; |
|||
|
|||
public TitleBar(Window hostWindow) |
|||
{ |
|||
_hostWindow = hostWindow; |
|||
} |
|||
|
|||
public TitleBar() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Attach() |
|||
{ |
|||
if (_disposables == null) |
|||
{ |
|||
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); |
|||
|
|||
layer?.Children.Add(this); |
|||
|
|||
if (_hostWindow != null) |
|||
{ |
|||
_disposables = new CompositeDisposable |
|||
{ |
|||
_hostWindow.GetObservable(Window.WindowDecorationMarginProperty) |
|||
.Subscribe(x => UpdateSize()), |
|||
|
|||
_hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) |
|||
.Subscribe(x => UpdateSize()), |
|||
|
|||
_hostWindow.GetObservable(Window.OffScreenMarginProperty) |
|||
.Subscribe(x => UpdateSize()), |
|||
|
|||
_hostWindow.GetObservable(Window.WindowStateProperty) |
|||
.Subscribe(x => |
|||
{ |
|||
PseudoClasses.Set(":minimized", x == WindowState.Minimized); |
|||
PseudoClasses.Set(":normal", x == WindowState.Normal); |
|||
PseudoClasses.Set(":maximized", x == WindowState.Maximized); |
|||
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); |
|||
}) |
|||
}; |
|||
|
|||
_captionButtons?.Attach(_hostWindow); |
|||
} |
|||
|
|||
UpdateSize(); |
|||
} |
|||
} |
|||
|
|||
private void UpdateSize() |
|||
{ |
|||
if (_hostWindow != null) |
|||
{ |
|||
Margin = new Thickness( |
|||
_hostWindow.OffScreenMargin.Left, |
|||
_hostWindow.OffScreenMargin.Top, |
|||
_hostWindow.OffScreenMargin.Right, |
|||
_hostWindow.OffScreenMargin.Bottom); |
|||
|
|||
if (_hostWindow.WindowState != WindowState.FullScreen) |
|||
{ |
|||
Height = _hostWindow.WindowDecorationMargin.Top; |
|||
|
|||
if (_captionButtons != null) |
|||
{ |
|||
_captionButtons.Height = Height; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Detach() |
|||
{ |
|||
if (_disposables != null) |
|||
{ |
|||
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); |
|||
|
|||
layer?.Children.Remove(this); |
|||
|
|||
_disposables.Dispose(); |
|||
_disposables = null; |
|||
|
|||
_captionButtons?.Detach(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
|||
{ |
|||
base.OnApplyTemplate(e); |
|||
|
|||
_captionButtons = e.NameScope.Get<CaptionButtons>("PART_CaptionButtons"); |
|||
|
|||
if (_hostWindow != null) |
|||
{ |
|||
_captionButtons.Attach(_hostWindow); |
|||
} |
|||
|
|||
UpdateSize(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class ExperimentalAcrylicBorder : Decorator |
|||
{ |
|||
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty = |
|||
Border.CornerRadiusProperty.AddOwner<ExperimentalAcrylicBorder>(); |
|||
|
|||
public static readonly StyledProperty<ExperimentalAcrylicMaterial> MaterialProperty = |
|||
AvaloniaProperty.Register<ExperimentalAcrylicBorder, ExperimentalAcrylicMaterial>(nameof(Material)); |
|||
|
|||
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); |
|||
|
|||
private IDisposable _subscription; |
|||
|
|||
static ExperimentalAcrylicBorder() |
|||
{ |
|||
AffectsRender<ExperimentalAcrylicBorder>( |
|||
MaterialProperty, |
|||
CornerRadiusProperty); |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the radius of the border rounded corners.
|
|||
/// </summary>
|
|||
public CornerRadius CornerRadius |
|||
{ |
|||
get { return GetValue(CornerRadiusProperty); } |
|||
set { SetValue(CornerRadiusProperty, value); } |
|||
} |
|||
|
|||
public ExperimentalAcrylicMaterial Material |
|||
{ |
|||
get => GetValue(MaterialProperty); |
|||
set => SetValue(MaterialProperty, value); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
|
|||
var tl = (e.Root as TopLevel); |
|||
|
|||
_subscription = tl.GetObservable(TopLevel.ActualTransparencyLevelProperty) |
|||
.Subscribe(x => |
|||
{ |
|||
switch (x) |
|||
{ |
|||
case WindowTransparencyLevel.Transparent: |
|||
case WindowTransparencyLevel.None: |
|||
Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.TransparentLevel; |
|||
break; |
|||
|
|||
case WindowTransparencyLevel.Blur: |
|||
Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.BlurLevel; |
|||
break; |
|||
|
|||
case WindowTransparencyLevel.AcrylicBlur: |
|||
Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.AcrylicBlurLevel; |
|||
break; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnDetachedFromVisualTree(e); |
|||
|
|||
_subscription?.Dispose(); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) |
|||
{ |
|||
var cornerRadius = CornerRadius; |
|||
|
|||
idc.DrawRectangle( |
|||
Material, |
|||
new RoundedRect( |
|||
new Rect(Bounds.Size), |
|||
cornerRadius.TopLeft, cornerRadius.TopRight, |
|||
cornerRadius.BottomRight, cornerRadius.BottomLeft)); |
|||
} |
|||
else |
|||
{ |
|||
_borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Measures the control.
|
|||
/// </summary>
|
|||
/// <param name="availableSize">The available size.</param>
|
|||
/// <returns>The desired size of the control.</returns>
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
return LayoutHelper.MeasureChild(Child, availableSize, Padding); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Arranges the control's child.
|
|||
/// </summary>
|
|||
/// <param name="finalSize">The size allocated to the control.</param>
|
|||
/// <returns>The space taken.</returns>
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
return LayoutHelper.ArrangeChild(Child, finalSize, Padding); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
/// <summary>
|
|||
/// Hint for Window Chrome when ClientArea is Extended.
|
|||
/// </summary>
|
|||
[Flags] |
|||
public enum ExtendClientAreaChromeHints |
|||
{ |
|||
/// <summary>
|
|||
/// The will be no chrome at all.
|
|||
/// </summary>
|
|||
NoChrome, |
|||
|
|||
/// <summary>
|
|||
/// The default for the platform.
|
|||
/// </summary>
|
|||
Default = SystemChrome, |
|||
|
|||
/// <summary>
|
|||
/// Use SystemChrome
|
|||
/// </summary>
|
|||
SystemChrome = 0x01, |
|||
|
|||
/// <summary>
|
|||
/// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used.
|
|||
/// This is because Windows Chrome can not be shown ontop of user content.
|
|||
/// </summary>
|
|||
PreferSystemChrome = 0x02, |
|||
|
|||
/// <summary>
|
|||
/// On OSX the titlebar is the thicker toolbar kind. Causes traffic lights to be positioned
|
|||
/// slightly lower than normal.
|
|||
/// </summary>
|
|||
OSXThickTitleBar = 0x08, |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System.Linq; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.VisualTree; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest |
|||
{ |
|||
public static ChromeOverlayLayer? GetOverlayLayer(IVisual visual) |
|||
{ |
|||
foreach (var v in visual.GetVisualAncestors()) |
|||
if (v is VisualLayerManager vlm) |
|||
if (vlm.OverlayLayer != null) |
|||
return vlm.ChromeOverlayLayer; |
|||
|
|||
if (visual is TopLevel tl) |
|||
{ |
|||
var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault(); |
|||
return layers?.ChromeOverlayLayer; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public bool HitTest(Point point) => Children.HitTestCustom(point); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> |
|||
<PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,96 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Threading; |
|||
using RemoteViewing.Vnc; |
|||
using RemoteViewing.Vnc.Server; |
|||
|
|||
namespace Avalonia.Headless.Vnc |
|||
{ |
|||
public class HeadlessVncFramebufferSource : IVncFramebufferSource |
|||
{ |
|||
public IHeadlessWindow Window { get; set; } |
|||
private object _lock = new object(); |
|||
public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); |
|||
|
|||
private VncButton _previousButtons; |
|||
public HeadlessVncFramebufferSource(VncServerSession session, Window window) |
|||
{ |
|||
Window = (IHeadlessWindow)window.PlatformImpl; |
|||
session.PointerChanged += (_, args) => |
|||
{ |
|||
var pt = new Point(args.X, args.Y); |
|||
|
|||
var buttons = (VncButton)args.PressedButtons; |
|||
|
|||
int TranslateButton(VncButton vncButton) => |
|||
vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; |
|||
|
|||
var modifiers = (RawInputModifiers)(((int)buttons & 7) << 4); |
|||
|
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
Window?.MouseMove(pt); |
|||
foreach (var btn in CheckedButtons) |
|||
if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn)) |
|||
Window?.MouseUp(pt, TranslateButton(btn), modifiers); |
|||
|
|||
foreach (var btn in CheckedButtons) |
|||
if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn)) |
|||
Window?.MouseDown(pt, TranslateButton(btn), modifiers); |
|||
_previousButtons = buttons; |
|||
}, DispatcherPriority.Input); |
|||
}; |
|||
|
|||
} |
|||
|
|||
[Flags] |
|||
enum VncButton |
|||
{ |
|||
Left = 1, |
|||
Middle = 2, |
|||
Right = 4, |
|||
ScrollUp = 8, |
|||
ScrollDown = 16 |
|||
} |
|||
|
|||
|
|||
private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; |
|||
|
|||
public VncFramebuffer Capture() |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
using (var bmpRef = Window.GetLastRenderedFrame()) |
|||
{ |
|||
if (bmpRef?.Item == null) |
|||
return _framebuffer; |
|||
var bmp = bmpRef.Item; |
|||
if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) |
|||
{ |
|||
_framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, |
|||
VncPixelFormat.RGB32); |
|||
} |
|||
|
|||
using (var fb = bmp.Lock()) |
|||
{ |
|||
var buf = _framebuffer.GetBuffer(); |
|||
if (_framebuffer.Stride == fb.RowBytes) |
|||
Marshal.Copy(fb.Address, buf, 0, buf.Length); |
|||
else |
|||
for (var y = 0; y < fb.Size.Height; y++) |
|||
{ |
|||
var sourceStart = fb.RowBytes * y; |
|||
var dstStart = _framebuffer.Stride * y; |
|||
var row = fb.Size.Width * 4; |
|||
Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return _framebuffer; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using System.Net; |
|||
using System.Net.Sockets; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Headless; |
|||
using Avalonia.Headless.Vnc; |
|||
using RemoteViewing.Vnc; |
|||
using RemoteViewing.Vnc.Server; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
public static class HeadlessVncPlatformExtensions |
|||
{ |
|||
public static int StartWithHeadlessVncPlatform<T>( |
|||
this T builder, |
|||
string host, int port, |
|||
string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); |
|||
tcpServer.Start(); |
|||
return builder |
|||
.UseHeadless(false) |
|||
.AfterSetup(_ => |
|||
{ |
|||
var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); |
|||
lt.Startup += async delegate |
|||
{ |
|||
while (true) |
|||
{ |
|||
var client = await tcpServer.AcceptTcpClientAsync(); |
|||
var options = new VncServerSessionOptions |
|||
{ |
|||
AuthenticationMethod = AuthenticationMethod.None |
|||
}; |
|||
var session = new VncServerSession(); |
|||
|
|||
session.SetFramebufferSource(new HeadlessVncFramebufferSource( |
|||
session, lt.MainWindow)); |
|||
session.Connect(client.GetStream(), options); |
|||
} |
|||
|
|||
}; |
|||
}) |
|||
.StartWithClassicDesktopLifetime(args, shutdownMode); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,94 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
public static class AvaloniaHeadlessPlatform |
|||
{ |
|||
class RenderTimer : DefaultRenderTimer |
|||
{ |
|||
private readonly int _framesPerSecond; |
|||
private Action _forceTick; |
|||
protected override IDisposable StartCore(Action<TimeSpan> tick) |
|||
{ |
|||
bool cancelled = false; |
|||
var st = Stopwatch.StartNew(); |
|||
_forceTick = () => tick(st.Elapsed); |
|||
DispatcherTimer.Run(() => |
|||
{ |
|||
if (cancelled) |
|||
return false; |
|||
tick(st.Elapsed); |
|||
return !cancelled; |
|||
}, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render); |
|||
return Disposable.Create(() => |
|||
{ |
|||
_forceTick = null; |
|||
cancelled = true; |
|||
}); |
|||
} |
|||
|
|||
public RenderTimer(int framesPerSecond) : base(framesPerSecond) |
|||
{ |
|||
_framesPerSecond = framesPerSecond; |
|||
} |
|||
|
|||
public void ForceTick() => _forceTick?.Invoke(); |
|||
} |
|||
|
|||
class HeadlessWindowingPlatform : IWindowingPlatform |
|||
{ |
|||
public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); |
|||
|
|||
public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); |
|||
|
|||
public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); |
|||
} |
|||
|
|||
internal static void Initialize() |
|||
{ |
|||
AvaloniaLocator.CurrentMutable |
|||
.Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface()) |
|||
.Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>() |
|||
.Bind<IStandardCursorFactory>().ToSingleton<HeadlessCursorFactoryStub>() |
|||
.Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub()) |
|||
.Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>() |
|||
.Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>() |
|||
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice()) |
|||
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) |
|||
.Bind<IRenderTimer>().ToConstant(new RenderTimer(60)) |
|||
.Bind<IFontManagerImpl>().ToSingleton<HeadlessFontManagerStub>() |
|||
.Bind<ITextShaperImpl>().ToSingleton<HeadlessTextShaperStub>() |
|||
.Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform()) |
|||
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>(); |
|||
} |
|||
|
|||
|
|||
public static void ForceRenderTimerTick(int count = 1) |
|||
{ |
|||
var timer = AvaloniaLocator.Current.GetService<IRenderTimer>() as RenderTimer; |
|||
for (var c = 0; c < count; c++) |
|||
timer?.ForceTick(); |
|||
|
|||
} |
|||
} |
|||
|
|||
public static class AvaloniaHeadlessPlatformExtensions |
|||
{ |
|||
public static T UseHeadless<T>(this T builder, bool headlessDrawing = true) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
if (headlessDrawing) |
|||
builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); |
|||
return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,434 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Utilities; |
|||
using Avalonia.Visuals.Media.Imaging; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface |
|||
{ |
|||
public static void Initialize() |
|||
{ |
|||
AvaloniaLocator.CurrentMutable |
|||
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface()); |
|||
} |
|||
|
|||
public IEnumerable<string> InstalledFontNames { get; } = new[] { "Tahoma" }; |
|||
|
|||
public bool SupportsIndividualRoundRects => throw new NotImplementedException(); |
|||
|
|||
public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans) |
|||
{ |
|||
return new HeadlessFormattedTextStub(text, constraint); |
|||
} |
|||
|
|||
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect); |
|||
|
|||
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) |
|||
{ |
|||
var tl = new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)); |
|||
var br = new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y)); |
|||
return new HeadlessGeometryStub(new Rect(tl, br)); |
|||
} |
|||
|
|||
public IGeometryImpl CreateRectangleGeometry(Rect rect) |
|||
{ |
|||
return new HeadlessGeometryStub(rect); |
|||
} |
|||
|
|||
public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); |
|||
|
|||
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget(); |
|||
|
|||
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) |
|||
{ |
|||
return new HeadlessBitmapStub(size, dpi); |
|||
} |
|||
|
|||
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) |
|||
{ |
|||
return new HeadlessBitmapStub(size, dpi); |
|||
} |
|||
|
|||
public IBitmapImpl LoadBitmap(string fileName) |
|||
{ |
|||
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); |
|||
} |
|||
|
|||
public IBitmapImpl LoadBitmap(Stream stream) |
|||
{ |
|||
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); |
|||
} |
|||
|
|||
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) |
|||
{ |
|||
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); |
|||
} |
|||
|
|||
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|||
{ |
|||
return new HeadlessBitmapStub(new Size(width, width), new Vector(96, 96)); |
|||
} |
|||
|
|||
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|||
{ |
|||
return new HeadlessBitmapStub(new Size(height, height), new Vector(96, 96)); |
|||
} |
|||
|
|||
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|||
{ |
|||
return new HeadlessBitmapStub(destinationSize, new Vector(96, 96)); |
|||
} |
|||
|
|||
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) |
|||
{ |
|||
width = 100; |
|||
return new HeadlessGlyphRunStub(); |
|||
} |
|||
|
|||
class HeadlessGeometryStub : IGeometryImpl |
|||
{ |
|||
public HeadlessGeometryStub(Rect bounds) |
|||
{ |
|||
Bounds = bounds; |
|||
} |
|||
|
|||
public Rect Bounds { get; set; } |
|||
public virtual bool FillContains(Point point) => Bounds.Contains(point); |
|||
|
|||
public Rect GetRenderBounds(IPen pen) |
|||
{ |
|||
if(pen is null) |
|||
{ |
|||
return Bounds; |
|||
} |
|||
|
|||
return Bounds.Inflate(pen.Thickness / 2); |
|||
} |
|||
|
|||
public bool StrokeContains(IPen pen, Point point) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
public IGeometryImpl Intersect(IGeometryImpl geometry) |
|||
=> new HeadlessGeometryStub(geometry.Bounds.Intersect(Bounds)); |
|||
|
|||
public ITransformedGeometryImpl WithTransform(Matrix transform) => |
|||
new HeadlessTransformedGeometryStub(this, transform); |
|||
} |
|||
|
|||
class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl |
|||
{ |
|||
public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) |
|||
{ |
|||
|
|||
} |
|||
|
|||
static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) |
|||
{ |
|||
if (b is HeadlessTransformedGeometryStub transformed) |
|||
{ |
|||
b = transformed.SourceGeometry; |
|||
transform = transformed.Transform * transform; |
|||
} |
|||
|
|||
return (b, transform, b.Bounds.TransformToAABB(transform)); |
|||
} |
|||
|
|||
private HeadlessTransformedGeometryStub((IGeometryImpl b, Matrix transform, Rect bounds) fix) : base(fix.bounds) |
|||
{ |
|||
SourceGeometry = fix.b; |
|||
Transform = fix.transform; |
|||
} |
|||
|
|||
|
|||
public IGeometryImpl SourceGeometry { get; } |
|||
public Matrix Transform { get; } |
|||
} |
|||
|
|||
class HeadlessGlyphRunStub : IGlyphRunImpl |
|||
{ |
|||
public void Dispose() |
|||
{ |
|||
} |
|||
} |
|||
|
|||
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl |
|||
{ |
|||
public HeadlessStreamingGeometryStub() : base(Rect.Empty) |
|||
{ |
|||
} |
|||
|
|||
public IStreamGeometryImpl Clone() |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
public IStreamGeometryContextImpl Open() |
|||
{ |
|||
return new HeadlessStreamingGeometryContextStub(this); |
|||
} |
|||
|
|||
class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl |
|||
{ |
|||
private readonly HeadlessStreamingGeometryStub _parent; |
|||
private double _x1, _y1, _x2, _y2; |
|||
public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
|
|||
void Track(Point pt) |
|||
{ |
|||
if (_x1 > pt.X) |
|||
_x1 = pt.X; |
|||
if (_x2 < pt.X) |
|||
_x2 = pt.X; |
|||
if (_y1 > pt.Y) |
|||
_y1 = pt.Y; |
|||
if (_y2 < pt.Y) |
|||
_y2 = pt.Y; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1); |
|||
} |
|||
|
|||
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) |
|||
=> Track(point); |
|||
|
|||
public void BeginFigure(Point startPoint, bool isFilled = true) => Track(startPoint); |
|||
|
|||
public void CubicBezierTo(Point point1, Point point2, Point point3) |
|||
{ |
|||
Track(point1); |
|||
Track(point2); |
|||
Track(point3); |
|||
} |
|||
|
|||
public void QuadraticBezierTo(Point control, Point endPoint) |
|||
{ |
|||
Track(control); |
|||
Track(endPoint); |
|||
} |
|||
|
|||
public void LineTo(Point point) => Track(point); |
|||
|
|||
public void EndFigure(bool isClosed) |
|||
{ |
|||
Dispose(); |
|||
} |
|||
|
|||
public void SetFillRule(FillRule fillRule) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
|
|||
class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl |
|||
{ |
|||
public Size Size { get; } |
|||
|
|||
public HeadlessBitmapStub(Size size, Vector dpi) |
|||
{ |
|||
Size = size; |
|||
Dpi = dpi; |
|||
var pixel = Size * (Dpi / 96); |
|||
PixelSize = new PixelSize(Math.Max(1, (int)pixel.Width), Math.Max(1, (int)pixel.Height)); |
|||
} |
|||
|
|||
public HeadlessBitmapStub(PixelSize size, Vector dpi) |
|||
{ |
|||
PixelSize = size; |
|||
Dpi = dpi; |
|||
Size = PixelSize.ToSizeWithDpi(dpi); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
return new HeadlessDrawingContextStub(); |
|||
} |
|||
|
|||
public Vector Dpi { get; } |
|||
public PixelSize PixelSize { get; } |
|||
public int Version { get; set; } |
|||
public void Save(string fileName) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Save(Stream stream) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public ILockedFramebuffer Lock() |
|||
{ |
|||
Version++; |
|||
var mem = Marshal.AllocHGlobal(PixelSize.Width * PixelSize.Height * 4); |
|||
return new LockedFramebuffer(mem, PixelSize, PixelSize.Width * 4, Dpi, PixelFormat.Rgba8888, |
|||
() => Marshal.FreeHGlobal(mem)); |
|||
} |
|||
} |
|||
|
|||
class HeadlessDrawingContextStub : IDrawingContextImpl |
|||
{ |
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Matrix Transform { get; set; } |
|||
public void Clear(Color color) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IRenderTargetBitmapImpl CreateLayer(Size size) |
|||
{ |
|||
return new HeadlessBitmapStub(size, new Vector(96, 96)); |
|||
} |
|||
|
|||
public void PushClip(Rect clip) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PopClip() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PushOpacity(double opacity) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PopOpacity() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PushOpacityMask(IBrush mask, Rect bounds) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PopOpacityMask() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PushGeometryClip(IGeometryImpl clip) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PopGeometryClip() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Custom(ICustomDrawOperation custom) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void DrawLine(IPen pen, Point p1, Point p2) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) |
|||
{ |
|||
} |
|||
|
|||
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) |
|||
{ |
|||
} |
|||
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadow = default) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void PushClip(RoundedRect clip) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
class HeadlessRenderTarget : IRenderTarget |
|||
{ |
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
return new HeadlessDrawingContextStub(); |
|||
} |
|||
} |
|||
|
|||
class HeadlessFormattedTextStub : IFormattedTextImpl |
|||
{ |
|||
public HeadlessFormattedTextStub(string text, Size constraint) |
|||
{ |
|||
Text = text; |
|||
Constraint = constraint; |
|||
Bounds = new Rect(Constraint.Constrain(new Size(50, 50))); |
|||
} |
|||
|
|||
public Size Constraint { get; } |
|||
public Rect Bounds { get; } |
|||
public string Text { get; } |
|||
|
|||
|
|||
public IEnumerable<FormattedTextLine> GetLines() |
|||
{ |
|||
return new[] { new FormattedTextLine(Text.Length, 10) }; |
|||
} |
|||
|
|||
public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult(); |
|||
|
|||
public Rect HitTestTextPosition(int index) => new Rect(); |
|||
|
|||
public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[length]; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,201 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Media.TextFormatting; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
class HeadlessClipboardStub : IClipboard |
|||
{ |
|||
private string _text; |
|||
private IDataObject _data; |
|||
|
|||
public Task<string> GetTextAsync() |
|||
{ |
|||
return Task.Run(() => _text); |
|||
} |
|||
|
|||
public Task SetTextAsync(string text) |
|||
{ |
|||
return Task.Run(() => _text = text); |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
return Task.Run(() => _text = null); |
|||
} |
|||
|
|||
public Task SetDataObjectAsync(IDataObject data) |
|||
{ |
|||
return Task.Run(() => _data = data); |
|||
} |
|||
|
|||
public Task<string[]> GetFormatsAsync() |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public async Task<object> GetDataAsync(string format) |
|||
{ |
|||
return await Task.Run(() => _data); |
|||
} |
|||
} |
|||
|
|||
class HeadlessCursorFactoryStub : IStandardCursorFactory |
|||
{ |
|||
|
|||
public IPlatformHandle GetCursor(StandardCursorType cursorType) |
|||
{ |
|||
return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); |
|||
} |
|||
} |
|||
|
|||
class HeadlessPlatformSettingsStub : IPlatformSettings |
|||
{ |
|||
public Size DoubleClickSize { get; } = new Size(2, 2); |
|||
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); |
|||
} |
|||
|
|||
class HeadlessSystemDialogsStub : ISystemDialogImpl |
|||
{ |
|||
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) |
|||
{ |
|||
return Task.Run(() => (string[])null); |
|||
} |
|||
|
|||
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) |
|||
{ |
|||
return Task.Run(() => (string)null); |
|||
} |
|||
} |
|||
|
|||
class HeadlessGlyphTypefaceImpl : IGlyphTypefaceImpl |
|||
{ |
|||
public short DesignEmHeight => 10; |
|||
|
|||
public int Ascent => 5; |
|||
|
|||
public int Descent => 5; |
|||
|
|||
public int LineGap => 2; |
|||
|
|||
public int UnderlinePosition => 5; |
|||
|
|||
public int UnderlineThickness => 5; |
|||
|
|||
public int StrikethroughPosition => 5; |
|||
|
|||
public int StrikethroughThickness => 2; |
|||
|
|||
public bool IsFixedPitch => true; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
} |
|||
|
|||
public ushort GetGlyph(uint codepoint) |
|||
{ |
|||
return 1; |
|||
} |
|||
|
|||
public int GetGlyphAdvance(ushort glyph) |
|||
{ |
|||
return 1; |
|||
} |
|||
|
|||
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) |
|||
{ |
|||
return glyphs.ToArray().Select(x => (int)x).ToArray(); |
|||
} |
|||
|
|||
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) |
|||
{ |
|||
return codepoints.ToArray().Select(x => (ushort)x).ToArray(); |
|||
} |
|||
} |
|||
|
|||
class HeadlessTextShaperStub : ITextShaperImpl |
|||
{ |
|||
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) |
|||
{ |
|||
return new GlyphRun(new GlyphTypeface(typeface), 10, |
|||
new ReadOnlySlice<ushort>(new ushort[] { 1, 2, 3 }), |
|||
new ReadOnlySlice<double>(new double[] { 1, 2, 3 }), |
|||
new ReadOnlySlice<Vector>(new Vector[] { new Vector(1, 1), new Vector(2, 2), new Vector(3, 3) }), |
|||
text, |
|||
new ReadOnlySlice<ushort>(new ushort[] { 1, 2, 3 })); |
|||
} |
|||
} |
|||
|
|||
class HeadlessFontManagerStub : IFontManagerImpl |
|||
{ |
|||
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) |
|||
{ |
|||
return new HeadlessGlyphTypefaceImpl(); |
|||
} |
|||
|
|||
public string GetDefaultFontFamilyName() |
|||
{ |
|||
return "Arial"; |
|||
} |
|||
|
|||
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) |
|||
{ |
|||
return new List<string> { "Arial" }; |
|||
} |
|||
|
|||
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) |
|||
{ |
|||
fontKey = new FontKey("Arial", fontWeight, fontStyle); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
class HeadlessIconLoaderStub : IPlatformIconLoader |
|||
{ |
|||
|
|||
class IconStub : IWindowIconImpl |
|||
{ |
|||
public void Save(Stream outputStream) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
public IWindowIconImpl LoadIcon(string fileName) |
|||
{ |
|||
return new IconStub(); |
|||
} |
|||
|
|||
public IWindowIconImpl LoadIcon(Stream stream) |
|||
{ |
|||
return new IconStub(); |
|||
} |
|||
|
|||
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) |
|||
{ |
|||
return new IconStub(); |
|||
} |
|||
} |
|||
|
|||
class HeadlessScreensStub : IScreenImpl |
|||
{ |
|||
public int ScreenCount { get; } = 1; |
|||
|
|||
public IReadOnlyList<Screen> AllScreens { get; } = new[] |
|||
{ |
|||
new Screen(1, new PixelRect(0, 0, 1920, 1280), |
|||
new PixelRect(0, 0, 1920, 1280), true), |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using System.Threading; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface |
|||
{ |
|||
public HeadlessPlatformThreadingInterface() |
|||
{ |
|||
_thread = Thread.CurrentThread; |
|||
} |
|||
|
|||
private AutoResetEvent _event = new AutoResetEvent(false); |
|||
private Thread _thread; |
|||
private object _lock = new object(); |
|||
private DispatcherPriority? _signaledPriority; |
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
while (!cancellationToken.IsCancellationRequested) |
|||
{ |
|||
DispatcherPriority? signaled = null; |
|||
lock (_lock) |
|||
{ |
|||
signaled = _signaledPriority; |
|||
_signaledPriority = null; |
|||
} |
|||
if(signaled.HasValue) |
|||
Signaled?.Invoke(signaled); |
|||
WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20)); |
|||
} |
|||
} |
|||
|
|||
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
var cancelled = false; |
|||
var enqueued = false; |
|||
var l = new object(); |
|||
var timer = new Timer(_ => |
|||
{ |
|||
lock (l) |
|||
{ |
|||
if (cancelled || enqueued) |
|||
return; |
|||
enqueued = true; |
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
lock (l) |
|||
{ |
|||
enqueued = false; |
|||
if (cancelled) |
|||
return; |
|||
tick(); |
|||
} |
|||
}, priority); |
|||
} |
|||
}, null, interval, interval); |
|||
return Disposable.Create(() => |
|||
{ |
|||
lock (l) |
|||
{ |
|||
timer.Dispose(); |
|||
cancelled = true; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void Signal(DispatcherPriority priority) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (_signaledPriority == null || _signaledPriority.Value > priority) |
|||
{ |
|||
_signaledPriority = priority; |
|||
} |
|||
_event.Set(); |
|||
} |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; |
|||
public event Action<DispatcherPriority?> Signaled; |
|||
} |
|||
} |
|||
@ -0,0 +1,342 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform.Surfaces; |
|||
using Avalonia.Controls.Primitives.PopupPositioning; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow |
|||
{ |
|||
private IKeyboardDevice _keyboard; |
|||
private Stopwatch _st = Stopwatch.StartNew(); |
|||
private Pointer _mousePointer; |
|||
private WriteableBitmap _lastRenderedFrame; |
|||
private object _sync = new object(); |
|||
public bool IsPopup { get; } |
|||
|
|||
public HeadlessWindowImpl(bool isPopup) |
|||
{ |
|||
IsPopup = isPopup; |
|||
Surfaces = new object[] { this }; |
|||
_keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>(); |
|||
_mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); |
|||
MouseDevice = new MouseDevice(_mousePointer); |
|||
ClientSize = new Size(1024, 768); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Closed?.Invoke(); |
|||
_lastRenderedFrame?.Dispose(); |
|||
_lastRenderedFrame = null; |
|||
} |
|||
|
|||
public Size ClientSize { get; set; } |
|||
public double Scaling { get; } = 1; |
|||
public IEnumerable<object> Surfaces { get; } |
|||
public Action<RawInputEventArgs> Input { get; set; } |
|||
public Action<Rect> Paint { get; set; } |
|||
public Action<Size> Resized { get; set; } |
|||
public Action<double> ScalingChanged { get; set; } |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) |
|||
=> new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>()); |
|||
|
|||
public void Invalidate(Rect rect) |
|||
{ |
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) |
|||
{ |
|||
InputRoot = inputRoot; |
|||
} |
|||
|
|||
public IInputRoot InputRoot { get; set; } |
|||
|
|||
public Point PointToClient(PixelPoint point) => point.ToPoint(Scaling); |
|||
|
|||
public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, Scaling); |
|||
|
|||
public void SetCursor(IPlatformHandle cursor) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Action Closed { get; set; } |
|||
public IMouseDevice MouseDevice { get; } |
|||
|
|||
public void Show() |
|||
{ |
|||
Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); |
|||
} |
|||
|
|||
public void Hide() |
|||
{ |
|||
Dispatcher.UIThread.Post(() => Deactivated?.Invoke(), DispatcherPriority.Input); |
|||
} |
|||
|
|||
public void BeginMoveDrag() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void BeginResizeDrag(WindowEdge edge) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public PixelPoint Position { get; set; } |
|||
public Action<PixelPoint> PositionChanged { get; set; } |
|||
public void Activate() |
|||
{ |
|||
Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); |
|||
} |
|||
|
|||
public Action Deactivated { get; set; } |
|||
public Action Activated { get; set; } |
|||
public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); |
|||
public Size MaxClientSize { get; } = new Size(1920, 1280); |
|||
public void Resize(Size clientSize) |
|||
{ |
|||
// Emulate X11 behavior here
|
|||
if (IsPopup) |
|||
DoResize(clientSize); |
|||
else |
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
DoResize(clientSize); |
|||
}); |
|||
} |
|||
|
|||
void DoResize(Size clientSize) |
|||
{ |
|||
// Uncomment this check and experience a weird bug in layout engine
|
|||
if (ClientSize != clientSize) |
|||
{ |
|||
ClientSize = clientSize; |
|||
Resized?.Invoke(clientSize); |
|||
} |
|||
} |
|||
|
|||
public void SetMinMaxSize(Size minSize, Size maxSize) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetTopmost(bool value) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IScreenImpl Screen { get; } = new HeadlessScreensStub(); |
|||
public WindowState WindowState { get; set; } |
|||
public Action<WindowState> WindowStateChanged { get; set; } |
|||
public void SetTitle(string title) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void ShowDialog(IWindowImpl parent) |
|||
{ |
|||
Show(); |
|||
} |
|||
|
|||
public void SetSystemDecorations(bool enabled) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetIcon(IWindowIconImpl icon) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void ShowTaskbarIcon(bool value) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void CanResize(bool value) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Func<bool> Closing { get; set; } |
|||
|
|||
class FramebufferProxy : ILockedFramebuffer |
|||
{ |
|||
private readonly ILockedFramebuffer _fb; |
|||
private readonly Action _onDispose; |
|||
private bool _disposed; |
|||
|
|||
public FramebufferProxy(ILockedFramebuffer fb, Action onDispose) |
|||
{ |
|||
_fb = fb; |
|||
_onDispose = onDispose; |
|||
} |
|||
public void Dispose() |
|||
{ |
|||
if (_disposed) |
|||
return; |
|||
_disposed = true; |
|||
_fb.Dispose(); |
|||
_onDispose(); |
|||
} |
|||
|
|||
public IntPtr Address => _fb.Address; |
|||
public PixelSize Size => _fb.Size; |
|||
public int RowBytes => _fb.RowBytes; |
|||
public Vector Dpi => _fb.Dpi; |
|||
public PixelFormat Format => _fb.Format; |
|||
} |
|||
|
|||
public ILockedFramebuffer Lock() |
|||
{ |
|||
var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, Scaling), new Vector(96, 96) * Scaling); |
|||
var fb = bmp.Lock(); |
|||
return new FramebufferProxy(fb, () => |
|||
{ |
|||
lock (_sync) |
|||
{ |
|||
_lastRenderedFrame?.Dispose(); |
|||
_lastRenderedFrame = bmp; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public IRef<IWriteableBitmapImpl> GetLastRenderedFrame() |
|||
{ |
|||
lock (_sync) |
|||
return _lastRenderedFrame?.PlatformImpl?.CloneAs<IWriteableBitmapImpl>(); |
|||
} |
|||
|
|||
private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; |
|||
|
|||
// TODO: Hook recent Popup changes.
|
|||
IPopupPositioner IPopupImpl.PopupPositioner => null; |
|||
|
|||
public Size MaxAutoSizeHint => new Size(1920, 1080); |
|||
|
|||
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } |
|||
|
|||
public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; |
|||
|
|||
public Action GotInputWhenDisabled { get; set; } |
|||
|
|||
public bool IsClientAreaExtendedToDecorations => false; |
|||
|
|||
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; } |
|||
|
|||
public bool NeedsManagedDecorations => false; |
|||
|
|||
public Thickness ExtendedMargins => new Thickness(); |
|||
|
|||
public Thickness OffScreenMargin => new Thickness(); |
|||
|
|||
public Action LostFocus { get; set; } |
|||
|
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); |
|||
|
|||
void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers) |
|||
{ |
|||
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers)); |
|||
} |
|||
|
|||
void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers) |
|||
{ |
|||
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyUp, key, modifiers)); |
|||
} |
|||
|
|||
void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) |
|||
{ |
|||
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, |
|||
button == 0 ? RawPointerEventType.LeftButtonDown : |
|||
button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, |
|||
point, modifiers)); |
|||
} |
|||
|
|||
void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) |
|||
{ |
|||
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, |
|||
RawPointerEventType.Move, point, modifiers)); |
|||
} |
|||
|
|||
void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) |
|||
{ |
|||
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, |
|||
button == 0 ? RawPointerEventType.LeftButtonUp : |
|||
button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, |
|||
point, modifiers)); |
|||
} |
|||
|
|||
void IWindowImpl.Move(PixelPoint point) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IPopupImpl CreatePopup() |
|||
{ |
|||
// TODO: Hook recent Popup changes.
|
|||
return null; |
|||
} |
|||
|
|||
public void SetWindowManagerAddShadowHint(bool enabled) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetParent(IWindowImpl parent) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetEnabled(bool enable) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetSystemDecorations(SystemDecorations enabled) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void BeginMoveDrag(PointerPressedEventArgs e) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
public interface IHeadlessWindow |
|||
{ |
|||
IRef<IWriteableBitmapImpl> GetLastRenderedFrame(); |
|||
void KeyPress(Key key, RawInputModifiers modifiers); |
|||
void KeyRelease(Key key, RawInputModifiers modifiers); |
|||
void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); |
|||
void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); |
|||
void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
using Avalonia; |
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
internal struct UvBounds |
|||
{ |
|||
public UvBounds(Orientation orientation, Rect rect) |
|||
{ |
|||
if (orientation == Orientation.Horizontal) |
|||
{ |
|||
UMin = rect.Left; |
|||
UMax = rect.Right; |
|||
VMin = rect.Top; |
|||
VMax = rect.Bottom; |
|||
} |
|||
else |
|||
{ |
|||
UMin = rect.Top; |
|||
UMax = rect.Bottom; |
|||
VMin = rect.Left; |
|||
VMax = rect.Right; |
|||
} |
|||
} |
|||
|
|||
public double UMin { get; } |
|||
|
|||
public double UMax { get; } |
|||
|
|||
public double VMin { get; } |
|||
|
|||
public double VMax { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
internal struct UvMeasure |
|||
{ |
|||
internal static readonly UvMeasure Zero = default(UvMeasure); |
|||
|
|||
internal double U { get; set; } |
|||
|
|||
internal double V { get; set; } |
|||
|
|||
public UvMeasure(Orientation orientation, double width, double height) |
|||
{ |
|||
if (orientation == Orientation.Horizontal) |
|||
{ |
|||
U = width; |
|||
V = height; |
|||
} |
|||
else |
|||
{ |
|||
U = height; |
|||
V = width; |
|||
} |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj is UvMeasure measure) |
|||
{ |
|||
return (measure.U == U) && (measure.V == V); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return base.GetHashCode(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
internal class WrapItem |
|||
{ |
|||
public WrapItem(int index) |
|||
{ |
|||
this.Index = index; |
|||
} |
|||
|
|||
public int Index { get; } |
|||
|
|||
public UvMeasure? Measure { get; internal set; } |
|||
|
|||
public UvMeasure? Position { get; internal set; } |
|||
|
|||
public ILayoutable Element { get; internal set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,327 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Layout; |
|||
using System; |
|||
using System.Collections.Specialized; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
/// <summary>
|
|||
/// Arranges elements by wrapping them to fit the available space.
|
|||
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
|
|||
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
|
|||
/// </summary>
|
|||
public class WrapLayout : VirtualizingLayout |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
|
|||
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
|
|||
/// </summary>
|
|||
public double HorizontalSpacing |
|||
{ |
|||
get { return (double)GetValue(HorizontalSpacingProperty); } |
|||
set { SetValue(HorizontalSpacingProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> HorizontalSpacingProperty = |
|||
AvaloniaProperty.Register<WrapLayout, double>(nameof(HorizontalSpacing), 0); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
|
|||
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
|
|||
/// </summary>
|
|||
public double VerticalSpacing |
|||
{ |
|||
get { return (double)GetValue(VerticalSpacingProperty); } |
|||
set { SetValue(VerticalSpacingProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> VerticalSpacingProperty = |
|||
AvaloniaProperty.Register<WrapLayout, double>( |
|||
nameof(VerticalSpacing), 0d); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the orientation of the WrapLayout.
|
|||
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
|
|||
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
|
|||
/// </summary>
|
|||
public Orientation Orientation |
|||
{ |
|||
get { return (Orientation)GetValue(OrientationProperty); } |
|||
set { SetValue(OrientationProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Identifies the <see cref="Orientation"/> dependency property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Orientation> OrientationProperty = |
|||
AvaloniaProperty.Register<WrapLayout, Orientation>( |
|||
nameof(Orientation), |
|||
Orientation.Horizontal); |
|||
|
|||
/// <inheritdoc />
|
|||
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) |
|||
{ |
|||
var state = new WrapLayoutState(context); |
|||
context.LayoutState = state; |
|||
base.InitializeForContextCore(context); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) |
|||
{ |
|||
context.LayoutState = null; |
|||
base.UninitializeForContextCore(context); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
var state = (WrapLayoutState)context.LayoutState; |
|||
|
|||
switch (args.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
state.RemoveFromIndex(args.NewStartingIndex); |
|||
break; |
|||
case NotifyCollectionChangedAction.Move: |
|||
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); |
|||
state.RemoveFromIndex(minIndex); |
|||
|
|||
state.RecycleElementAt(args.OldStartingIndex); |
|||
state.RecycleElementAt(args.NewStartingIndex); |
|||
break; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
state.RemoveFromIndex(args.OldStartingIndex); |
|||
break; |
|||
case NotifyCollectionChangedAction.Replace: |
|||
state.RemoveFromIndex(args.NewStartingIndex); |
|||
state.RecycleElementAt(args.NewStartingIndex); |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
state.Clear(); |
|||
break; |
|||
} |
|||
|
|||
base.OnItemsChangedCore(context, source, args); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) |
|||
{ |
|||
var totalMeasure = UvMeasure.Zero; |
|||
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); |
|||
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); |
|||
var realizationBounds = new UvBounds(Orientation, context.RealizationRect); |
|||
var position = UvMeasure.Zero; |
|||
|
|||
var state = (WrapLayoutState)context.LayoutState; |
|||
if (state.Orientation != Orientation) |
|||
{ |
|||
state.SetOrientation(Orientation); |
|||
} |
|||
|
|||
if (spacingMeasure.Equals(state.Spacing) == false) |
|||
{ |
|||
state.ClearPositions(); |
|||
state.Spacing = spacingMeasure; |
|||
} |
|||
|
|||
if (state.AvailableU != parentMeasure.U) |
|||
{ |
|||
state.ClearPositions(); |
|||
state.AvailableU = parentMeasure.U; |
|||
} |
|||
|
|||
double currentV = 0; |
|||
for (int i = 0; i < context.ItemCount; i++) |
|||
{ |
|||
bool measured = false; |
|||
WrapItem item = state.GetItemAt(i); |
|||
if (item.Measure == null) |
|||
{ |
|||
item.Element = context.GetOrCreateElementAt(i); |
|||
item.Element.Measure(availableSize); |
|||
item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); |
|||
measured = true; |
|||
} |
|||
|
|||
UvMeasure currentMeasure = item.Measure.Value; |
|||
if (currentMeasure.U == 0) |
|||
{ |
|||
continue; // ignore collapsed items
|
|||
} |
|||
|
|||
if (item.Position == null) |
|||
{ |
|||
if (parentMeasure.U < position.U + currentMeasure.U) |
|||
{ |
|||
// New Row
|
|||
position.U = 0; |
|||
position.V += currentV + spacingMeasure.V; |
|||
currentV = 0; |
|||
} |
|||
|
|||
item.Position = position; |
|||
} |
|||
|
|||
position = item.Position.Value; |
|||
|
|||
double vEnd = position.V + currentMeasure.V; |
|||
if (vEnd < realizationBounds.VMin) |
|||
{ |
|||
// Item is "above" the bounds
|
|||
if (item.Element != null) |
|||
{ |
|||
context.RecycleElement(item.Element); |
|||
item.Element = null; |
|||
} |
|||
} |
|||
else if (position.V > realizationBounds.VMax) |
|||
{ |
|||
// Item is "below" the bounds.
|
|||
if (item.Element != null) |
|||
{ |
|||
context.RecycleElement(item.Element); |
|||
item.Element = null; |
|||
} |
|||
|
|||
// We don't need to measure anything below the bounds
|
|||
break; |
|||
} |
|||
else if (measured == false) |
|||
{ |
|||
// Always measure elements that are within the bounds
|
|||
item.Element = context.GetOrCreateElementAt(i); |
|||
item.Element.Measure(availableSize); |
|||
|
|||
currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); |
|||
if (currentMeasure.Equals(item.Measure) == false) |
|||
{ |
|||
// this item changed size; we need to recalculate layout for everything after this
|
|||
state.RemoveFromIndex(i + 1); |
|||
item.Measure = currentMeasure; |
|||
|
|||
// did the change make it go into the new row?
|
|||
if (parentMeasure.U < position.U + currentMeasure.U) |
|||
{ |
|||
// New Row
|
|||
position.U = 0; |
|||
position.V += currentV + spacingMeasure.V; |
|||
currentV = 0; |
|||
} |
|||
|
|||
item.Position = position; |
|||
} |
|||
} |
|||
|
|||
position.U += currentMeasure.U + spacingMeasure.U; |
|||
currentV = Math.Max(currentMeasure.V, currentV); |
|||
} |
|||
|
|||
// update value with the last line
|
|||
// if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it
|
|||
// if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here
|
|||
// for the last condition it is zeros so adding it will make no difference
|
|||
// this way is faster than an if condition in every loop for checking the last item
|
|||
totalMeasure.U = parentMeasure.U; |
|||
totalMeasure.V = state.GetHeight(); |
|||
|
|||
totalMeasure.U = Math.Ceiling(totalMeasure.U); |
|||
|
|||
return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) |
|||
{ |
|||
if (context.ItemCount > 0) |
|||
{ |
|||
var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height); |
|||
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); |
|||
var realizationBounds = new UvBounds(Orientation, context.RealizationRect); |
|||
|
|||
var state = (WrapLayoutState)context.LayoutState; |
|||
bool Arrange(WrapItem item, bool isLast = false) |
|||
{ |
|||
if (item.Measure.HasValue == false) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (item.Position == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var desiredMeasure = item.Measure.Value; |
|||
if (desiredMeasure.U == 0) |
|||
{ |
|||
return true; // if an item is collapsed, avoid adding the spacing
|
|||
} |
|||
|
|||
UvMeasure position = item.Position.Value; |
|||
|
|||
// Stretch the last item to fill the available space
|
|||
if (isLast) |
|||
{ |
|||
desiredMeasure.U = parentMeasure.U - position.U; |
|||
} |
|||
|
|||
if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax)) |
|||
{ |
|||
// place the item
|
|||
var child = context.GetOrCreateElementAt(item.Index); |
|||
if (Orientation == Orientation.Horizontal) |
|||
{ |
|||
child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V)); |
|||
} |
|||
else |
|||
{ |
|||
child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U)); |
|||
} |
|||
} |
|||
else if (position.V > realizationBounds.VMax) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
for (var i = 0; i < context.ItemCount; i++) |
|||
{ |
|||
bool continueArranging = Arrange(state.GetItemAt(i)); |
|||
if (continueArranging == false) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return finalSize; |
|||
} |
|||
|
|||
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty) |
|||
{ |
|||
InvalidateMeasure(); |
|||
InvalidateArrange(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
internal class WrapLayoutState |
|||
{ |
|||
private List<WrapItem> _items = new List<WrapItem>(); |
|||
private VirtualizingLayoutContext _context; |
|||
|
|||
public WrapLayoutState(VirtualizingLayoutContext context) |
|||
{ |
|||
this._context = context; |
|||
} |
|||
|
|||
public Orientation Orientation { get; private set; } |
|||
|
|||
public UvMeasure Spacing { get; internal set; } |
|||
|
|||
public double AvailableU { get; internal set; } |
|||
|
|||
internal WrapItem GetItemAt(int index) |
|||
{ |
|||
if (index < 0) |
|||
{ |
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
|
|||
if (index <= (_items.Count - 1)) |
|||
{ |
|||
return _items[index]; |
|||
} |
|||
else |
|||
{ |
|||
WrapItem item = new WrapItem(index); |
|||
_items.Add(item); |
|||
return item; |
|||
} |
|||
} |
|||
|
|||
internal void Clear() |
|||
{ |
|||
_items.Clear(); |
|||
} |
|||
|
|||
internal void RemoveFromIndex(int index) |
|||
{ |
|||
if (index >= _items.Count) |
|||
{ |
|||
// Item was added/removed but we haven't realized that far yet
|
|||
return; |
|||
} |
|||
|
|||
int numToRemove = _items.Count - index; |
|||
_items.RemoveRange(index, numToRemove); |
|||
} |
|||
|
|||
internal void SetOrientation(Orientation orientation) |
|||
{ |
|||
foreach (var item in _items.Where(i => i.Measure.HasValue)) |
|||
{ |
|||
UvMeasure measure = item.Measure.Value; |
|||
double v = measure.V; |
|||
measure.V = measure.U; |
|||
measure.U = v; |
|||
item.Measure = measure; |
|||
item.Position = null; |
|||
} |
|||
|
|||
Orientation = orientation; |
|||
AvailableU = 0; |
|||
} |
|||
|
|||
internal void ClearPositions() |
|||
{ |
|||
foreach (var item in _items) |
|||
{ |
|||
item.Position = null; |
|||
} |
|||
} |
|||
|
|||
internal double GetHeight() |
|||
{ |
|||
if (_items.Count == 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
bool calculateAverage = true; |
|||
if ((_items.Count == _context.ItemCount) && _items[_items.Count - 1].Position.HasValue) |
|||
{ |
|||
calculateAverage = false; |
|||
} |
|||
|
|||
UvMeasure? lastPosition = null; |
|||
double maxV = 0; |
|||
|
|||
int itemCount = _items.Count; |
|||
for (int i = _items.Count - 1; i >= 0; i--) |
|||
{ |
|||
var item = _items[i]; |
|||
if (item.Position == null) |
|||
{ |
|||
itemCount--; |
|||
continue; |
|||
} |
|||
|
|||
if (lastPosition != null) |
|||
{ |
|||
if (lastPosition.Value.V > item.Position.Value.V) |
|||
{ |
|||
// This is a row above the last item. Exit and calculate the average
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
lastPosition = item.Position; |
|||
maxV = Math.Max(maxV, item.Measure.Value.V); |
|||
} |
|||
|
|||
double totalHeight = lastPosition.Value.V + maxV; |
|||
if (calculateAverage) |
|||
{ |
|||
return (totalHeight / itemCount) * _context.ItemCount; |
|||
} |
|||
else |
|||
{ |
|||
return totalHeight; |
|||
} |
|||
} |
|||
|
|||
internal void RecycleElementAt(int index) |
|||
{ |
|||
var element = _context.GetOrCreateElementAt(index); |
|||
_context.RecycleElement(element); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<Style Selector="CaptionButtons"> |
|||
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/> |
|||
<Setter Property="MaxHeight" Value="30" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<StackPanel Spacing="2" Margin="0 0 7 0" VerticalAlignment="Stretch" TextBlock.FontSize="10" Orientation="Horizontal"> |
|||
<StackPanel.Styles> |
|||
<Style Selector="Panel"> |
|||
<Setter Property="Width" Value="45" /> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
</Style> |
|||
<Style Selector="Panel:pointerover"> |
|||
<Setter Property="Background" Value="#7F7f7f7f" /> |
|||
</Style> |
|||
<Style Selector="Panel#PART_CloseButton:pointerover"> |
|||
<Setter Property="Background" Value="#7FFF0000" /> |
|||
</Style> |
|||
<Style Selector="Viewbox"> |
|||
<Setter Property="Width" Value="11" /> |
|||
<Setter Property="Margin" Value="2" /> |
|||
</Style> |
|||
</StackPanel.Styles> |
|||
<Panel x:Name="PART_FullScreenButton"> |
|||
<Viewbox> |
|||
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" /> |
|||
</Viewbox> |
|||
</Panel> |
|||
|
|||
<Panel x:Name="PART_MinimiseButton"> |
|||
<Viewbox> |
|||
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" Data="M2048 1229v-205h-2048v205h2048z" /> |
|||
</Viewbox> |
|||
</Panel> |
|||
|
|||
<Panel x:Name="PART_RestoreButton"> |
|||
<Viewbox> |
|||
<Viewbox.RenderTransform> |
|||
<RotateTransform Angle="-90" /> |
|||
</Viewbox.RenderTransform> |
|||
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}"/> |
|||
</Viewbox> |
|||
</Panel> |
|||
|
|||
<Panel x:Name="PART_CloseButton"> |
|||
<Viewbox> |
|||
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" /> |
|||
</Viewbox> |
|||
</Panel> |
|||
</StackPanel> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
<Style Selector="CaptionButtons Panel#PART_RestoreButton Path"> |
|||
<Setter Property="Data" Value="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z" /> |
|||
</Style> |
|||
<Style Selector="CaptionButtons:maximized Panel#PART_RestoreButton Path"> |
|||
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" /> |
|||
</Style> |
|||
<Style Selector="CaptionButtons Panel#PART_FullScreenButton Path"> |
|||
<Setter Property="Data" Value="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z" /> |
|||
</Style> |
|||
<Style Selector="CaptionButtons:fullscreen Panel#PART_FullScreenButton Path"> |
|||
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" /> |
|||
</Style> |
|||
<Style Selector="CaptionButtons:fullscreen Panel#PART_RestoreButton, CaptionButtons:fullscreen Panel#PART_MinimiseButton"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
</Styles> |
|||
@ -0,0 +1,53 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<Design.PreviewWith> |
|||
<Border> |
|||
<TitleBar Background="SkyBlue" Height="30" Width="300" Foreground="Black" /> |
|||
</Border> |
|||
</Design.PreviewWith> |
|||
<Style Selector="TitleBar"> |
|||
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/> |
|||
<Setter Property="VerticalAlignment" Value="Top" /> |
|||
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Panel HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="Stretch"> |
|||
<Panel x:Name="PART_MouseTracker" Height="1" VerticalAlignment="Top" /> |
|||
<Panel x:Name="PART_Container"> |
|||
<Border x:Name="PART_Background" Background="{TemplateBinding Background}" /> |
|||
<CaptionButtons x:Name="PART_CaptionButtons" VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" MaxHeight="30" /> |
|||
</Panel> |
|||
</Panel> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="TitleBar:fullscreen"> |
|||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="TitleBar /template/ Border#PART_Background"> |
|||
<Setter Property="IsHitTestVisible" Value="False" /> |
|||
</Style> |
|||
|
|||
<Style Selector="TitleBar:fullscreen /template/ Border#PART_Background"> |
|||
<Setter Property="IsHitTestVisible" Value="True" /> |
|||
</Style> |
|||
|
|||
<Style Selector="TitleBar:fullscreen /template/ Panel#PART_MouseTracker"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
</Style> |
|||
|
|||
<Style Selector="TitleBar:fullscreen /template/ Panel#PART_Container"> |
|||
<Setter Property="RenderTransform" Value="translateY(-30px)" /> |
|||
<Setter Property="Transitions"> |
|||
<Transitions> |
|||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:.25" /> |
|||
</Transitions> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="TitleBar:fullscreen:pointerover /template/ Panel#PART_Container"> |
|||
<Setter Property="RenderTransform" Value="none" /> |
|||
</Style> |
|||
</Styles> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue