From d29657340424fafdd80b300e98c4578faffe0c7a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 31 May 2023 11:15:12 +0200 Subject: [PATCH 01/46] fix: Warning CS0436 --- .../Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index f3af312d1a..ae2d3dd847 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -9,8 +9,6 @@ - - From fb9baae0125525856292857cd699a54626039828 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 31 May 2023 10:43:31 +0200 Subject: [PATCH 02/46] Fixed ManagedDispatcherImpl wait --- .../Platform/ManagedDispatcherImpl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index fdc098777a..c8225f775b 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -99,9 +99,15 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl continue; } - if (_nextTimer != null) + TimeSpan? nextTimer; + lock (_lock) + { + nextTimer = _nextTimer; + } + + if (nextTimer != null) { - var waitFor = _clock.Elapsed - _nextTimer.Value; + var waitFor = nextTimer.Value - _clock.Elapsed; if (waitFor.TotalMilliseconds < 1) continue; _wakeup.WaitOne(waitFor); @@ -112,4 +118,4 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl registration.Dispose(); } -} \ No newline at end of file +} From af343bdf027c9b56e8d92ffcafcdb1d3347514b8 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 31 May 2023 18:28:33 +0200 Subject: [PATCH 03/46] Implemented a proper framebuffer for RemoteServerTopLevelImpl --- .../Offscreen/OffscreenTopLevelImpl.cs | 19 +-- .../RemoteServerTopLevelImpl.Framebuffer.cs | 116 +++++++++++++ .../Remote/Server/RemoteServerTopLevelImpl.cs | 154 ++++++++---------- .../Remote/PreviewerWindowImpl.cs | 2 +- .../Remote/PreviewerWindowingPlatform.cs | 2 +- 5 files changed, 190 insertions(+), 103 deletions(-) create mode 100644 src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index f379120638..106ac8dff5 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using Avalonia.Threading; namespace Avalonia.Controls.Embedding.Offscreen { @@ -17,7 +13,6 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; - private ManualRenderTimer _manualRenderTimer = new(); public IInputRoot? InputRoot { get; private set; } public bool IsDisposed { get; private set; } @@ -27,21 +22,10 @@ namespace Avalonia.Controls.Embedding.Offscreen IsDisposed = true; } - class ManualRenderTimer : IRenderTimer - { - static Stopwatch St = Stopwatch.StartNew(); - public event Action? Tick; - public bool RunsInBackground => false; - public void TriggerTick() => Tick?.Invoke(St.Elapsed); - } - public Compositor Compositor { get; } public OffscreenTopLevelImplBase() - { - Compositor = new Compositor(new RenderLoop(_manualRenderTimer), null, false, - MediaContext.Instance, false); - } + => Compositor = new Compositor(null); public abstract IEnumerable Surfaces { get; } @@ -76,7 +60,6 @@ namespace Avalonia.Controls.Embedding.Offscreen public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } - /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs new file mode 100644 index 0000000000..361c3033b5 --- /dev/null +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs @@ -0,0 +1,116 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Remote.Protocol.Viewport; +using PlatformPixelFormat = Avalonia.Platform.PixelFormat; +using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; + +namespace Avalonia.Controls.Remote.Server +{ + internal partial class RemoteServerTopLevelImpl + { + private enum FrameStatus + { + NotRendered, + Rendered, + CopiedToMessage + } + + private sealed class Framebuffer + { + public static Framebuffer Empty { get; } = + new(ProtocolPixelFormat.Rgba8888, default, new Vector(96.0, 96.0)); + + private readonly object _dataLock = new(); + private readonly byte[] _data; // for rendering only + private readonly byte[] _dataCopy; // for messages only + private FrameStatus _status = FrameStatus.NotRendered; + + public Framebuffer(ProtocolPixelFormat format, Size clientSize, Vector dpi) + { + var frameSize = PixelSize.FromSizeWithDpi(clientSize, dpi); + if (frameSize.Width <= 0 || frameSize.Height <= 0) + frameSize = PixelSize.Empty; + + var bpp = format == ProtocolPixelFormat.Rgb565 ? 2 : 4; + var stride = frameSize.Width * bpp; + var dataLength = Math.Max(0, stride * frameSize.Height); + + Format = format; + FrameSize = frameSize; + ClientSize = clientSize; + Dpi = dpi; + + (Stride, _data, _dataCopy) = dataLength > 0 ? + (stride, new byte[dataLength], new byte[dataLength]) : + (0, Array.Empty(), Array.Empty()); + } + + public ProtocolPixelFormat Format { get; } + + public Size ClientSize { get; } + + public Vector Dpi { get; } + + public PixelSize FrameSize { get; } + + public int Stride { get; } + + public FrameStatus GetStatus() + { + lock (_dataLock) + return _status; + } + + public ILockedFramebuffer Lock(Action onUnlocked) + { + var handle = GCHandle.Alloc(_data, GCHandleType.Pinned); + Monitor.Enter(_dataLock); + + try + { + return new LockedFramebuffer( + handle.AddrOfPinnedObject(), + FrameSize, + Stride, + Dpi, + new PlatformPixelFormat((PixelFormatEnum)Format), + () => + { + handle.Free(); + Array.Copy(_data, _dataCopy, _data.Length); + _status = FrameStatus.Rendered; + Monitor.Exit(_dataLock); + onUnlocked(); + }); + } + catch + { + handle.Free(); + Monitor.Exit(_dataLock); + throw; + } + } + + /// The returned message must be kept around, as it contains a shared buffer. + public FrameMessage ToMessage(long sequenceId) + { + lock (_dataLock) + _status = FrameStatus.CopiedToMessage; + + return new FrameMessage + { + SequenceId = sequenceId, + Data = _dataCopy, + Format = Format, + Width = FrameSize.Width, + Height = FrameSize.Height, + Stride = Stride, + DpiX = Dpi.X, + DpiY = Dpi.Y + }; + } + } + } +} diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 8e4123a790..f8fc1957a4 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -11,7 +10,6 @@ using Avalonia.Platform; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Input; using Avalonia.Remote.Protocol.Viewport; -using Avalonia.Rendering; using Avalonia.Threading; using Key = Avalonia.Input.Key; using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; @@ -20,28 +18,29 @@ using ProtocolMouseButton = Avalonia.Remote.Protocol.Input.MouseButton; namespace Avalonia.Controls.Remote.Server { [Unstable] - internal class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl + internal partial class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl { private readonly IAvaloniaRemoteTransportConnection _transport; - private LockedFramebuffer? _framebuffer; private readonly object _lock = new(); + private readonly Action _sendLastFrameIfNeeded; + private readonly Action _renderAndSendFrameIfNeeded; + private Framebuffer _framebuffer = Framebuffer.Empty; private long _lastSentFrame = -1; private long _lastReceivedFrame = -1; private long _nextFrameNumber = 1; private ClientViewportAllocatedMessage? _pendingAllocation; - private bool _queuedNextRender; - private bool _inRender; - private Vector _dpi = new Vector(96, 96); - private ProtocolPixelFormat[]? _supportedFormats; + private Vector _dpi = new(96, 96); + private ProtocolPixelFormat? _format; public RemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) { + _sendLastFrameIfNeeded = SendLastFrameIfNeeded; + _renderAndSendFrameIfNeeded = RenderAndSendFrameIfNeeded; + _transport = transport; _transport.OnMessage += OnMessage; KeyboardDevice = AvaloniaLocator.Current.GetRequiredService(); - QueueNextRender(); - Compositor.AfterCommit += QueueNextRender; } private static RawPointerEventType GetAvaloniaEventType(ProtocolMouseButton button, bool pressed) @@ -118,23 +117,23 @@ namespace Avalonia.Controls.Remote.Server { _lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame); } - Dispatcher.UIThread.Post(RenderIfNeeded); + Dispatcher.UIThread.Post(_sendLastFrameIfNeeded); } - if(obj is ClientRenderInfoMessage renderInfo) + if (obj is ClientRenderInfoMessage renderInfo) { - lock(_lock) + lock (_lock) { _dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY); - _queuedNextRender = true; } - - Dispatcher.UIThread.Post(RenderIfNeeded); + Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); } if (obj is ClientSupportedPixelFormatsMessage supportedFormats) { lock (_lock) - _supportedFormats = supportedFormats.Formats; - Dispatcher.UIThread.Post(RenderIfNeeded); + { + _format = TryGetValidPixelFormat(supportedFormats.Formats); + } + Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); } if (obj is MeasureViewportMessage measure) Dispatcher.UIThread.Post(() => @@ -161,7 +160,7 @@ namespace Avalonia.Controls.Remote.Server } _dpi = new Vector(allocation.DpiX, allocation.DpiY); ClientSize = new Size(allocation.Width, allocation.Height); - RenderIfNeeded(); + RenderAndSendFrameIfNeeded(); }); _pendingAllocation = allocated; @@ -250,10 +249,24 @@ namespace Avalonia.Controls.Remote.Server } } + private static ProtocolPixelFormat? TryGetValidPixelFormat(ProtocolPixelFormat[]? formats) + { + if (formats is not null) + { + foreach (var format in formats) + { + if (format is >= 0 and <= ProtocolPixelFormat.MaxValue) + return format; + } + } + + return null; + } + protected void SetDpi(Vector dpi) { _dpi = dpi; - RenderIfNeeded(); + RenderAndSendFrameIfNeeded(); } protected virtual Size Measure(Size constraint) @@ -265,88 +278,63 @@ namespace Avalonia.Controls.Remote.Server public override IEnumerable Surfaces => new[] { this }; - private FrameMessage RenderFrame(int width, int height, ProtocolPixelFormat? format) + private Framebuffer GetOrCreateFramebuffer() { - var scalingX = _dpi.X / 96.0; - var scalingY = _dpi.Y / 96.0; - - width = (int)(width * scalingX); - height = (int)(height * scalingY); - - var fmt = format ?? ProtocolPixelFormat.Rgba8888; - var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4; - var data = new byte[width * height * bpp]; - var handle = GCHandle.Alloc(data, GCHandleType.Pinned); - - try - { - if (width > 0 && height > 0) - { - _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, new((PixelFormatEnum)fmt), - null); - Paint?.Invoke(new Rect(0, 0, width, height)); - } - } - finally + lock (_lock) { - _framebuffer = null; - handle.Free(); + if (_format is not { } format) + _framebuffer = Framebuffer.Empty; + else if (_framebuffer.Format != format || _framebuffer.ClientSize != ClientSize || _framebuffer.Dpi != _dpi) + _framebuffer = new Framebuffer(format, ClientSize, _dpi); + + return _framebuffer; } - return new FrameMessage - { - Data = data, - Format = fmt, - Width = width, - Height = height, - Stride = width * bpp, - DpiX = _dpi.X, - DpiY = _dpi.Y - }; } public ILockedFramebuffer Lock() - { - if (_framebuffer == null) - throw new InvalidOperationException("Paint was not requested, wait for Paint event"); - return _framebuffer; - } + => GetOrCreateFramebuffer().Lock(_sendLastFrameIfNeeded); - protected void RenderIfNeeded() + private void SendLastFrameIfNeeded() { - lock (_lock) - { - if (_lastReceivedFrame != _lastSentFrame || !_queuedNextRender || _supportedFormats == null) - return; + if (IsDisposed) + return; - } - - var format = ProtocolPixelFormat.Rgba8888; - foreach(var fmt in _supportedFormats) - if (fmt <= ProtocolPixelFormat.MaxValue) - { - format = fmt; - break; - } + Framebuffer framebuffer; + long sequenceId; - _inRender = true; - var frame = RenderFrame((int) ClientSize.Width, (int) ClientSize.Height, format); lock (_lock) { + // Ideally we should only send a frame if its status is Rendered: since the renderer might not be + // initialized at the start, we're sending black frames in this case. However, this was the historical + // behavior and some external programs are depending on receiving a frame asap. + if (_lastReceivedFrame != _lastSentFrame || _framebuffer.GetStatus() == FrameStatus.CopiedToMessage) + return; + + framebuffer = _framebuffer; _lastSentFrame = _nextFrameNumber++; - frame.SequenceId = _lastSentFrame; - _queuedNextRender = false; + sequenceId = _lastSentFrame; } - _inRender = false; - _transport.Send(frame); + + _transport.Send(framebuffer.ToMessage(sequenceId)); } - private void QueueNextRender() + protected void RenderAndSendFrameIfNeeded() { - if (!_inRender && !IsDisposed) + if (IsDisposed) + return; + + lock (_lock) { - _queuedNextRender = true; - DispatcherTimer.RunOnce(RenderIfNeeded, TimeSpan.FromMilliseconds(2), DispatcherPriority.Background); + if (_lastReceivedFrame != _lastSentFrame || _format is null) + return; } + + var framebuffer = GetOrCreateFramebuffer(); + + if (framebuffer.Stride > 0) + Paint?.Invoke(new Rect(framebuffer.ClientSize)); + + SendLastFrameIfNeeded(); } public override IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index e0fcf8e530..5fe9f203e7 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.DesignerSupport.Remote Height = clientSize.Height }); ClientSize = clientSize; - RenderIfNeeded(); + RenderAndSendFrameIfNeeded(); } public void Move(PixelPoint point) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index d2787e73ae..ba9dd592ce 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -52,7 +52,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(Keyboard) .Bind().ToSingleton() .Bind().ToConstant(new ManagedDispatcherImpl(null)) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new UiThreadRenderTimer(60)) .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton(); From c45d49942bc87831b7242bf49901117d4bb38fef Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 31 May 2023 19:22:13 +0200 Subject: [PATCH 04/46] RemoteServerTopLevelImpl cleanup --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 221 +++++++++--------- 1 file changed, 108 insertions(+), 113 deletions(-) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index f8fc1957a4..632e7fa8a3 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -111,45 +111,38 @@ namespace Avalonia.Controls.Remote.Server { lock (_lock) { - if (obj is FrameReceivedMessage lastFrame) + switch (obj) { - lock (_lock) - { + case FrameReceivedMessage lastFrame: _lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame); - } - Dispatcher.UIThread.Post(_sendLastFrameIfNeeded); - } - if (obj is ClientRenderInfoMessage renderInfo) - { - lock (_lock) - { + Dispatcher.UIThread.Post(_sendLastFrameIfNeeded); + break; + + case ClientRenderInfoMessage renderInfo: _dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY); - } - Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); - } - if (obj is ClientSupportedPixelFormatsMessage supportedFormats) - { - lock (_lock) - { + Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); + break; + + case ClientSupportedPixelFormatsMessage supportedFormats: _format = TryGetValidPixelFormat(supportedFormats.Formats); - } - Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); - } - if (obj is MeasureViewportMessage measure) - Dispatcher.UIThread.Post(() => - { - var m = Measure(new Size(measure.Width, measure.Height)); - _transport.Send(new MeasureViewportMessage + Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); + break; + + case MeasureViewportMessage measure: + Dispatcher.UIThread.Post(() => { - Width = m.Width, - Height = m.Height + var m = Measure(new Size(measure.Width, measure.Height)); + _transport.Send(new MeasureViewportMessage + { + Width = m.Width, + Height = m.Height + }); }); - }); - if (obj is ClientViewportAllocatedMessage allocated) - { - lock (_lock) - { + break; + + case ClientViewportAllocatedMessage allocated: if (_pendingAllocation == null) + { Dispatcher.UIThread.Post(() => { ClientViewportAllocatedMessage allocation; @@ -158,93 +151,95 @@ namespace Avalonia.Controls.Remote.Server allocation = _pendingAllocation!; _pendingAllocation = null; } + _dpi = new Vector(allocation.DpiX, allocation.DpiY); ClientSize = new Size(allocation.Width, allocation.Height); RenderAndSendFrameIfNeeded(); }); + } _pendingAllocation = allocated; - } - } - if(obj is PointerMovedEventMessage pointer) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawPointerEventArgs( - MouseDevice, - 0, - InputRoot!, - RawPointerEventType.Move, - new Point(pointer.X, pointer.Y), - GetAvaloniaRawInputModifiers(pointer.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is PointerPressedEventMessage pressed) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawPointerEventArgs( - MouseDevice, - 0, - InputRoot!, - GetAvaloniaEventType(pressed.Button, true), - new Point(pressed.X, pressed.Y), - GetAvaloniaRawInputModifiers(pressed.Modifiers))); - }, DispatcherPriority.Input); - } - if (obj is PointerReleasedEventMessage released) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawPointerEventArgs( - MouseDevice, - 0, - InputRoot!, - GetAvaloniaEventType(released.Button, false), - new Point(released.X, released.Y), - GetAvaloniaRawInputModifiers(released.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is ScrollEventMessage scroll) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawMouseWheelEventArgs( - MouseDevice, - 0, - InputRoot!, - new Point(scroll.X, scroll.Y), - new Vector(scroll.DeltaX, scroll.DeltaY), - GetAvaloniaRawInputModifiers(scroll.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is KeyEventMessage key) - { - Dispatcher.UIThread.Post(() => - { - Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - - Input?.Invoke(new RawKeyEventArgs( - KeyboardDevice, - 0, - InputRoot!, - key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - (Key)key.Key, - GetAvaloniaRawInputModifiers(key.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is TextInputEventMessage text) - { - Dispatcher.UIThread.Post(() => - { - Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - - Input?.Invoke(new RawTextInputEventArgs( - KeyboardDevice, - 0, - InputRoot!, - text.Text)); - }, DispatcherPriority.Input); + break; + + case PointerMovedEventMessage pointer: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawPointerEventArgs( + MouseDevice, + 0, + InputRoot!, + RawPointerEventType.Move, + new Point(pointer.X, pointer.Y), + GetAvaloniaRawInputModifiers(pointer.Modifiers))); + }, DispatcherPriority.Input); + break; + + case PointerPressedEventMessage pressed: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawPointerEventArgs( + MouseDevice, + 0, + InputRoot!, + GetAvaloniaEventType(pressed.Button, true), + new Point(pressed.X, pressed.Y), + GetAvaloniaRawInputModifiers(pressed.Modifiers))); + }, DispatcherPriority.Input); + break; + + case PointerReleasedEventMessage released: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawPointerEventArgs( + MouseDevice, + 0, + InputRoot!, + GetAvaloniaEventType(released.Button, false), + new Point(released.X, released.Y), + GetAvaloniaRawInputModifiers(released.Modifiers))); + }, DispatcherPriority.Input); + break; + + case ScrollEventMessage scroll: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawMouseWheelEventArgs( + MouseDevice, + 0, + InputRoot!, + new Point(scroll.X, scroll.Y), + new Vector(scroll.DeltaX, scroll.DeltaY), + GetAvaloniaRawInputModifiers(scroll.Modifiers))); + }, DispatcherPriority.Input); + break; + + case KeyEventMessage key: + Dispatcher.UIThread.Post(() => + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + + Input?.Invoke(new RawKeyEventArgs( + KeyboardDevice, + 0, + InputRoot!, + key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + (Key)key.Key, + GetAvaloniaRawInputModifiers(key.Modifiers))); + }, DispatcherPriority.Input); + break; + + case TextInputEventMessage text: + Dispatcher.UIThread.Post(() => + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + + Input?.Invoke(new RawTextInputEventArgs( + KeyboardDevice, + 0, + InputRoot!, + text.Text)); + }, DispatcherPriority.Input); + break; } } } From 4adf0b68cb32e4c968f006bc08d3179e6134a6b7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 31 May 2023 23:17:30 +0200 Subject: [PATCH 05/46] Fixed previewer render scaling --- .../RemoteServerTopLevelImpl.Framebuffer.cs | 32 +++++++++---------- .../Remote/Server/RemoteServerTopLevelImpl.cs | 20 +++++------- .../Remote/PreviewerWindowImpl.cs | 6 +++- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs index 361c3033b5..593adfb225 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs @@ -19,17 +19,18 @@ namespace Avalonia.Controls.Remote.Server private sealed class Framebuffer { - public static Framebuffer Empty { get; } = - new(ProtocolPixelFormat.Rgba8888, default, new Vector(96.0, 96.0)); + public static Framebuffer Empty { get; } = new(ProtocolPixelFormat.Rgba8888, default, 1.0); + private readonly double _dpi; + private readonly PixelSize _frameSize; private readonly object _dataLock = new(); private readonly byte[] _data; // for rendering only private readonly byte[] _dataCopy; // for messages only private FrameStatus _status = FrameStatus.NotRendered; - public Framebuffer(ProtocolPixelFormat format, Size clientSize, Vector dpi) + public Framebuffer(ProtocolPixelFormat format, Size clientSize, double renderScaling) { - var frameSize = PixelSize.FromSizeWithDpi(clientSize, dpi); + var frameSize = PixelSize.FromSize(clientSize, renderScaling); if (frameSize.Width <= 0 || frameSize.Height <= 0) frameSize = PixelSize.Empty; @@ -37,10 +38,11 @@ namespace Avalonia.Controls.Remote.Server var stride = frameSize.Width * bpp; var dataLength = Math.Max(0, stride * frameSize.Height); + _dpi = renderScaling * 96.0; + _frameSize = frameSize; Format = format; - FrameSize = frameSize; ClientSize = clientSize; - Dpi = dpi; + RenderScaling = renderScaling; (Stride, _data, _dataCopy) = dataLength > 0 ? (stride, new byte[dataLength], new byte[dataLength]) : @@ -51,9 +53,7 @@ namespace Avalonia.Controls.Remote.Server public Size ClientSize { get; } - public Vector Dpi { get; } - - public PixelSize FrameSize { get; } + public double RenderScaling { get; } public int Stride { get; } @@ -72,9 +72,9 @@ namespace Avalonia.Controls.Remote.Server { return new LockedFramebuffer( handle.AddrOfPinnedObject(), - FrameSize, + _frameSize, Stride, - Dpi, + new Vector(_dpi, _dpi), new PlatformPixelFormat((PixelFormatEnum)Format), () => { @@ -93,7 +93,7 @@ namespace Avalonia.Controls.Remote.Server } } - /// The returned message must be kept around, as it contains a shared buffer. + /// The returned message must NOT be kept around, as it contains a shared buffer. public FrameMessage ToMessage(long sequenceId) { lock (_dataLock) @@ -104,11 +104,11 @@ namespace Avalonia.Controls.Remote.Server SequenceId = sequenceId, Data = _dataCopy, Format = Format, - Width = FrameSize.Width, - Height = FrameSize.Height, + Width = _frameSize.Width, + Height = _frameSize.Height, Stride = Stride, - DpiX = Dpi.X, - DpiY = Dpi.Y + DpiX = _dpi, + DpiY = _dpi }; } } diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 632e7fa8a3..49af6a71a0 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -29,7 +29,6 @@ namespace Avalonia.Controls.Remote.Server private long _lastReceivedFrame = -1; private long _nextFrameNumber = 1; private ClientViewportAllocatedMessage? _pendingAllocation; - private Vector _dpi = new(96, 96); private ProtocolPixelFormat? _format; public RemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) @@ -119,8 +118,11 @@ namespace Avalonia.Controls.Remote.Server break; case ClientRenderInfoMessage renderInfo: - _dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY); - Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); + Dispatcher.UIThread.Post(() => + { + RenderScaling = renderInfo.DpiX / 96.0; + RenderAndSendFrameIfNeeded(); + }); break; case ClientSupportedPixelFormatsMessage supportedFormats: @@ -152,7 +154,7 @@ namespace Avalonia.Controls.Remote.Server _pendingAllocation = null; } - _dpi = new Vector(allocation.DpiX, allocation.DpiY); + RenderScaling = allocation.DpiX / 96.0; ClientSize = new Size(allocation.Width, allocation.Height); RenderAndSendFrameIfNeeded(); }); @@ -258,12 +260,6 @@ namespace Avalonia.Controls.Remote.Server return null; } - protected void SetDpi(Vector dpi) - { - _dpi = dpi; - RenderAndSendFrameIfNeeded(); - } - protected virtual Size Measure(Size constraint) { var l = (Layoutable) InputRoot!; @@ -279,8 +275,8 @@ namespace Avalonia.Controls.Remote.Server { if (_format is not { } format) _framebuffer = Framebuffer.Empty; - else if (_framebuffer.Format != format || _framebuffer.ClientSize != ClientSize || _framebuffer.Dpi != _dpi) - _framebuffer = new Framebuffer(format, ClientSize, _dpi); + else if (_framebuffer.Format != format || _framebuffer.ClientSize != ClientSize || _framebuffer.RenderScaling != RenderScaling) + _framebuffer = new Framebuffer(format, ClientSize, RenderScaling); return _framebuffer; } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 5fe9f203e7..b6c0c3ae3d 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -53,7 +53,11 @@ namespace Avalonia.DesignerSupport.Remote // In previewer mode we completely ignore client-side viewport size if (obj is ClientViewportAllocatedMessage alloc) { - Dispatcher.UIThread.Post(() => SetDpi(new Vector(alloc.DpiX, alloc.DpiY))); + Dispatcher.UIThread.Post(() => + { + RenderScaling = alloc.DpiX / 96.0; + RenderAndSendFrameIfNeeded(); + }); return; } base.OnMessage(transport, obj); From bfdd3fa325880fa88bce9047b817ab2eb2435e60 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 1 Jun 2023 07:13:36 +0200 Subject: [PATCH 06/46] Prevent infinite loop --- samples/Sandbox/MainWindow.axaml | 1 + .../Media/TextFormatting/TextLineImpl.cs | 22 +++++++++------ .../Media/TextFormatting/TextLineTests.cs | 28 ++++++++++++++++++- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..f0f099b95b 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,5 @@ + diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index c2ec78e187..0b1bd2e189 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -371,14 +371,16 @@ namespace Avalonia.Media.TextFormatting IndexedTextRun currentIndexedRun = _indexedTextRuns[i]; - while(currentIndexedRun.TextSourceCharacterIndex != currentPosition) + while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) { - if(i + 1 < _indexedTextRuns.Count) + if (i + 1 == _indexedTextRuns.Count) { - i++; - - currentIndexedRun = _indexedTextRuns[i]; + break; } + + i++; + + currentIndexedRun = _indexedTextRuns[i]; } return currentIndexedRun; @@ -604,12 +606,14 @@ namespace Avalonia.Media.TextFormatting while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) { - if (i + 1 < _indexedTextRuns.Count) + if (i + 1 == _indexedTextRuns.Count) { - i++; - - currentIndexedRun = _indexedTextRuns[i]; + break; } + + i++; + + currentIndexedRun = _indexedTextRuns[i]; } return currentIndexedRun; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 1d07e780e6..6373fb4e91 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -9,6 +9,7 @@ using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; using Avalonia.Utilities; using Xunit; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { @@ -1072,7 +1073,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } [Fact] - public void Should_GetTextBounds_BiDi() + public void Should_GetTextBounds_Bidi() { var text = "אבגדה 12345 ABCDEF אבגדה"; @@ -1120,6 +1121,31 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_Bidi_2() + { + var text = "אבג ABC אבג 123"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + var bounds = textLine.GetTextBounds(0, text.Length); + + Assert.Equal(5, bounds.Count); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, bounds.Last().Rectangle.Right); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From 3d9ef67c493144dbacb4347c487bce36ee77e5dd Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 1 Jun 2023 15:42:51 +0600 Subject: [PATCH 07/46] Fixed _requestedCommits enumeration --- src/Avalonia.Base/Media/MediaContext.Compositor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index 9bdd77960d..4ddc2ea9eb 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Rendering.Composition; @@ -78,7 +79,7 @@ partial class MediaContext // Nothing to do, and there are no pending commits return false; - foreach (var c in _requestedCommits) + foreach (var c in _requestedCommits.ToArray()) CommitCompositor(c); _requestedCommits.Clear(); From 685682c5dba9ae69afd8a36215ad5ebd013b037b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 1 Jun 2023 15:43:15 +0600 Subject: [PATCH 08/46] Fixed race condition in Compositor.Commit --- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index dde9dcd6fb..5838811e9e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -130,7 +130,7 @@ namespace Avalonia.Rendering.Composition Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); - _nextCommit ??= new(); + var commit = _nextCommit ??= new(); (_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead); while (_invokeBeforeCommitRead.Count > 0) @@ -188,7 +188,7 @@ namespace Avalonia.Rendering.Composition }, TaskContinuationOptions.ExecuteSynchronously); _nextCommit = null; - return _pendingBatch; + return commit; } } From 7808da21603f63e8d3974cc701bae3a758cd966b Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 2 Jun 2023 12:25:56 +0800 Subject: [PATCH 09/46] feat: attach transitions after ToggleSwitch loaded. --- src/Avalonia.Controls/ToggleSwitch.cs | 34 +++++++++++++++++-- .../Controls/ToggleSwitch.xaml | 21 ++++++------ .../Controls/ToggleSwitch.xaml | 18 +++++++--- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index a68a022e67..989c3cb261 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Metadata; +using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -42,6 +43,10 @@ namespace Avalonia.Controls x.UpdateKnobPos(x.IsChecked.Value); } }); + KnobTransitionsProperty.Changed.AddClassHandler((x, e) => + { + x.AssignTransitions(); + }); } /// @@ -68,6 +73,8 @@ namespace Avalonia.Controls public static readonly StyledProperty OnContentTemplateProperty = AvaloniaProperty.Register(nameof(OnContentTemplate)); + public static readonly StyledProperty KnobTransitionsProperty = AvaloniaProperty.Register(nameof(KnobTransitions)); + /// /// Gets or Sets the Content that is displayed when in the On State. /// @@ -116,6 +123,14 @@ namespace Avalonia.Controls set { SetValue(OnContentTemplateProperty, value); } } + public Transitions KnobTransitions + { + get { return GetValue(KnobTransitionsProperty); } + set { SetValue(KnobTransitionsProperty, value); } + } + + + private void OffContentChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) @@ -176,8 +191,23 @@ namespace Avalonia.Controls { UpdateKnobPos(IsChecked.Value); } + } - + + protected override void OnLoaded() + { + base.OnLoaded(); + AssignTransitions(); + } + + private void AssignTransitions() + { + if (_knobsPanel != null) + { + _knobsPanel.Transitions = KnobTransitions; + } + } + private void KnobsPanel_PointerPressed(object? sender, Input.PointerPressedEventArgs e) { _switchStartPoint = e.GetPosition(_switchKnob); diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml index 47d7953096..63c11f6ff8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml @@ -28,6 +28,14 @@ + + + + + @@ -134,16 +142,9 @@ - - diff --git a/src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml index a51aeb9ee6..1178c96419 100644 --- a/src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml @@ -46,6 +46,14 @@ + + + + + - - - - - + + + - - - - - - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml.cs new file mode 100644 index 0000000000..c75da84519 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml.cs @@ -0,0 +1,131 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Diagnostics.Controls +{ + [TemplatePart("PART_ClearButton", typeof(Button))] + partial class BrushEditor : TemplatedControl + { + private readonly EventHandler clearHandler; + private Button? _cleraButton = default; + private readonly ColorView _colorView = new() + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + }; + + + public BrushEditor() + { + FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = _colorView }); + _colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); + clearHandler = (s, e) => Brush = default; + } + + protected override Type StyleKeyOverride => typeof(BrushEditor); + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + if (_cleraButton is not null) + { + _cleraButton.Click -= clearHandler; + } + _cleraButton = e.NameScope.Find /// The type of the property change sender. - /// /// The type of the property.. + /// /// The type of the property. /// The property changed observable. /// /// The method to call. The parameters are the sender and the event args. diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 6f62c3be1d..57fedb3d69 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -18,7 +18,7 @@ namespace Avalonia.Platform /// Creates an ellipse geometry implementation. /// /// The bounds of the ellipse. - /// An ellipse geometry.. + /// An ellipse geometry. IGeometryImpl CreateEllipseGeometry(Rect rect); /// diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 3a82bf02e0..7dbb0872f5 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -306,7 +306,7 @@ namespace Avalonia.Utilities /// if the value could not be converted. /// /// The value to convert. - /// The type to convert to.. + /// The type to convert to. /// The culture to use. /// A value of . [RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)] From 7548238388fce993e66b27d3542a4130875484e4 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 13 Jun 2023 21:21:56 -0400 Subject: [PATCH 45/46] Fix EllipseGeometry clone method --- src/Avalonia.Base/Media/EllipseGeometry.cs | 41 +++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/EllipseGeometry.cs b/src/Avalonia.Base/Media/EllipseGeometry.cs index 8211855324..84d74e888e 100644 --- a/src/Avalonia.Base/Media/EllipseGeometry.cs +++ b/src/Avalonia.Base/Media/EllipseGeometry.cs @@ -56,6 +56,10 @@ namespace Avalonia.Media /// /// Gets or sets a rect that defines the bounds of the ellipse. /// + /// + /// When set, this takes priority over the other properties that define an + /// ellipse using a center point and X/Y-axis radii. + /// public Rect Rect { get => GetValue(RectProperty); @@ -65,6 +69,10 @@ namespace Avalonia.Media /// /// Gets or sets a double that defines the radius in the X-axis of the ellipse. /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// public double RadiusX { get => GetValue(RadiusXProperty); @@ -74,6 +82,10 @@ namespace Avalonia.Media /// /// Gets or sets a double that defines the radius in the Y-axis of the ellipse. /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// public double RadiusY { get => GetValue(RadiusYProperty); @@ -83,6 +95,10 @@ namespace Avalonia.Media /// /// Gets or sets a point that defines the center of the ellipse. /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// public Point Center { get => GetValue(CenterProperty); @@ -92,7 +108,30 @@ namespace Avalonia.Media /// public override Geometry Clone() { - return new EllipseGeometry(Rect); + // Note that the ellipse properties are used in two modes: + // + // 1. Rect-only Mode: + // Directly set the rectangle bounds the ellipse will fill + // + // 2. Center + Radii Mode: + // Set a center-point and then X/Y-axis radii that are used to + // calculate the rectangle bounds the ellipse will fill. + // This is the only mode supported by WPF. + // + // Rendering the ellipse will only ever use one of these two modes + // based on if the Rect property is set (not equal to default). + // + // This means it would normally be fine to copy ONLY the Rect property + // when it is set. However, while it would render the same, it isn't + // a true clone. We want to include all the properties here regardless + // of the rendering mode that will eventually be used. + return new EllipseGeometry() + { + Rect = Rect, + RadiusX = RadiusX, + RadiusY = RadiusY, + Center = Center, + }; } /// From 36ea4a69c836a07b74df6970df656cdf67330385 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 13 Jun 2023 21:43:02 -0400 Subject: [PATCH 46/46] Comment syntax fixes --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 2 +- src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs | 2 +- .../Primitives/PopupPositioning/IPopupPositioner.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index af64e5646f..b3f41eb420 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -334,7 +334,7 @@ namespace Avalonia /// . /// /// The type of the property change sender. - /// /// The type of the property. + /// The type of the property. /// The property changed observable. /// /// The method to call. The parameters are the sender and the event args. diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 3406432ce7..f418d4e14a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -687,7 +687,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// This method resolves the sos and eos values for the run /// and adds the run to the list - /// /// + /// /// The index of the start of the run (in x9 removed units) /// The length of the run (in x9 removed units) /// The level of the run diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 0c9bb89caa..4029782772 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -216,7 +216,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// /// If the adjusted position also ends up being constrained, the resulting position of the /// FlipX adjustment will be the one before the adjustment. - /// /// + /// FlipX = 4, ///