Browse Source

Merge branch 'master' into extended-file-picker

pull/19783/head
Max Katz 4 months ago
committed by GitHub
parent
commit
5defe75b54
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 32
      src/Avalonia.Base/Input/InputElement.cs
  2. 4
      src/Avalonia.Base/Input/MouseDevice.cs
  3. 11
      src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs
  4. 53
      src/Avalonia.Base/Input/Pointer.cs
  5. 17
      src/Avalonia.Base/Input/PointerEventArgs.cs
  6. 47
      src/Avalonia.Controls/LayoutTransformControl.cs
  7. 9
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  8. 6
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  9. 5
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  10. 1
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  11. 14
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  12. 26
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  13. 60
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  14. 6
      src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
  15. 6
      src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs
  16. 29
      tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
  17. 49
      tests/Avalonia.LeakTests/ControlTests.cs
  18. 47
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

32
src/Avalonia.Base/Input/InputElement.cs

@ -178,6 +178,14 @@ namespace Avalonia.Input
nameof(PointerReleased),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerCaptureChanging"/> routed event.
/// </summary>
internal static readonly RoutedEvent<PointerCaptureChangingEventArgs> PointerCaptureChangingEvent =
RoutedEvent.Register<InputElement, PointerCaptureChangingEventArgs>(
nameof(PointerCaptureChanging),
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="PointerCaptureLost"/> routed event.
/// </summary>
@ -240,6 +248,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerMoved(e));
PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerPressed(e));
PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerReleased(e));
PointerCaptureChangingEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureChanging(e));
PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureLost(e));
PointerWheelChangedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerWheelChanged(e));
@ -381,9 +390,19 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); }
}
/// <summary>
/// Occurs when the control or its child control is about to lose capture,
/// event will not be triggered for a parent control if capture was transferred to another child of that parent control.
/// </summary>
internal event EventHandler<PointerCaptureChangingEventArgs>? PointerCaptureChanging
{
add => AddHandler(PointerCaptureChangingEvent, value);
remove => RemoveHandler(PointerCaptureChangingEvent, value);
}
/// <summary>
/// Occurs when the control or its child control loses the pointer capture for any reason,
/// event will not be triggered for a parent control if capture was transferred to another child of that parent control
/// event will not be triggered for a parent control if capture was transferred to another child of that parent control.
/// </summary>
public event EventHandler<PointerCaptureLostEventArgs>? PointerCaptureLost
{
@ -770,6 +789,17 @@ namespace Avalonia.Input
/// <returns>Last focusable element if available/>.</returns>
protected internal virtual InputElement? GetLastFocusableElementOverride() => null;
/// <summary>
/// Invoked when an unhandled <see cref="PointerCaptureChangingEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
internal virtual void OnPointerCaptureChanging(PointerCaptureChangingEventArgs e)
{
}
/// <summary>
/// Invoked when an unhandled <see cref="PointerCaptureLostEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling

4
src/Avalonia.Base/Input/MouseDevice.cs

@ -131,7 +131,7 @@ namespace Avalonia.Input
if (source != null)
{
_pointer.Capture(source);
_pointer.Capture(source, CaptureSource.Implicit);
var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings;
if (settings is not null)
@ -206,7 +206,7 @@ namespace Avalonia.Input
}
finally
{
_pointer.Capture(null);
_pointer.Capture(null, CaptureSource.Implicit);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
_lastMouseDownButton = default;

11
src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs

@ -28,6 +28,7 @@ internal sealed class DataObjectToDataTransferWrapper(IDataObject dataObject)
var items = new List<PlatformDataTransferItem>();
var nonFileFormats = new List<DataFormat>();
var nonFileFormatStrings = new List<string>();
var hasFiles = false;
foreach (var formatString in DataObject.GetDataFormats())
{
@ -35,18 +36,28 @@ internal sealed class DataObjectToDataTransferWrapper(IDataObject dataObject)
if (formatString == DataFormats.Files)
{
if (hasFiles)
continue;
// This is not ideal as we're reading the filenames ahead of time to generate the appropriate items.
// We don't really care about that for this legacy wrapper.
if (DataObject.Get(formatString) is IEnumerable<IStorageItem> storageItems)
{
hasFiles = true;
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
else if (formatString == DataFormats.FileNames)
{
if (hasFiles)
continue;
if (DataObject.Get(formatString) is IEnumerable<string> fileNames)
{
hasFiles = true;
foreach (var fileName in fileNames)
{
if (StorageProviderHelpers.TryCreateBclStorageItem(fileName) is { } storageItem)

53
src/Avalonia.Base/Input/Pointer.cs

@ -6,6 +6,13 @@ using Avalonia.VisualTree;
namespace Avalonia.Input
{
internal enum CaptureSource
{
Explicit,
Implicit,
Platform
}
public class Pointer : IPointer, IDisposable
{
private static int s_NextFreePointerId = 1000;
@ -30,46 +37,60 @@ namespace Avalonia.Input
protected virtual void PlatformCapture(IInputElement? element)
{
}
internal void PlatformCaptureLost()
{
if (Captured != null)
Capture(null, platformInitiated: true);
Capture(null, CaptureSource.Platform);
}
public void Capture(IInputElement? control)
{
Capture(control, platformInitiated: false);
Capture(control, CaptureSource.Explicit);
}
private void Capture(IInputElement? control, bool platformInitiated)
internal void Capture(IInputElement? control, CaptureSource source)
{
var oldCapture = Captured;
if (oldCapture == control)
return;
if (oldCapture is Visual v1)
v1.DetachedFromVisualTree -= OnCaptureDetached;
var oldVisual = oldCapture as Visual;
IInputElement? commonParent = null;
if (oldVisual != null)
{
commonParent = FindCommonParent(control, oldCapture);
foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType<IInputElement>())
{
if (notifyTarget == commonParent)
break;
var args = new PointerCaptureChangingEventArgs(notifyTarget, this, control, source);
notifyTarget.RaiseEvent(args);
if (args.Handled)
return;
}
}
if (oldVisual != null)
oldVisual.DetachedFromVisualTree -= OnCaptureDetached;
Captured = control;
if (!platformInitiated)
if (source != CaptureSource.Platform)
PlatformCapture(control);
if (oldCapture is Visual v2)
{
var commonParent = FindCommonParent(control, oldCapture);
foreach (var notifyTarget in v2.GetSelfAndVisualAncestors().OfType<IInputElement>())
if (oldVisual != null)
foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType<IInputElement>())
{
if (notifyTarget == commonParent)
break;
notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this));
}
}
if (Captured is Visual v3)
v3.DetachedFromVisualTree += OnCaptureDetached;
if (Captured is Visual newVisual)
newVisual.DetachedFromVisualTree += OnCaptureDetached;
if (Captured != null)
CaptureGestureRecognizer(null);
@ -92,7 +113,7 @@ namespace Avalonia.Input
public IInputElement? Captured { get; private set; }
public PointerType Type { get; }
public bool IsPrimary { get; }

17
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -202,11 +202,26 @@ namespace Avalonia.Input
{
public IPointer Pointer { get; }
[Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")]
[Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.")]
public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;
}
}
internal class PointerCaptureChangingEventArgs : RoutedEventArgs
{
public IPointer Pointer { get; }
public CaptureSource CaptureSource { get; }
public IInputElement? NewValue { get; }
internal PointerCaptureChangingEventArgs(object source, IPointer pointer, IInputElement? newValue, CaptureSource captureSource) : base(InputElement.PointerCaptureChangingEvent)
{
Pointer = pointer;
Source = source;
NewValue = newValue;
CaptureSource = captureSource;
}
}
}

47
src/Avalonia.Controls/LayoutTransformControl.cs

@ -145,6 +145,19 @@ namespace Avalonia.Controls
// Return result to allocate enough space for the transformation
return transformedDesiredSize;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
SubscribeLayoutTransform(LayoutTransform as Transform);
ApplyLayoutTransform();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
UnsubscribeLayoutTransform(LayoutTransform as Transform);
}
private IDisposable? _renderTransformChangedEvent;
@ -224,7 +237,6 @@ namespace Avalonia.Controls
/// Transformation matrix corresponding to _matrixTransform.
/// </summary>
private Matrix _transformation = Matrix.Identity;
private IDisposable? _transformChangedEvent;
/// <summary>
/// Returns true if Size a is smaller than Size b in either dimension.
@ -424,19 +436,34 @@ namespace Avalonia.Controls
private void OnLayoutTransformChanged(AvaloniaPropertyChangedEventArgs e)
{
var newTransform = e.NewValue as Transform;
_transformChangedEvent?.Dispose();
_transformChangedEvent = null;
if (newTransform != null)
if (this.IsAttachedToVisualTree)
{
_transformChangedEvent = Observable.FromEventPattern(
v => newTransform.Changed += v, v => newTransform.Changed -= v)
.Subscribe(_ => ApplyLayoutTransform());
UnsubscribeLayoutTransform(e.OldValue as Transform);
SubscribeLayoutTransform(e.NewValue as Transform);
}
ApplyLayoutTransform();
}
private void OnTransformChanged(object? sender, EventArgs e)
{
ApplyLayoutTransform();
}
private void SubscribeLayoutTransform(Transform? transform)
{
if (transform != null)
{
transform.Changed += OnTransformChanged;
}
}
private void UnsubscribeLayoutTransform(Transform? transform)
{
if (transform != null)
{
transform.Changed -= OnTransformChanged;
}
}
}
}

9
src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@ -58,6 +58,7 @@ namespace Avalonia.FreeDesktop
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath);
_pathHandler.Add(_statusNotifierItemDbusObj);
_connection.AddMethodHandler(_pathHandler);
_statusNotifierItemDbusObj.ActivationDelegate += () => OnClicked?.Invoke();
WatchAsync();
}
@ -112,13 +113,19 @@ namespace Avalonia.FreeDesktop
#endif
var tid = s_trayIconInstanceId++;
// make sure not to add the path handle and connection method handler twice
if (_statusNotifierItemDbusObj!.PathHandler is null)
_pathHandler.Add(_statusNotifierItemDbusObj!);
_connection.RemoveMethodHandler(_pathHandler.Path);
_connection.AddMethodHandler(_pathHandler);
_sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}");
await _dBus!.RequestNameAsync(_sysTrayServiceName, 0);
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
_statusNotifierItemDbusObj!.SetTitleAndTooltip(_tooltipText);
_statusNotifierItemDbusObj.SetIcon(_icon);
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
}
private void DestroyTrayIcon()

6
src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -327,15 +327,21 @@ namespace Avalonia.X11.Clipboard
{
List<DataFormat>? nonFileFormats = null;
var items = new List<IAsyncDataTransferItem>();
var hasFiles = false;
foreach (var format in formats)
{
if (DataFormat.File.Equals(format))
{
if (hasFiles)
continue;
// We're reading the filenames ahead of time to generate the appropriate items.
// This is async, so it should be fine.
if (await reader.TryGetAsync(format) is IEnumerable<IStorageItem> storageItems)
{
hasFiles = true;
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}

5
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -61,7 +61,10 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_local_only(IntPtr chooser, bool local_only);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_do_overwrite_confirmation(IntPtr chooser, bool do_overwrite_confirmation);

1
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -108,6 +108,7 @@ namespace Avalonia.X11.NativeDialogs
}
gtk_window_set_modal(dlg, true);
gtk_file_chooser_set_local_only(dlg, false);
var tcs = new TaskCompletionSource<(string[]?, FilePickerFileType?)>();
List<IDisposable>? disposables = null;

14
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -50,7 +50,7 @@ namespace Avalonia.Skia
skFontStyle = SKFontStyle.BoldItalic;
break;
default:
skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle);
skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, fontStyle.ToSkia());
break;
}
@ -63,7 +63,12 @@ namespace Avalonia.Skia
if (skTypeface != null)
{
fontKey = new Typeface(skTypeface.FamilyName, (FontStyle)skTypeface.FontStyle.Slant, (FontWeight)skTypeface.FontStyle.Weight, (FontStretch)skTypeface.FontStyle.Width);
// ToDo: create glyph typeface here to get the correct style/weight/stretch
fontKey = new Typeface(
skTypeface.FamilyName,
skTypeface.FontStyle.Slant.ToAvalonia(),
(FontWeight)skTypeface.FontStyle.Weight,
(FontStretch)skTypeface.FontStyle.Width);
return true;
}
@ -78,8 +83,7 @@ namespace Avalonia.Skia
{
glyphTypeface = null;
var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
(SKFontStyleSlant)style);
var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, style.ToSkia());
var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
@ -127,7 +131,7 @@ namespace Avalonia.Skia
var set = _skFontManager.GetFontStyles(familyName);
if(set.Count == 0)
if (set.Count == 0)
{
return false;
}

26
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -16,7 +16,6 @@ namespace Avalonia.Skia
internal class GlyphTypefaceImpl : IGlyphTypeface2
{
private bool _isDisposed;
private readonly SKTypeface _typeface;
private readonly NameTable? _nameTable;
private readonly OS2Table? _os2Table;
private readonly HorizontalHeadTable? _hhTable;
@ -24,7 +23,7 @@ namespace Avalonia.Skia
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
_typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
Face = new Face(GetTable) { UnitsPerEm = typeface.UnitsPerEm };
@ -96,6 +95,11 @@ namespace Avalonia.Skia
var style = _os2Table != null ? GetFontStyle(_os2Table.FontStyle) : FontStyle.Normal;
if (typeface.FontStyle.Slant == SKFontStyleSlant.Oblique)
{
style = FontStyle.Oblique;
}
Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style;
var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal;
@ -205,6 +209,8 @@ namespace Avalonia.Skia
}
}
public SKTypeface SKTypeface { get; }
public Face Face { get; }
public Font Font { get; }
@ -300,12 +306,12 @@ namespace Avalonia.Skia
private static FontStyle GetFontStyle(OS2Table.FontStyleSelection styleSelection)
{
if((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0)
if ((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0)
{
return FontStyle.Italic;
}
if((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0)
if ((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0)
{
return FontStyle.Oblique;
}
@ -315,18 +321,18 @@ namespace Avalonia.Skia
private Blob? GetTable(Face face, Tag tag)
{
var size = _typeface.GetTableSize(tag);
var size = SKTypeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
return _typeface.TryGetTableData(tag, 0, size, data) ?
return SKTypeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
public SKFont CreateSKFont(float size)
=> new(_typeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
=> new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
{
LinearMetrics = true,
Embolden = (FontSimulations & FontSimulations.Bold) != 0
@ -348,7 +354,7 @@ namespace Avalonia.Skia
Font.Dispose();
Face.Dispose();
_typeface.Dispose();
SKTypeface.Dispose();
}
public void Dispose()
@ -359,14 +365,14 @@ namespace Avalonia.Skia
public bool TryGetTable(uint tag, out byte[] table)
{
return _typeface.TryGetTableData(tag, out table);
return SKTypeface.TryGetTableData(tag, out table);
}
public bool TryGetStream([NotNullWhen(true)] out Stream? stream)
{
try
{
var asset = _typeface.OpenStream();
var asset = SKTypeface.OpenStream();
var size = asset.Length;
var buffer = new byte[size];

60
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -1,8 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
@ -70,7 +70,7 @@ namespace Avalonia.Skia
{
return new SKPoint((float)p.X, (float)p.Y);
}
public static SKPoint ToSKPoint(this Vector p)
{
return new SKPoint((float)p.X, (float)p.Y);
@ -80,17 +80,17 @@ namespace Avalonia.Skia
{
return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
}
internal static SKRect ToSKRect(this LtrbRect r)
{
return new SKRect((float)r.Left, (float)r.Right, (float)r.Right, (float)r.Bottom);
}
public static SKRectI ToSKRectI(this PixelRect r)
{
return new SKRectI(r.X, r.Y, r.Right, r.Bottom);
}
internal static SKRectI ToSKRectI(this LtrbPixelRect r)
{
return new SKRectI(r.Left, r.Top, r.Right, r.Bottom);
@ -106,7 +106,7 @@ namespace Avalonia.Skia
{
r.RadiiTopLeft.ToSKPoint(), r.RadiiTopRight.ToSKPoint(),
r.RadiiBottomRight.ToSKPoint(), r.RadiiBottomLeft.ToSKPoint(),
});
});
return result;
}
@ -115,17 +115,17 @@ namespace Avalonia.Skia
{
return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
}
internal static LtrbRect ToAvaloniaLtrbRect(this SKRect r)
{
return new LtrbRect(r.Left, r.Top, r.Right, r.Bottom);
}
public static PixelRect ToAvaloniaPixelRect(this SKRectI r)
{
return new PixelRect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
}
internal static LtrbPixelRect ToAvaloniaLtrbPixelRect(this SKRectI r)
{
return new LtrbPixelRect(r.Left, r.Top, r.Right, r.Bottom);
@ -173,7 +173,7 @@ namespace Avalonia.Skia
return sm;
}
internal static Matrix ToAvaloniaMatrix(this SKMatrix m) => new(
m.ScaleX, m.SkewY, m.Persp0,
m.SkewX, m.ScaleY, m.Persp1,
@ -253,9 +253,12 @@ namespace Avalonia.Skia
switch (m)
{
default:
case GradientSpreadMethod.Pad: return SKShaderTileMode.Clamp;
case GradientSpreadMethod.Reflect: return SKShaderTileMode.Mirror;
case GradientSpreadMethod.Repeat: return SKShaderTileMode.Repeat;
case GradientSpreadMethod.Pad:
return SKShaderTileMode.Clamp;
case GradientSpreadMethod.Reflect:
return SKShaderTileMode.Mirror;
case GradientSpreadMethod.Repeat:
return SKShaderTileMode.Repeat;
}
}
@ -264,9 +267,12 @@ namespace Avalonia.Skia
switch (a)
{
default:
case TextAlignment.Left: return SKTextAlign.Left;
case TextAlignment.Center: return SKTextAlign.Center;
case TextAlignment.Right: return SKTextAlign.Right;
case TextAlignment.Left:
return SKTextAlign.Left;
case TextAlignment.Center:
return SKTextAlign.Center;
case TextAlignment.Right:
return SKTextAlign.Right;
}
}
@ -295,9 +301,12 @@ namespace Avalonia.Skia
switch (a)
{
default:
case SKTextAlign.Left: return TextAlignment.Left;
case SKTextAlign.Center: return TextAlignment.Center;
case SKTextAlign.Right: return TextAlignment.Right;
case SKTextAlign.Left:
return TextAlignment.Left;
case SKTextAlign.Center:
return TextAlignment.Center;
case SKTextAlign.Right:
return TextAlignment.Right;
}
}
@ -308,7 +317,18 @@ namespace Avalonia.Skia
SKFontStyleSlant.Upright => FontStyle.Normal,
SKFontStyleSlant.Italic => FontStyle.Italic,
SKFontStyleSlant.Oblique => FontStyle.Oblique,
_ => throw new ArgumentOutOfRangeException(nameof (slant), slant, null)
_ => throw new ArgumentOutOfRangeException(nameof(slant), slant, null)
};
}
public static SKFontStyleSlant ToSkia(this FontStyle style)
{
return style switch
{
FontStyle.Normal => SKFontStyleSlant.Upright,
FontStyle.Italic => SKFontStyleSlant.Italic,
FontStyle.Oblique => SKFontStyleSlant.Oblique,
_ => throw new ArgumentOutOfRangeException(nameof(style), style, null)
};
}

6
src/Windows/Avalonia.Win32/OleDataObjectHelper.cs

@ -29,15 +29,15 @@ internal static class OleDataObjectHelper
tymed = TYMED.TYMED_HGLOBAL
};
public static unsafe object? TryGet(this Win32Com.IDataObject _oleDataObject, DataFormat format)
public static unsafe object? TryGet(this Win32Com.IDataObject oleDataObject, DataFormat format)
{
var formatEtc = format.ToFormatEtc();
if (_oleDataObject.QueryGetData(&formatEtc) != (uint)HRESULT.S_OK)
if (oleDataObject.QueryGetData(&formatEtc) != (uint)HRESULT.S_OK)
return null;
var medium = new STGMEDIUM();
if (_oleDataObject.GetData(&formatEtc, &medium) != (uint)HRESULT.S_OK)
if (oleDataObject.GetData(&formatEtc, &medium) != (uint)HRESULT.S_OK)
return null;
try

6
src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs

@ -54,15 +54,21 @@ internal sealed class OleDataObjectToDataTransferWrapper(Win32Com.IDataObject ol
{
List<DataFormat>? nonFileFormats = null;
var items = new List<PlatformDataTransferItem>();
var hasFiles = false;
foreach (var format in Formats)
{
if (DataFormat.File.Equals(format))
{
if (hasFiles)
continue;
// This is not ideal as we're reading the filenames ahead of time to generate the appropriate items.
// However, it's unlikely to be a heavy operation.
if (_oleDataObject.TryGet(format) is IEnumerable<IStorageItem> storageItems)
{
hasFiles = true;
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}

29
tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs

@ -306,6 +306,35 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
[Fact]
public void Should_Apply_Transform_On_Attach_To_VisualTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var transform = new SkewTransform() { AngleX = -45, AngleY = -45 };
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
100,
transform);
transform.AngleX = 45;
transform.AngleY = 45;
var window = new Window { Content = lt };
window.Show();
Matrix actual = lt.TransformRoot.RenderTransform.Value;
Matrix expected = Matrix.CreateSkew(Matrix.ToRadians(45), Matrix.ToRadians(45));
Assert.Equal(expected.M11, actual.M11, 3);
Assert.Equal(expected.M12, actual.M12, 3);
Assert.Equal(expected.M21, actual.M21, 3);
Assert.Equal(expected.M22, actual.M22, 3);
Assert.Equal(expected.M31, actual.M31, 3);
Assert.Equal(expected.M32, actual.M32, 3);
}
}
private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize)
{

49
tests/Avalonia.LeakTests/ControlTests.cs

@ -13,6 +13,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
@ -1000,6 +1001,54 @@ namespace Avalonia.LeakTests
}
}
[Fact]
public void LayoutTransformControl_Is_Freed()
{
using (Start())
{
var transform = new RotateTransform { Angle = 90 };
Func<Window> run = () =>
{
var window = new Window
{
Content = new LayoutTransformControl
{
LayoutTransform = transform,
Child = new Canvas()
}
};
window.Show();
// Do a layout and make sure that LayoutTransformControl gets added to visual tree
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.IsType<LayoutTransformControl>(window.Presenter.Child);
Assert.NotEmpty(window.Presenter.Child.GetVisualChildren());
// Clear the content and ensure the LayoutTransformControl is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<LayoutTransformControl>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
// We are keeping transform alive to simulate a resource that outlives the control.
GC.KeepAlive(transform);
}
}
private FuncControlTemplate CreateWindowTemplate()
{
return new FuncControlTemplate<Window>((parent, scope) =>

47
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Fonts.Inter;
using Avalonia.Headless;
using Avalonia.Media;
@ -103,7 +101,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
Assert.True(FontManager.Current.TryGetGlyphTypeface(Typeface.Default, out _));
for (int i = 0;i < 10; i++)
for (int i = 0; i < 10; i++)
{
FontManager.Current.TryGetGlyphTypeface(new Typeface("Unknown"), out _);
}
@ -313,7 +311,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("微軟正黑體"), out var glyphTypeface));
Assert.Equal("Microsoft JhengHei",glyphTypeface.FamilyName);
Assert.Equal("Microsoft JhengHei", glyphTypeface.FamilyName);
}
}
}
@ -325,7 +323,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
using (AvaloniaLocator.EnterScope())
{
FontManager.Current.AddFontCollection(new InterFontCollection());
FontManager.Current.AddFontCollection(new InterFontCollection());
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("fonts:Inter#Inter"),
out var glyphTypeface));
@ -346,12 +344,12 @@ namespace Avalonia.Skia.UnitTests.Media
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.BindToSelf(new FontManagerOptions
{
DefaultFamilyName = s_fontUri,
FontFamilyMappings = new Dictionary<string, FontFamily>
{
{ "Segoe UI", new FontFamily("fonts:Inter#Inter") }
AvaloniaLocator.CurrentMutable.BindToSelf(new FontManagerOptions
{
DefaultFamilyName = s_fontUri,
FontFamilyMappings = new Dictionary<string, FontFamily>
{
{ "Segoe UI", new FontFamily("fonts:Inter#Inter") }
}
});
@ -428,6 +426,33 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
[Win32Fact("Windows specific font")]
public void Should_Get_Regular_Font_After_Matching_Italic_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
Assert.True(FontManager.Current.TryMatchCharacter('こ', FontStyle.Italic, FontWeight.Normal, FontStretch.Normal, null, null, out var italicTypeface));
Assert.Equal(FontSimulations.None, italicTypeface.GlyphTypeface.FontSimulations);
Assert.Equal("Yu Gothic UI", italicTypeface.GlyphTypeface.FamilyName);
Assert.NotEqual(FontStyle.Normal, italicTypeface.Style);
Assert.True(FontManager.Current.TryMatchCharacter('こ', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var regularTypeface));
Assert.Equal("Yu Gothic UI", regularTypeface.GlyphTypeface.FamilyName);
Assert.Equal(FontStyle.Normal, regularTypeface.Style);
Assert.NotEqual(((GlyphTypefaceImpl)italicTypeface.GlyphTypeface).SKTypeface, ((GlyphTypefaceImpl)regularTypeface.GlyphTypeface).SKTypeface);
}
}
}
[Fact]
public void Should_Fallback_When_Font_Family_Is_Empty()
{

Loading…
Cancel
Save