From c8f029386d88a62a9403c98adb52e519a5529ac3 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 14:01:35 +0300 Subject: [PATCH 001/481] Map devices, add some native api --- src/Avalonia.Input/IPenDevice.cs | 10 + src/Avalonia.Input/ITouchPadDevice.cs | 10 + src/Avalonia.Input/PenDevice.cs | 488 ++++++++++++++++++ src/Avalonia.Input/TouchPadDevice.cs | 488 ++++++++++++++++++ .../Interop/UnmanagedMethods.cs | 64 +++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 155 ++++++ 6 files changed, 1215 insertions(+) create mode 100644 src/Avalonia.Input/IPenDevice.cs create mode 100644 src/Avalonia.Input/ITouchPadDevice.cs create mode 100644 src/Avalonia.Input/PenDevice.cs create mode 100644 src/Avalonia.Input/TouchPadDevice.cs diff --git a/src/Avalonia.Input/IPenDevice.cs b/src/Avalonia.Input/IPenDevice.cs new file mode 100644 index 0000000000..1cc0fcf76d --- /dev/null +++ b/src/Avalonia.Input/IPenDevice.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Input +{ + /// + /// Represents a pen/stylus device. + /// + public interface IPenDevice : IPointerDevice + { + + } +} diff --git a/src/Avalonia.Input/ITouchPadDevice.cs b/src/Avalonia.Input/ITouchPadDevice.cs new file mode 100644 index 0000000000..ea6c57f948 --- /dev/null +++ b/src/Avalonia.Input/ITouchPadDevice.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Input +{ + /// + /// Represents a touch pad device. + /// + public interface ITouchPadDevice : IPointerDevice + { + + } +} diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs new file mode 100644 index 0000000000..dd3d60ebb9 --- /dev/null +++ b/src/Avalonia.Input/PenDevice.cs @@ -0,0 +1,488 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Represents a pen/stylus device. + /// + public class PenDevice : IPenDevice, IDisposable + { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + + private readonly Pointer _pointer; + private bool _disposed; + private PixelPoint? _position; + + public PenDevice(Pointer? pointer = null) + { + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + } + + /// + /// Gets the control that is currently capturing by the mouse, if any. + /// + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. To set the mouse capture, call the + /// method. + /// + [Obsolete("Use IPointer instead")] + public IInputElement? Captured => _pointer.Captured; + + /// + /// Gets the mouse position, in screen coordinates. + /// + [Obsolete("Use events instead")] + public PixelPoint Position + { + get => _position ?? new PixelPoint(-1, -1); + protected set => _position = value; + } + + /// + /// Captures mouse input to the specified control. + /// + /// The control. + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. The current mouse capture control is exposed + /// by the property. + /// + public void Capture(IInputElement? control) + { + _pointer.Capture(control); + } + + /// + /// Gets the mouse position relative to a control. + /// + /// The control. + /// The mouse position in the control's coordinates. + public Point GetPosition(IVisual relativeTo) + { + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); + + if (relativeTo.VisualRoot == null) + { + throw new InvalidOperationException("Control is not attached to visual tree."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete + var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); + return rootPoint * transform!.Value; + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawPointerEventArgs margs) + ProcessRawEvent(margs); + } + + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + + public void SceneInvalidated(IInputRoot root, Rect rect) + { + // Pointer is outside of the target area + if (_position == null ) + { + if (root.PointerOverElement != null) + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + return; + } + + + var clientPoint = root.PointToClient(_position.Value); + + if (rect.Contains(clientPoint)) + { + if (_pointer.Captured == null) + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, + PointerPointProperties.None, KeyModifiers.None); + } + else + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, + PointerPointProperties.None, KeyModifiers.None); + } + } + } + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; + return rv; + } + + private void ProcessRawEvent(RawPointerEventArgs e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + var mouse = (PenDevice)e.Device; + if(mouse._disposed) + return; + + _position = e.Root.PointToScreen(e.Position); + var props = CreateProperties(e); + var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + switch (e.Type) + { + case RawPointerEventType.LeaveWindow: + LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + break; + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, keyModifiers); + break; + case RawPointerEventType.LeftButtonUp: + case RawPointerEventType.RightButtonUp: + case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Move: + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Wheel: + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + break; + } + } + + private void LeaveWindow(IPenDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + _position = null; + ClearPointerOver(this, timestamp, root, properties, inputModifiers); + } + + + PointerPointProperties CreateProperties(RawPointerEventArgs args) + { + + var kind = PointerUpdateKind.Other; + + if (args.Type == RawPointerEventType.LeftButtonDown) + kind = PointerUpdateKind.LeftButtonPressed; + if (args.Type == RawPointerEventType.MiddleButtonDown) + kind = PointerUpdateKind.MiddleButtonPressed; + if (args.Type == RawPointerEventType.RightButtonDown) + kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; + if (args.Type == RawPointerEventType.LeftButtonUp) + kind = PointerUpdateKind.LeftButtonReleased; + if (args.Type == RawPointerEventType.MiddleButtonUp) + kind = PointerUpdateKind.MiddleButtonReleased; + if (args.Type == RawPointerEventType.RightButtonUp) + kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; + + return new PointerPointProperties(args.InputModifiers, kind); + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + _pointer.Capture(hit); + var source = GetSource(hit); + if (source != null) + { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; + } + } + + return false; + } + + private bool MouseMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + IInputElement? source; + + if (_pointer.Captured == null) + { + source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); + } + else + { + SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); + source = _pointer.Captured; + } + + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); + + source.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool MouseUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); + + source?.RaiseEvent(e); + _pointer.Capture(null); + return e.Handled; + } + + return false; + } + + private bool MouseWheel(IPenDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private IInteractive? GetSource(IVisual? hit) + { + if (hit is null) + return null; + + return _pointer.Captured ?? + (hit as IInteractive) ?? + hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + } + + private IInputElement? HitTest(IInputElement root, Point p) + { + root = root ?? throw new ArgumentNullException(nameof(root)); + + return _pointer.Captured ?? root.InputHitTest(p); + } + + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, _pointer, null, default, + timestamp, properties, inputModifiers); + } + + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.PointerOverElement; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); + + if (element!=null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + ClearChildrenPointerOver(e, root,true); + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if(clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.InputHitTest(p); + + if (element != root.PointerOverElement) + { + if (element != null) + { + SetPointerOver(device, timestamp, root, element, properties, inputModifiers); + } + else + { + ClearPointerOver(device, timestamp, root, properties, inputModifiers); + } + } + + return element; + } + + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); + + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); + if (el!=null && branch!=null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e,branch,false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } + } +} diff --git a/src/Avalonia.Input/TouchPadDevice.cs b/src/Avalonia.Input/TouchPadDevice.cs new file mode 100644 index 0000000000..fcd254f588 --- /dev/null +++ b/src/Avalonia.Input/TouchPadDevice.cs @@ -0,0 +1,488 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Represents a touch pad device. + /// + public class TouchPadDevice : ITouchPadDevice, IDisposable + { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + + private readonly Pointer _pointer; + private bool _disposed; + private PixelPoint? _position; + + public TouchPadDevice(Pointer? pointer = null) + { + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + } + + /// + /// Gets the control that is currently capturing by the mouse, if any. + /// + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. To set the mouse capture, call the + /// method. + /// + [Obsolete("Use IPointer instead")] + public IInputElement? Captured => _pointer.Captured; + + /// + /// Gets the mouse position, in screen coordinates. + /// + [Obsolete("Use events instead")] + public PixelPoint Position + { + get => _position ?? new PixelPoint(-1, -1); + protected set => _position = value; + } + + /// + /// Captures mouse input to the specified control. + /// + /// The control. + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. The current mouse capture control is exposed + /// by the property. + /// + public void Capture(IInputElement? control) + { + _pointer.Capture(control); + } + + /// + /// Gets the mouse position relative to a control. + /// + /// The control. + /// The mouse position in the control's coordinates. + public Point GetPosition(IVisual relativeTo) + { + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); + + if (relativeTo.VisualRoot == null) + { + throw new InvalidOperationException("Control is not attached to visual tree."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete + var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); + return rootPoint * transform!.Value; + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawPointerEventArgs margs) + ProcessRawEvent(margs); + } + + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + + public void SceneInvalidated(IInputRoot root, Rect rect) + { + // Pointer is outside of the target area + if (_position == null ) + { + if (root.PointerOverElement != null) + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + return; + } + + + var clientPoint = root.PointToClient(_position.Value); + + if (rect.Contains(clientPoint)) + { + if (_pointer.Captured == null) + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, + PointerPointProperties.None, KeyModifiers.None); + } + else + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, + PointerPointProperties.None, KeyModifiers.None); + } + } + } + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; + return rv; + } + + private void ProcessRawEvent(RawPointerEventArgs e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + var mouse = (TouchPadDevice)e.Device; + if(mouse._disposed) + return; + + _position = e.Root.PointToScreen(e.Position); + var props = CreateProperties(e); + var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + switch (e.Type) + { + case RawPointerEventType.LeaveWindow: + LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + break; + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, keyModifiers); + break; + case RawPointerEventType.LeftButtonUp: + case RawPointerEventType.RightButtonUp: + case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Move: + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Wheel: + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + break; + } + } + + private void LeaveWindow(ITouchPadDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + _position = null; + ClearPointerOver(this, timestamp, root, properties, inputModifiers); + } + + + PointerPointProperties CreateProperties(RawPointerEventArgs args) + { + + var kind = PointerUpdateKind.Other; + + if (args.Type == RawPointerEventType.LeftButtonDown) + kind = PointerUpdateKind.LeftButtonPressed; + if (args.Type == RawPointerEventType.MiddleButtonDown) + kind = PointerUpdateKind.MiddleButtonPressed; + if (args.Type == RawPointerEventType.RightButtonDown) + kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; + if (args.Type == RawPointerEventType.LeftButtonUp) + kind = PointerUpdateKind.LeftButtonReleased; + if (args.Type == RawPointerEventType.MiddleButtonUp) + kind = PointerUpdateKind.MiddleButtonReleased; + if (args.Type == RawPointerEventType.RightButtonUp) + kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; + + return new PointerPointProperties(args.InputModifiers, kind); + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(ITouchPadDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + _pointer.Capture(hit); + var source = GetSource(hit); + if (source != null) + { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; + } + } + + return false; + } + + private bool MouseMove(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + IInputElement? source; + + if (_pointer.Captured == null) + { + source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); + } + else + { + SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); + source = _pointer.Captured; + } + + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); + + source.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool MouseUp(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); + + source?.RaiseEvent(e); + _pointer.Capture(null); + return e.Handled; + } + + return false; + } + + private bool MouseWheel(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private IInteractive? GetSource(IVisual? hit) + { + if (hit is null) + return null; + + return _pointer.Captured ?? + (hit as IInteractive) ?? + hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + } + + private IInputElement? HitTest(IInputElement root, Point p) + { + root = root ?? throw new ArgumentNullException(nameof(root)); + + return _pointer.Captured ?? root.InputHitTest(p); + } + + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, _pointer, null, default, + timestamp, properties, inputModifiers); + } + + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.PointerOverElement; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); + + if (element!=null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + ClearChildrenPointerOver(e, root,true); + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if(clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.InputHitTest(p); + + if (element != root.PointerOverElement) + { + if (element != null) + { + SetPointerOver(device, timestamp, root, element, properties, inputModifiers); + } + else + { + ClearPointerOver(device, timestamp, root, properties, inputModifiers); + } + } + + return element; + } + + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); + + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); + if (el!=null && branch!=null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e,branch,false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c74c5fbc01..db81e8197f 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -514,6 +514,33 @@ namespace Avalonia.Win32.Interop CS_DROPSHADOW = 0x00020000 } + [Flags] + public enum PointerDeviceChangeFlags + { + PDC_ARRIVAL = 0x001, + PDC_REMOVAL = 0x002, + PDC_ORIENTATION_0 = 0x004, + PDC_ORIENTATION_90 = 0x008, + PDC_ORIENTATION_180 = 0x010, + PDC_ORIENTATION_270 = 0x020, + PDC_MODE_DEFAULT = 0x040, + PDC_MODE_CENTERED = 0x080, + PDC_MAPPING_CHANGE = 0x100, + PDC_RESOLUTION = 0x200, + PDC_ORIGIN = 0x400, + PDC_MODE_ASPECTRATIOPRESERVED = 0x800 + } + + public enum InputType + { + NONE = 0x00000000, + POINTER = 0x00000001, + TOUCH = 0x00000002, + PEN = 0x00000003, + MOUSE = 0x00000004, + TOUCHPAD = 0x00000005 + } + public enum WindowsMessage : uint { WM_NULL = 0x0000, @@ -689,6 +716,25 @@ namespace Avalonia.Win32.Interop WM_EXITSIZEMOVE = 0x0232, WM_DROPFILES = 0x0233, WM_MDIREFRESHMENU = 0x0234, + + WM_POINTERDEVICECHANGE = 0x0238, + WM_POINTERDEVICEINRANGE = 0x239, + WM_POINTERDEVICEOUTOFRANGE = 0x23A, + WM_NCPOINTERUPDATE = 0x0241, + WM_NCPOINTERDOWN = 0x0242, + WM_NCPOINTERUP = 0x0243, + WM_POINTERUPDATE = 0x0245, + WM_POINTERDOWN = 0x0246, + WM_POINTERUP = 0x0247, + WM_POINTERENTER = 0x0249, + WM_POINTERLEAVE = 0x024A, + WM_POINTERACTIVATE = 0x024B, + WM_POINTERCAPTURECHANGED = 0x024C, + WM_TOUCHHITTESTING = 0x024D, + WM_POINTERWHEEL = 0x024E, + WM_POINTERHWHEEL = 0x024F, + WM_POINTERHITTEST = 0x0250, + WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, WM_IME_CONTROL = 0x0283, @@ -903,6 +949,24 @@ namespace Avalonia.Win32.Interop public const int SizeOf_BITMAPINFOHEADER = 40; + [DllImport("user32.dll", SetLastError = true)] + public static extern int EnableMouseInPointer(bool enable); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerCursorId(uint pointerID, out uint cursorId); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerType(uint pointerID, out InputType pointerType); + + [DllImport("user32.dll", SetLastError = true)] + public static extern void GetUnpredictedMessagePos(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool IsMouseInPointerEnabled(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SkipPointerFrameMessages(uint pointerID); + [DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 88a0744e3e..e16901af2c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -166,6 +166,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { + if (BelowWin8) + { + break; + } shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { @@ -195,6 +199,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { + if (BelowWin8) + { + break; + } if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -219,11 +227,19 @@ namespace Avalonia.Win32 } // Mouse capture is lost case WindowsMessage.WM_CANCELMODE: + if (BelowWin8) + { + break; + } _mouseDevice.Capture(null); break; case WindowsMessage.WM_MOUSEMOVE: { + if (BelowWin8) + { + break; + } if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -254,6 +270,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { + if (BelowWin8) + { + break; + } e = new RawMouseWheelEventArgs( _mouseDevice, timestamp, @@ -265,6 +285,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { + if (BelowWin8) + { + break; + } e = new RawMouseWheelEventArgs( _mouseDevice, timestamp, @@ -276,6 +300,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSELEAVE: { + if (BelowWin8) + { + break; + } _trackingMouse = false; e = new RawPointerEventArgs( _mouseDevice, @@ -291,6 +319,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { + if (BelowWin8) + { + break; + } e = new RawPointerEventArgs( _mouseDevice, timestamp, @@ -311,6 +343,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { + if (BelowWin8) + { + break; + } var touchInputCount = wParam.ToInt32(); var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; @@ -338,6 +374,117 @@ namespace Avalonia.Win32 break; } + + + + + + + + + + + + case WindowsMessage.WM_POINTERDEVICECHANGE: + case WindowsMessage.WM_POINTERDEVICEINRANGE: + case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: + { + + break; + } + case WindowsMessage.WM_NCPOINTERUPDATE: + case WindowsMessage.WM_NCPOINTERDOWN: + case WindowsMessage.WM_NCPOINTERUP: + { + + break; + } + case WindowsMessage.WM_POINTERUPDATE: + case WindowsMessage.WM_POINTERDOWN: + case WindowsMessage.WM_POINTERUP: + { + + break; + } + case WindowsMessage.WM_POINTERENTER: + case WindowsMessage.WM_POINTERLEAVE: + { + + break; + } + case WindowsMessage.WM_POINTERACTIVATE: + case WindowsMessage.WM_POINTERCAPTURECHANGED: + { + + break; + } + case WindowsMessage.WM_TOUCHHITTESTING: + { + + break; + } + case WindowsMessage.WM_POINTERWHEEL: + { + var pointerId = ToPointerId(wParam); + GetPointerType(pointerId, out var type); + IInputDevice device = _mouseDevice; + switch (type) + { + case InputType.PEN: + + break; + case InputType.TOUCH: + device = _touchDevice; + break; + case InputType.TOUCHPAD: + + break; + } + + + var delta = GetWheelDelta(wParam); + var point = PointFromLParam(lParam); + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(point), + new Vector(0, delta / wheelDelta), + GetMouseModifiers(wParam)); + break; + } + case WindowsMessage.WM_POINTERHWHEEL: + { + + break; + } + case WindowsMessage.WM_POINTERHITTEST: + { + + break; + } + + + + + + + + + + + + + + + + + + + + + + case WindowsMessage.WM_NCPAINT: { if (!HasFullDecorations) @@ -540,6 +687,14 @@ namespace Avalonia.Win32 } } + public static uint GetWheelDelta(IntPtr wParam) => HIWORD(wParam); + public static uint ToPointerId(IntPtr wParam) => LOWORD(wParam); + public static uint LOWORD(IntPtr param) => (uint)param & 0xffff; + public static uint HIWORD(IntPtr param) => (uint)param >> 16; + + + public bool BelowWin8 => Win32Platform.WindowsVersion < PlatformConstants.Windows8; + private void UpdateInputMethod(IntPtr hkl) { // note: for non-ime language, also create it so that emoji panel tracks cursor From 44fda62a9b93cb0f3f6c0096cbfc654f378ab919 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 16:56:47 +0300 Subject: [PATCH 002/481] Add PointerInfo, TouchInfo and PenInfo --- src/Avalonia.Input/ITouchPadDevice.cs | 10 - src/Avalonia.Input/TouchPadDevice.cs | 488 ------------------ .../Interop/UnmanagedMethods.cs | 166 +++++- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 14 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 + 5 files changed, 159 insertions(+), 521 deletions(-) delete mode 100644 src/Avalonia.Input/ITouchPadDevice.cs delete mode 100644 src/Avalonia.Input/TouchPadDevice.cs diff --git a/src/Avalonia.Input/ITouchPadDevice.cs b/src/Avalonia.Input/ITouchPadDevice.cs deleted file mode 100644 index ea6c57f948..0000000000 --- a/src/Avalonia.Input/ITouchPadDevice.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Represents a touch pad device. - /// - public interface ITouchPadDevice : IPointerDevice - { - - } -} diff --git a/src/Avalonia.Input/TouchPadDevice.cs b/src/Avalonia.Input/TouchPadDevice.cs deleted file mode 100644 index fcd254f588..0000000000 --- a/src/Avalonia.Input/TouchPadDevice.cs +++ /dev/null @@ -1,488 +0,0 @@ -using System; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Represents a touch pad device. - /// - public class TouchPadDevice : ITouchPadDevice, IDisposable - { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - - private readonly Pointer _pointer; - private bool _disposed; - private PixelPoint? _position; - - public TouchPadDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if (!e.Handled && e is RawPointerEventArgs margs) - ProcessRawEvent(margs); - } - - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - var mouse = (TouchPadDevice)e.Device; - if(mouse._disposed) - return; - - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); - switch (e.Type) - { - case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); - break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); - break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); - break; - } - } - - private void LeaveWindow(ITouchPadDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - - PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); - } - - private MouseButton _lastMouseDownButton; - private bool MouseDown(ITouchPadDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; - } - } - - return false; - } - - private bool MouseMove(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } - - if (source is object) - { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); - - source.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool MouseUp(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, - _lastMouseDownButton); - - source?.RaiseEvent(e); - _pointer.Capture(null); - return e.Handled; - } - - return false; - } - - private bool MouseWheel(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; - } - - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() - { - _disposed = true; - _pointer?.Dispose(); - } - } -} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index db81e8197f..5aecd8e8f0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -225,20 +225,17 @@ namespace Avalonia.Win32.Interop [Flags] public enum ModifierKeys { - MK_CONTROL = 0x0008, + MK_NONE = 0x0001, MK_LBUTTON = 0x0001, - - MK_MBUTTON = 0x0010, - MK_RBUTTON = 0x0002, - MK_SHIFT = 0x0004, - - MK_ALT = 0x0020, + MK_SHIFT = 0x0004, + MK_CONTROL = 0x0008, + MK_MBUTTON = 0x0010, + MK_ALT = 0x0020, MK_XBUTTON1 = 0x0020, - MK_XBUTTON2 = 0x0040 } @@ -531,14 +528,14 @@ namespace Avalonia.Win32.Interop PDC_MODE_ASPECTRATIOPRESERVED = 0x800 } - public enum InputType + public enum PointerInputType { - NONE = 0x00000000, - POINTER = 0x00000001, - TOUCH = 0x00000002, - PEN = 0x00000003, - MOUSE = 0x00000004, - TOUCHPAD = 0x00000005 + PT_NONE = 0x00000000, + PT_POINTER = 0x00000001, + PT_TOUCH = 0x00000002, + PT_PEN = 0x00000003, + PT_MOUSE = 0x00000004, + PT_TOUCHPAD = 0x00000005 } public enum WindowsMessage : uint @@ -882,6 +879,134 @@ namespace Avalonia.Win32.Interop SCF_ISSECURE = 0x00000001, } + [Flags] + public enum PointerFlags + { + POINTER_FLAG_NONE = 0x00000000, + POINTER_FLAG_NEW = 0x00000001, + POINTER_FLAG_INRANGE = 0x00000002, + POINTER_FLAG_INCONTACT = 0x00000004, + POINTER_FLAG_FIRSTBUTTON = 0x00000010, + POINTER_FLAG_SECONDBUTTON = 0x00000020, + POINTER_FLAG_THIRDBUTTON = 0x00000040, + POINTER_FLAG_FOURTHBUTTON = 0x00000080, + POINTER_FLAG_FIFTHBUTTON = 0x00000100, + POINTER_FLAG_PRIMARY = 0x00002000, + POINTER_FLAG_CONFIDENCE = 0x00000400, + POINTER_FLAG_CANCELED = 0x00000800, + POINTER_FLAG_DOWN = 0x00010000, + POINTER_FLAG_UPDATE = 0x00020000, + POINTER_FLAG_UP = 0x00040000, + POINTER_FLAG_WHEEL = 0x00080000, + POINTER_FLAG_HWHEEL = 0x00100000, + POINTER_FLAG_CAPTURECHANGED = 0x00200000, + POINTER_FLAG_HASTRANSFORM = 0x00400000 + } + + public enum PointerButtonChangeType : ulong + { + POINTER_CHANGE_NONE, + POINTER_CHANGE_FIRSTBUTTON_DOWN, + POINTER_CHANGE_FIRSTBUTTON_UP, + POINTER_CHANGE_SECONDBUTTON_DOWN, + POINTER_CHANGE_SECONDBUTTON_UP, + POINTER_CHANGE_THIRDBUTTON_DOWN, + POINTER_CHANGE_THIRDBUTTON_UP, + POINTER_CHANGE_FOURTHBUTTON_DOWN, + POINTER_CHANGE_FOURTHBUTTON_UP, + POINTER_CHANGE_FIFTHBUTTON_DOWN, + POINTER_CHANGE_FIFTHBUTTON_UP + } + + [Flags] + public enum PenFlags + { + PEN_FLAGS_NONE = 0x00000000, + PEN_FLAGS_BARREL = 0x00000001, + PEN_FLAGS_INVERTED = 0x00000002, + PEN_FLAGS_ERASER = 0x00000004, + } + + [Flags] + public enum PenMask + { + PEN_MASK_NONE = 0x00000000, + PEN_MASK_PRESSURE = 0x00000001, + PEN_MASK_ROTATION = 0x00000002, + PEN_MASK_TILT_X = 0x00000004, + PEN_MASK_TILT_Y = 0x00000008 + } + + [Flags] + public enum TouchFlags + { + TOUCH_FLAG_NONE = 0x00000000 + } + + [Flags] + public enum TouchMask + { + TOUCH_MASK_NONE = 0x00000000, + TOUCH_MASK_CONTACTAREA = 0x00000001, + TOUCH_MASK_ORIENTATION = 0x00000002, + TOUCH_MASK_PRESSURE = 0x00000004, + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct POINTER_TOUCH_INFO + { + public POINTER_INFO pointerInfo; + public TouchFlags touchFlags; + public TouchMask touchMask; + public int rcContactLeft; + public int rcContactTop; + public int rcContactRight; + public int rcContactBottom; + public int rcContactRawLeft; + public int rcContactRawTop; + public int rcContactRawRight; + public int rcContactRawBottom; + public uint orientation; + public uint pressure; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct POINTER_PEN_INFO + { + public POINTER_INFO pointerInfo; + public PenFlags penFlags; + public PenMask penMask; + public uint pressure; + public uint rotation; + public int tiltX; + public int tiltY; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct POINTER_INFO + { + public PointerInputType pointerType; + public uint pointerId; + public uint frameId; + public PointerFlags pointerFlags; + public IntPtr sourceDevice; + public IntPtr hwndTarget; + public int ptPixelLocationX; + public int ptPixelLocationY; + public int ptHimetricLocationX; + public int ptHimetricLocationY; + public int ptPixelLocationRawX; + public int ptPixelLocationRawY; + public int ptHimetricLocationRawX; + public int ptHimetricLocationRawY; + public uint dwTime; + public uint historyCount; + public int inputData; + public ModifierKeys dwKeyStates; + public UInt64 PerformanceCount; + public PointerButtonChangeType ButtonChangeType; + } + [StructLayout(LayoutKind.Sequential)] public struct RGBQUAD { @@ -956,7 +1081,16 @@ namespace Avalonia.Win32.Interop public static extern bool GetPointerCursorId(uint pointerID, out uint cursorId); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetPointerType(uint pointerID, out InputType pointerType); + public static extern bool GetPointerType(uint pointerID, out PointerInputType pointerType); + + [DllImport("User32.dll", SetLastError = true)] + public static extern bool GetPointerInfo(uint pointerID, out POINTER_INFO pointerInfo); + + [DllImport("User32.dll", SetLastError = true)] + public static extern bool GetPointerPenInfo(uint pointerID, out POINTER_PEN_INFO penInfo); + + [DllImport("User32.dll", SetLastError = true)] + public static extern bool GetPointerTouchInfo(uint pointerID, out POINTER_TOUCH_INFO touchInfo); [DllImport("user32.dll", SetLastError = true)] public static extern void GetUnpredictedMessagePos(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index e16901af2c..8a0654a9ba 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -427,17 +427,17 @@ namespace Avalonia.Win32 { var pointerId = ToPointerId(wParam); GetPointerType(pointerId, out var type); - IInputDevice device = _mouseDevice; + IInputDevice device; switch (type) { - case InputType.PEN: - + case PointerInputType.PT_PEN: + device = _penDevice; break; - case InputType.TOUCH: + case PointerInputType.PT_TOUCH: device = _touchDevice; break; - case InputType.TOUCHPAD: - + default: + device = _mouseDevice; break; } @@ -445,7 +445,7 @@ namespace Avalonia.Win32 var delta = GetWheelDelta(wParam); var point = PointFromLParam(lParam); e = new RawMouseWheelEventArgs( - _mouseDevice, + device, timestamp, _owner, PointToClient(point), diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e4f5268285..8f6a7e9741 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Win32 private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; + private readonly PenDevice _penDevice; private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; private readonly IGlPlatformSurface _gl; @@ -96,6 +97,7 @@ namespace Avalonia.Win32 { _touchDevice = new TouchDevice(); _mouseDevice = new WindowsMouseDevice(); + _penDevice = new PenDevice(); #if USE_MANAGED_DRAG _managedDrag = new ManagedWindowResizeDragHelper(this, capture => From 1192e86d90f0d6f4df29acc442a21031e7fb0c3e Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 18:39:36 +0300 Subject: [PATCH 003/481] Investigate all the messages and note short comments about them and provide a link to a documentation. In case anyone will need this in the future --- .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 139 ++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 3 files changed, 99 insertions(+), 44 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5aecd8e8f0..93457f7bbd 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -730,7 +730,7 @@ namespace Avalonia.Win32.Interop WM_TOUCHHITTESTING = 0x024D, WM_POINTERWHEEL = 0x024E, WM_POINTERHWHEEL = 0x024F, - WM_POINTERHITTEST = 0x0250, + DM_POINTERHITTEST = 0x0250, WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8a0654a9ba..3587eddc85 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -227,10 +227,6 @@ namespace Avalonia.Win32 } // Mouse capture is lost case WindowsMessage.WM_CANCELMODE: - if (BelowWin8) - { - break; - } _mouseDevice.Capture(null); break; @@ -263,7 +259,8 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.Move, - DipFromLParam(lParam), GetMouseModifiers(wParam)); + DipFromLParam(lParam), + GetMouseModifiers(wParam)); break; } @@ -279,7 +276,8 @@ namespace Avalonia.Win32 timestamp, _owner, PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); + new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), + GetMouseModifiers(wParam)); break; } @@ -294,7 +292,8 @@ namespace Avalonia.Win32 timestamp, _owner, PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); + new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), + GetMouseModifiers(wParam)); break; } @@ -310,7 +309,8 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.LeaveWindow, - new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); + new Point(-1, -1), + WindowsKeyboardDevice.Instance.Modifiers); break; } @@ -386,16 +386,24 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERDEVICECHANGE: + { + //notifies about changes in the settings of a monitor that has a digitizer attached to it. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange + break; + } case WindowsMessage.WM_POINTERDEVICEINRANGE: case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: { - + //notifies about proximity of pointer device to the digitizer. + //contains pointer id and proximity. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdeviceinrange break; } case WindowsMessage.WM_NCPOINTERUPDATE: case WindowsMessage.WM_NCPOINTERDOWN: case WindowsMessage.WM_NCPOINTERUP: { + //NC stands for non-client area - window header and window border break; } @@ -407,60 +415,79 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_POINTERENTER: - case WindowsMessage.WM_POINTERLEAVE: { + //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. + //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM + //note: by using a pen there can be a pointer leave or enter inside a window coords + //when you are just lift up the pen above the display break; } - case WindowsMessage.WM_POINTERACTIVATE: - case WindowsMessage.WM_POINTERCAPTURECHANGED: + case WindowsMessage.WM_POINTERLEAVE: { + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + e = new RawPointerEventArgs( + device, + timestamp, + _owner, + RawPointerEventType.LeaveWindow, + point, + WindowsKeyboardDevice.Instance.Modifiers); break; } - case WindowsMessage.WM_TOUCHHITTESTING: + case WindowsMessage.WM_POINTERACTIVATE: { - + //occurs when a pointer activates an inactive window. + //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate break; } + case WindowsMessage.WM_POINTERCAPTURECHANGED: + { + _mouseDevice.Capture(null); + return IntPtr.Zero; + } case WindowsMessage.WM_POINTERWHEEL: { - var pointerId = ToPointerId(wParam); - GetPointerType(pointerId, out var type); - IInputDevice device; - switch (type) - { - case PointerInputType.PT_PEN: - device = _penDevice; - break; - case PointerInputType.PT_TOUCH: - device = _touchDevice; - break; - default: - device = _mouseDevice; - break; - } - + GetDeviceInfo(wParam, out var device, out var info); - var delta = GetWheelDelta(wParam); - var point = PointFromLParam(lParam); - e = new RawMouseWheelEventArgs( - device, - timestamp, - _owner, - PointToClient(point), - new Vector(0, delta / wheelDelta), - GetMouseModifiers(wParam)); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var delta = new Vector(0, GetWheelDelta(wParam) / wheelDelta); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } case WindowsMessage.WM_POINTERHWHEEL: { + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var delta = new Vector(GetWheelDelta(wParam) / wheelDelta, 0); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } - case WindowsMessage.WM_POINTERHITTEST: + case WindowsMessage.DM_POINTERHITTEST: { - + //DM stands for direct manipulation. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/directmanipulation/direct-manipulation-portal + break; + } + case WindowsMessage.WM_TOUCHHITTESTING: + { + //This is to determine the most probable touch target. + //provides an input bounding box and receives hit proximity + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-touchhittesting + //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-touch_hit_testing_input + break; + } + case WindowsMessage.WM_PARENTNOTIFY: + { + //This message is sent in a dialog scenarios. Contains mouse position in an old-way, + //but listed in the wm_pointer reference + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-parentnotify break; } @@ -687,13 +714,36 @@ namespace Avalonia.Win32 } } + private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) + { + var pointerId = ToPointerId(wParam); + GetPointerType(pointerId, out var type);//ToDo we can cache this and invalidate in WM_POINTERDEVICECHANGE + switch (type) + { + case PointerInputType.PT_PEN: + device = _penDevice; + GetPointerPenInfo(pointerId, out var penInfo); + info = penInfo.pointerInfo; + break; + case PointerInputType.PT_TOUCH: + device = _touchDevice; + GetPointerTouchInfo(pointerId, out var touchInfo); + info = touchInfo.pointerInfo; + break; + default: + device = _mouseDevice; + GetPointerInfo(pointerId, out info); + break; + } + } + public static uint GetWheelDelta(IntPtr wParam) => HIWORD(wParam); public static uint ToPointerId(IntPtr wParam) => LOWORD(wParam); public static uint LOWORD(IntPtr param) => (uint)param & 0xffff; public static uint HIWORD(IntPtr param) => (uint)param >> 16; - public bool BelowWin8 => Win32Platform.WindowsVersion < PlatformConstants.Windows8; + public readonly bool BelowWin8 = Win32Platform.WindowsVersion < PlatformConstants.Windows8; private void UpdateInputMethod(IntPtr hkl) { @@ -747,6 +797,11 @@ namespace Avalonia.Win32 private static RawInputModifiers GetMouseModifiers(IntPtr wParam) { var keys = (ModifierKeys)ToInt32(wParam); + return GetInputModifiers(keys); + } + + private static RawInputModifiers GetInputModifiers(ModifierKeys keys) + { var modifiers = WindowsKeyboardDevice.Instance.Modifiers; if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON)) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8f6a7e9741..0f99252d4d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -76,7 +76,7 @@ namespace Avalonia.Win32 private bool _multitouch; private IInputRoot _owner; private WindowProperties _windowProperties; - private bool _trackingMouse; + private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk private bool _topmost; private double _scaling = 1; private WindowState _showWindowState; From 35a55c9a56dca3ccc957a559e0b50e1aa48fdab0 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 21:37:54 +0300 Subject: [PATCH 004/481] Enable WM_Pointer for mouse events, handle up/down events, some fixes --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 73 +++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 5 ++ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 3587eddc85..5e31bba925 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -166,7 +166,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -199,7 +199,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -232,7 +232,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEMOVE: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -267,7 +267,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -283,7 +283,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -299,7 +299,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSELEAVE: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -319,7 +319,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -343,7 +343,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -400,18 +400,53 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_NCPOINTERUPDATE: - case WindowsMessage.WM_NCPOINTERDOWN: - case WindowsMessage.WM_NCPOINTERUP: { //NC stands for non-client area - window header and window border - + //As I found above in an old message handling - we dont need to handle NC pointer move/updates. + //All we need is pointer down and up. So this is skipped for now. break; } - case WindowsMessage.WM_POINTERUPDATE: + case WindowsMessage.WM_NCPOINTERDOWN: + case WindowsMessage.WM_NCPOINTERUP: case WindowsMessage.WM_POINTERDOWN: case WindowsMessage.WM_POINTERUP: { + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var eventType = info.ButtonChangeType switch + { + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, + + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, + }; + if (eventType == RawPointerEventType.NonClientLeftButtonDown && + (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + break; + } + case WindowsMessage.WM_POINTERUPDATE: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + e = new RawPointerEventArgs(device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); break; } case WindowsMessage.WM_POINTERENTER: @@ -455,7 +490,7 @@ namespace Avalonia.Win32 var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector(0, GetWheelDelta(wParam) / wheelDelta); + var delta = new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } @@ -465,7 +500,7 @@ namespace Avalonia.Win32 var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector(GetWheelDelta(wParam) / wheelDelta, 0); + var delta = new Vector((ToInt32(wParam) >> 16) / wheelDelta, 0); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } @@ -716,7 +751,7 @@ namespace Avalonia.Win32 private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) { - var pointerId = ToPointerId(wParam); + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetPointerType(pointerId, out var type);//ToDo we can cache this and invalidate in WM_POINTERDEVICECHANGE switch (type) { @@ -737,13 +772,7 @@ namespace Avalonia.Win32 } } - public static uint GetWheelDelta(IntPtr wParam) => HIWORD(wParam); - public static uint ToPointerId(IntPtr wParam) => LOWORD(wParam); - public static uint LOWORD(IntPtr param) => (uint)param & 0xffff; - public static uint HIWORD(IntPtr param) => (uint)param >> 16; - - - public readonly bool BelowWin8 = Win32Platform.WindowsVersion < PlatformConstants.Windows8; + public readonly bool Win8Plus = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; private void UpdateInputMethod(IntPtr hkl) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0f99252d4d..0ad198e991 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -126,6 +126,11 @@ namespace Avalonia.Win32 egl.Display is AngleWin32EglDisplay angleDisplay && angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + if (Win8Plus && !IsMouseInPointerEnabled()) + { + EnableMouseInPointer(true); + } + CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); UpdateInputMethod(GetKeyboardLayout(0)); From 4cea62fd13701931ba7aedbb2b2f6fce9f9477a6 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 23:34:43 +0300 Subject: [PATCH 005/481] PenDevice tuning, Add pen properties to the PointerPointProperties --- src/Avalonia.Input/IKeyboardDevice.cs | 2 + src/Avalonia.Input/IPointer.cs | 3 +- src/Avalonia.Input/PenDevice.cs | 134 +++--------------- src/Avalonia.Input/PointerPoint.cs | 30 +++- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 8 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 64 +++------ 6 files changed, 72 insertions(+), 169 deletions(-) diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 9506dc36fb..b3ea7c5f4f 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -47,6 +47,8 @@ namespace Avalonia.Input MiddleMouseButton = 64, XButton1MouseButton = 128, XButton2MouseButton = 256, + BarrelPenButton = 512, + PenEraser = 1024, KeyboardMask = Alt | Control | Shift | Meta } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index 7af48cef82..361f3ac370 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -13,6 +13,7 @@ namespace Avalonia.Input public enum PointerType { Mouse, - Touch + Touch, + Pen } } diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index dd3d60ebb9..3c0f3c7709 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Input @@ -13,17 +12,13 @@ namespace Avalonia.Input /// public class PenDevice : IPenDevice, IDisposable { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - private readonly Pointer _pointer; private bool _disposed; private PixelPoint? _position; public PenDevice(Pointer? pointer = null) { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Pen, true); } /// @@ -37,16 +32,6 @@ namespace Avalonia.Input [Obsolete("Use IPointer instead")] public IInputElement? Captured => _pointer.Captured; - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - /// /// Captures mouse input to the specified control. /// @@ -76,7 +61,7 @@ namespace Avalonia.Input } #pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); + var rootPoint = relativeTo.VisualRoot.PointToClient(_position ?? new PixelPoint(-1, -1)); #pragma warning restore CS0618 // Type or member is obsolete var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform!.Value; @@ -120,29 +105,13 @@ namespace Avalonia.Input } } } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } private void ProcessRawEvent(RawPointerEventArgs e) { e = e ?? throw new ArgumentNullException(nameof(e)); - var mouse = (PenDevice)e.Device; - if(mouse._disposed) + var pen = (PenDevice)e.Device; + if(pen._disposed) return; _position = e.Root.PointToScreen(e.Position); @@ -151,34 +120,16 @@ namespace Avalonia.Input switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + LeaveWindow(pen, e.Timestamp, e.Root, props, keyModifiers); break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); + case RawPointerEventType.PenBegin: + e.Handled = PenDown(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + case RawPointerEventType.PenEnd: + e.Handled = PenUp(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + case RawPointerEventType.PenUpdate: + e.Handled = PenMove(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; } } @@ -196,35 +147,18 @@ namespace Avalonia.Input PointerPointProperties CreateProperties(RawPointerEventArgs args) { - var kind = PointerUpdateKind.Other; - if (args.Type == RawPointerEventType.LeftButtonDown) + if (args.Type == RawPointerEventType.PenBegin) kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) + if (args.Type == RawPointerEventType.PenEnd) kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; return new PointerPointProperties(args.InputModifiers, kind); } private MouseButton _lastMouseDownButton; - private bool MouseDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, + private bool PenDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { @@ -239,21 +173,8 @@ namespace Avalonia.Input var source = GetSource(hit); if (source != null) { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, 1); source.RaiseEvent(e); return e.Handled; } @@ -262,7 +183,7 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + private bool PenMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); @@ -292,7 +213,7 @@ namespace Avalonia.Input return false; } - private bool MouseUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + private bool PenUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); @@ -314,27 +235,6 @@ namespace Avalonia.Input return false; } - private bool MouseWheel(IPenDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - private IInteractive? GetSource(IVisual? hit) { if (hit is null) diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 9f8285a8e1..16fab7410c 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -20,6 +20,14 @@ namespace Avalonia.Input public bool IsRightButtonPressed { get; } public bool IsXButton1Pressed { get; } public bool IsXButton2Pressed { get; } + public bool IsBarrelButtonPressed { get; } + public bool IsEraser { get; } + + public float Twist { get; } + public float Pressure { get; } + public float XTilt { get; } + public float YTilt { get; } + public PointerUpdateKind PointerUpdateKind { get; } @@ -36,10 +44,12 @@ namespace Avalonia.Input IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); + IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.BarrelPenButton); + IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state - + if (kind == PointerUpdateKind.LeftButtonPressed) IsLeftButtonPressed = true; if (kind == PointerUpdateKind.LeftButtonReleased) @@ -60,6 +70,20 @@ namespace Avalonia.Input IsXButton2Pressed = true; if (kind == PointerUpdateKind.XButton2Released) IsXButton2Pressed = false; + if (kind == PointerUpdateKind.BarrelButtonPressed) + IsBarrelButtonPressed = true; + if (kind == PointerUpdateKind.BarrelButtonReleased) + IsBarrelButtonPressed = false; + } + + public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, + float twist, float pressure, float xTilt, float yTilt + ) : this (modifiers, kind) + { + Twist = twist; + Pressure = pressure; + XTilt = xTilt; + YTilt = yTilt; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -77,7 +101,9 @@ namespace Avalonia.Input RightButtonReleased, XButton1Released, XButton2Released, - Other + Other, + BarrelButtonPressed, + BarrelButtonReleased } public static class PointerUpdateKindExtensions diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 62a1dd5d84..ea160efef5 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -21,7 +21,13 @@ namespace Avalonia.Input.Raw TouchBegin, TouchUpdate, TouchEnd, - TouchCancel + TouchCancel, + PenBegin, + PenUpdate, + PenEnd, + PenCancel, + BarrelUp, + BarrelDown, } /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5e31bba925..db11508ed7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -374,17 +374,6 @@ namespace Avalonia.Win32 break; } - - - - - - - - - - - case WindowsMessage.WM_POINTERDEVICECHANGE: { //notifies about changes in the settings of a monitor that has a digitizer attached to it. @@ -414,7 +403,10 @@ namespace Avalonia.Win32 GetDeviceInfo(wParam, out var device, out var info); var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - var eventType = info.ButtonChangeType switch + + if (info.ButtonChangeType != PointerButtonChangeType.POINTER_CHANGE_NONE) + { + var eventType = info.ButtonChangeType switch { PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, @@ -428,12 +420,13 @@ namespace Avalonia.Win32 PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, }; - if (eventType == RawPointerEventType.NonClientLeftButtonDown && - (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; + if (eventType == RawPointerEventType.NonClientLeftButtonDown && + (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); } - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); break; } case WindowsMessage.WM_POINTERUPDATE: @@ -446,7 +439,8 @@ namespace Avalonia.Win32 var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - e = new RawPointerEventArgs(device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); + e = new RawPointerEventArgs( + device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); break; } case WindowsMessage.WM_POINTERENTER: @@ -462,14 +456,10 @@ namespace Avalonia.Win32 { GetDeviceInfo(wParam, out var device, out var info); var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); e = new RawPointerEventArgs( - device, - timestamp, - _owner, - RawPointerEventType.LeaveWindow, - point, - WindowsKeyboardDevice.Instance.Modifiers); + device, timestamp, _owner, RawPointerEventType.LeaveWindow, point, modifiers); break; } case WindowsMessage.WM_POINTERACTIVATE: @@ -520,33 +510,11 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_PARENTNOTIFY: { - //This message is sent in a dialog scenarios. Contains mouse position in an old-way, - //but listed in the wm_pointer reference + //This message is sent in a dialog scenarios. Contains mouse position. + //Old message, but listed in the wm_pointer reference //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-parentnotify break; } - - - - - - - - - - - - - - - - - - - - - - case WindowsMessage.WM_NCPAINT: { if (!HasFullDecorations) From d3f3166914428a945d24b8369c2b6c18ff0aab34 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 14:16:35 +0300 Subject: [PATCH 006/481] Provide Pressure, Tilt, and other parameters to PointerPointProperties. Provide touch events (still a lot to do), use point position from pointer info. --- src/Avalonia.Input/PenDevice.cs | 25 ++-- src/Avalonia.Input/PointerPoint.cs | 12 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 4 - .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 111 ++++++++++++------ 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index 3c0f3c7709..c993e7fac4 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -32,6 +32,14 @@ namespace Avalonia.Input [Obsolete("Use IPointer instead")] public IInputElement? Captured => _pointer.Captured; + public bool IsEraser { get; set; } + public bool IsInverted { get; set; } + public bool IsBarrel { get; set; } + public int XTilt { get; set; } + public int YTilt { get; set; } + public uint Pressure { get; set; } + public uint Twist { get; set; } + /// /// Captures mouse input to the specified control. /// @@ -122,13 +130,13 @@ namespace Avalonia.Input case RawPointerEventType.LeaveWindow: LeaveWindow(pen, e.Timestamp, e.Root, props, keyModifiers); break; - case RawPointerEventType.PenBegin: + case RawPointerEventType.LeftButtonDown: e.Handled = PenDown(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.PenEnd: + case RawPointerEventType.LeftButtonUp: e.Handled = PenUp(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.PenUpdate: + case RawPointerEventType.Move: e.Handled = PenMove(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; } @@ -145,16 +153,17 @@ namespace Avalonia.Input } - PointerPointProperties CreateProperties(RawPointerEventArgs args) + private PointerPointProperties CreateProperties(RawPointerEventArgs args) { var kind = PointerUpdateKind.Other; - if (args.Type == RawPointerEventType.PenBegin) + if (args.Type == RawPointerEventType.LeftButtonDown) kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.PenEnd) + if (args.Type == RawPointerEventType.LeftButtonUp) kind = PointerUpdateKind.LeftButtonReleased; - - return new PointerPointProperties(args.InputModifiers, kind); + + return new PointerPointProperties(args.InputModifiers, kind, + Twist, Pressure, XTilt, YTilt, IsEraser, IsInverted, IsBarrel); } private MouseButton _lastMouseDownButton; diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 16fab7410c..07ef130fcc 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -22,6 +22,7 @@ namespace Avalonia.Input public bool IsXButton2Pressed { get; } public bool IsBarrelButtonPressed { get; } public bool IsEraser { get; } + public bool IsInverted { get; } public float Twist { get; } public float Pressure { get; } @@ -70,20 +71,19 @@ namespace Avalonia.Input IsXButton2Pressed = true; if (kind == PointerUpdateKind.XButton2Released) IsXButton2Pressed = false; - if (kind == PointerUpdateKind.BarrelButtonPressed) - IsBarrelButtonPressed = true; - if (kind == PointerUpdateKind.BarrelButtonReleased) - IsBarrelButtonPressed = false; } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, - float twist, float pressure, float xTilt, float yTilt + float twist, float pressure, float xTilt, float yTilt, bool isEraser, bool isInverted, bool isBarrel ) : this (modifiers, kind) { Twist = twist; Pressure = pressure; XTilt = xTilt; YTilt = yTilt; + IsEraser = isEraser; + IsInverted = isInverted; + IsBarrelButtonPressed = isBarrel; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -102,8 +102,6 @@ namespace Avalonia.Input XButton1Released, XButton2Released, Other, - BarrelButtonPressed, - BarrelButtonReleased } public static class PointerUpdateKindExtensions diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 6a35ec3fad..c75eb69bfd 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -22,10 +22,6 @@ namespace Avalonia.Input.Raw TouchUpdate, TouchEnd, TouchCancel, - PenBegin, - PenUpdate, - PenEnd, - PenCancel, BarrelUp, BarrelDown, Magnify, diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 93457f7bbd..b3cab5d052 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1003,7 +1003,7 @@ namespace Avalonia.Win32.Interop public uint historyCount; public int inputData; public ModifierKeys dwKeyStates; - public UInt64 PerformanceCount; + public ulong PerformanceCount; public PointerButtonChangeType ButtonChangeType; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index db11508ed7..34cd9acf25 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -22,8 +22,8 @@ namespace Avalonia.Win32 uint timestamp = unchecked((uint)GetMessageTime()); RawInputEventArgs e = null; var shouldTakeFocus = false; - - switch ((WindowsMessage)msg) + var message = (WindowsMessage)msg; + switch (message) { case WindowsMessage.WM_ACTIVATE: { @@ -180,7 +180,7 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, - (WindowsMessage)msg switch + message switch { WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, @@ -212,7 +212,7 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, - (WindowsMessage)msg switch + message switch { WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, @@ -327,7 +327,7 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, - (WindowsMessage)msg switch + message switch { WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType .NonClientLeftButtonDown, @@ -400,33 +400,13 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERDOWN: case WindowsMessage.WM_POINTERUP: { + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var eventType = GetEventType(message, info); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); - if (info.ButtonChangeType != PointerButtonChangeType.POINTER_CHANGE_NONE) - { - var eventType = info.ButtonChangeType switch - { - PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, - PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, - PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, - PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, - PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, - - PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, - PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, - PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, - PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, - PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, - }; - if (eventType == RawPointerEventType.NonClientLeftButtonDown && - (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; - } - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); - } + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); break; } case WindowsMessage.WM_POINTERUPDATE: @@ -436,11 +416,11 @@ namespace Avalonia.Win32 break; } GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); + var eventType = device is TouchDevice ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; - e = new RawPointerEventArgs( - device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); break; } case WindowsMessage.WM_POINTERENTER: @@ -455,7 +435,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: { GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); e = new RawPointerEventArgs( @@ -478,7 +458,7 @@ namespace Avalonia.Win32 { GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); var delta = new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); @@ -488,7 +468,7 @@ namespace Avalonia.Win32 { GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); var delta = new Vector((ToInt32(wParam) >> 16) / wheelDelta, 0); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); @@ -696,7 +676,7 @@ namespace Avalonia.Win32 { Input(e); - if ((WindowsMessage)msg == WindowsMessage.WM_KEYDOWN) + if (message == WindowsMessage.WM_KEYDOWN) { // Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to // be ignored. This should be safe to do as WM_CHAR should only be produced in @@ -717,6 +697,44 @@ namespace Avalonia.Win32 } } + private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) + { + switch (info.pointerType) + { + case PointerInputType.PT_PEN: + return ToEventType(info.ButtonChangeType); + case PointerInputType.PT_TOUCH: + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || + !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) + { + return RawPointerEventType.TouchCancel; + } + return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN + ? RawPointerEventType.TouchBegin + : RawPointerEventType.TouchEnd; + default: + var eventType = ToEventType(info.ButtonChangeType); + if (eventType == RawPointerEventType.LeftButtonDown && + message == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + return eventType; + } + } + + private unsafe void ApplyPenInfo(POINTER_PEN_INFO penInfo) + { + _penDevice.IsBarrel = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); + _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); + _penDevice.IsInverted = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED); + + _penDevice.XTilt = penInfo.tiltX; + _penDevice.YTilt = penInfo.tiltY; + _penDevice.Pressure = penInfo.pressure; + _penDevice.Twist = penInfo.rotation; + } + private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); @@ -727,6 +745,8 @@ namespace Avalonia.Win32 device = _penDevice; GetPointerPenInfo(pointerId, out var penInfo); info = penInfo.pointerInfo; + + ApplyPenInfo(penInfo); break; case PointerInputType.PT_TOUCH: device = _touchDevice; @@ -740,6 +760,25 @@ namespace Avalonia.Win32 } } + private static RawPointerEventType ToEventType(PointerButtonChangeType type) + { + return type switch + { + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, + + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, + _ => RawPointerEventType.Move + }; + } + public readonly bool Win8Plus = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; private void UpdateInputMethod(IntPtr hkl) From c046165714c37e20b305cfae823347966cd95aca Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 15:36:29 +0300 Subject: [PATCH 007/481] Add History fetch methods (will be implemented a bit later). Raise touch events. --- .../Interop/UnmanagedMethods.cs | 25 +++-- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 103 +++++++++++------- 2 files changed, 76 insertions(+), 52 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b3cab5d052..d71efcb074 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1074,32 +1074,35 @@ namespace Avalonia.Win32.Interop public const int SizeOf_BITMAPINFOHEADER = 40; + [DllImport("user32.dll", SetLastError = true)] + public static extern bool IsMouseInPointerEnabled(); + [DllImport("user32.dll", SetLastError = true)] public static extern int EnableMouseInPointer(bool enable); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetPointerCursorId(uint pointerID, out uint cursorId); + public static extern bool GetPointerCursorId(uint pointerId, out uint cursorId); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetPointerType(uint pointerID, out PointerInputType pointerType); + public static extern bool GetPointerType(uint pointerId, out PointerInputType pointerType); - [DllImport("User32.dll", SetLastError = true)] - public static extern bool GetPointerInfo(uint pointerID, out POINTER_INFO pointerInfo); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerInfo(uint pointerId, out POINTER_INFO pointerInfo); - [DllImport("User32.dll", SetLastError = true)] - public static extern bool GetPointerPenInfo(uint pointerID, out POINTER_PEN_INFO penInfo); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_INFO[] pointerInfos); - [DllImport("User32.dll", SetLastError = true)] - public static extern bool GetPointerTouchInfo(uint pointerID, out POINTER_TOUCH_INFO touchInfo); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerPenInfo(uint pointerId, out POINTER_PEN_INFO penInfo); [DllImport("user32.dll", SetLastError = true)] - public static extern void GetUnpredictedMessagePos(); + public static extern bool GetPointerPenInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_PEN_INFO[] penInfos); [DllImport("user32.dll", SetLastError = true)] - public static extern bool IsMouseInPointerEnabled(); + public static extern bool GetPointerTouchInfo(uint pointerId, out POINTER_TOUCH_INFO touchInfo); [DllImport("user32.dll", SetLastError = true)] - public static extern bool SkipPointerFrameMessages(uint pointerID); + public static extern bool GetPointerTouchInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_TOUCH_INFO[] touchInfos); [DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 34cd9acf25..fda68e1561 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -401,26 +401,41 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUP: { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var eventType = GetEventType(message, info); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + if (device is TouchDevice) + { + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); + } + else + { + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + } break; } case WindowsMessage.WM_POINTERUPDATE: { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); - var eventType = device is TouchDevice ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + if (device is TouchDevice) + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, + RawPointerEventType.TouchUpdate, point, modifiers, info.pointerId); + } + else + { + e = new RawPointerEventArgs(device, timestamp, _owner, + RawPointerEventType.Move, point, modifiers); + } break; } case WindowsMessage.WM_POINTERENTER: @@ -434,7 +449,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERLEAVE: { - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -456,7 +471,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERWHEEL: { - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -466,7 +481,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERHWHEEL: { - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -697,36 +712,10 @@ namespace Avalonia.Win32 } } - private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) - { - switch (info.pointerType) - { - case PointerInputType.PT_PEN: - return ToEventType(info.ButtonChangeType); - case PointerInputType.PT_TOUCH: - if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || - !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) - { - return RawPointerEventType.TouchCancel; - } - return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN - ? RawPointerEventType.TouchBegin - : RawPointerEventType.TouchEnd; - default: - var eventType = ToEventType(info.ButtonChangeType); - if (eventType == RawPointerEventType.LeftButtonDown && - message == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; - } - return eventType; - } - } - private unsafe void ApplyPenInfo(POINTER_PEN_INFO penInfo) { _penDevice.IsBarrel = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); - _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); + _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER); _penDevice.IsInverted = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED); _penDevice.XTilt = penInfo.tiltX; @@ -735,10 +724,11 @@ namespace Avalonia.Win32 _penDevice.Twist = penInfo.rotation; } - private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) + private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - GetPointerType(pointerId, out var type);//ToDo we can cache this and invalidate in WM_POINTERDEVICECHANGE + GetPointerType(pointerId, out var type); + //GetPointerCursorId(pointerId, out var cursorId); switch (type) { case PointerInputType.PT_PEN: @@ -758,6 +748,37 @@ namespace Avalonia.Win32 GetPointerInfo(pointerId, out info); break; } + + if (info.dwTime != 0) + { + timestamp = info.dwTime; + } + } + + private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) + { + switch (info.pointerType) + { + case PointerInputType.PT_PEN: + return ToEventType(info.ButtonChangeType); + case PointerInputType.PT_TOUCH: + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || + !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) + { + return RawPointerEventType.TouchCancel; + } + return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN + ? RawPointerEventType.TouchBegin + : RawPointerEventType.TouchEnd; + default: + var eventType = ToEventType(info.ButtonChangeType); + if (eventType == RawPointerEventType.LeftButtonDown && + message == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + return eventType; + } } private static RawPointerEventType ToEventType(PointerButtonChangeType type) From cbc813235a80918b5f0a57ca54f5abe5b47b08cb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 16:35:25 +0300 Subject: [PATCH 008/481] Combine some message handlers --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 68 +++++++------------ 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index fda68e1561..0266134199 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -399,26 +399,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCPOINTERUP: case WindowsMessage.WM_POINTERDOWN: case WindowsMessage.WM_POINTERUP: - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); - var eventType = GetEventType(message, info); - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - - if (device is TouchDevice) - { - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); - } - else - { - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); - } - break; - } case WindowsMessage.WM_POINTERUPDATE: { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); + GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + var eventType = GetEventType(message, info); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -428,13 +412,11 @@ namespace Avalonia.Win32 { break; } - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, - RawPointerEventType.TouchUpdate, point, modifiers, info.pointerId); + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); } else { - e = new RawPointerEventArgs(device, timestamp, _owner, - RawPointerEventType.Move, point, modifiers); + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); } break; } @@ -449,7 +431,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERLEAVE: { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); + GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -457,6 +439,18 @@ namespace Avalonia.Win32 device, timestamp, _owner, RawPointerEventType.LeaveWindow, point, modifiers); break; } + case WindowsMessage.WM_POINTERWHEEL: + case WindowsMessage.WM_POINTERHWHEEL: + { + GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var val = (ToInt32(wParam) >> 16) / wheelDelta; + var delta = message == WindowsMessage.WM_POINTERHWHEEL ? new Vector(0, val) : new Vector(val, 0); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); + break; + } case WindowsMessage.WM_POINTERACTIVATE: { //occurs when a pointer activates an inactive window. @@ -469,26 +463,6 @@ namespace Avalonia.Win32 _mouseDevice.Capture(null); return IntPtr.Zero; } - case WindowsMessage.WM_POINTERWHEEL: - { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); - - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); - break; - } - case WindowsMessage.WM_POINTERHWHEEL: - { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); - - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector((ToInt32(wParam) >> 16) / wheelDelta, 0); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); - break; - } case WindowsMessage.DM_POINTERHITTEST: { //DM stands for direct manipulation. @@ -724,7 +698,7 @@ namespace Avalonia.Win32 _penDevice.Twist = penInfo.rotation; } - private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) + private void GetDevicePointerInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetPointerType(pointerId, out var type); @@ -757,6 +731,12 @@ namespace Avalonia.Win32 private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) { + if (message == WindowsMessage.WM_POINTERUPDATE) + { + return info.pointerType == PointerInputType.PT_TOUCH + ? RawPointerEventType.TouchUpdate + : RawPointerEventType.Move; + } switch (info.pointerType) { case PointerInputType.PT_PEN: From c880572c5f711edb016a727f17003dabc2b1427f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 19:27:17 +0300 Subject: [PATCH 009/481] Handle history pointer infos for touch and pen --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0266134199..0aaa9d4ce0 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -402,6 +402,62 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + + if (info.historyCount > 1) + { + if (info.pointerType == PointerInputType.PT_TOUCH) + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyCount = (int)info.historyCount; + var historyTouchInfos = new POINTER_TOUCH_INFO[historyCount]; + if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyTouchInfo = historyTouchInfos[i]; + var historyInfo = historyTouchInfo.pointerInfo; + var historyEventType = GetEventType(message, historyInfo); + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); + var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; + Input?.Invoke(new RawTouchEventArgs(_touchDevice, historyTimestamp, _owner, + historyEventType, historyPoint, historyModifiers, historyInfo.pointerId)); + } + } + } + else if (info.pointerType == PointerInputType.PT_PEN) + { + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyCount = (int)info.historyCount; + var historyPenInfos = new POINTER_PEN_INFO[historyCount]; + if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyPenInfo = historyPenInfos[i]; + var historyInfo = historyPenInfo.pointerInfo; + var historyEventType = GetEventType(message, historyInfo); + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); + var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; + + ApplyPenInfo(historyPenInfo); + Input?.Invoke(new RawPointerEventArgs(_penDevice, historyTimestamp, _owner, + historyEventType, historyPoint, historyModifiers)); + } + } + } + } + var eventType = GetEventType(message, info); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -447,7 +503,7 @@ namespace Avalonia.Win32 var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); var val = (ToInt32(wParam) >> 16) / wheelDelta; - var delta = message == WindowsMessage.WM_POINTERHWHEEL ? new Vector(0, val) : new Vector(val, 0); + var delta = message == WindowsMessage.WM_POINTERWHEEL ? new Vector(0, val) : new Vector(val, 0); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } From 8cbd26ffdf6709e5e22487aeadc3d6b99f55af2c Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 19:49:32 +0300 Subject: [PATCH 010/481] Remove redundant changes, improve tab control pointer type check --- src/Avalonia.Controls/TabControl.cs | 6 ++++-- src/Avalonia.Input/IKeyboardDevice.cs | 2 -- src/Avalonia.Input/PointerPoint.cs | 2 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 306a9d3e6a..955b29f3f9 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -211,7 +211,8 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) { e.Handled = UpdateSelectionFromEventSource(e.Source); } @@ -219,7 +220,8 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + if (e.InitialPressMouseButton == MouseButton.Left && + (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index b3ea7c5f4f..9506dc36fb 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -47,8 +47,6 @@ namespace Avalonia.Input MiddleMouseButton = 64, XButton1MouseButton = 128, XButton2MouseButton = 256, - BarrelPenButton = 512, - PenEraser = 1024, KeyboardMask = Alt | Control | Shift | Meta } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 07ef130fcc..cdfcb8da3a 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -101,7 +101,7 @@ namespace Avalonia.Input RightButtonReleased, XButton1Released, XButton2Released, - Other, + Other } public static class PointerUpdateKindExtensions diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c75eb69bfd..d6406121c7 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -22,8 +22,6 @@ namespace Avalonia.Input.Raw TouchUpdate, TouchEnd, TouchCancel, - BarrelUp, - BarrelDown, Magnify, Rotate, Swipe From 5127006d1b84880b03fbfca7a25889c376fcc8be Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 22:12:34 +0300 Subject: [PATCH 011/481] fix touch issues --- .../GestureRecognizers/ScrollGestureRecognizer.cs | 2 +- src/Avalonia.Input/PointerPoint.cs | 2 -- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 7 +++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index f8cedb636f..1d97fdfe53 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -101,7 +101,7 @@ namespace Avalonia.Input.GestureRecognizers if (_scrolling) { var vector = _trackedRootPoint - rootPoint; - var elapsed = _lastMoveTimestamp.HasValue ? + var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ? TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : TimeSpan.Zero; diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index cdfcb8da3a..728bb1c579 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -45,8 +45,6 @@ namespace Avalonia.Input IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); - IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.BarrelPenButton); - IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0aaa9d4ce0..52981ac7ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -488,6 +488,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + if (device is TouchDevice) + { + break; + } var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -798,8 +802,7 @@ namespace Avalonia.Win32 case PointerInputType.PT_PEN: return ToEventType(info.ButtonChangeType); case PointerInputType.PT_TOUCH: - if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || - !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) { return RawPointerEventType.TouchCancel; } From 8dee09b830462ed3e0c5d6197c44a95f2049a935 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 23:13:39 +0300 Subject: [PATCH 012/481] Allow double click for pen device --- src/Avalonia.Input/PenDevice.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index c993e7fac4..aa4f89bb85 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Input @@ -12,6 +13,10 @@ namespace Avalonia.Input /// public class PenDevice : IPenDevice, IDisposable { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + private readonly Pointer _pointer; private bool _disposed; private PixelPoint? _position; @@ -182,8 +187,21 @@ namespace Avalonia.Input var source = GetSource(hit); if (source != null) { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, 1); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } From 600bb3198c775a76ac9034a5473fea795056be7b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 23 Jan 2022 21:20:02 -0500 Subject: [PATCH 013/481] Add pressure preview --- samples/ControlCatalog/Pages/PointersPage.cs | 266 ++++++++++++------- 1 file changed, 169 insertions(+), 97 deletions(-) diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index 2901013cea..9702e8e3e7 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; @@ -25,7 +26,8 @@ public class PointersPage : Decorator Items = new[] { new TabItem() { Header = "Contacts", Content = new PointerContactsTab() }, - new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() } + new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }, + new TabItem() { Header = "Pressure", Content = new PointerPressureTab() } } }; } @@ -148,7 +150,7 @@ public class PointersPage : Decorator { Children = { - new PointerCanvas(slider, status), + new PointerCanvas(slider, status, true), new Border { Background = Brushes.LightYellow, @@ -182,140 +184,210 @@ public class PointersPage : Decorator } }; } + } - class PointerCanvas : Control + public class PointerPressureTab : Decorator + { + public PointerPressureTab() { - private readonly Slider _slider; - private readonly TextBlock _status; - private int _events; - private Stopwatch _stopwatch = Stopwatch.StartNew(); - private Dictionary _pointers = new(); - class PointerPoints + this[TextBlock.ForegroundProperty] = Brushes.Black; + + var status = new TextBlock() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + FontSize = 12 + }; + Child = new Grid { - struct CanvasPoint + Children = { - public IBrush Brush; - public Point Point; - public double Radius; + new PointerCanvas(null, status, false), + status } + }; + } + } + + class PointerCanvas : Control + { + private readonly Slider? _slider; + private readonly TextBlock _status; + private readonly bool _drawPoints; + private int _events; + private Stopwatch _stopwatch = Stopwatch.StartNew(); + private IDisposable? _statusUpdated; + private Dictionary _pointers = new(); + private PointerPointProperties? _lastProperties; + class PointerPoints + { + struct CanvasPoint + { + public IBrush Brush; + public Point Point; + public double Radius; + public double Pressure; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; - readonly CanvasPoint[] _points = new CanvasPoint[1000]; - int _index; - - public void Render(DrawingContext context) + public void Render(DrawingContext context, bool drawPoints) + { + + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) { - - CanvasPoint? prev = null; - for (var c = 0; c < _points.Length; c++) + var i = (c + _index) % _points.Length; + var pt = _points[i]; + var thickness = pt.Pressure == 0 ? 1 : (pt.Pressure / 1024) * 5; + + if (drawPoints) { - var i = (c + _index) % _points.Length; - var pt = _points[i]; if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) - context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point); - prev = pt; + context.DrawLine(new Pen(Brushes.Black, thickness), prev.Value.Point, pt.Point); if (pt.Brush != null) context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius); - } - - } - - void AddPoint(Point pt, IBrush brush, double radius) - { - _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius }; - _index = (_index + 1) % _points.Length; - } - - public void HandleEvent(PointerEventArgs e, Visual v) - { - e.Handled = true; - if (e.RoutedEvent == PointerPressedEvent) - AddPoint(e.GetPosition(v), Brushes.Green, 10); - else if (e.RoutedEvent == PointerReleasedEvent) - AddPoint(e.GetPosition(v), Brushes.Red, 10); else { - var pts = e.GetIntermediatePoints(v); - for (var c = 0; c < pts.Count; c++) - { - var pt = pts[c]; - AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, - c == pts.Count - 1 ? 5 : 2); - } + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) + context.DrawLine(new Pen(Brushes.Black, thickness, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), prev.Value.Point, pt.Point); } + prev = pt; } + } - - public PointerCanvas(Slider slider, TextBlock status) + + void AddPoint(Point pt, IBrush brush, double radius, float pressure) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) { - _slider = slider; - _status = status; - DispatcherTimer.Run(() => + e.Handled = true; + var currentPoint = e.GetCurrentPoint(v); + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(currentPoint.Position, Brushes.Green, 10, currentPoint.Properties.Pressure); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(currentPoint.Position, Brushes.Red, 10, currentPoint.Properties.Pressure); + else { - if (_stopwatch.Elapsed.TotalSeconds > 1) + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) { - _status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds); - _stopwatch.Restart(); - _events = 0; + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); } - - return this.GetVisualRoot() != null; - }, TimeSpan.FromMilliseconds(10)); + } } + } + public PointerCanvas(Slider? slider, TextBlock status, bool drawPoints) + { + _slider = slider; + _status = status; + _drawPoints = drawPoints; + } - void HandleEvent(PointerEventArgs e) - { - _events++; - Thread.Sleep((int)_slider.Value); - InvalidateVisual(); + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); - if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + _statusUpdated = DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalSeconds > 1) { - _pointers.Remove(e.Pointer.Id); - return; + _status.Text = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} +IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} +IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} +IsEraser: {_lastProperties?.IsEraser} +IsInverted: {_lastProperties?.IsInverted} +Pressure: {_lastProperties?.Pressure} +XTilt: {_lastProperties?.XTilt} +YTilt: {_lastProperties?.YTilt} +Twist: {_lastProperties?.Twist}"; + _stopwatch.Restart(); + _events = 0; } - if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) - _pointers[e.Pointer.Id] = pt = new PointerPoints(); - pt.HandleEvent(e, this); - - - } - - public override void Render(DrawingContext context) + return true; + }, TimeSpan.FromMilliseconds(10)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _statusUpdated?.Dispose(); + } + + void HandleEvent(PointerEventArgs e) + { + _events++; + if (_slider != null) { - context.FillRectangle(Brushes.White, Bounds); - foreach(var pt in _pointers.Values) - pt.Render(context); - base.Render(context); + Thread.Sleep((int)_slider.Value); } + InvalidateVisual(); - protected override void OnPointerPressed(PointerPressedEventArgs e) + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) { - if (e.ClickCount == 2) - { - _pointers.Clear(); - InvalidateVisual(); - return; - } - - HandleEvent(e); - base.OnPointerPressed(e); + _pointers.Remove(e.Pointer.Id); + return; } + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; - protected override void OnPointerMoved(PointerEventArgs e) + if (e.Pointer.Type != PointerType.Pen + || lastPointer.Properties.Pressure > 0) { - HandleEvent(e); - base.OnPointerMoved(e); + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach (var pt in _pointers.Values) + pt.Render(context, _drawPoints); + base.Render(context); + } - protected override void OnPointerReleased(PointerReleasedEventArgs e) + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) { - HandleEvent(e); - base.OnPointerReleased(e); + _pointers.Clear(); + InvalidateVisual(); + return; } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); } - } } From bef72b3477574e8db361ee22f58cba6354ce435a Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 11:39:13 +0300 Subject: [PATCH 014/481] fix some issues by requests --- src/Avalonia.Controls/TabControl.cs | 6 +-- .../ScrollGestureRecognizer.cs | 3 +- src/Avalonia.Input/MouseDevice.cs | 39 +++++++------------ src/Avalonia.Input/PenDevice.cs | 13 +++---- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 955b29f3f9..306a9d3e6a 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -211,8 +211,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && - (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) { e.Handled = UpdateSelectionFromEventSource(e.Source); } @@ -220,8 +219,7 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.InitialPressMouseButton == MouseButton.Left && - (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) + if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 1d97fdfe53..889b7e3b82 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -66,7 +66,8 @@ namespace Avalonia.Input.GestureRecognizers public void PointerPressed(PointerPressedEventArgs e) { - if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) + if (e.Pointer.IsPrimary && + (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { EndGesture(); _tracking = e.Pointer; diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 087a806f77..38f63402ba 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -205,31 +205,20 @@ namespace Avalonia.Input PointerPointProperties CreateProperties(RawPointerEventArgs args) { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); + return new PointerPointProperties(args.InputModifiers, args.Type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed, + RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed, + RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed, + RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased, + RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased, + RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released, + RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released, + _ => PointerUpdateKind.Other, + }); } private MouseButton _lastMouseDownButton; diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index aa4f89bb85..0fef462831 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -160,13 +160,12 @@ namespace Avalonia.Input private PointerPointProperties CreateProperties(RawPointerEventArgs args) { - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - + var kind = args.Type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + _ => PointerUpdateKind.Other, + }; return new PointerPointProperties(args.InputModifiers, kind, Twist, Pressure, XTilt, YTilt, IsEraser, IsInverted, IsBarrel); } From dc9fc4ab146432c1b07e0af563487c6b53a7a47d Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 12:01:30 +0300 Subject: [PATCH 015/481] Handle Pointer History. Put into intermediate points. --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 52981ac7ec..7b5e42e490 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -402,6 +402,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + Point[]? intermediatePoints = null; if (info.historyCount > 1) { @@ -456,6 +457,23 @@ namespace Avalonia.Win32 } } } + else + { + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyCount = (int)info.historyCount; + var historyInfos = new POINTER_INFO[historyCount]; + if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) + { + //last info is the same as the current so skip it + intermediatePoints = new Point[historyCount - 1]; + for (int i = 0;i < historyCount - 1; i++) + { + var historyInfo = historyInfos[i]; + intermediatePoints[i] = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + } + } + } } var eventType = GetEventType(message, info); @@ -468,11 +486,17 @@ namespace Avalonia.Win32 { break; } - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId) + { + IntermediatePoints = intermediatePoints + }; } else { - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) + { + IntermediatePoints = intermediatePoints + }; } break; } From f04adadc89f04ba881c0c3811786706038059b7e Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 12:52:58 +0300 Subject: [PATCH 016/481] Release mouse capture if pen is close to digitizer. Release pen capture if pen is out of range of digitizer. --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 7b5e42e490..dd961aec48 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -381,13 +381,18 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_POINTERDEVICEINRANGE: - case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: { + _mouseDevice.Capture(null); //notifies about proximity of pointer device to the digitizer. //contains pointer id and proximity. //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdeviceinrange break; } + case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: + { + _penDevice.Capture(null); + break; + } case WindowsMessage.WM_NCPOINTERUPDATE: { //NC stands for non-client area - window header and window border @@ -545,6 +550,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERCAPTURECHANGED: { _mouseDevice.Capture(null); + _penDevice.Capture(null); return IntPtr.Zero; } case WindowsMessage.DM_POINTERHITTEST: From 903685839645adf7aa55e718372b26797c75c25f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 17:02:22 +0300 Subject: [PATCH 017/481] fix MK_NONE --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index d71efcb074..0431808a43 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -225,7 +225,7 @@ namespace Avalonia.Win32.Interop [Flags] public enum ModifierKeys { - MK_NONE = 0x0001, + MK_NONE = 0x0000, MK_LBUTTON = 0x0001, MK_RBUTTON = 0x0002, From a26059630f19e5ec8ceb5d65aff29193ee74a036 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 1 Feb 2022 14:57:08 +0100 Subject: [PATCH 018/481] Add failing test for control template value priority. Add tests to check that template values are assigned with Style priority and move other XAML control template tests alongside it in a new test class. --- .../Xaml/BasicTests.cs | 66 +------- .../Xaml/ControlTemplateTests.cs | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f20faa2287..16685c5d2d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -281,70 +281,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(expected4, grid.RowDefinitions[3].Height); } - [Fact] - public void ControlTemplate_With_Nested_Child_Is_Operational() - { - var xaml = @" - - - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - var parent = (ContentControl)template.Build(new ContentControl()).Control; - - Assert.Equal("parent", parent.Name); - - var child = parent.Content as ContentControl; - - Assert.NotNull(child); - - Assert.Equal("child", child.Name); - } - - [Fact] - public void ControlTemplate_With_TargetType_Is_Operational() - { - var xaml = @" - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - Assert.Equal(typeof(ContentControl), template.TargetType); - - Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); - } - - [Fact] - public void ControlTemplate_With_Panel_Children_Are_Added() - { - var xaml = @" - - - - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - var panel = (Panel)template.Build(new ContentControl()).Control; - - Assert.Equal(2, panel.Children.Count); - - var foo = panel.Children[0]; - var bar = panel.Children[1]; - - Assert.Equal("Foo", foo.Name); - Assert.Equal("Bar", bar.Name); - } - [Fact] public void Named_x_Control_Is_Added_To_NameScope_Simple() { @@ -361,7 +297,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Standart_TypeConverter_Is_Used() + public void Standard_TypeConverter_Is_Used() { var xaml = @""; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs new file mode 100644 index 0000000000..fbb16d9f2e --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -0,0 +1,143 @@ +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ControlTemplateTests : XamlTestBase + { + [Fact] + public void Inline_ControlTemplate_Values_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + + [Fact] + public void Style_ControlTemplate_Values_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Dock.Top, DockPanel.GetDock(presenter)); + + var diagnostic = presenter.GetDiagnostic(DockPanel.DockProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_TemplateBindings_Are_Set_With_TemplatedParent_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal("Foo", presenter.Content); + + var diagnostic = presenter.GetDiagnostic(ContentPresenter.ContentProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + [Fact] public void ControlTemplate_With_Nested_Child_Is_Operational() { From f7ff0a02893396f5ed28a033005d9907b97af096 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Feb 2022 23:48:49 +0100 Subject: [PATCH 023/481] More failing tests for ControlTemplate resources. --- .../Xaml/ControlTemplateTests.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index f74aaaed76..2c38a82031 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -107,6 +107,74 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void ControlTemplate_StaticResources_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + Red + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_DynamicResources_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + Red + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + [Fact] public void ControlTemplate_TemplateBindings_Are_Set_With_TemplatedParent_Priority() { From cb7488baa7ed96fedd0308f8363c7003f4484893 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 21 Feb 2022 22:54:58 +0100 Subject: [PATCH 024/481] Use Style priority for StaticResource in ControlTemplate. --- ...amlIlControlTemplatePriorityTransformer.cs | 33 +++++++++++-------- .../XamlIlAvaloniaPropertyHelper.cs | 32 ++++++++++++++++-- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs index eede21396d..e0090eb2e8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs @@ -18,26 +18,31 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // The node is a candidate for transformation if: // - It's a property assignment to an Avalonia property // - There's a ControlTemplate ancestor - // - There's just a direct call setter available + // - The property has a single value if (node is XamlPropertyAssignmentNode prop && prop.Property is XamlIlAvaloniaProperty avaloniaProperty && context.ParentNodes().Any(IsControlTemplate) && - prop.PossibleSetters.Count == 1 && - prop.PossibleSetters[0] is XamlDirectCallPropertySetter) + prop.Values.Count == 1) { - // Check if there are any setters on the property which accept a binding priority - - // this filters the candidates down to styled and attached properties. If so, then - // use this setter with BindingPriority.Style. - var setPriorityValueSetter = - avaloniaProperty.Setters.FirstOrDefault(x => x.Parameters[0] == bindingPriorityType); - - if(setPriorityValueSetter != null - && prop.Values.Count == 1 - && setPriorityValueSetter.Parameters[1].IsAssignableFrom(prop.Values[0].Type.GetClrType())) + var priorityValueSetters = new List(); + + // Iterate through the possible setters, trying to find a setter on the property + // which has a BindingPriority parameter followed by the parameter of the existing + // setter. + foreach (var setter in prop.PossibleSetters) + { + var s = avaloniaProperty.Setters.FirstOrDefault(x => + x.Parameters[0] == bindingPriorityType && + x.Parameters[1] == setter.Parameters[0]); + if (s != null) + priorityValueSetters.Add(s); + } + + // If any BindingPriority setters were found, use those. + if (priorityValueSetters.Count > 0) { - prop.PossibleSetters = new List { setPriorityValueSetter }; + prop.PossibleSetters = priorityValueSetters; prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.Style)); - return node; } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index bb439b8816..5c7a80e680 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -185,9 +185,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions AvaloniaXamlIlWellKnownTypes types) :base(original, original.Name, original.DeclaringType, original.Getter, original.Setters) { + var assignBinding = original.CustomAttributes.Any(ca => ca.Type.Equals(types.AssignBindingAttribute)); + AvaloniaProperty = field; CustomAttributes = original.CustomAttributes; - if (!original.CustomAttributes.Any(ca => ca.Type.Equals(types.AssignBindingAttribute))) + if (!assignBinding) Setters.Insert(0, new BindingSetter(types, original.DeclaringType, field)); // Styled and attached properties can be set with a BindingPriority when they're @@ -196,7 +198,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions field.FieldType.GenericTypeDefinition == types.AvaloniaAttachedPropertyT) { var propertyType = field.FieldType.GenericArguments[0]; - Setters.Insert(1, new SetValueWithPrioritySetter(types, original.DeclaringType, field, propertyType)); + Setters.Insert(0, new SetValueWithPrioritySetter(types, original.DeclaringType, field, propertyType)); + if (!assignBinding) + Setters.Insert(1, new BindingWithPrioritySetter(types, original.DeclaringType, field)); } Setters.Insert(0, new UnsetValueSetter(types, original.DeclaringType, field)); @@ -249,6 +253,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + class BindingWithPrioritySetter : AvaloniaPropertyCustomSetter + { + public BindingWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, + IXamlType declaringType, + IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) + { + Parameters = new[] { types.BindingPriority, types.IBinding }; + } + + public override void Emit(IXamlILEmitter emitter) + { + using (var bloc = emitter.LocalsPool.GetLocal(Types.IBinding)) + using (var priorityLocal = emitter.LocalsPool.GetLocal(Types.Int)) + emitter + .Stloc(bloc.Local) + .Stloc(priorityLocal.Local) + .Ldsfld(AvaloniaProperty) + .Ldloc(bloc.Local) + // TODO: provide anchor? + .Ldnull(); + emitter.EmitCall(Types.AvaloniaObjectBindMethod, true); + } + } + class SetValueWithPrioritySetter : AvaloniaPropertyCustomSetter { public SetValueWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index e0cfcd0ef6..a4897d581c 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit e0cfcd0ef687e613ef8905e8d0891974b68c565d +Subproject commit a4897d581c7f155543102f0d744a47863e1fbfea From a142b7ea04578e2b4cc5e521a38288fe9b3cd790 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Feb 2022 23:14:46 +0100 Subject: [PATCH 025/481] Detect control template from markup extensions. Allow markup extensions to detect whether they're in a control template and use that info in `DynamicResourceExtension` to use `Style` priority. --- .../Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj | 1 + src/Markup/Avalonia.Markup.Xaml/Extensions.cs | 4 ++-- .../MarkupExtensions/DynamicResourceExtension.cs | 8 ++++++-- .../Runtime/IAvaloniaXamlIlControlTemplateProvider.cs | 8 ++++++++ .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 5 ++++- 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 86132c5d27..4d9e216916 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs index 263750c316..d937a83010 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs @@ -24,10 +24,10 @@ namespace Avalonia.Markup.Xaml public static IEnumerable GetParents(this IServiceProvider sp) { return sp.GetService().Parents.OfType(); - - } + public static bool IsInControlTemplate(this IServiceProvider sp) => sp.GetService() != null; + public static Type ResolveType(this IServiceProvider ctx, string namespacePrefix, string type) { var tr = ctx.GetService(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 087611bd59..65e38508bc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -11,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public class DynamicResourceExtension : IBinding { private object? _anchor; + private BindingPriority _priority; public DynamicResourceExtension() { @@ -25,6 +26,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IBinding ProvideValue(IServiceProvider serviceProvider) { + if (serviceProvider.IsInControlTemplate()) + _priority = BindingPriority.Style; + var provideTarget = serviceProvider.GetService(); if (!(provideTarget.TargetObject is IStyledElement)) @@ -53,12 +57,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source); + return InstancedBinding.OneWay(source, _priority); } else if (_anchor is IResourceProvider resourceProvider) { var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source); + return InstancedBinding.OneWay(source, _priority); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs new file mode 100644 index 0000000000..ed3f5bfdff --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.Markup.Xaml.XamlIl.Runtime +{ + public interface IAvaloniaXamlIlControlTemplateProvider + { + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index c48f386ffd..a0e7fd7dcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -42,7 +42,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime class DeferredParentServiceProvider : IAvaloniaXamlIlParentStackProvider, IServiceProvider, - IRootObjectProvider + IRootObjectProvider, + IAvaloniaXamlIlControlTemplateProvider { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; @@ -75,6 +76,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return this; if (serviceType == typeof(IRootObjectProvider)) return this; + if (serviceType == typeof(IAvaloniaXamlIlControlTemplateProvider)) + return this; return _parentProvider?.GetService(serviceType); } From beaf956af72b4ad38a56f92b6d9ec048fedb2815 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Feb 2022 23:23:15 +0100 Subject: [PATCH 026/481] Use TemplatedParent priority in ControlTemplates. --- ...AvaloniaXamlIlControlTemplatePriorityTransformer.cs | 2 +- .../MarkupExtensions/DynamicResourceExtension.cs | 2 +- .../Xaml/ControlTemplateTests.cs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs index e0090eb2e8..6cab68e756 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs @@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (priorityValueSetters.Count > 0) { prop.PossibleSetters = priorityValueSetters; - prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.Style)); + prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.TemplatedParent)); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 65e38508bc..f13ecab4e1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -27,7 +27,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IBinding ProvideValue(IServiceProvider serviceProvider) { if (serviceProvider.IsInControlTemplate()) - _priority = BindingPriority.Style; + _priority = BindingPriority.TemplatedParent; var provideTarget = serviceProvider.GetService(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index 2c38a82031..2d6ed1dc62 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -38,7 +38,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -103,7 +103,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Dock.Top, DockPanel.GetDock(presenter)); var diagnostic = presenter.GetDiagnostic(DockPanel.DockProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -137,7 +137,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -171,7 +171,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } From c646343beee6da4f8568751ffd56bdd2da1f92ff Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 20 Mar 2022 23:54:04 -0400 Subject: [PATCH 027/481] WIP on intermediate points win --- src/Avalonia.Input/PointerEventArgs.cs | 3 +- src/Avalonia.Input/PointerPoint.cs | 17 +++ src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 10 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 141 +++++++++++------- 4 files changed, 112 insertions(+), 59 deletions(-) diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 0604d09dc4..a30c1c51dc 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -128,7 +128,8 @@ namespace Avalonia.Input for (var c = 0; c < previousPoints.Count; c++) { var pt = previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); + var pointProperties = new PointerPointProperties(_properties, pt); + points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), pointProperties); } points[points.Length - 1] = GetCurrentPoint(relativeTo); diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 728bb1c579..ba69add7d8 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -84,6 +84,23 @@ namespace Avalonia.Input IsBarrelButtonPressed = isBarrel; } + internal PointerPointProperties(PointerPointProperties basedOn, Raw.RawPointerPoint rawPoint) + { + IsLeftButtonPressed = basedOn.IsLeftButtonPressed; + IsMiddleButtonPressed = basedOn.IsMiddleButtonPressed; + IsRightButtonPressed = basedOn.IsRightButtonPressed; + IsXButton1Pressed = basedOn.IsXButton1Pressed; + IsXButton2Pressed = basedOn.IsXButton2Pressed; + IsInverted = basedOn.IsInverted; + IsEraser = basedOn.IsEraser; + IsBarrelButtonPressed = basedOn.IsBarrelButtonPressed; + + Twist = rawPoint.Twist; + Pressure = rawPoint.Pressure; + XTilt = rawPoint.XTilt; + YTilt = rawPoint.YTilt; + } + public static PointerPointProperties None { get; } = new PointerPointProperties(); } diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c157fa059c..cd99cdc23b 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -128,10 +128,16 @@ namespace Avalonia.Input.Raw /// Pointer position, in client DIPs. /// public Point Position { get; set; } - + + public float Twist { get; set; } + public float Pressure { get; set; } + public float XTilt { get; set; } + public float YTilt { get; set; } + + public RawPointerPoint() { - Position = default; + this = default; } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 1ef3cbaba2..5c17caa13b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text; @@ -413,78 +415,105 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); - Point[]? intermediatePoints = null; + if (info.pointerType == PointerInputType.PT_TOUCH + && ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + var historyCount = (int)info.historyCount; + Lazy> intermediatePoints = null; if (info.historyCount > 1) { - if (info.pointerType == PointerInputType.PT_TOUCH) + intermediatePoints = new Lazy>(() => { - if (ShouldIgnoreTouchEmulatedMessage()) + var list = new List(historyCount - 1); + if (info.pointerType == PointerInputType.PT_TOUCH) { - break; - } - - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyCount = (int)info.historyCount; - var historyTouchInfos = new POINTER_TOUCH_INFO[historyCount]; - if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyTouchInfos = ArrayPool.Shared.Rent(historyCount); + try + { + if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyTouchInfo = historyTouchInfos[i]; + var historyInfo = historyTouchInfo.pointerInfo; + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + list.Add(new RawPointerPoint + { + Position = historyPoint, + }); + } + } + } + finally { - var historyTouchInfo = historyTouchInfos[i]; - var historyInfo = historyTouchInfo.pointerInfo; - var historyEventType = GetEventType(message, historyInfo); - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); - var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; - Input?.Invoke(new RawTouchEventArgs(_touchDevice, historyTimestamp, _owner, - historyEventType, historyPoint, historyModifiers, historyInfo.pointerId)); + ArrayPool.Shared.Return(historyTouchInfos); } } - } - else if (info.pointerType == PointerInputType.PT_PEN) - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyCount = (int)info.historyCount; - var historyPenInfos = new POINTER_PEN_INFO[historyCount]; - if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) + else if (info.pointerType == PointerInputType.PT_PEN) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyPenInfos = ArrayPool.Shared.Rent(historyCount); + try { - var historyPenInfo = historyPenInfos[i]; - var historyInfo = historyPenInfo.pointerInfo; - var historyEventType = GetEventType(message, historyInfo); - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); - var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; - - ApplyPenInfo(historyPenInfo); - Input?.Invoke(new RawPointerEventArgs(_penDevice, historyTimestamp, _owner, - historyEventType, historyPoint, historyModifiers)); + if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyPenInfo = historyPenInfos[i]; + var historyInfo = historyPenInfo.pointerInfo; + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + list.Add(new RawPointerPoint + { + Position = historyPoint, + Pressure = historyPenInfo.pressure, + Twist = historyPenInfo.rotation, + XTilt = historyPenInfo.tiltX, + YTilt = historyPenInfo.tiltX + }); + } + } + } + finally + { + ArrayPool.Shared.Return(historyPenInfos); } } - } - else - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyCount = (int)info.historyCount; - var historyInfos = new POINTER_INFO[historyCount]; - if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) + else { - //last info is the same as the current so skip it - intermediatePoints = new Point[historyCount - 1]; - for (int i = 0;i < historyCount - 1; i++) + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyInfos = ArrayPool.Shared.Rent(historyCount); + try + { + if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyInfo = historyInfos[i]; + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + list.Add(new RawPointerPoint + { + Position = historyPoint + }); + } + } + } + finally { - var historyInfo = historyInfos[i]; - intermediatePoints[i] = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + ArrayPool.Shared.Return(historyInfos); } } - } + return list; + }); } var eventType = GetEventType(message, info); From d8f7dc5d1f2200a114404f9ed9cc7f34a58d0091 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 24 Mar 2022 20:28:56 -0400 Subject: [PATCH 028/481] Make EnableWmPointerEvents optional --- src/Windows/Avalonia.Win32/Win32Platform.cs | 9 +++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 66 +++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 14 ++-- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 5cfbab40e4..25a6717122 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -63,8 +63,17 @@ namespace Avalonia /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. /// + [Obsolete("Multitouch is always enabled")] public bool? EnableMultitouch { get; set; } = true; + /// + /// Enables Win8+ WM_POINTER events support. The default value is false. + /// + /// + /// Required for extended Pen and Touch support. + /// + public bool? EnableWmPointerEvents { get; set; } = false; + /// /// Embeds popups to the window when set to true. The default value is false. /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5c17caa13b..d52115425f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -174,7 +174,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -207,7 +207,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -240,7 +240,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEMOVE: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -275,7 +275,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -291,7 +291,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -307,7 +307,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSELEAVE: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -327,7 +327,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -351,7 +351,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -384,12 +384,20 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERDEVICECHANGE: { + if (!_wmPointerEnabled) + { + break; + } //notifies about changes in the settings of a monitor that has a digitizer attached to it. //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange break; } case WindowsMessage.WM_POINTERDEVICEINRANGE: { + if (!_wmPointerEnabled) + { + break; + } _mouseDevice.Capture(null); //notifies about proximity of pointer device to the digitizer. //contains pointer id and proximity. @@ -398,11 +406,19 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: { + if (!_wmPointerEnabled) + { + break; + } _penDevice.Capture(null); break; } case WindowsMessage.WM_NCPOINTERUPDATE: { + if (!_wmPointerEnabled) + { + break; + } //NC stands for non-client area - window header and window border //As I found above in an old message handling - we dont need to handle NC pointer move/updates. //All we need is pointer down and up. So this is skipped for now. @@ -415,8 +431,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); - if (info.pointerType == PointerInputType.PT_TOUCH - && ShouldIgnoreTouchEmulatedMessage()) + if (!_wmPointerEnabled) { break; } @@ -522,10 +537,6 @@ namespace Avalonia.Win32 if (device is TouchDevice) { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId) { IntermediatePoints = intermediatePoints @@ -542,6 +553,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERENTER: { + if (!_wmPointerEnabled) + { + break; + } //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM @@ -552,6 +567,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + if (!_wmPointerEnabled) + { + break; + } if (device is TouchDevice) { break; @@ -567,6 +586,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERHWHEEL: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + if (!_wmPointerEnabled) + { + break; + } var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -577,6 +600,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERACTIVATE: { + if (!_wmPointerEnabled) + { + break; + } //occurs when a pointer activates an inactive window. //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate @@ -584,6 +611,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERCAPTURECHANGED: { + if (!_wmPointerEnabled) + { + break; + } _mouseDevice.Capture(null); _penDevice.Capture(null); return IntPtr.Zero; @@ -913,8 +944,6 @@ namespace Avalonia.Win32 }; } - public readonly bool Win8Plus = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; - private void UpdateInputMethod(IntPtr hkl) { // note: for non-ime language, also create it so that emoji panel tracks cursor @@ -951,10 +980,7 @@ namespace Avalonia.Win32 private bool ShouldIgnoreTouchEmulatedMessage() { - if (!_multitouch) - { - return false; - } + // Note: GetMessageExtraInfo doesn't work with WM_POINTER events. // MI_WP_SIGNATURE // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index becbbe7561..53dde352f7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Win32 private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; - private bool _multitouch; + private bool _wmPointerEnabled; private IInputRoot _owner; private WindowProperties _windowProperties; private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk @@ -128,7 +128,10 @@ namespace Avalonia.Win32 egl.Display is AngleWin32EglDisplay angleDisplay && angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; - if (Win8Plus && !IsMouseInPointerEnabled()) + _wmPointerEnabled = Win32Platform.Options.EnableWmPointerEvents + ?? Win32Platform.WindowsVersion >= PlatformConstants.Windows8; + + if (_wmPointerEnabled && !IsMouseInPointerEnabled()) { EnableMouseInPointer(true); } @@ -795,12 +798,7 @@ namespace Avalonia.Win32 Handle = new WindowImplPlatformHandle(this); - _multitouch = Win32Platform.Options.EnableMultitouch ?? true; - - if (_multitouch) - { - RegisterTouchWindow(_hwnd, 0); - } + RegisterTouchWindow(_hwnd, 0); if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) { From b599f295f6b94d01bec4d267bc6759da1d69833c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 01:10:45 -0400 Subject: [PATCH 029/481] Add RawPointerId to the RawPointerEventArgs --- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 5 ++++ src/Avalonia.Input/Raw/RawTouchEventArgs.cs | 17 ++++++++++--- src/Avalonia.Input/TouchDevice.cs | 13 +++++----- src/Shared/RawEventGrouping.cs | 12 ++++------ .../TouchDeviceTests.cs | 24 ++++++++++++------- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 58ea076379..1faa20fbf1 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -87,6 +87,11 @@ namespace Avalonia.Input.Raw InputModifiers = inputModifiers; } + /// + /// Gets the raw pointer identifier. + /// + public long RawPointerId { get; set; } + /// /// Gets the pointer properties and position, in client DIPs. /// diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs index 020b40e55b..6706a45f48 100644 --- a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs @@ -1,15 +1,26 @@ +using System; + namespace Avalonia.Input.Raw { public class RawTouchEventArgs : RawPointerEventArgs { public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, RawPointerEventType type, Point position, RawInputModifiers inputModifiers, - long touchPointId) + long rawPointerId) : base(device, timestamp, root, type, position, inputModifiers) { - TouchPointId = touchPointId; + RawPointerId = rawPointerId; + } + + public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, + RawPointerEventType type, RawPointerPoint point, RawInputModifiers inputModifiers, + long rawPointerId) + : base(device, timestamp, root, type, point, inputModifiers) + { + RawPointerId = rawPointerId; } - public long TouchPointId { get; set; } + [Obsolete("Use RawPointerId")] + public long TouchPointId { get => RawPointerId; set => RawPointerId = value; } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 54dcc4051e..e914d860fd 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -40,14 +40,14 @@ namespace Avalonia.Input { if (ev.Handled || _disposed) return; - var args = (RawTouchEventArgs)ev; - if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) + var args = (RawPointerEventArgs)ev; + if (!_pointers.TryGetValue(args.RawPointerId, out var pointer)) { if (args.Type == RawPointerEventType.TouchEnd) return; var hit = args.InputHitTestResult; - _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), + _pointers[args.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, _pointers.Count == 0); pointer.Capture(hit); } @@ -88,7 +88,7 @@ namespace Avalonia.Input if (args.Type == RawPointerEventType.TouchEnd) { - _pointers.Remove(args.TouchPointId); + _pointers.Remove(args.RawPointerId); using (pointer) { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, @@ -101,7 +101,7 @@ namespace Avalonia.Input if (args.Type == RawPointerEventType.TouchCancel) { - _pointers.Remove(args.TouchPointId); + _pointers.Remove(args.RawPointerId); using (pointer) pointer.Capture(null); _lastPointer = null; @@ -129,8 +129,7 @@ namespace Avalonia.Input public IPointer? TryGetPointer(RawPointerEventArgs ev) { - return ev is RawTouchEventArgs args - && _pointers.TryGetValue(args.TouchPointId, out var pointer) + return _pointers.TryGetValue(ev.RawPointerId, out var pointer) ? pointer : null; } diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 084593ffc6..966744888c 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -2,10 +2,8 @@ using System; using System.Collections.Generic; using Avalonia.Collections.Pooled; -using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Threading; -using JetBrains.Annotations; namespace Avalonia; @@ -19,7 +17,7 @@ internal class RawEventGrouper : IDisposable private readonly Action _eventCallback; private readonly Queue _inputQueue = new(); private readonly Action _dispatchFromQueue; - readonly Dictionary _lastTouchPoints = new(); + readonly Dictionary _lastTouchPoints = new(); RawInputEventArgs? _lastEvent; public RawEventGrouper(Action eventCallback) @@ -49,7 +47,7 @@ internal class RawEventGrouper : IDisposable _lastEvent = null; if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate) - _lastTouchPoints.Remove(touchUpdate.TouchPointId); + _lastTouchPoints.Remove(touchUpdate.RawPointerId); _eventCallback?.Invoke(ev); @@ -88,11 +86,11 @@ internal class RawEventGrouper : IDisposable { if (args is RawTouchEventArgs touchEvent) { - if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent)) + if (_lastTouchPoints.TryGetValue(touchEvent.RawPointerId, out var lastTouchEvent)) MergeEvents(lastTouchEvent, touchEvent); else { - _lastTouchPoints[touchEvent.TouchPointId] = touchEvent; + _lastTouchPoints[touchEvent.RawPointerId] = touchEvent; AddToQueue(touchEvent); } } @@ -105,7 +103,7 @@ internal class RawEventGrouper : IDisposable { _lastTouchPoints.Clear(); if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent) - _lastTouchPoints[touchEvent.TouchPointId] = touchEvent; + _lastTouchPoints[touchEvent.RawPointerId] = touchEvent; } AddToQueue(args); } diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs index 80c5a45c1a..7b7d547346 100644 --- a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs @@ -219,30 +219,36 @@ namespace Avalonia.Input.UnitTests { for (int i = 0; i < touchPointIds.Length; i++) { - inputManager.ProcessInput(new RawTouchEventArgs(device, 0, + inputManager.ProcessInput(new RawPointerEventArgs(device, 0, root, type, new Point(0, 0), - RawInputModifiers.None, - touchPointIds[i])); + RawInputModifiers.None) + { + RawPointerId = touchPointIds[i] + }); } } private static void TapOnce(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0) { - inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp, + inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp, root, RawPointerEventType.TouchBegin, new Point(0, 0), - RawInputModifiers.None, - touchPointId)); - inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp, + RawInputModifiers.None) + { + RawPointerId = touchPointId + }); + inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp, root, RawPointerEventType.TouchEnd, new Point(0, 0), - RawInputModifiers.None, - touchPointId)); + RawInputModifiers.None) + { + RawPointerId = touchPointId + }); } } } From afb828d4ff3aac67f122b2badd6d5c7399c9081b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:00:29 -0400 Subject: [PATCH 030/481] Move IntermediatePoints creation and update RawInputModifiers --- src/Avalonia.Input/IKeyboardDevice.cs | 7 +- src/Avalonia.Input/PenDevice.cs | 404 ++++-------------- src/Avalonia.Input/PointerPoint.cs | 12 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 4 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 379 ++++++++-------- src/Windows/Avalonia.Win32/WindowImpl.cs | 7 + 6 files changed, 278 insertions(+), 535 deletions(-) diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index d0e84e5ad0..1b9a056272 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -42,12 +42,17 @@ namespace Avalonia.Input Control = 2, Shift = 4, Meta = 8, + LeftMouseButton = 16, RightMouseButton = 32, MiddleMouseButton = 64, XButton1MouseButton = 128, XButton2MouseButton = 256, - KeyboardMask = Alt | Control | Shift | Meta + KeyboardMask = Alt | Control | Shift | Meta, + + PenInverted = 512, + PenEraser = 1024, + PenBarrelButton = 2048 } public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index 0fef462831..d22b48562c 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.VisualTree; @@ -13,72 +13,14 @@ namespace Avalonia.Input /// public class PenDevice : IPenDevice, IDisposable { + private readonly Dictionary _pointers = new(); + private readonly Dictionary _lastPositions = new(); private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; + private MouseButton _lastMouseDownButton; - private readonly Pointer _pointer; private bool _disposed; - private PixelPoint? _position; - - public PenDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Pen, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - public bool IsEraser { get; set; } - public bool IsInverted { get; set; } - public bool IsBarrel { get; set; } - public int XTilt { get; set; } - public int YTilt { get; set; } - public uint Pressure { get; set; } - public uint Twist { get; set; } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(_position ?? new PixelPoint(-1, -1)); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } public void ProcessRawEvent(RawInputEventArgs e) { @@ -86,151 +28,93 @@ namespace Avalonia.Input ProcessRawEvent(margs); } - public void TopLevelClosed(IInputRoot root) + private void ProcessRawEvent(RawPointerEventArgs e) { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } + e = e ?? throw new ArgumentNullException(nameof(e)); - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) + if (!_pointers.TryGetValue(e.RawPointerId, out var pointer)) { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); + if (e.Type == RawPointerEventType.LeftButtonUp + || e.Type == RawPointerEventType.TouchEnd) + return; - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } + _pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(), + PointerType.Pen, _pointers.Count == 0); } - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - var pen = (PenDevice)e.Device; - if(pen._disposed) - return; + _lastPositions[e.RawPointerId] = e.Root.PointToScreen(e.Position); + + var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(), + e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt); + var keyModifiers = e.InputModifiers.ToKeyModifiers(); - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + bool shouldReleasePointer = false; switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(pen, e.Timestamp, e.Root, props, keyModifiers); + shouldReleasePointer = true; break; case RawPointerEventType.LeftButtonDown: - e.Handled = PenDown(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = PenDown(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.LeftButtonUp: - e.Handled = PenUp(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = PenUp(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Move: - e.Handled = PenMove(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = PenMove(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult, e.IntermediatePoints); break; } - } - - private void LeaveWindow(IPenDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - private PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - var kind = args.Type switch + if (shouldReleasePointer) { - RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, - RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, - _ => PointerUpdateKind.Other, - }; - return new PointerPointProperties(args.InputModifiers, kind, - Twist, Pressure, XTilt, YTilt, IsEraser, IsInverted, IsBarrel); + pointer.Dispose(); + _pointers.Remove(e.RawPointerId); + _lastPositions.Remove(e.RawPointerId); + } } - private MouseButton _lastMouseDownButton; - private bool PenDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) + private bool PenDown(Pointer pointer, ulong timestamp, + IInputElement root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); + var source = pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } + pointer.Capture(source); + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; } return false; } - private bool PenMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) + private bool PenMove(Pointer pointer, ulong timestamp, + IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest, + Lazy?>? intermediatePoints) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } + var source = pointer.Captured ?? hitTest; - if (source is object) + if (source is not null) { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, root, + p, timestamp, properties, inputModifiers, intermediatePoints); source.RaiseEvent(e); return e.Handled; @@ -239,176 +123,52 @@ namespace Avalonia.Input return false; } - private bool PenUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) + private bool PenUp(Pointer pointer, ulong timestamp, + IInputElement root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); + var source = pointer.Captured ?? hitTest; if (source is not null) { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + var e = new PointerReleasedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers, _lastMouseDownButton); source?.RaiseEvent(e); - _pointer.Capture(null); + pointer.Capture(null); return e.Handled; } return false; } - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) + public void Dispose() { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; + if (_disposed) + return; + var values = _pointers.Values.ToList(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); } - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); + [Obsolete] + IInputElement? IPointerDevice.Captured => _pointers.Values + .FirstOrDefault(p => p.IsPrimary)?.Captured; - IInputElement? branch = null; + [Obsolete] + void IPointerDevice.Capture(IInputElement? control) => _pointers.Values + .FirstOrDefault(p => p.IsPrimary)?.Capture(control); - IInputElement? el = element; + [Obsolete] + Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1); - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() + public IPointer? TryGetPointer(RawPointerEventArgs ev) { - _disposed = true; - _pointer?.Dispose(); + return _pointers.TryGetValue(ev.RawPointerId, out var pointer) + ? pointer + : null; } } } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index ba69add7d8..c704aa28c6 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -1,3 +1,5 @@ +using Avalonia.Input.Raw; + namespace Avalonia.Input { public sealed class PointerPoint @@ -45,6 +47,9 @@ namespace Avalonia.Input IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); + IsInverted = modifiers.HasAllFlags(RawInputModifiers.PenInverted); + IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser); + IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.PenBarrelButton); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state @@ -72,19 +77,16 @@ namespace Avalonia.Input } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, - float twist, float pressure, float xTilt, float yTilt, bool isEraser, bool isInverted, bool isBarrel + float twist, float pressure, float xTilt, float yTilt ) : this (modifiers, kind) { Twist = twist; Pressure = pressure; XTilt = xTilt; YTilt = yTilt; - IsEraser = isEraser; - IsInverted = isInverted; - IsBarrelButtonPressed = isBarrel; } - internal PointerPointProperties(PointerPointProperties basedOn, Raw.RawPointerPoint rawPoint) + internal PointerPointProperties(PointerPointProperties basedOn, RawPointerPoint rawPoint) { IsLeftButtonPressed = basedOn.IsLeftButtonPressed; IsMiddleButtonPressed = basedOn.IsMiddleButtonPressed; diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 1faa20fbf1..0e4e0ed3e2 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -56,11 +56,12 @@ namespace Avalonia.Input.Raw Contract.Requires(device != null); Contract.Requires(root != null); + Point = new RawPointerPoint(); Position = position; Type = type; InputModifiers = inputModifiers; } - + /// /// Initializes a new instance of the class. /// @@ -145,6 +146,7 @@ namespace Avalonia.Input.Raw public RawPointerPoint() { this = default; + Pressure = 0.5f; } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d52115425f..6619c60152 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -380,48 +380,6 @@ namespace Avalonia.Win32 return IntPtr.Zero; } - break; - } - case WindowsMessage.WM_POINTERDEVICECHANGE: - { - if (!_wmPointerEnabled) - { - break; - } - //notifies about changes in the settings of a monitor that has a digitizer attached to it. - //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange - break; - } - case WindowsMessage.WM_POINTERDEVICEINRANGE: - { - if (!_wmPointerEnabled) - { - break; - } - _mouseDevice.Capture(null); - //notifies about proximity of pointer device to the digitizer. - //contains pointer id and proximity. - //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdeviceinrange - break; - } - case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: - { - if (!_wmPointerEnabled) - { - break; - } - _penDevice.Capture(null); - break; - } - case WindowsMessage.WM_NCPOINTERUPDATE: - { - if (!_wmPointerEnabled) - { - break; - } - //NC stands for non-client area - window header and window border - //As I found above in an old message handling - we dont need to handle NC pointer move/updates. - //All we need is pointer down and up. So this is skipped for now. break; } case WindowsMessage.WM_NCPOINTERDOWN: @@ -430,194 +388,92 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUP: case WindowsMessage.WM_POINTERUPDATE: { - GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); if (!_wmPointerEnabled) { break; } - - var historyCount = (int)info.historyCount; - Lazy> intermediatePoints = null; - if (info.historyCount > 1) - { - intermediatePoints = new Lazy>(() => - { - var list = new List(historyCount - 1); - if (info.pointerType == PointerInputType.PT_TOUCH) - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyTouchInfos = ArrayPool.Shared.Rent(historyCount); - try - { - if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) - { - var historyTouchInfo = historyTouchInfos[i]; - var historyInfo = historyTouchInfo.pointerInfo; - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - list.Add(new RawPointerPoint - { - Position = historyPoint, - }); - } - } - } - finally - { - ArrayPool.Shared.Return(historyTouchInfos); - } - } - else if (info.pointerType == PointerInputType.PT_PEN) - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyPenInfos = ArrayPool.Shared.Rent(historyCount); - try - { - if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) - { - var historyPenInfo = historyPenInfos[i]; - var historyInfo = historyPenInfo.pointerInfo; - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - list.Add(new RawPointerPoint - { - Position = historyPoint, - Pressure = historyPenInfo.pressure, - Twist = historyPenInfo.rotation, - XTilt = historyPenInfo.tiltX, - YTilt = historyPenInfo.tiltX - }); - } - } - } - finally - { - ArrayPool.Shared.Return(historyPenInfos); - } - } - else - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyInfos = ArrayPool.Shared.Rent(historyCount); - try - { - if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) - { - var historyInfo = historyInfos[i]; - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - list.Add(new RawPointerPoint - { - Position = historyPoint - }); - } - } - } - finally - { - ArrayPool.Shared.Return(historyInfos); - } - } - return list; - }); - } - + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); var eventType = GetEventType(message, info); - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - if (device is TouchDevice) - { - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId) - { - IntermediatePoints = intermediatePoints - }; - } - else - { - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) - { - IntermediatePoints = intermediatePoints - }; - } - break; - } - case WindowsMessage.WM_POINTERENTER: - { - if (!_wmPointerEnabled) - { - break; - } - //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. - //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM - - //note: by using a pen there can be a pointer leave or enter inside a window coords - //when you are just lift up the pen above the display + var args = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); + args.IntermediatePoints = CreateLazyIntermediatePoints(info); + e = args; break; } + case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: case WindowsMessage.WM_POINTERLEAVE: + case WindowsMessage.WM_POINTERCAPTURECHANGED: { - GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); if (!_wmPointerEnabled) { break; } - if (device is TouchDevice) - { - break; - } - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - - e = new RawPointerEventArgs( - device, timestamp, _owner, RawPointerEventType.LeaveWindow, point, modifiers); + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); + var eventType = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; + e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); break; } case WindowsMessage.WM_POINTERWHEEL: case WindowsMessage.WM_POINTERHWHEEL: { - GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); if (!_wmPointerEnabled) { break; } + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); var val = (ToInt32(wParam) >> 16) / wheelDelta; var delta = message == WindowsMessage.WM_POINTERWHEEL ? new Vector(0, val) : new Vector(val, 0); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point.Position, delta, modifiers) + { + RawPointerId = info.pointerId + }; break; } - case WindowsMessage.WM_POINTERACTIVATE: + case WindowsMessage.WM_POINTERDEVICEINRANGE: { if (!_wmPointerEnabled) { break; } + + // Do not generate events, but release mouse capture on any other device input. + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); + if (device != _mouseDevice) + { + _mouseDevice.Capture(null); + return IntPtr.Zero; + } + break; + } + case WindowsMessage.WM_POINTERACTIVATE: + { //occurs when a pointer activates an inactive window. //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate break; } - case WindowsMessage.WM_POINTERCAPTURECHANGED: + case WindowsMessage.WM_POINTERDEVICECHANGE: { - if (!_wmPointerEnabled) - { - break; - } - _mouseDevice.Capture(null); - _penDevice.Capture(null); - return IntPtr.Zero; + //notifies about changes in the settings of a monitor that has a digitizer attached to it. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange + break; + } + case WindowsMessage.WM_NCPOINTERUPDATE: + { + //NC stands for non-client area - window header and window border + //As I found above in an old message handling - we dont need to handle NC pointer move/updates. + //All we need is pointer down and up. So this is skipped for now. + break; + } + case WindowsMessage.WM_POINTERENTER: + { + //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. + //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM + + //note: by using a pen there can be a pointer leave or enter inside a window coords + //when you are just lift up the pen above the display + break; } case WindowsMessage.DM_POINTERHITTEST: { @@ -839,6 +695,11 @@ namespace Avalonia.Win32 _ignoreWmChar = e.Handled; } + if (s_intermediatePointsPooledList.Count > 0) + { + s_intermediatePointsPooledList.Dispose(); + } + if (e.Handled) { return IntPtr.Zero; @@ -851,40 +712,110 @@ namespace Avalonia.Win32 } } - private unsafe void ApplyPenInfo(POINTER_PEN_INFO penInfo) + private unsafe Lazy> CreateLazyIntermediatePoints(POINTER_INFO info) { - _penDevice.IsBarrel = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); - _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER); - _penDevice.IsInverted = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED); - - _penDevice.XTilt = penInfo.tiltX; - _penDevice.YTilt = penInfo.tiltY; - _penDevice.Pressure = penInfo.pressure; - _penDevice.Twist = penInfo.rotation; + // Limit history size with reasonable value. + // With sizeof(POINTER_TOUCH_INFO) * 100 we can get maximum 14400 bytes. + var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize); + if (historyCount > 1) + { + return new Lazy>(() => + { + s_intermediatePointsPooledList.Clear(); + s_intermediatePointsPooledList.Capacity = historyCount; + if (info.pointerType == PointerInputType.PT_TOUCH) + { + if (GetPointerTouchInfoHistory(info.pointerId, ref historyCount, s_historyTouchInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyTouchInfo = s_historyTouchInfos[i]; + s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyTouchInfo)); + } + } + } + else if (info.pointerType == PointerInputType.PT_PEN) + { + if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyPenInfo = s_historyPenInfos[i]; + s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyPenInfo)); + } + } + } + else + { + // Currently Windows does not return history info for mouse input, but we handle it just for case. + if (GetPointerInfoHistory(info.pointerId, ref historyCount, s_historyInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyInfo = s_historyInfos[i]; + s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyInfo)); + } + } + } + return s_intermediatePointsPooledList; + }); + } + + return null; + } + + private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId) + { + return device is TouchDevice + ? new RawTouchEventArgs(device, timestamp, _owner, eventType, point, modifiers, rawPointerId) + : new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) + { + RawPointerId = rawPointerId + }; } - private void GetDevicePointerInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) + private void GetDevicePointerInfo(IntPtr wParam, + out IPointerDevice device, out POINTER_INFO info, out RawPointerPoint point, + out RawInputModifiers modifiers, ref uint timestamp) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetPointerType(pointerId, out var type); - //GetPointerCursorId(pointerId, out var cursorId); + + modifiers = default; + switch (type) { case PointerInputType.PT_PEN: device = _penDevice; GetPointerPenInfo(pointerId, out var penInfo); info = penInfo.pointerInfo; - - ApplyPenInfo(penInfo); + point = CreateRawPointerPoint(penInfo); + if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL)) + { + modifiers |= RawInputModifiers.PenBarrelButton; + } + if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER)) + { + modifiers |= RawInputModifiers.PenEraser; + } + if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED)) + { + modifiers |= RawInputModifiers.PenInverted; + } break; case PointerInputType.PT_TOUCH: device = _touchDevice; GetPointerTouchInfo(pointerId, out var touchInfo); info = touchInfo.pointerInfo; + point = CreateRawPointerPoint(touchInfo); break; default: device = _mouseDevice; GetPointerInfo(pointerId, out info); + point = CreateRawPointerPoint(info); break; } @@ -892,6 +823,44 @@ namespace Avalonia.Win32 { timestamp = info.dwTime; } + + modifiers |= GetInputModifiers(info.dwKeyStates); + } + + private RawPointerPoint CreateRawPointerPoint(POINTER_INFO pointerInfo) + { + var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY)); + return new RawPointerPoint + { + Position = point + }; + } + private RawPointerPoint CreateRawPointerPoint(POINTER_TOUCH_INFO info) + { + var pointerInfo = info.pointerInfo; + var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY)); + return new RawPointerPoint + { + Position = point, + // POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default. + // But in our API we use range from 0.0 to 1.0. + Pressure = info.pressure / 1024f + }; + } + private RawPointerPoint CreateRawPointerPoint(POINTER_PEN_INFO info) + { + var pointerInfo = info.pointerInfo; + var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY)); + return new RawPointerPoint + { + Position = point, + // POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default. + // But in our API we use range from 0.0 to 1.0. + Pressure = info.pressure / 1024f, + Twist = info.rotation, + XTilt = info.tiltX, + YTilt = info.tiltX + }; } private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) @@ -904,8 +873,6 @@ namespace Avalonia.Win32 } switch (info.pointerType) { - case PointerInputType.PT_PEN: - return ToEventType(info.ButtonChangeType); case PointerInputType.PT_TOUCH: if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 64295f8925..e0f6348c23 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -21,6 +21,7 @@ using Avalonia.Win32.OpenGl; using Avalonia.Win32.WinRT; using Avalonia.Win32.WinRT.Composition; using static Avalonia.Win32.Interop.UnmanagedMethods; +using Avalonia.Collections.Pooled; namespace Avalonia.Win32 { @@ -95,6 +96,12 @@ namespace Avalonia.Win32 private uint _langid; private bool _ignoreWmChar; + private const int MaxPointerHistorySize = 512; + private readonly static PooledList s_intermediatePointsPooledList = new(); + private readonly static POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize]; + private readonly static POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize]; + private readonly static POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize]; + public WindowImpl() { _touchDevice = new TouchDevice(); From 99e444f5f19a9312ac4eb99076084b18b92040e3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:01:07 -0400 Subject: [PATCH 031/481] Update documentation of pointer types --- src/Avalonia.Input/IPointer.cs | 40 +++++++++++- src/Avalonia.Input/PointerEventArgs.cs | 19 +++++- src/Avalonia.Input/PointerPoint.cs | 86 ++++++++++++++++++++++++-- 3 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index 361f3ac370..98f7c81f02 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -1,15 +1,53 @@ namespace Avalonia.Input { + /// + /// Identifies specific pointer generated by input device. + /// + /// + /// Some devices, for instance, touchscreen might generate a pointer on each physical contact. + /// public interface IPointer { + /// + /// Gets a unique identifier for the input pointer. + /// int Id { get; } + + /// + /// Captures pointer input to the specified control. + /// + /// The control. + /// + /// When an element captures the pointer, it receives pointer input whether the cursor is + /// within the control's bounds or not. The current pointer capture control is exposed + /// by the property. + /// void Capture(IInputElement? control); + + /// + /// Gets the control that is currently capturing by the pointer, if any. + /// + /// + /// When an element captures the pointer, it receives pointer input whether the cursor is + /// within the control's bounds or not. To set the pointer capture, call the + /// method. + /// IInputElement? Captured { get; } + + /// + /// Gets the pointer device type. + /// PointerType Type { get; } + + /// + /// Gets a value that indicates whether the input is from the primary pointer when multiple pointers are registered. + /// bool IsPrimary { get; } - } + /// + /// Enumerates pointer device types. + /// public enum PointerType { Mouse, diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 79335eb9fc..058c2f9cc1 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -67,7 +67,14 @@ namespace Avalonia.Input public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer; } + /// + /// Gets specific pointer generated by input device. + /// public IPointer Pointer { get; } + + /// + /// Gets the time when the input occurred. + /// public ulong Timestamp { get; } private IPointerDevice? _device; @@ -91,7 +98,10 @@ namespace Avalonia.Input return mods; } } - + + /// + /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated. + /// public KeyModifiers KeyModifiers { get; } private Point GetPosition(Point pt, IVisual? relativeTo) @@ -102,7 +112,12 @@ namespace Avalonia.Input return pt; return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; } - + + /// + /// Gets the pointer position relative to a control. + /// + /// The control. + /// The pointer position in the control's coordinates. public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); [Obsolete("Use GetCurrentPoint")] diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index c704aa28c6..71145b5cb0 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -2,6 +2,9 @@ using Avalonia.Input.Raw; namespace Avalonia.Input { + /// + /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact. + /// public sealed class PointerPoint { public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) @@ -10,34 +13,109 @@ namespace Avalonia.Input Position = position; Properties = properties; } + + /// + /// Gets specific pointer generated by input device. + /// public IPointer Pointer { get; } + + /// + /// Gets extended information about the input pointer. + /// public PointerPointProperties Properties { get; } + + /// + /// Gets the location of the pointer input in client coordinates. + /// public Point Position { get; } } + /// + /// Provides extended properties for a PointerPoint object. + /// public sealed class PointerPointProperties { + /// + /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device. + /// public bool IsLeftButtonPressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device. + /// public bool IsMiddleButtonPressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device. + /// public bool IsRightButtonPressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1). + /// public bool IsXButton1Pressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2). + /// public bool IsXButton2Pressed { get; } + + /// + /// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed. + /// public bool IsBarrelButtonPressed { get; } + + /// + /// Gets a value that indicates whether the input is from a pen eraser. + /// public bool IsEraser { get; } + + /// + /// Gets a value that indicates whether the digitizer pen is inverted. + /// public bool IsInverted { get; } + /// + /// Gets the clockwise rotation in degrees of a pen device around its own major axis (such as when the user spins the pen in their fingers). + /// + /// + /// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0. + /// public float Twist { get; } - public float Pressure { get; } + + /// + /// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer. + /// + /// + /// A value from 0 to 1.0. The default value is 0.5. + /// + public float Pressure { get; } = 0.5f; + + /// + /// Gets the plane angle between the Y-Z plane and the plane that contains the Y axis and the axis of the input device (typically a pen/stylus). + /// + /// + /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted to the right of perpendicular, and between 0.0 and -90.0 when tilted to the left of perpendicular. The default value is 0.0. + /// public float XTilt { get; } - public float YTilt { get; } + /// + /// Gets the plane angle between the X-Z plane and the plane that contains the X axis and the axis of the input device (typically a pen/stylus). + /// + /// + /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted towards the user, and between 0.0 and -90.0 when tilted away from the user. The default value is 0.0. + /// + public float YTilt { get; } + /// + /// Gets the kind of pointer state change. + /// public PointerUpdateKind PointerUpdateKind { get; } private PointerPointProperties() - { + { } - + public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { PointerUpdateKind = kind; From bd2578d68359abcc44eb83118b907f74abaf3ae2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:31:57 -0400 Subject: [PATCH 032/481] Update control catalog pointers page --- samples/ControlCatalog.NetCore/Program.cs | 2 +- samples/ControlCatalog/MainView.xaml | 2 +- samples/ControlCatalog/Pages/PointerCanvas.cs | 221 ++++++++++ .../Pages/PointerContactsTab.cs | 109 +++++ samples/ControlCatalog/Pages/PointersPage.cs | 394 ------------------ .../ControlCatalog/Pages/PointersPage.xaml | 65 +++ .../ControlCatalog/Pages/PointersPage.xaml.cs | 76 ++++ 7 files changed, 473 insertions(+), 396 deletions(-) create mode 100644 samples/ControlCatalog/Pages/PointerCanvas.cs create mode 100644 samples/ControlCatalog/Pages/PointerContactsTab.cs delete mode 100644 samples/ControlCatalog/Pages/PointersPage.cs create mode 100644 samples/ControlCatalog/Pages/PointersPage.xaml create mode 100644 samples/ControlCatalog/Pages/PointersPage.xaml.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4b81935452..08ac17d0c4 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -114,7 +114,7 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true + EnableWmPointerEvents = true }) .UseSkia() .UseManagedSystemDialogs() diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 85f278b5fa..c61ba231e0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -106,7 +106,7 @@ - + diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs new file mode 100644 index 0000000000..b815a573f2 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -0,0 +1,221 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages; + +public class PointerCanvas : Control +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _events; + private IDisposable? _statusUpdated; + private Dictionary _pointers = new(); + private PointerPointProperties? _lastProperties; + class PointerPoints + { + struct CanvasPoint + { + public IBrush Brush; + public Point Point; + public double Radius; + public double? Pressure; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; + + public void Render(DrawingContext context, bool drawPoints) + { + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) + { + var i = (c + _index) % _points.Length; + var pt = _points[i]; + var pressure = (pt.Pressure ?? prev?.Pressure ?? 0.5); + var thickness = pressure * 10; + var radius = pressure * pt.Radius; + + if (drawPoints) + { + if (pt.Brush != null) + { + context.DrawEllipse(pt.Brush, null, pt.Point, radius, radius); + } + } + else + { + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null + && prev.Value.Pressure != null && pt.Pressure != null) + { + var linePen = new Pen(Brushes.Black, thickness, null, PenLineCap.Round, PenLineJoin.Round); + context.DrawLine(linePen, prev.Value.Point, pt.Point); + } + } + prev = pt; + } + + } + + void AddPoint(Point pt, IBrush brush, double radius, float? pressure = null) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) + { + e.Handled = true; + var currentPoint = e.GetCurrentPoint(v); + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(currentPoint.Position, Brushes.Green, 10); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(currentPoint.Position, Brushes.Red, 10); + else + { + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) + { + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); + } + } + } + } + + private int _threadSleep; + public static DirectProperty ThreadSleepProperty = + AvaloniaProperty.RegisterDirect(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v); + + public int ThreadSleep + { + get => _threadSleep; + set => SetAndRaise(ThreadSleepProperty, ref _threadSleep, value); + } + + private bool _drawOnlyPoints; + public static DirectProperty DrawOnlyPointsProperty = + AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v); + + public bool DrawOnlyPoints + { + get => _drawOnlyPoints; + set => SetAndRaise(DrawOnlyPointsProperty, ref _drawOnlyPoints, value); + } + + private string? _status; + public static DirectProperty StatusProperty = + AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v, + defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); + + public string? Status + { + get => _status; + set => SetAndRaise(StatusProperty, ref _status, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + _statusUpdated = DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalSeconds > 1) + { + Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} +IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} +IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} +IsEraser: {_lastProperties?.IsEraser} +IsInverted: {_lastProperties?.IsInverted} +Pressure: {_lastProperties?.Pressure} +XTilt: {_lastProperties?.XTilt} +YTilt: {_lastProperties?.YTilt} +Twist: {_lastProperties?.Twist}"; + _stopwatch.Restart(); + _events = 0; + } + + return true; + }, TimeSpan.FromMilliseconds(10)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _statusUpdated?.Dispose(); + } + + void HandleEvent(PointerEventArgs e) + { + _events++; + if (_threadSleep != 0) + { + Thread.Sleep(_threadSleep); + } + InvalidateVisual(); + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + _pointers.Remove(e.Pointer.Id); + return; + } + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; + + if (e.Pointer.Type != PointerType.Pen + || lastPointer.Properties.Pressure > 0) + { + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); + } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach (var pt in _pointers.Values) + pt.Render(context, _drawOnlyPoints); + base.Render(context); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) + { + _pointers.Clear(); + InvalidateVisual(); + return; + } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); + } +} diff --git a/samples/ControlCatalog/Pages/PointerContactsTab.cs b/samples/ControlCatalog/Pages/PointerContactsTab.cs new file mode 100644 index 0000000000..b6aabebf99 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointerContactsTab.cs @@ -0,0 +1,109 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace ControlCatalog.Pages; + +public class PointerContactsTab : Control +{ + class PointerInfo + { + public Point Point { get; set; } + public Color Color { get; set; } + } + + private static Color[] AllColors = new[] + { + Colors.Aqua, + Colors.Beige, + Colors.Chartreuse, + Colors.Coral, + Colors.Fuchsia, + Colors.Crimson, + Colors.Lavender, + Colors.Orange, + Colors.Orchid, + Colors.ForestGreen, + Colors.SteelBlue, + Colors.PapayaWhip, + Colors.PaleVioletRed, + Colors.Goldenrod, + Colors.Maroon, + Colors.Moccasin, + Colors.Navy, + Colors.Wheat, + Colors.Violet, + Colors.Sienna, + Colors.Indigo, + Colors.Honeydew + }; + + private Dictionary _pointers = new Dictionary(); + + public PointerContactsTab() + { + ClipToBounds = true; + } + + void UpdatePointer(PointerEventArgs e) + { + if (!_pointers.TryGetValue(e.Pointer, out var info)) + { + if (e.RoutedEvent == PointerMovedEvent) + return; + var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); + var color = colors[new Random().Next(0, colors.Length - 1)]; + _pointers[e.Pointer] = info = new PointerInfo { Color = color }; + } + + info.Point = e.GetPosition(this); + InvalidateVisual(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + UpdatePointer(e); + e.Pointer.Capture(this); + e.Handled = true; + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + UpdatePointer(e); + e.Handled = true; + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _pointers.Remove(e.Pointer); + InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); + foreach (var pt in _pointers.Values) + { + var brush = new ImmutableSolidColorBrush(pt.Color); + + context.DrawEllipse(brush, null, pt.Point, 75, 75); + } + } +} diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs deleted file mode 100644 index 0668c248c7..0000000000 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ /dev/null @@ -1,394 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Documents; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Threading; -using Avalonia.VisualTree; - -namespace ControlCatalog.Pages; - -public class PointersPage : Decorator -{ - public PointersPage() - { - Child = new TabControl - { - Items = new[] - { - new TabItem() { Header = "Contacts", Content = new PointerContactsTab() }, - new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }, - new TabItem() { Header = "Pressure", Content = new PointerPressureTab() } - } - }; - } - - - class PointerContactsTab : Control - { - class PointerInfo - { - public Point Point { get; set; } - public Color Color { get; set; } - } - - private static Color[] AllColors = new[] - { - Colors.Aqua, - Colors.Beige, - Colors.Chartreuse, - Colors.Coral, - Colors.Fuchsia, - Colors.Crimson, - Colors.Lavender, - Colors.Orange, - Colors.Orchid, - Colors.ForestGreen, - Colors.SteelBlue, - Colors.PapayaWhip, - Colors.PaleVioletRed, - Colors.Goldenrod, - Colors.Maroon, - Colors.Moccasin, - Colors.Navy, - Colors.Wheat, - Colors.Violet, - Colors.Sienna, - Colors.Indigo, - Colors.Honeydew - }; - - private Dictionary _pointers = new Dictionary(); - - public PointerContactsTab() - { - ClipToBounds = true; - } - - void UpdatePointer(PointerEventArgs e) - { - if (!_pointers.TryGetValue(e.Pointer, out var info)) - { - if (e.RoutedEvent == PointerMovedEvent) - return; - var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); - var color = colors[new Random().Next(0, colors.Length - 1)]; - _pointers[e.Pointer] = info = new PointerInfo {Color = color}; - } - - info.Point = e.GetPosition(this); - InvalidateVisual(); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - UpdatePointer(e); - e.Pointer.Capture(this); - e.Handled = true; - base.OnPointerPressed(e); - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - UpdatePointer(e); - e.Handled = true; - base.OnPointerMoved(e); - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - _pointers.Remove(e.Pointer); - e.Handled = true; - InvalidateVisual(); - } - - protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - _pointers.Remove(e.Pointer); - InvalidateVisual(); - } - - public override void Render(DrawingContext context) - { - context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); - foreach (var pt in _pointers.Values) - { - var brush = new ImmutableSolidColorBrush(pt.Color); - - context.DrawEllipse(brush, null, pt.Point, 75, 75); - } - } - } - - public class PointerIntermediatePointsTab : Decorator - { - public PointerIntermediatePointsTab() - { - this[TextElement.ForegroundProperty] = Brushes.Black; - var slider = new Slider - { - Margin = new Thickness(5), - Minimum = 0, - Maximum = 500 - }; - - var status = new TextBlock() - { - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - }; - Child = new Grid - { - Children = - { - new PointerCanvas(slider, status, true), - new Border - { - Background = Brushes.LightYellow, - Child = new StackPanel - { - Children = - { - new StackPanel - { - Orientation = Orientation.Horizontal, - Children = - { - new TextBlock { Text = "Thread sleep:" }, - new TextBlock() - { - [!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty) - .Select(x=>x.ToString()).ToBinding() - } - } - }, - slider - } - }, - - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Top, - Width = 300, - Height = 60 - }, - status - } - }; - } - } - - public class PointerPressureTab : Decorator - { - public PointerPressureTab() - { - this[TextBlock.ForegroundProperty] = Brushes.Black; - - var status = new TextBlock() - { - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - FontSize = 12 - }; - Child = new Grid - { - Children = - { - new PointerCanvas(null, status, false), - status - } - }; - } - } - - class PointerCanvas : Control - { - private readonly Slider? _slider; - private readonly TextBlock _status; - private readonly bool _drawPoints; - private int _events; - private Stopwatch _stopwatch = Stopwatch.StartNew(); - private IDisposable? _statusUpdated; - private Dictionary _pointers = new(); - private PointerPointProperties? _lastProperties; - class PointerPoints - { - struct CanvasPoint - { - public IBrush Brush; - public Point Point; - public double Radius; - public double Pressure; - } - - readonly CanvasPoint[] _points = new CanvasPoint[1000]; - int _index; - - public void Render(DrawingContext context, bool drawPoints) - { - - CanvasPoint? prev = null; - for (var c = 0; c < _points.Length; c++) - { - var i = (c + _index) % _points.Length; - var pt = _points[i]; - var thickness = pt.Pressure == 0 ? 1 : (pt.Pressure / 1024) * 5; - - if (drawPoints) - { - if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) - context.DrawLine(new Pen(Brushes.Black, thickness), prev.Value.Point, pt.Point); - if (pt.Brush != null) - context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius); - } - else - { - if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) - context.DrawLine(new Pen(Brushes.Black, thickness, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), prev.Value.Point, pt.Point); - } - prev = pt; - } - - } - - void AddPoint(Point pt, IBrush brush, double radius, float pressure) - { - _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; - _index = (_index + 1) % _points.Length; - } - - public void HandleEvent(PointerEventArgs e, Visual v) - { - e.Handled = true; - var currentPoint = e.GetCurrentPoint(v); - if (e.RoutedEvent == PointerPressedEvent) - AddPoint(currentPoint.Position, Brushes.Green, 10, currentPoint.Properties.Pressure); - else if (e.RoutedEvent == PointerReleasedEvent) - AddPoint(currentPoint.Position, Brushes.Red, 10, currentPoint.Properties.Pressure); - else - { - var pts = e.GetIntermediatePoints(v); - for (var c = 0; c < pts.Count; c++) - { - var pt = pts[c]; - AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, - c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); - } - } - } - } - - public PointerCanvas(Slider? slider, TextBlock status, bool drawPoints) - { - _slider = slider; - _status = status; - _drawPoints = drawPoints; - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - _statusUpdated = DispatcherTimer.Run(() => - { - if (_stopwatch.Elapsed.TotalSeconds > 1) - { - _status.Text = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} -PointerUpdateKind: {_lastProperties?.PointerUpdateKind} -IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} -IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} -IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} -IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} -IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} -IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} -IsEraser: {_lastProperties?.IsEraser} -IsInverted: {_lastProperties?.IsInverted} -Pressure: {_lastProperties?.Pressure} -XTilt: {_lastProperties?.XTilt} -YTilt: {_lastProperties?.YTilt} -Twist: {_lastProperties?.Twist}"; - _stopwatch.Restart(); - _events = 0; - } - - return true; - }, TimeSpan.FromMilliseconds(10)); - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - _statusUpdated?.Dispose(); - } - - void HandleEvent(PointerEventArgs e) - { - _events++; - if (_slider != null) - { - Thread.Sleep((int)_slider.Value); - } - InvalidateVisual(); - - if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) - { - _pointers.Remove(e.Pointer.Id); - return; - } - - var lastPointer = e.GetCurrentPoint(this); - _lastProperties = lastPointer.Properties; - - if (e.Pointer.Type != PointerType.Pen - || lastPointer.Properties.Pressure > 0) - { - if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) - _pointers[e.Pointer.Id] = pt = new PointerPoints(); - pt.HandleEvent(e, this); - } - } - - public override void Render(DrawingContext context) - { - context.FillRectangle(Brushes.White, Bounds); - foreach (var pt in _pointers.Values) - pt.Render(context, _drawPoints); - base.Render(context); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - if (e.ClickCount == 2) - { - _pointers.Clear(); - InvalidateVisual(); - return; - } - - HandleEvent(e); - base.OnPointerPressed(e); - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - HandleEvent(e); - base.OnPointerMoved(e); - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - HandleEvent(e); - base.OnPointerReleased(e); - } - } -} diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml b/samples/ControlCatalog/Pages/PointersPage.xaml new file mode 100644 index 0000000000..1281ec77b6 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Capture 1 + + + Capture 2 + + + + + diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs new file mode 100644 index 0000000000..977cee3d58 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public class PointersPage : UserControl +{ + public PointersPage() + { + this.InitializeComponent(); + + var border1 = this.Get("BorderCapture1"); + var border2 = this.Get("BorderCapture2"); + + border1.PointerPressed += Border_PointerPressed; + border1.PointerReleased += Border_PointerReleased; + border1.PointerCaptureLost += Border_PointerCaptureLost; + border1.PointerMoved += Border_PointerUpdated; + border1.PointerEnter += Border_PointerUpdated; + border1.PointerLeave += Border_PointerUpdated; + + border2.PointerPressed += Border_PointerPressed; + border2.PointerReleased += Border_PointerReleased; + border2.PointerCaptureLost += Border_PointerCaptureLost; + border2.PointerMoved += Border_PointerUpdated; + border2.PointerEnter += Border_PointerUpdated; + border2.PointerLeave += Border_PointerUpdated; + } + + private void Border_PointerUpdated(object sender, PointerEventArgs e) + { + var textBlock = (TextBlock)((Border)sender).Child; + var position = e.GetPosition((Border)sender); + textBlock.Text = @$"Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: {(int)position.X} {(int)position.Y}"; + e.Handled = true; + } + + private void Border_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e) + { + var textBlock = (TextBlock)((Border)sender).Child; + textBlock.Text = @$"Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: ??? ???"; + e.Handled = true; + } + + private void Border_PointerReleased(object sender, PointerReleasedEventArgs e) + { + if (e.Pointer.Captured == sender) + { + e.Pointer.Capture(null); + e.Handled = true; + } + else + { + throw new InvalidOperationException("How?"); + } + } + + private void Border_PointerPressed(object sender, PointerPressedEventArgs e) + { + e.Pointer.Capture((Border)sender); + e.Handled = true; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} From e7b281e3fd9cb86db8fbabb2af933821b9eba809 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:32:06 -0400 Subject: [PATCH 033/481] Fix GetIntermediatePoints reverse order --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 6619c60152..1f316a0995 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -714,8 +714,6 @@ namespace Avalonia.Win32 private unsafe Lazy> CreateLazyIntermediatePoints(POINTER_INFO info) { - // Limit history size with reasonable value. - // With sizeof(POINTER_TOUCH_INFO) * 100 we can get maximum 14400 bytes. var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize); if (historyCount > 1) { @@ -723,12 +721,15 @@ namespace Avalonia.Win32 { s_intermediatePointsPooledList.Clear(); s_intermediatePointsPooledList.Capacity = historyCount; + + // Pointers in history are ordered from newest to oldest, so we need to reverse iteration. + // Also we skip the newest pointer, because original event arguments already contains it. + if (info.pointerType == PointerInputType.PT_TOUCH) { if (GetPointerTouchInfoHistory(info.pointerId, ref historyCount, s_historyTouchInfos)) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + for (int i = historyCount - 1; i >= 1; i--) { var historyTouchInfo = s_historyTouchInfos[i]; s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyTouchInfo)); @@ -739,8 +740,8 @@ namespace Avalonia.Win32 { if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos)) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + uint timestamp = 0; + for (int i = historyCount - 1; i >= 1; i--) { var historyPenInfo = s_historyPenInfos[i]; s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyPenInfo)); @@ -752,8 +753,7 @@ namespace Avalonia.Win32 // Currently Windows does not return history info for mouse input, but we handle it just for case. if (GetPointerInfoHistory(info.pointerId, ref historyCount, s_historyInfos)) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + for (int i = historyCount - 1; i >= 1; i--) { var historyInfo = s_historyInfos[i]; s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyInfo)); From a08ccd91cba6efec6367ebc9e7d1d3cce226ae6b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 17 Apr 2022 03:44:22 -0400 Subject: [PATCH 034/481] Fix DataGrid scrolling --- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 67183781d3..ea7d91ed97 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -178,9 +178,9 @@ namespace Avalonia.Controls { var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); - // Do not handle PointerPressed with touch, + // Do not handle PointerPressed with touch or pen, // so we can start scroll gesture on the same event. - if (e.Pointer.Type != PointerType.Touch) + if (e.Pointer.Type != PointerType.Touch && e.Pointer.Type != PointerType.Pen) { e.Handled = handled; } From b094699f760b1f760174d841fd3a5fddfb7e663d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 19:36:06 +0300 Subject: [PATCH 035/481] Compositor works with X11. Somewhat --- samples/ControlCatalog.NetCore/Program.cs | 1 + src/Avalonia.Base/Avalonia.Base.csproj | 5 +- .../Collections/IAvaloniaReadOnlyList.cs | 1 + .../Collections/Pooled/PooledList.cs | 2 +- src/Avalonia.Base/Matrix.cs | 39 + .../Animations/AnimatedValueStore.cs | 40 + .../Animations/CompositionAnimation.cs | 63 ++ .../Animations/CompositionAnimationGroup.cs | 26 + .../Animations/ExpressionAnimation.cs | 34 + .../Animations/ExpressionAnimationInstance.cs | 44 ++ .../Animations/IAnimationInstance.cs | 11 + .../Animations/ICompositionAnimationBase.cs | 12 + .../Animations/ImplicitAnimationCollection.cs | 73 ++ .../Composition/Animations/Interpolators.cs | 73 ++ .../Animations/KeyFrameAnimation.cs | 53 ++ .../Animations/KeyFrameAnimationInstance.cs | 139 ++++ .../Composition/Animations/KeyFrames.cs | 80 ++ .../Animations/PropertySetSnapshot.cs | 46 ++ .../Composition/CompositingRenderer.cs | 194 +++++ .../Composition/CompositionDrawListVisual.cs | 44 ++ .../Composition/CompositionEasingFunction.cs | 97 +++ .../Composition/CompositionGradientBrush.cs | 16 + .../Composition/CompositionObject.cs | 125 +++ .../Composition/CompositionPropertySet.cs | 132 ++++ .../Composition/CompositionTarget.cs | 107 +++ .../Rendering/Composition/Compositor.cs | 143 ++++ .../Composition/CompositorRenderLoopTask.cs | 20 + .../Rendering/Composition/ContainerVisual.cs | 20 + .../Rendering/Composition/CustomDrawVisual.cs | 56 ++ .../Drawing/CompositionDrawList.cs | 81 ++ .../Drawing/CompositionDrawingContext.cs | 383 +++++++++ .../Rendering/Composition/Enums.cs | 120 +++ .../Expressions/BuiltInExpressionFfi.cs | 234 ++++++ .../Expressions/DelegateExpressionFfi.cs | 181 +++++ .../Composition/Expressions/Expression.cs | 331 ++++++++ .../ExpressionEvaluationContext.cs | 31 + .../Expressions/ExpressionParseException.cs | 14 + .../Expressions/ExpressionParser.cs | 298 +++++++ .../Expressions/ExpressionVariant.cs | 739 ++++++++++++++++++ .../Composition/Expressions/TokenParser.cs | 256 ++++++ .../Composition/ICompositionSurface.cs | 9 + .../Rendering/Composition/MatrixUtils.cs | 46 ++ .../Composition/Server/DrawingContextProxy.cs | 142 ++++ .../Composition/Server/ReadbackIndices.cs | 46 ++ .../Server/ServerCompositionBrush.cs | 7 + .../Server/ServerCompositionDrawListVisual.cs | 41 + .../Server/ServerCompositionGradientBrush.cs | 15 + .../ServerCompositionLinearGradientBrush.cs | 22 + .../Server/ServerCompositionSurface.cs | 9 + .../Server/ServerCompositionTarget.cs | 53 ++ .../Composition/Server/ServerCompositor.cs | 90 +++ .../Server/ServerContainerVisual.cs | 29 + .../Server/ServerCustomDrawVisual.cs | 31 + .../Composition/Server/ServerList.cs | 46 ++ .../Composition/Server/ServerObject.cs | 36 + .../Server/ServerSolidColorVisual.cs | 16 + .../Composition/Server/ServerSpriteVisual.cs | 20 + .../Composition/Server/ServerVisual.cs | 92 +++ .../Rendering/Composition/Transport/Batch.cs | 37 + .../Rendering/Composition/Transport/Change.cs | 82 ++ .../Composition/Transport/ChangeSet.cs | 36 + .../Composition/Transport/ChangeSetPool.cs | 42 + .../Transport/CustomDrawVisualChanges.cs | 20 + .../Transport/DrawListVisualChanges.cs | 48 ++ .../Composition/Transport/ListChange.cs | 19 + .../Composition/Transport/ListChangeSet.cs | 25 + .../Transport/ServerListProxyHelper.cs | 92 +++ .../Composition/Transport/VisualChanges.cs | 16 + .../Composition/Utils/CubicBezier.cs | 302 +++++++ .../Rendering/Composition/Utils/MathExt.cs | 23 + .../Rendering/Composition/Visual.cs | 64 ++ .../Rendering/Composition/VisualCollection.cs | 64 ++ src/Avalonia.Base/Rendering/IRenderer.cs | 6 + .../SceneGraph/DeferredDrawingContextImpl.cs | 1 + src/Avalonia.Base/Size.cs | 11 + .../Threading/DispatcherPriority.cs | 14 +- src/Avalonia.Base/Visual.cs | 25 +- src/Avalonia.Base/composition-schema.xml | 70 ++ .../HeadlessPlatformRenderInterface.cs | 2 + .../CompositionRoslynGenerator.cs | 21 + .../CompositionGenerator/Config.cs | 116 +++ .../CompositionGenerator/Extensions.cs | 90 +++ .../Generator.KeyFrameAnimation.cs | 59 ++ .../Generator.ListProxy.cs | 121 +++ .../CompositionGenerator/Generator.Utils.cs | 66 ++ .../CompositionGenerator/Generator.cs | 504 ++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 +- src/Avalonia.X11/X11Window.cs | 17 +- .../Media/DrawingContextImpl.cs | 7 + 89 files changed, 7106 insertions(+), 16 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Compositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Enums.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Visual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/VisualCollection.cs create mode 100644 src/Avalonia.Base/composition-schema.xml create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4464413e63..2a0755b900 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -111,6 +111,7 @@ namespace ControlCatalog.NetCore EnableMultiTouch = true, UseDBusMenu = true, EnableIme = true, + UseCompositor = true }) .With(new Win32PlatformOptions { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 8e4755b4b7..bcebdd504c 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -3,10 +3,13 @@ net6.0;netstandard2.0 Avalonia.Base Avalonia - True + True + true + $(BaseIntermediateOutputPath)\GeneratedFiles + diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index a6a5953827..cefbf642be 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 200a52fb0d..ffda1bedca 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled /// /// Returns the internal buffers to the ArrayPool. /// - public void Dispose() + public virtual void Dispose() { ReturnArray(); _size = 0; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index b08a0eb98a..6f00b08d13 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; using Avalonia.Utilities; namespace Avalonia @@ -106,6 +107,36 @@ namespace Avalonia (value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32); } + public static Matrix operator +(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 + value2.M11, + value1.M12 + value2.M12, + value1.M21 + value2.M21, + value1.M22 + value2.M22, + value1.M31 + value2.M31, + value1.M32 + value2.M32); + } + + public static Matrix operator -(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 - value2.M11, + value1.M12 - value2.M12, + value1.M21 - value2.M21, + value1.M22 - value2.M22, + value1.M31 - value2.M31, + value1.M32 - value2.M32); + } + + public static Matrix operator *(Matrix value1, double value2) + { + return new Matrix(value1.M11 * value2, + value1.M12 * value2, + value1.M21 * value2, + value1.M22 * value2, + value1.M31 * value2, + value1.M32 * value2); + } + /// /// Negates the given matrix by multiplying all values by -1. /// @@ -427,6 +458,14 @@ namespace Avalonia return true; } +#if !BUILDTASK + public static implicit operator Matrix4x4(Matrix m) + { + return new Matrix4x4(new Matrix3x2((float)m._m11, (float)m._m12, (float)m._m21, (float)m._m22, + (float)m._m31, (float)m._m32)); + } +#endif + public struct Decomposed { public Vector Translate; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs new file mode 100644 index 0000000000..180c45022f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -0,0 +1,40 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal struct AnimatedValueStore where T : struct + { + private T _direct; + private IAnimationInstance _animation; + private T? _lastAnimated; + + public T Direct => _direct; + + public T GetAnimated(ServerCompositor compositor) + { + if (_animation == null) + return _direct; + var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) + .CastOrDefault(); + _lastAnimated = v; + return v; + } + + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; + + public bool IsAnimation => _animation != null; + + public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + { + _animation = animation; + _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + } + + public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + { + _direct = value + }; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs new file mode 100644 index 0000000000..9375faaaae --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -0,0 +1,63 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace + +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase + { + private readonly CompositionPropertySet _propertySet; + internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + { + _propertySet = new CompositionPropertySet(compositor); + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public void ClearAllParameters() => _propertySet.ClearAll(); + + public void ClearParameter(string key) => _propertySet.Clear(key); + + void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); + + public void SetColorParameter(string key, Avalonia.Media.Color value) => SetVariant(key, value); + + public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); + + public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); + + public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); + + public void SetReferenceParameter(string key, CompositionObject compositionObject) => + _propertySet.Set(key, compositionObject); + + public void SetScalarParameter(string key, float value) => SetVariant(key, value); + + public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); + + public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); + + public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); + + // TODO: void SetExpressionReferenceParameter(string parameterName, IAnimationObject source) + + public string? Target { get; set; } + + internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, + ExpressionVariant? finalValue); + + internal PropertySetSnapshot CreateSnapshot(bool server) + => _propertySet.Snapshot(server, 1); + + void ICompositionAnimationBase.InternalOnly() + { + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs new file mode 100644 index 0000000000..833f7e498c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + + +namespace Avalonia.Rendering.Composition.Animations +{ + public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase + { + internal List Animations { get; } = new List(); + void ICompositionAnimationBase.InternalOnly() + { + + } + + public void Add(CompositionAnimation value) => Animations.Add(value); + public void Remove(CompositionAnimation value) => Animations.Remove(value); + public void RemoveAll() => Animations.Clear(); + + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs new file mode 100644 index 0000000000..a6f24c2e35 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -0,0 +1,34 @@ +// ReSharper disable CheckNamespace +using System; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ExpressionAnimation : CompositionAnimation + { + private string? _expression; + private Expression? _parsedExpression; + + internal ExpressionAnimation(Compositor compositor) : base(compositor) + { + } + + public string? Expression + { + get => _expression; + set + { + _expression = value; + _parsedExpression = null; + } + } + + private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); + + internal override IAnimationInstance CreateInstance( + ServerObject targetObject, ExpressionVariant? finalValue) + => new ExpressionAnimationInstance(ParsedExpression, + targetObject, finalValue, CreateSnapshot(true)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs new file mode 100644 index 0000000000..47b947b2e9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -0,0 +1,44 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class ExpressionAnimationInstance : IAnimationInstance + { + private readonly Expression _expression; + private readonly IExpressionObject _target; + private ExpressionVariant _startingValue; + private readonly ExpressionVariant? _finalValue; + private readonly PropertySetSnapshot _parameters; + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var ctx = new ExpressionEvaluationContext + { + Parameters = _parameters, + Target = _target, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance, + StartingValue = _startingValue, + FinalValue = _finalValue ?? _startingValue, + CurrentValue = currentValue + }; + return _expression.Evaluate(ref ctx); + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startingValue = startingValue; + } + + public ExpressionAnimationInstance(Expression expression, + IExpressionObject target, + ExpressionVariant? finalValue, + PropertySetSnapshot parameters) + { + _expression = expression; + _target = target; + _finalValue = finalValue; + _parameters = parameters; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs new file mode 100644 index 0000000000..a0b066ae0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IAnimationInstance + { + ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); + void Start(TimeSpan startedAt, ExpressionVariant startingValue); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs new file mode 100644 index 0000000000..bf40fd3ad2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace + +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public interface ICompositionAnimationBase + { + internal void InternalOnly(); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs new file mode 100644 index 0000000000..be91352527 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ImplicitAnimationCollection : CompositionObject, IDictionary + { + private Dictionary _inner = new Dictionary(); + private IDictionary _innerface; + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + { + _innerface = _inner; + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); + + void ICollection>.Add(KeyValuePair item) => _innerface.Add(item); + + public void Clear() => _inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex); + + bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item); + + public int Count => _inner.Count; + + bool ICollection>.IsReadOnly => _innerface.IsReadOnly; + + public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value); + + public bool ContainsKey(string key) => _inner.ContainsKey(key); + + public bool Remove(string key) => _inner.Remove(key); + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) => + _inner.TryGetValue(key, out value); + + public ICompositionAnimationBase this[string key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + ICollection IDictionary.Keys => _innerface.Keys; + + ICollection IDictionary.Values => + _innerface.Values; + + // UWP compat + public uint Size => (uint) Count; + + public IReadOnlyDictionary GetView() => + new Dictionary(this); + + public bool HasKey(string key) => ContainsKey(key); + public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation); + + public ICompositionAnimationBase? Lookup(string key) + { + _inner.TryGetValue(key, out var rv); + return rv; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs new file mode 100644 index 0000000000..62b790701a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -0,0 +1,73 @@ +using System; +using System.Numerics; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IInterpolator + { + T Interpolate(T from, T to, float progress); + } + + class ScalarInterpolator : IInterpolator + { + public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress; + + public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); + } + + class Vector2Interpolator : IInterpolator + { + public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) + => Vector2.Lerp(@from, to, progress); + + public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); + } + + class Vector3Interpolator : IInterpolator + { + public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) + => Vector3.Lerp(@from, to, progress); + + public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); + } + + class Vector4Interpolator : IInterpolator + { + public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) + => Vector4.Lerp(@from, to, progress); + + public static Vector4Interpolator Instance { get; } = new Vector4Interpolator(); + } + + class QuaternionInterpolator : IInterpolator + { + public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) + => Quaternion.Lerp(@from, to, progress); + + public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator(); + } + + class ColorInterpolator : IInterpolator + { + static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a))); + + public static Avalonia.Media.Color + LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + new Avalonia.Media.Color(Lerp(to.A, @from.A, progress), + Lerp(to.R, @from.R, progress), + Lerp(to.G, @from.G, progress), + Lerp(to.B, @from.B, progress)); + + public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress) + => LerpRGB(@from, to, progress); + + public static ColorInterpolator Instance { get; } = new ColorInterpolator(); + } + + class BooleanInterpolator : IInterpolator + { + public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from; + + public static BooleanInterpolator Instance { get; } = new BooleanInterpolator(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs new file mode 100644 index 0000000000..065dfd7a8e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -0,0 +1,53 @@ +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class KeyFrameAnimation : CompositionAnimation + { + internal KeyFrameAnimation(Compositor compositor) : base(compositor) + { + } + + public AnimationDelayBehavior DelayBehavior { get; set; } + public System.TimeSpan DelayTime { get; set; } + public AnimationDirection Direction { get; set; } + public System.TimeSpan Duration { get; set; } + public AnimationIterationBehavior IterationBehavior { get; set; } + public int IterationCount { get; set; } = 1; + public AnimationStopBehavior StopBehavior { get; set; } + + private protected abstract IKeyFrames KeyFrames { get; } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) => + KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction); + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value) + => KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, new LinearEasingFunction(Compositor)); + } + + public enum AnimationDelayBehavior + { + SetInitialValueAfterDelay, + SetInitialValueBeforeDelay + } + + public enum AnimationDirection + { + Normal, + Reverse, + Alternate, + AlternateReverse + } + + public enum AnimationIterationBehavior + { + Count, + Forever + } + + public enum AnimationStopBehavior + { + LeaveCurrentValue, + SetToInitialValue, + SetToFinalValue + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs new file mode 100644 index 0000000000..b90a02148d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -0,0 +1,139 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrameAnimationInstance : IAnimationInstance where T : struct + { + private readonly IInterpolator _interpolator; + private readonly ServerKeyFrame[] _keyFrames; + private readonly PropertySetSnapshot _snapshot; + private readonly ExpressionVariant? _finalValue; + private readonly IExpressionObject _target; + private readonly AnimationDelayBehavior _delayBehavior; + private readonly TimeSpan _delayTime; + private readonly AnimationDirection _direction; + private readonly TimeSpan _duration; + private readonly AnimationIterationBehavior _iterationBehavior; + private readonly int _iterationCount; + private readonly AnimationStopBehavior _stopBehavior; + private TimeSpan _startedAt; + private T _startingValue; + + public KeyFrameAnimationInstance( + IInterpolator interpolator, ServerKeyFrame[] keyFrames, + PropertySetSnapshot snapshot, ExpressionVariant? finalValue, + IExpressionObject target, + AnimationDelayBehavior delayBehavior, TimeSpan delayTime, + AnimationDirection direction, TimeSpan duration, + AnimationIterationBehavior iterationBehavior, + int iterationCount, AnimationStopBehavior stopBehavior) + { + _interpolator = interpolator; + _keyFrames = keyFrames; + _snapshot = snapshot; + _finalValue = finalValue; + _target = target; + _delayBehavior = delayBehavior; + _delayTime = delayTime; + _direction = direction; + _duration = duration; + _iterationBehavior = iterationBehavior; + _iterationCount = iterationCount; + _stopBehavior = stopBehavior; + if (_keyFrames.Length == 0) + throw new InvalidOperationException("Animation has no key frames"); + if(_duration.Ticks <= 0) + throw new InvalidOperationException("Invalid animation duration"); + } + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var elapsed = now - _startedAt; + var starting = ExpressionVariant.Create(_startingValue); + var ctx = new ExpressionEvaluationContext + { + Parameters = _snapshot, + Target = _target, + CurrentValue = currentValue, + FinalValue = _finalValue ?? starting, + StartingValue = starting, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance + }; + + if (elapsed < _delayTime) + { + if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0])); + return currentValue; + } + + elapsed -= _delayTime; + var iterationNumber = elapsed.Ticks / _duration.Ticks; + if (_iterationBehavior == AnimationIterationBehavior.Count + && iterationNumber >= _iterationCount) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1])); + + + var evenIterationNumber = iterationNumber % 2 == 0; + elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); + + var reverse = + _direction == AnimationDirection.Alternate + ? !evenIterationNumber + : _direction == AnimationDirection.AlternateReverse + ? evenIterationNumber + : _direction == AnimationDirection.Reverse; + + var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; + if (reverse) + iterationProgress = 1 - iterationProgress; + + var left = new ServerKeyFrame + { + Value = _startingValue + }; + var right = _keyFrames[_keyFrames.Length - 1]; + for (var c = 0; c < _keyFrames.Length; c++) + { + var kf = _keyFrames[c]; + if (kf.Key < iterationProgress) + { + // this is the last frame + if (c == _keyFrames.Length - 1) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf)); + + left = kf; + right = _keyFrames[c + 1]; + break; + } + } + + var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); + + var easedKeyProgress = right.EasingFunction.Ease((float) keyProgress); + if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) + return currentValue; + + return ExpressionVariant.Create(_interpolator.Interpolate( + GetKeyFrame(ref ctx, left), + GetKeyFrame(ref ctx, right), + easedKeyProgress + )); + } + + T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame f) + { + if (f.Expression != null) + return f.Expression.Evaluate(ref ctx).CastOrDefault(); + else + return f.Value; + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startedAt = startedAt; + _startingValue = startingValue.CastOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs new file mode 100644 index 0000000000..d7f2504061 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrames : List>, IKeyFrames + { + void Validate(float key) + { + if (key < 0 || key > 1) + throw new ArgumentException("Key frame key"); + if (Count > 0 && this[Count - 1].NormalizedProgressKey > key) + throw new ArgumentException("Key frame key " + key + " is less than the previous one"); + } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Expression = Expression.Parse(value), + EasingFunction = easingFunction + }); + } + + public void Insert(float normalizedProgressKey, T value, CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Value = value, + EasingFunction = easingFunction + }); + } + + public ServerKeyFrame[] Snapshot() + { + var frames = new ServerKeyFrame[Count]; + for (var c = 0; c < Count; c++) + { + var f = this[c]; + frames[c] = new ServerKeyFrame + { + Expression = f.Expression, + Value = f.Value, + EasingFunction = f.EasingFunction.Snapshot(), + Key = f.NormalizedProgressKey + }; + } + return frames; + } + } + + struct KeyFrame + { + public float NormalizedProgressKey; + public T Value; + public Expression Expression; + public CompositionEasingFunction EasingFunction; + } + + struct ServerKeyFrame + { + public T Value; + public Expression Expression; + public IEasingFunction EasingFunction; + public float Key; + } + + + + interface IKeyFrames + { + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, CompositionEasingFunction easingFunction); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs new file mode 100644 index 0000000000..ca703dfc6f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject + { + private readonly Dictionary _dic; + + public struct Value + { + public ExpressionVariant Variant; + public IExpressionObject Object; + + public Value(IExpressionObject o) + { + Object = o; + Variant = default; + } + + public static implicit operator Value(ExpressionVariant v) => new Value + { + Variant = v + }; + } + + public PropertySetSnapshot(Dictionary dic) + { + _dic = dic; + } + + public ExpressionVariant GetParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Variant; + } + + public IExpressionObject GetObjectParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Object; + } + + public ExpressionVariant GetProperty(string name) => GetParameter(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs new file mode 100644 index 0000000000..07ac54b634 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition; + +public class CompositingRenderer : RendererBase, IRendererWithCompositor +{ + private readonly IRenderRoot _root; + private readonly Compositor _compositor; + private readonly IDeferredRendererLock? _rendererLock; + CompositionDrawingContext _recorder = new(); + DrawingContext _recordingContext; + private HashSet _dirty = new(); + private HashSet _recalculateChildren = new(); + private readonly CompositionTarget _target; + private bool _queuedUpdate; + private Action _update; + + public CompositingRenderer(IRenderRoot root, + Compositor compositor, + IDeferredRendererLock? rendererLock = null) + { + _root = root; + _compositor = compositor; + _recordingContext = new DrawingContext(_recorder); + _rendererLock = rendererLock ?? new ManagedDeferredRendererLock(); + _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); + _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); + _update = Update; + } + + public bool DrawFps { get; set; } + public bool DrawDirtyRects { get; set; } + public event EventHandler? SceneInvalidated; + + void QueueUpdate() + { + if(_queuedUpdate) + return; + _queuedUpdate = true; + Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + } + public void AddDirty(IVisual visual) + { + _dirty.Add((Visual)visual); + QueueUpdate(); + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + if(res == null) + yield break; + for (var index = res.Count - 1; index >= 0; index--) + { + var v = res[index]; + if (v is CompositionDrawListVisual dv) + { + if (filter == null || filter(dv.Visual)) + yield return dv.Visual; + } + } + } + + public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + { + // TODO: Optimize + return HitTest(p, root, filter).FirstOrDefault(); + } + + public void RecalculateChildren(IVisual visual) + { + _recalculateChildren.Add((Visual)visual); + QueueUpdate(); + } + + private void SyncChildren(Visual v) + { + //TODO: Optimize by moving that logic to Visual itself + if(v.CompositionVisual == null) + return; + var compositionChildren = v.CompositionVisual.Children; + var visualChildren = (AvaloniaList)v.GetVisualChildren(); + if (compositionChildren.Count == visualChildren.Count) + { + bool mismatch = false; + for(var c=0; c _target.IsEnabled = true; + + public void Stop() + { + _target.IsEnabled = true; + } + + public void Dispose() + { + Stop(); + _target.Dispose(); + // Wait for the composition batch to be applied and rendered to guarantee that + // render target is not used anymore and can be safely disposed + _compositor.RequestCommitAsync().Wait(); + } + + + public Compositor Compositor => _compositor; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs new file mode 100644 index 0000000000..9f02055412 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -0,0 +1,44 @@ +using System.Numerics; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawListVisual : CompositionContainerVisual +{ + public Visual Visual { get; } + + private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private CompositionDrawList? _drawList; + public CompositionDrawList? DrawList + { + get => _drawList; + set + { + _drawList?.Dispose(); + _drawList = value; + Changes.DrawCommands = value?.Clone(); + } + } + + private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + { + Visual = visual; + } + + internal override bool HitTest(Vector2 point) + { + if (DrawList == null) + return false; + var pt = new Point(point.X, point.Y); + if (Visual is ICustomHitTest custom) + return custom.HitTest(pt); + foreach (var op in DrawList) + if (op.Item.HitTest(pt)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs new file mode 100644 index 0000000000..73db243e93 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionEasingFunction : CompositionObject + { + internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + internal abstract IEasingFunction Snapshot(); + } + + internal interface IEasingFunction + { + float Ease(float progress); + } + + public sealed class DelegateCompositionEasingFunction : CompositionEasingFunction + { + private readonly Easing _func; + + public delegate float EasingDelegate(float progress); + + internal DelegateCompositionEasingFunction(Compositor compositor, EasingDelegate func) : base(compositor) + { + _func = new Easing(func); + } + + class Easing : IEasingFunction + { + private readonly EasingDelegate _func; + + public Easing(EasingDelegate func) + { + _func = func; + } + + public float Ease(float progress) => _func(progress); + } + + internal override IEasingFunction Snapshot() => _func; + } + + public class LinearEasingFunction : CompositionEasingFunction + { + public LinearEasingFunction(Compositor compositor) : base(compositor) + { + } + + class Linear : IEasingFunction + { + public float Ease(float progress) => progress; + } + + private static readonly Linear Instance = new Linear(); + internal override IEasingFunction Snapshot() => Instance; + } + + public class CubicBezierEasingFunction : CompositionEasingFunction + { + private CubicBezier _bezier; + public Vector2 ControlPoint1 { get; } + public Vector2 ControlPoint2 { get; } + //cubic-bezier(0.25, 0.1, 0.25, 1.0) + internal CubicBezierEasingFunction(Compositor compositor, Vector2 controlPoint1, Vector2 controlPoint2) : base(compositor) + { + ControlPoint1 = controlPoint1; + ControlPoint2 = controlPoint2; + if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) + throw new ArgumentException(); + _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); + } + + class EasingFunction : IEasingFunction + { + private readonly CubicBezier _bezier; + + public EasingFunction(CubicBezier bezier) + { + _bezier = bezier; + } + + public float Ease(float progress) => (float)_bezier.Solve(progress); + } + + internal static IEasingFunction Ease { get; } = new EasingFunction(new CubicBezier(0.25, 0.1, 0.25, 1)); + + internal override IEasingFunction Snapshot() => new EasingFunction(_bezier); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs new file mode 100644 index 0000000000..cf222550dd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionGradientBrush : CompositionBrush + { + internal CompositionGradientBrush(Compositor compositor, ServerCompositionGradientBrush server) : base(compositor, server) + { + ColorStops = new CompositionGradientStopCollection(compositor, server.Stops); + } + + public CompositionGradientStopCollection ColorStops { get; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs new file mode 100644 index 0000000000..2417ecaba8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionObject : IDisposable, IExpressionObject + { + public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + internal CompositionObject(Compositor compositor, ServerObject server) + { + Compositor = compositor; + Server = server; + } + + public Compositor Compositor { get; } + internal ServerObject Server { get; } + public bool IsDisposed { get; private set; } + private ChangeSet? _changes; + + private static void ThrowInvalidOperation() => + throw new InvalidOperationException("There is no server-side counterpart for this object"); + + private protected ChangeSet Changes + { + get + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (Server == null) ThrowInvalidOperation(); + var currentBatch = Compositor.CurrentBatch; + if (_changes != null && _changes.Batch != currentBatch) + _changes = null; + if (_changes == null) + { + _changes = ChangeSetPool.Get(Server!, currentBatch); + currentBatch.Changes!.Add(_changes); + Compositor.QueueImplicitBatchCommit(); + } + + return _changes; + } + } + + private protected abstract IChangeSetPool ChangeSetPool { get; } + + public void Dispose() + { + Changes.Dispose = true; + IsDisposed = true; + } + + internal virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void StartAnimation(string propertyName, CompositionAnimation animation) + => StartAnimation(propertyName, animation, null); + + internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue = null) + { + throw new ArgumentException("Unknown property " + propertyName); + } + + public void StartAnimationGroup(ICompositionAnimationBase grp) + { + if (grp is CompositionAnimation animation) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(animation.Target, animation); + } + else if (grp is CompositionAnimationGroup group) + { + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(a.Target, a); + } + } + } + + bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (animation.Target == target) + { + StartAnimation(animation.Target, animation, finalValue); + return true; + } + else + { + StartAnimation(animation.Target, animation); + return false; + } + } + + internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue) + { + if (grp is CompositionAnimation animation) + return StartAnimationGroupPart(animation, target, finalValue); + if (grp is CompositionAnimationGroup group) + { + var matched = false; + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (StartAnimationGroupPart(a, target, finalValue)) + matched = true; + } + + return matched; + } + + throw new ArgumentException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs new file mode 100644 index 0000000000..004c2676ff --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionPropertySet : CompositionObject + { + private readonly Dictionary _variants = new Dictionary(); + private readonly Dictionary _objects = new Dictionary(); + + internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); + + internal void Set(string key, ExpressionVariant value) + { + _objects.Remove(key); + _variants[key] = value; + } + + internal void Set(string key, CompositionObject obj) + { + _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); + _variants.Remove(key); + } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); + + public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); + + public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value); + + public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value); + + public void InsertScalar(string propertyName, float value) => Set(propertyName, value); + public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value); + + public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value); + + public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value); + + + CompositionGetValueStatus TryGetVariant(string key, out T value) where T : struct + { + value = default; + if (!_variants.TryGetValue(key, out var v)) + return _objects.ContainsKey(key) + ? CompositionGetValueStatus.TypeMismatch + : CompositionGetValueStatus.NotFound; + + return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch; + } + + public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value) + => TryGetVariant(propertyName, out value); + + + public CompositionGetValueStatus TryGetScalar(string propertyName, out float value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value) + => TryGetVariant(propertyName, out value); + + + public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value); + + public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value) + => TryGetVariant(propertyName, out value); + + internal void ClearAll() + { + _objects.Clear(); + _variants.Clear(); + } + + internal void Clear(string key) + { + _objects.Remove(key); + _variants.Remove(key); + } + + internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + { + var dic = new Dictionary(_objects.Count + _variants.Count); + foreach (var o in _objects) + { + if (o.Value is CompositionPropertySet ps) + { + if (allowedNestingLevel <= 0) + throw new InvalidOperationException("PropertySet depth limit reached"); + dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + } + else if (o.Value.Server == null) + throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); + else + dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + } + + foreach (var v in _variants) + dic[v.Key] = v.Value; + + return new PropertySetSnapshot(dic); + } + } + + public enum CompositionGetValueStatus + { + Succeeded, + TypeMismatch, + NotFound + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs new file mode 100644 index 0000000000..a8835ca668 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionTarget + { + partial void OnRootChanged() + { + if (Root != null) + Root.Root = this; + } + + partial void OnRootChanging() + { + if (Root != null) + Root.Root = null; + } + + public PooledList? TryHitTest(Vector2 point) + { + Server.Readback.NextRead(); + if (Root == null) + return null; + var res = new PooledList(); + HitTestCore(Root, point, res); + return res; + } + + public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + { + if (visual.Root != this) + return null; + var v = visual; + var m = Matrix3x2.Identity; + while (v != null) + { + if (!TryGetInvertedTransform(v, out var cm)) + return null; + m = m * cm; + v = v.Parent; + } + + return Vector2.Transform(point, m); + } + + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + { + var m = visual.TryGetServerTransform(); + if (m == null) + { + matrix = default; + return false; + } + + // TODO: Use Matrix3x3 + var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); + + return Matrix3x2.Invert(m32, out matrix); + } + + bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + { + if (TryGetInvertedTransform(visual, out var m)) + { + v = Vector2.Transform(v, m); + return true; + } + + return false; + } + + bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + { + //TODO: Check readback too + if (visual.Visible == false) + return false; + if (!TryTransformTo(visual, ref point)) + return false; + if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + { + bool success = false; + // Hit-test the current node + if (visual.HitTest(point)) + { + result.Add(visual); + success = true; + } + + // Inspect children too + if(visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) + { + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result); + if (hit) + return true; + } + + return success; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs new file mode 100644 index 0000000000..217d8dd803 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; + + +namespace Avalonia.Rendering.Composition +{ + public partial class Compositor + { + private ServerCompositor _server; + private Batch _currentBatch; + private bool _implicitBatchCommitQueued; + private Action _implicitBatchCommit; + + internal Batch CurrentBatch => _currentBatch; + internal ServerCompositor Server => _server; + internal CompositionEasingFunction DefaultEasing { get; } + + private Compositor(ServerCompositor server) + { + _server = server; + _currentBatch = new Batch(); + _implicitBatchCommit = ImplicitBatchCommit; + DefaultEasing = new CubicBezierEasingFunction(this, + new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); + } + + public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + { + return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + } + + public Task RequestCommitAsync() + { + var batch = CurrentBatch; + _currentBatch = new Batch(); + batch.CommitedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + return batch.Completed; + } + + public static Compositor Create(IRenderLoop timer) + { + return new Compositor(new ServerCompositor(timer)); + } + + public void Dispose() + { + + } + + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, + new ServerCompositionSolidColorVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual(Avalonia.Media.Color color) + { + var v = new CompositionSolidColorVisual(this, new ServerCompositionSolidColorVisual(_server)); + v.Color = color; + return v; + } + + public CompositionSpriteVisual CreateSpriteVisual() => new CompositionSpriteVisual(this, new ServerCompositionSpriteVisual(_server)); + + public CompositionLinearGradientBrush CreateLinearGradientBrush() + => new CompositionLinearGradientBrush(this, new ServerCompositionLinearGradientBrush(_server)); + + public CompositionColorGradientStop CreateColorGradientStop() + => new CompositionColorGradientStop(this, new ServerCompositionColorGradientStop(_server)); + + public CompositionColorGradientStop CreateColorGradientStop(float offset, Avalonia.Media.Color color) + { + var stop = CreateColorGradientStop(); + stop.Offset = offset; + stop.Color = color; + return stop; + } + + // We want to make it 100% async later + /* + public CompositionBitmapSurface LoadBitmapSurface(Stream stream) + { + var bmp = _server.Backend.LoadCpuMemoryBitmap(stream); + return new CompositionBitmapSurface(this, bmp); + } + + public async Task LoadBitmapSurfaceAsync(Stream stream) + { + var bmp = await Task.Run(() => _server.Backend.LoadCpuMemoryBitmap(stream)); + return new CompositionBitmapSurface(this, bmp); + } + */ + public CompositionColorBrush CreateColorBrush(Avalonia.Media.Color color) => + new CompositionColorBrush(this, new ServerCompositionColorBrush(_server)) {Color = color}; + + public CompositionSurfaceBrush CreateSurfaceBrush() => + new CompositionSurfaceBrush(this, new ServerCompositionSurfaceBrush(_server)); + + /* + public CompositionGaussianBlurEffectBrush CreateGaussianBlurEffectBrush() => + new CompositionGaussianBlurEffectBrush(this, new ServerCompositionGaussianBlurEffectBrush(_server)); + + public CompositionBackdropBrush CreateBackdropBrush() => + new CompositionBackdropBrush(this, new ServerCompositionBackdropBrush(Server));*/ + + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); + + public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) + { + Expression = expression + }; + + public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); + + public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); + + internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => + new CustomDrawVisual(this, renderer, hitTest); + + public void QueueImplicitBatchCommit() + { + if(_implicitBatchCommitQueued) + return; + _implicitBatchCommitQueued = true; + Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); + } + + private void ImplicitBatchCommit() + { + _implicitBatchCommitQueued = false; + RequestCommitAsync(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs new file mode 100644 index 0000000000..074c0a9ccf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs @@ -0,0 +1,20 @@ +using System; + +namespace Avalonia.Rendering.Composition; + +partial class Compositor +{ + class CompositorRenderLoopTask : IRenderLoopTask + { + public bool NeedsUpdate { get; } + public void Update(TimeSpan time) + { + throw new NotImplementedException(); + } + + public void Render() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs new file mode 100644 index 0000000000..f650d3e995 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -0,0 +1,20 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionContainerVisual : CompositionVisual + { + public CompositionVisualCollection Children { get; } + internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + { + Children = new CompositionVisualCollection(this, server.Children); + } + + private protected override void OnRootChanged() + { + foreach (var ch in Children) + ch.Root = Root; + base.OnRootChanged(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs new file mode 100644 index 0000000000..0505d6a46c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs @@ -0,0 +1,56 @@ +using System; +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable + { + private readonly ICustomDrawVisualHitTest? _hitTest; + + internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest) : base(compositor, + new ServerCustomDrawVisual(compositor.Server, renderer)) + { + _hitTest = hitTest; + } + + private TData? _data; + + static bool Eq(TData? left, TData? right) + { + if (left == null && right == null) + return true; + if (left == null) + return false; + return left.Equals(right); + } + + public TData? Data + { + get => _data; + set + { + if (!Eq(_data, value)) + { + ((CustomDrawVisualChanges) Changes).Data.Value = value; + _data = value; + } + } + } + + private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; + } + + public interface ICustomDrawVisualRenderer + { + void Render(IDrawingContextImpl canvas, TData? data); + } + + public interface ICustomDrawVisualHitTest + { + bool HitTest(TData data, Vector2 vector2); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs new file mode 100644 index 0000000000..aca8ef7c46 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -0,0 +1,81 @@ +using System; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionDrawList : PooledList> +{ + public CompositionDrawList() + { + + } + + public CompositionDrawList(int capacity) : base(capacity) + { + + } + + public override void Dispose() + { + foreach(var item in this) + item.Dispose(); + base.Dispose(); + } + + public CompositionDrawList Clone() + { + var clone = new CompositionDrawList(Count); + foreach (var r in this) + clone.Add(r.Clone()); + return clone; + } +} + +internal class CompositionDrawListBuilder +{ + private CompositionDrawList? _operations; + private bool _owns; + + public void Reset(CompositionDrawList? previousOperations) + { + _operations = previousOperations; + _owns = false; + } + + public CompositionDrawList DrawOperations => _operations ?? new CompositionDrawList(); + + void MakeWritable(int atIndex) + { + if(_owns) + return; + _owns = true; + var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex)); + if (_operations != null) + { + for (var c = 0; c < atIndex; c++) + newOps.Add(_operations[c].Clone()); + } + + _operations = newOps; + } + + public void ReplaceDrawOperation(int index, IDrawOperation node) + { + MakeWritable(index); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void AddDrawOperation(IDrawOperation node) + { + MakeWritable(DrawOperations.Count); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void TrimTo(int count) + { + if (count < DrawOperations.Count) + DrawOperations.RemoveRange(count, DrawOperations.Count - count); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs new file mode 100644 index 0000000000..c8e5d9e064 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.VisualTree; +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawingContext : IDrawingContextImpl +{ + private CompositionDrawListBuilder _builder = new(); + private int _drawOperationIndex; + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + public void Clear(Color color) + { + // Cannot clear a deferred scene. + } + + /// + public void Dispose() + { + // Nothing to do here since we allocate no unmanaged resources. + } + + public void BeginUpdate(CompositionDrawList list) + { + _builder.Reset(list); + _drawOperationIndex = 0; + } + + public CompositionDrawList EndUpdate() + { + _builder.TrimTo(_drawOperationIndex); + return _builder.DrawOperations!; + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var next = NextDrawAs(); + + if (next == null || + !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + { + // This method is currently only used to composite layers so shouldn't be called here. + throw new NotSupportedException(); + } + + /// + public void DrawLine(IPen pen, Point p1, Point p2) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) + { + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationIndex; + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + public void Custom(ICustomDrawOperation custom) + { + var next = NextDrawAs(); + if (next == null || !next.Item.Equals(Transform, custom)) + Add(new CustomDrawOperation(custom, Transform)); + else + ++_drawOperationIndex; + } + + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + { + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + } + + else + { + ++_drawOperationIndex; + } + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + + /// + public void PopClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new ClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopGeometryClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new GeometryClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacity() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new OpacityNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacityMask() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null, null)) + { + Add(new OpacityMaskNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(Rect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushGeometryClip(IGeometryImpl? clip) + { + if (clip is null) + return; + + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new GeometryClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacity(double opacity) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(opacity)) + { + Add(new OpacityNode(opacity)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(mask, bounds)) + { + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationIndex; + } + } + + private void Add(T node) where T : class, IDrawOperation + { + if (_drawOperationIndex < _builder!.DrawOperations.Count) + { + _builder.ReplaceDrawOperation(_drawOperationIndex, node); + } + else + { + _builder.AddDrawOperation(node); + } + + ++_drawOperationIndex; + } + + private IRef? NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationIndex < _builder!.DrawOperations.Count + ? _builder.DrawOperations[_drawOperationIndex] as IRef + : null; + } + + private IDictionary? CreateChildScene(IBrush? brush) + { + /* + var visualBrush = brush as VisualBrush; + + if (visualBrush != null) + { + var visual = visualBrush.Visual; + + if (visual != null) + { + (visual as IVisualBrushInitialize)?.EnsureInitialized(); + var scene = new Scene(visual); + _sceneBuilder.UpdateAll(scene); + return new Dictionary { { visualBrush.Visual, scene } }; + } + }*/ + + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Enums.cs b/src/Avalonia.Base/Rendering/Composition/Enums.cs new file mode 100644 index 0000000000..e349845cbf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Enums.cs @@ -0,0 +1,120 @@ +using System; + +namespace Avalonia.Rendering.Composition +{ + public enum CompositionBlendMode + { + /// No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg) + Clear, + + /// Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg) + Src, + + /// Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg) + Dst, + + /// Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg) + SrcOver, + + /// Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg) + DstOver, + + /// The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg) + SrcIn, + + /// Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg) + DstIn, + + /// Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg) + SrcOut, + + /// Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg) + DstOut, + + /// Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg) + SrcATop, + + /// Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg) + DstATop, + + /// The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg) + Xor, + + /// Display the sum of the source image and destination image. [Porter Duff Compositing Operators] + Plus, + + /// Multiplies all components (= alpha and color). [Separable Blend Modes] + Modulate, + + /// Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes] + Screen, + + /// Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes] + Overlay, + + /// Selects the darker of the backdrop and source colors. [Separable Blend Modes] + Darken, + + /// Selects the lighter of the backdrop and source colors. [Separable Blend Modes] + Lighten, + + /// Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorDodge, + + /// Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorBurn, + + /// Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + HardLight, + + /// Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + SoftLight, + + /// Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes] + Difference, + + /// Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes] + Exclusion, + + /// The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes] + Multiply, + + /// Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes] + Hue, + + /// Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes] + Saturation, + + /// Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes] + Color, + + /// Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes] + Luminosity, + } + + public enum CompositionGradientExtendMode + { + Clamp, + Wrap, + Mirror + } + + [Flags] + public enum CompositionTileMode + { + None = 0, + TileX = 1, + TileY = 2, + FlipX = 4, + FlipY = 8, + Tile = TileX | TileY, + Flip = FlipX | FlipY + } + + public enum CompositionStretch + { + None = 0, + Fill = 1, + //TODO: Uniform, UniformToFill + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs new file mode 100644 index 0000000000..db9a26e301 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface + { + private readonly DelegateExpressionFfi _registry; + + static float Lerp(float a, float b, float p) => p * (b - a) + a; + + static Matrix3x2 Inverse(Matrix3x2 m) + { + Matrix3x2.Invert(m, out var r); + return r; + } + + static Matrix4x4 Inverse(Matrix4x4 m) + { + Matrix4x4.Invert(m, out var r); + return r; + } + + static float SmoothStep(float edge0, float edge1, float x) + { + var t = MathExt.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); + } + + static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x) + { + return new Vector2( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y) + + ); + } + static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x) + { + return new Vector3( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z) + + ); + } + + static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x) + { + return new Vector4( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z), + SmoothStep(edge0.W, edge1.W, x.W) + ); + } + + private BuiltInExpressionFfi() + { + _registry = new DelegateExpressionFfi + { + {"Abs", (float f) => Math.Abs(f)}, + {"Abs", (Vector2 v) => Vector2.Abs(v)}, + {"Abs", (Vector3 v) => Vector3.Abs(v)}, + {"Abs", (Vector4 v) => Vector4.Abs(v)}, + + {"ACos", (float f) => (float) Math.Acos(f)}, + {"ASin", (float f) => (float) Math.Asin(f)}, + {"ATan", (float f) => (float) Math.Atan(f)}, + {"Ceil", (float f) => (float) Math.Ceiling(f)}, + + {"Clamp", (float a1, float a2, float a3) => MathExt.Clamp(a1, a2, a3)}, + {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, + {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, + {"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, + + {"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)}, + {"Cos", (float a) => (float) Math.Cos(a)}, + + /* + TODO: + ColorHsl(Float h, Float s, Float l) + ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress) + */ + + { + "ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( + (byte) MathExt.Clamp(a, 0, 255), + (byte) MathExt.Clamp(r, 0, 255), + (byte) MathExt.Clamp(g, 0, 255), + (byte) MathExt.Clamp(b, 0, 255) + ) + }, + + {"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, + {"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, + {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, + + {"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, + + {"Floor", (float v) => (float) Math.Floor(v)}, + + {"Inverse", (Matrix3x2 v) => Inverse(v)}, + {"Inverse", (Matrix4x4 v) => Inverse(v)}, + + + {"Length", (Vector2 a1) => a1.Length()}, + {"Length", (Vector3 a1) => a1.Length()}, + {"Length", (Vector4 a1) => a1.Length()}, + {"Length", (Quaternion a1) => a1.Length()}, + + {"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, + {"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, + + {"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)}, + {"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, + {"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, + + + {"Ln", (float f) => (float) Math.Log(f)}, + {"Log10", (float f) => (float) Math.Log10(f)}, + + {"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)}, + {"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)}, + {"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)}, + {"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)}, + { + "Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) => + new Matrix3x2(m11, m12, m21, m22, m31, m32) + }, + {"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)}, + {"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)}, + {"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)}, + { + "Matrix4x4", + (float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44) => + new Matrix4x4( + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44) + }, + + + {"Max", (float a1, float a2) => Math.Max(a1, a2)}, + {"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, + {"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)}, + {"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, + + + {"Min", (float a1, float a2) => Math.Min(a1, a2)}, + {"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)}, + {"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)}, + {"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, + + {"Mod", (float a, float b) => a % b}, + + {"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, + {"Normalize", (Vector2 a) => Vector2.Normalize(a)}, + {"Normalize", (Vector3 a) => Vector3.Normalize(a)}, + {"Normalize", (Vector4 a) => Vector4.Normalize(a)}, + + {"Pow", (float a, float b) => (float) Math.Pow(a, b)}, + {"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)}, + {"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, + + {"Round", (float a) => (float) Math.Round(a)}, + + {"Scale", (Matrix3x2 a, float b) => a * b}, + {"Scale", (Matrix4x4 a, float b) => a * b}, + {"Scale", (Vector2 a, float b) => a * b}, + {"Scale", (Vector3 a, float b) => a * b}, + {"Scale", (Vector4 a, float b) => a * b}, + + {"Sin", (float a) => (float) Math.Sin(a)}, + + {"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)}, + + // I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one + {"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)}, + + {"Sqrt", (float a) => (float) Math.Sqrt(a)}, + {"Square", (float a) => a * a}, + {"Tan", (float a) => (float) Math.Tan(a)}, + + {"ToRadians", (float a) => (float) (a * Math.PI / 180)}, + {"ToDegrees", (float a) => (float) (a * 180d / Math.PI)}, + + {"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, + {"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, + + {"Vector2", (float a, float b) => new Vector2(a, b)}, + {"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, + {"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)}, + {"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)}, + {"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, + {"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, + }; + } + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) => + _registry.Call(name, arguments, out result); + + public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs new file mode 100644 index 0000000000..002cf37522 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable + { + struct FfiRecord + { + public VariantType[] Types; + public Func, ExpressionVariant> Delegate; + } + + private readonly Dictionary>> + _registry = new Dictionary>>(); + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) + { + result = default; + if (!_registry.TryGetValue(name, out var nameGroup)) + return false; + if (!nameGroup.TryGetValue(arguments.Count, out var countGroup)) + return false; + foreach (var record in countGroup) + { + var match = true; + for (var c = 0; c < arguments.Count; c++) + { + if (record.Types[c] != arguments[c].Type) + { + match = false; + break; + } + } + + if (match) + { + result = record.Delegate(arguments); + return true; + } + } + + return false; + } + + // Stub for collection initializer + IEnumerator IEnumerable.GetEnumerator() => Array.Empty().GetEnumerator(); + + void Add(string name, Func, ExpressionVariant> cb, + params Type[] types) + { + if (!_registry.TryGetValue(name, out var nameGroup)) + _registry[name] = nameGroup = + new Dictionary>(); + if (!nameGroup.TryGetValue(types.Length, out var countGroup)) + nameGroup[types.Length] = countGroup = new List(); + + countGroup.Add(new FfiRecord + { + Types = types.Select(t => TypeMap[t]).ToArray(), + Delegate = cb + }); + } + + static readonly Dictionary TypeMap = new Dictionary + { + [typeof(bool)] = VariantType.Boolean, + [typeof(float)] = VariantType.Scalar, + [typeof(Vector2)] = VariantType.Vector2, + [typeof(Vector3)] = VariantType.Vector3, + [typeof(Vector4)] = VariantType.Vector4, + [typeof(Matrix3x2)] = VariantType.Matrix3x2, + [typeof(Matrix4x4)] = VariantType.Matrix4x4, + [typeof(Quaternion)] = VariantType.Quaternion, + [typeof(Color)] = VariantType.Color + }; + + public void Add(string name, Func cb) where T1 : struct + { + Add(name, args => cb(args[0].CastOrDefault()), typeof(T1)); + } + + public void Add(string name, Func cb) where T1 : struct where T2 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault()), typeof(T1), typeof(T2)); + } + + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault(), args[2].CastOrDefault()), typeof(T1), typeof(T2), + typeof(T3)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + } + + + public void Add(string name, + Func cb) + where T1 : struct + where T2 : struct + where T3 : struct + where T4 : struct + where T5 : struct + where T6 : struct + where T7 : struct + where T8 : struct + where T9 : struct + where T10 : struct + where T11 : struct + where T12 : struct + where T13 : struct + where T14 : struct + where T15 : struct + where T16 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault() + ), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), + typeof(T5), typeof(T6), typeof(T7), typeof(T8), + typeof(T9), typeof(T10), typeof(T11), typeof(T12), + typeof(T13), typeof(T14), typeof(T15), typeof(T16) + ); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs new file mode 100644 index 0000000000..5577d2b52a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal abstract class Expression + { + public abstract ExpressionType Type { get; } + public static Expression Parse(string expression) + { + return ExpressionParser.Parse(expression.AsSpan()); + } + + public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + + protected abstract string Print(); + public override string ToString() => Print(); + + internal static string OperatorName(ExpressionType t) + { + var attr = typeof(ExpressionType).GetMember(t.ToString())[0] + .GetCustomAttribute(); + if (attr != null) + return attr.Name; + return t.ToString(); + } + } + + internal class PrettyPrintStringAttribute : Attribute + { + public string Name { get; } + + public PrettyPrintStringAttribute(string name) + { + Name = name; + } + } + + internal enum ExpressionType + { + // Binary operators + [PrettyPrintString("+")] + Add, + [PrettyPrintString("-")] + Subtract, + [PrettyPrintString("/")] + Divide, + [PrettyPrintString("*")] + Multiply, + [PrettyPrintString(">")] + MoreThan, + [PrettyPrintString("<")] + LessThan, + [PrettyPrintString(">=")] + MoreThanOrEqual, + [PrettyPrintString("<=")] + LessThanOrEqual, + [PrettyPrintString("&&")] + LogicalAnd, + [PrettyPrintString("||")] + LogicalOr, + [PrettyPrintString("%")] + Remainder, + [PrettyPrintString("==")] + Equals, + [PrettyPrintString("!=")] + NotEquals, + // Unary operators + [PrettyPrintString("!")] + Not, + [PrettyPrintString("-")] + UnaryMinus, + // The rest + MemberAccess, + Parameter, + FunctionCall, + Keyword, + Constant, + ConditionalExpression + } + + internal enum ExpressionKeyword + { + StartingValue, + CurrentValue, + FinalValue, + Target, + Pi, + True, + False + } + + internal class ConditionalExpression : Expression + { + public Expression Condition { get; } + public Expression TruePart { get; } + public Expression FalsePart { get; } + public override ExpressionType Type => ExpressionType.ConditionalExpression; + + public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart) + { + Condition = condition; + TruePart = truePart; + FalsePart = falsePart; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var cond = Condition.Evaluate(ref context); + if (cond.Type == VariantType.Boolean && cond.Boolean) + return TruePart.Evaluate(ref context); + return FalsePart.Evaluate(ref context); + } + + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; + } + + internal class ConstantExpression : Expression + { + public float Constant { get; } + public override ExpressionType Type => ExpressionType.Constant; + + public ConstantExpression(float constant) + { + Constant = constant; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); + } + + internal class FunctionCallExpression : Expression + { + public string Name { get; } + public List Parameters { get; } + public override ExpressionType Type => ExpressionType.FunctionCall; + + public FunctionCallExpression(string name, List parameters) + { + Name = name; + Parameters = parameters; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (context.ForeignFunctionInterface == null) + return default; + var args = new List(); + foreach (var expr in Parameters) + args.Add(expr.Evaluate(ref context)); + if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) + return default; + return res; + } + + protected override string Print() + { + return Name + "( (" + string.Join("), (", Parameters) + ") )"; + } + } + + internal class MemberAccessExpression : Expression + { + public override ExpressionType Type => ExpressionType.MemberAccess; + public Expression Target { get; } + public string Member { get; } + + public MemberAccessExpression(Expression target, string member) + { + Target = target; + Member = string.Intern(member); + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Target is KeywordExpression ke + && ke.Keyword == ExpressionKeyword.Target) + return context.Target.GetProperty(Member); + if (Target is ParameterExpression pe) + { + var obj = context.Parameters?.GetObjectParameter(pe.Name); + if (obj != null) + return obj.GetProperty(Member); + } + + return Target.Evaluate(ref context).GetProperty(Member); + } + + protected override string Print() + { + return "(" + Target.ToString() + ")." + Member; + } + } + + internal class ParameterExpression : Expression + { + public string Name { get; } + public override ExpressionType Type => ExpressionType.Parameter; + + public ParameterExpression(string name) + { + Name = name; + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + return context.Parameters?.GetParameter(Name) ?? default; + } + + protected override string Print() + { + return "{" + Name + "}"; + } + } + + internal class KeywordExpression : Expression + { + public override ExpressionType Type => ExpressionType.Keyword; + public ExpressionKeyword Keyword { get; } + + public KeywordExpression(ExpressionKeyword keyword) + { + Keyword = keyword; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Keyword == ExpressionKeyword.StartingValue) + return context.StartingValue; + if (Keyword == ExpressionKeyword.CurrentValue) + return context.CurrentValue; + if (Keyword == ExpressionKeyword.FinalValue) + return context.FinalValue; + if (Keyword == ExpressionKeyword.Target) + // should be handled by MemberAccess + return default; + if (Keyword == ExpressionKeyword.True) + return true; + if (Keyword == ExpressionKeyword.False) + return false; + if (Keyword == ExpressionKeyword.Pi) + return (float) Math.PI; + return default; + } + + protected override string Print() + { + return "[" + Keyword + "]"; + } + } + + internal class UnaryExpression : Expression + { + public Expression Parameter { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Type == ExpressionType.Not) + return !Parameter.Evaluate(ref context); + if (Type == ExpressionType.UnaryMinus) + return -Parameter.Evaluate(ref context); + return default; + } + + protected override string Print() + { + return OperatorName(Type) + Parameter; + } + + public UnaryExpression(Expression parameter, ExpressionType type) + { + Parameter = parameter; + Type = type; + } + } + + internal class BinaryExpression : Expression + { + public Expression Left { get; } + public Expression Right { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var left = Left.Evaluate(ref context); + var right = Right.Evaluate(ref context); + if (Type == ExpressionType.Add) + return left + right; + if (Type == ExpressionType.Subtract) + return left - right; + if (Type == ExpressionType.Multiply) + return left * right; + if (Type == ExpressionType.Divide) + return left / right; + if (Type == ExpressionType.Remainder) + return left % right; + if (Type == ExpressionType.MoreThan) + return left > right; + if (Type == ExpressionType.LessThan) + return left < right; + if (Type == ExpressionType.MoreThanOrEqual) + return left > right; + if (Type == ExpressionType.LessThanOrEqual) + return left < right; + if (Type == ExpressionType.LogicalAnd) + return left.And(right); + if (Type == ExpressionType.LogicalOr) + return left.Or(right); + if (Type == ExpressionType.Equals) + return left.EqualsTo(right); + if (Type == ExpressionType.NotEquals) + return left.NotEqualsTo(right); + return default; + } + + protected override string Print() + { + return "(" + Left + OperatorName(Type) + Right + ")"; + } + + public BinaryExpression(Expression left, Expression right, ExpressionType type) + { + Left = left; + Right = right; + Type = type; + } + } + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs new file mode 100644 index 0000000000..a7ddabd70d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal struct ExpressionEvaluationContext + { + public ExpressionVariant StartingValue { get; set; } + public ExpressionVariant CurrentValue { get; set; } + public ExpressionVariant FinalValue { get; set; } + public IExpressionObject Target { get; set; } + public IExpressionParameterCollection Parameters { get; set; } + public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; } + } + + internal interface IExpressionObject + { + ExpressionVariant GetProperty(string name); + } + + internal interface IExpressionParameterCollection + { + public ExpressionVariant GetParameter(string name); + + public IExpressionObject GetObjectParameter(string name); + } + + internal interface IExpressionForeignFunctionInterface + { + bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs new file mode 100644 index 0000000000..6a207a3bf7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParseException : Exception + { + public int Position { get; } + + public ExpressionParseException(string message, int position) : base(message) + { + Position = position; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs new file mode 100644 index 0000000000..5924bb8f1b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +// ReSharper disable StringLiteralTypo + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParser + { + public static Expression Parse(ReadOnlySpan s) + { + var p = new TokenParser(s); + var parsed = ParseTillTerminator(ref p, "", false, false, out _); + p.SkipWhitespace(); + if (p.Length != 0) + throw new ExpressionParseException("Unexpected data ", p.Position); + return parsed; + } + + private static ReadOnlySpan Dot => ".".AsSpan(); + static bool TryParseAtomic(ref TokenParser parser, + [MaybeNullWhen(returnValue: false)] out Expression expr) + { + // We can parse keywords, parameter names and constants + expr = null; + if (parser.TryParseKeywordLowerCase("this.startingvalue")) + expr = new KeywordExpression(ExpressionKeyword.StartingValue); + else if(parser.TryParseKeywordLowerCase("this.currentvalue")) + expr = new KeywordExpression(ExpressionKeyword.CurrentValue); + else if(parser.TryParseKeywordLowerCase("this.finalvalue")) + expr = new KeywordExpression(ExpressionKeyword.FinalValue); + else if(parser.TryParseKeywordLowerCase("pi")) + expr = new KeywordExpression(ExpressionKeyword.Pi); + else if(parser.TryParseKeywordLowerCase("true")) + expr = new KeywordExpression(ExpressionKeyword.True); + else if(parser.TryParseKeywordLowerCase("false")) + expr = new KeywordExpression(ExpressionKeyword.False); + else if (parser.TryParseKeywordLowerCase("this.target")) + expr = new KeywordExpression(ExpressionKeyword.Target); + + if (expr != null) + return true; + + if (parser.TryParseIdentifier(out var identifier)) + { + expr = new ParameterExpression(identifier.ToString()); + return true; + } + + if(parser.TryParseFloat(out var scalar)) + { + expr = new ConstantExpression(scalar); + return true; + } + + return false; + + } + + static bool TryParseOperator(ref TokenParser parser, out ExpressionType op) + { + op = (ExpressionType) (-1); + if (parser.TryConsume("||")) + op = ExpressionType.LogicalOr; + else if (parser.TryConsume("&&")) + op = ExpressionType.LogicalAnd; + else if (parser.TryConsume(">=")) + op = ExpressionType.MoreThanOrEqual; + else if (parser.TryConsume("<=")) + op = ExpressionType.LessThanOrEqual; + else if (parser.TryConsume("==")) + op = ExpressionType.Equals; + else if (parser.TryConsume("!=")) + op = ExpressionType.NotEquals; + else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop)) + { +#pragma warning disable CS8509 + op = sop switch +#pragma warning restore CS8509 + { + '+' => ExpressionType.Add, + '-' => ExpressionType.Subtract, + '/' => ExpressionType.Divide, + '*' => ExpressionType.Multiply, + '<' => ExpressionType.LessThan, + '>' => ExpressionType.MoreThan, + '%' => ExpressionType.Remainder + }; + } + else + return false; + + return true; + } + + + struct ExpressionOperatorGroup + { + private List _expressions; + private List _operators; + private Expression? _first; + + public bool NotEmpty => !Empty; + public bool Empty => _expressions == null && _first == null; + + public void AppendFirst(Expression expr) + { + if (NotEmpty) + throw new InvalidOperationException(); + _first = expr; + } + + public void AppendWithOperator(Expression expr, ExpressionType op) + { + if (_expressions == null) + { + if (_first == null) + throw new InvalidOperationException(); + _expressions = new List(); + _expressions.Add(_first); + _first = null; + _operators = new List(); + } + _expressions.Add(expr); + _operators.Add(op); + } + + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/ + private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[] + { + // multiplicative + new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder}, + // additive + new[] {ExpressionType.Add, ExpressionType.Subtract}, + // relational + new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual}, + // equality + new[] {ExpressionType.Equals, ExpressionType.NotEquals}, + // conditional AND + new[] {ExpressionType.LogicalAnd}, + // conditional OR + new[]{ ExpressionType.LogicalOr}, + }; + + private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed = + OperatorPrecedenceGroups.Reverse().ToArray(); + + // a*b+c [a,b,c] [*,+], call with (0, 2) + // ToExpression(a*b) + ToExpression(c) + // a+b*c -> ToExpression(a) + ToExpression(b*c) + Expression ToExpression(int from, int to) + { + if (to - from == 0) + return _expressions[from]; + + if (to - from == 1) + return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]); + + foreach (var grp in OperatorPrecedenceGroupsReversed) + { + for (var c = from; c < to; c++) + { + var currentOperator = _operators[c]; + foreach(var operatorFromGroup in grp) + if (currentOperator == operatorFromGroup) + { + // We are dividing the expression right here + var left = ToExpression(from, c); + var right = ToExpression(c + 1, to); + return new BinaryExpression(left, right, currentOperator); + } + } + } + + // We shouldn't ever get here, if we are, there is something wrong in the code + throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0); + } + + public Expression ToExpression() + { + if (_expressions == null) + return _first ?? throw new InvalidOperationException(); + return ToExpression(0, _expressions.Count - 1); + } + } + + static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars, + bool throwOnTerminator, + bool throwOnEnd, + out char? token) + { + ExpressionOperatorGroup left = default; + token = null; + while (true) + { + if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken)) + { + if (throwOnTerminator || left.Empty) + throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1); + token = consumedToken; + return left.ToExpression(); + } + parser.SkipWhitespace(); + if (parser.Length == 0) + { + if (throwOnEnd || left.Empty) + throw new ExpressionParseException("Unexpected end of expression", parser.Position); + return left.ToExpression(); + } + + ExpressionType? op = null; + if (left.NotEmpty) + { + if (parser.TryConsume('?')) + { + var truePart = ParseTillTerminator(ref parser, ":", + false, true, out _); + // pass through the current parsing rules to consume the rest + var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd, + out token); + + return new ConditionalExpression(left.ToExpression(), truePart, falsePart); + } + + // We expect a binary operator here + if (!TryParseOperator(ref parser, out var sop)) + throw new ExpressionParseException("Unexpected token", parser.Position); + op = sop; + } + + // We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part + var applyNegation = false; + while (parser.TryConsume('!')) + applyNegation = !applyNegation; + + var applyUnaryMinus = false; + while (parser.TryConsume('-')) + applyUnaryMinus = !applyUnaryMinus; + + Expression? parsed; + + if (parser.TryConsume('(')) + parsed = ParseTillTerminator(ref parser, ")", false, true, out _); + else if (parser.TryParseCall(out var functionName)) + { + var parameterList = new List(); + while (true) + { + parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken)); + if (closingToken == ')') + break; + if (closingToken != ',') + throw new ExpressionParseException("Unexpected end of the expression", parser.Position); + } + + parsed = new FunctionCallExpression(functionName.ToString(), parameterList); + } + else if (TryParseAtomic(ref parser, out parsed)) + { + // do nothing + } + else + throw new ExpressionParseException("Unexpected token", parser.Position); + + + // Parse any following member accesses + while (parser.TryConsume('.')) + { + if(!parser.TryParseIdentifier(out var memberName)) + throw new ExpressionParseException("Unexpected token", parser.Position); + + parsed = new MemberAccessExpression(parsed, memberName.ToString()); + } + + // Apply ! operator + if (applyNegation) + parsed = new UnaryExpression(parsed, ExpressionType.Not); + + if (applyUnaryMinus) + { + if(parsed is ConstantExpression constexpr) + parsed = new ConstantExpression(-constexpr.Constant); + else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus); + } + + if (left.Empty) + left.AppendFirst(parsed); + else + left.AppendWithOperator(parsed, op!.Value); + } + + + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs new file mode 100644 index 0000000000..8c6af5cb0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -0,0 +1,739 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal enum VariantType + { + Invalid, + Boolean, + Scalar, + Double, + Vector2, + Vector3, + Vector4, + AvaloniaMatrix, + Matrix3x2, + Matrix4x4, + Quaternion, + Color + } + + [StructLayout(LayoutKind.Explicit)] + internal struct ExpressionVariant + { + [FieldOffset(0)] public VariantType Type; + + [FieldOffset(4)] public bool Boolean; + [FieldOffset(4)] public float Scalar; + [FieldOffset(4)] public double Double; + [FieldOffset(4)] public Vector2 Vector2; + [FieldOffset(4)] public Vector3 Vector3; + [FieldOffset(4)] public Vector4 Vector4; + [FieldOffset(4)] public Matrix AvaloniaMatrix; + [FieldOffset(4)] public Matrix3x2 Matrix3x2; + [FieldOffset(4)] public Matrix4x4 Matrix4x4; + [FieldOffset(4)] public Quaternion Quaternion; + [FieldOffset(4)] public Color Color; + + + public ExpressionVariant GetProperty(string property) + { + if (Type == VariantType.Vector2) + { + if (ReferenceEquals(property, "X")) + return Vector2.X; + if (ReferenceEquals(property, "Y")) + return Vector2.Y; + return default; + } + + if (Type == VariantType.Vector3) + { + if (ReferenceEquals(property, "X")) + return Vector3.X; + if (ReferenceEquals(property, "Y")) + return Vector3.Y; + if (ReferenceEquals(property, "Z")) + return Vector3.Z; + if(ReferenceEquals(property, "XY")) + return new Vector2(Vector3.X, Vector3.Y); + if(ReferenceEquals(property, "YX")) + return new Vector2(Vector3.Y, Vector3.X); + if(ReferenceEquals(property, "XZ")) + return new Vector2(Vector3.X, Vector3.Z); + if(ReferenceEquals(property, "ZX")) + return new Vector2(Vector3.Z, Vector3.X); + if(ReferenceEquals(property, "YZ")) + return new Vector2(Vector3.Y, Vector3.Z); + if(ReferenceEquals(property, "ZY")) + return new Vector2(Vector3.Z, Vector3.Y); + return default; + } + + if (Type == VariantType.Vector4) + { + if (ReferenceEquals(property, "X")) + return Vector4.X; + if (ReferenceEquals(property, "Y")) + return Vector4.Y; + if (ReferenceEquals(property, "Z")) + return Vector4.Z; + if (ReferenceEquals(property, "W")) + return Vector4.W; + return default; + } + + if (Type == VariantType.Matrix3x2) + { + if (ReferenceEquals(property, "M11")) + return Matrix3x2.M11; + if (ReferenceEquals(property, "M12")) + return Matrix3x2.M12; + if (ReferenceEquals(property, "M21")) + return Matrix3x2.M21; + if (ReferenceEquals(property, "M22")) + return Matrix3x2.M22; + if (ReferenceEquals(property, "M31")) + return Matrix3x2.M31; + if (ReferenceEquals(property, "M32")) + return Matrix3x2.M32; + return default; + } + + if (Type == VariantType.AvaloniaMatrix) + { + if (ReferenceEquals(property, "M11")) + return AvaloniaMatrix.M11; + if (ReferenceEquals(property, "M12")) + return AvaloniaMatrix.M12; + if (ReferenceEquals(property, "M21")) + return AvaloniaMatrix.M21; + if (ReferenceEquals(property, "M22")) + return AvaloniaMatrix.M22; + if (ReferenceEquals(property, "M31")) + return AvaloniaMatrix.M31; + if (ReferenceEquals(property, "M32")) + return AvaloniaMatrix.M32; + return default; + } + + if (Type == VariantType.Matrix4x4) + { + if (ReferenceEquals(property, "M11")) + return Matrix4x4.M11; + if (ReferenceEquals(property, "M12")) + return Matrix4x4.M12; + if (ReferenceEquals(property, "M13")) + return Matrix4x4.M13; + if (ReferenceEquals(property, "M14")) + return Matrix4x4.M14; + if (ReferenceEquals(property, "M21")) + return Matrix4x4.M21; + if (ReferenceEquals(property, "M22")) + return Matrix4x4.M22; + if (ReferenceEquals(property, "M23")) + return Matrix4x4.M23; + if (ReferenceEquals(property, "M24")) + return Matrix4x4.M24; + if (ReferenceEquals(property, "M31")) + return Matrix4x4.M31; + if (ReferenceEquals(property, "M32")) + return Matrix4x4.M32; + if (ReferenceEquals(property, "M33")) + return Matrix4x4.M33; + if (ReferenceEquals(property, "M34")) + return Matrix4x4.M34; + if (ReferenceEquals(property, "M41")) + return Matrix4x4.M41; + if (ReferenceEquals(property, "M42")) + return Matrix4x4.M42; + if (ReferenceEquals(property, "M43")) + return Matrix4x4.M43; + if (ReferenceEquals(property, "M44")) + return Matrix4x4.M44; + return default; + } + + if (Type == VariantType.Quaternion) + { + if (ReferenceEquals(property, "X")) + return Quaternion.X; + if (ReferenceEquals(property, "Y")) + return Quaternion.Y; + if (ReferenceEquals(property, "Z")) + return Quaternion.Z; + if (ReferenceEquals(property, "W")) + return Quaternion.W; + return default; + } + + if (Type == VariantType.Color) + { + if (ReferenceEquals(property, "A")) + return Color.A; + if (ReferenceEquals(property, "R")) + return Color.R; + if (ReferenceEquals(property, "G")) + return Color.G; + if (ReferenceEquals(property, "B")) + return Color.B; + return default; + } + + return default; + } + + public static implicit operator ExpressionVariant(bool value) => + new ExpressionVariant + { + Type = VariantType.Boolean, + Boolean = value + }; + + public static implicit operator ExpressionVariant(float scalar) => + new ExpressionVariant + { + Type = VariantType.Scalar, + Scalar = scalar + }; + + public static implicit operator ExpressionVariant(double d) => + new ExpressionVariant + { + Type = VariantType.Double, + Double = d + }; + + + public static implicit operator ExpressionVariant(Vector2 value) => + new ExpressionVariant + { + Type = VariantType.Vector2, + Vector2 = value + }; + + + public static implicit operator ExpressionVariant(Vector3 value) => + new ExpressionVariant + { + Type = VariantType.Vector3, + Vector3 = value + }; + + + public static implicit operator ExpressionVariant(Vector4 value) => + new ExpressionVariant + { + Type = VariantType.Vector4, + Vector4 = value + }; + + public static implicit operator ExpressionVariant(Matrix3x2 value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + Matrix3x2 = value + }; + + public static implicit operator ExpressionVariant(Matrix value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + AvaloniaMatrix = value + }; + + public static implicit operator ExpressionVariant(Matrix4x4 value) => + new ExpressionVariant + { + Type = VariantType.Matrix4x4, + Matrix4x4 = value + }; + + public static implicit operator ExpressionVariant(Quaternion value) => + new ExpressionVariant + { + Type = VariantType.Quaternion, + Quaternion = value + }; + + public static implicit operator ExpressionVariant(Avalonia.Media.Color value) => + new ExpressionVariant + { + Type = VariantType.Color, + Color = value + }; + + public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar + right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double + right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 + right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 + right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 + right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 + right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix + right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 + right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion + right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar - right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double - right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 - right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 - right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 - right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 - right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix - right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 - right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion - right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left) + { + + if (left.Type == VariantType.Scalar) + return -left.Scalar; + + if (left.Type == VariantType.Double) + return -left.Double; + + if (left.Type == VariantType.Vector2) + return -left.Vector2; + + if (left.Type == VariantType.Vector3) + return -left.Vector3; + + if (left.Type == VariantType.Vector4) + return -left.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return -left.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return -left.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return -left.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return -left.Quaternion; + + return default; + } + + public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar * right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double * right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 * right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 * right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 * right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 * right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 * right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 * right.Scalar; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2) + return left.Matrix3x2 * right.Matrix3x2; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar) + return left.Matrix3x2 * right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix * right.AvaloniaMatrix; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) + return left.AvaloniaMatrix * (double)right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) + return left.AvaloniaMatrix * right.Double; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) + return left.Matrix4x4 * right.Matrix4x4; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar) + return left.Matrix4x4 * right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion * right.Quaternion; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar) + return left.Quaternion * right.Scalar; + + return default; + } + + public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar / right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double / right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 / right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 / right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 / right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 / right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 / right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 / right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion / right.Quaternion; + + return default; + } + + public ExpressionVariant EqualsTo(ExpressionVariant right) + { + if (Type != right.Type || Type == VariantType.Invalid) + return default; + + if (Type == VariantType.Scalar) + return Scalar == right.Scalar; + + + if (Type == VariantType.Double) + return Double == right.Double; + + if (Type == VariantType.Vector2) + return Vector2 == right.Vector2; + + if (Type == VariantType.Vector3) + return Vector3 == right.Vector3; + + if (Type == VariantType.Vector4) + return Vector4 == right.Vector4; + + if (Type == VariantType.Boolean) + return Boolean == right.Boolean; + + if (Type == VariantType.Matrix3x2) + return Matrix3x2 == right.Matrix3x2; + + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix == right.AvaloniaMatrix; + + if (Type == VariantType.Matrix4x4) + return Matrix4x4 == right.Matrix4x4; + + if (Type == VariantType.Quaternion) + return Quaternion == right.Quaternion; + + return default; + } + + public ExpressionVariant NotEqualsTo(ExpressionVariant right) + { + var r = EqualsTo(right); + if (r.Type == VariantType.Boolean) + return !r.Boolean; + return default; + } + + public static ExpressionVariant operator !(ExpressionVariant v) + { + if (v.Type == VariantType.Boolean) + return !v.Boolean; + return default; + } + + public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar % right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double % right.Double; + return default; + } + + public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar < right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double < right.Double; + return default; + } + + public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar > right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double > right.Double; + return default; + } + + public ExpressionVariant And(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public ExpressionVariant Or(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public bool TryCast(out T res) where T : struct + { + if (typeof(T) == typeof(bool)) + { + if (Type == VariantType.Boolean) + { + res = (T) (object) Boolean; + return true; + } + } + + if (typeof(T) == typeof(float)) + { + if (Type == VariantType.Scalar) + { + res = (T) (object) Scalar; + return true; + } + } + + if (typeof(T) == typeof(double)) + { + if (Type == VariantType.Double) + { + res = (T) (object) Double; + return true; + } + } + + if (typeof(T) == typeof(Vector2)) + { + if (Type == VariantType.Vector2) + { + res = (T) (object) Vector2; + return true; + } + } + + if (typeof(T) == typeof(Vector3)) + { + if (Type == VariantType.Vector3) + { + res = (T) (object) Vector3; + return true; + } + } + + if (typeof(T) == typeof(Vector4)) + { + if (Type == VariantType.Vector4) + { + res = (T) (object) Vector4; + return true; + } + } + + if (typeof(T) == typeof(Matrix3x2)) + { + if (Type == VariantType.Matrix3x2) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix)) + { + if (Type == VariantType.AvaloniaMatrix) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix4x4)) + { + if (Type == VariantType.Matrix4x4) + { + res = (T) (object) Matrix4x4; + return true; + } + } + + if (typeof(T) == typeof(Quaternion)) + { + if (Type == VariantType.Quaternion) + { + res = (T) (object) Quaternion; + return true; + } + } + + if (typeof(T) == typeof(Avalonia.Media.Color)) + { + if (Type == VariantType.Color) + { + res = (T) (object) Color; + return true; + } + } + + res = default(T); + return false; + } + + public static ExpressionVariant Create(T v) where T : struct + { + if (typeof(T) == typeof(bool)) + return (bool) (object) v; + + if (typeof(T) == typeof(float)) + return (float) (object) v; + + if (typeof(T) == typeof(Vector2)) + return (Vector2) (object) v; + + if (typeof(T) == typeof(Vector3)) + return (Vector3) (object) v; + + if (typeof(T) == typeof(Vector4)) + return (Vector4) (object) v; + + if (typeof(T) == typeof(Matrix3x2)) + return (Matrix3x2) (object) v; + + if (typeof(T) == typeof(Matrix)) + return (Matrix) (object) v; + + if (typeof(T) == typeof(Matrix4x4)) + return (Matrix4x4) (object) v; + + if (typeof(T) == typeof(Quaternion)) + return (Quaternion) (object) v; + + if (typeof(T) == typeof(Avalonia.Media.Color)) + return (Avalonia.Media.Color) (object) v; + + throw new ArgumentException("Invalid variant type: " + typeof(T)); + } + + public T CastOrDefault() where T : struct + { + TryCast(out var r); + return r; + } + + public override string ToString() + { + if (Type == VariantType.Boolean) + return Boolean.ToString(); + if (Type == VariantType.Scalar) + return Scalar.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Double) + return Double.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Vector2) + return Vector2.ToString(); + if (Type == VariantType.Vector3) + return Vector3.ToString(); + if (Type == VariantType.Vector4) + return Vector4.ToString(); + if (Type == VariantType.Quaternion) + return Quaternion.ToString(); + if (Type == VariantType.Matrix3x2) + return Matrix3x2.ToString(); + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix.ToString(); + if (Type == VariantType.Matrix4x4) + return Matrix4x4.ToString(); + if (Type == VariantType.Color) + return Color.ToString(); + if (Type == VariantType.Invalid) + return "Invalid"; + return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs new file mode 100644 index 0000000000..1050c7274c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -0,0 +1,256 @@ +using System; +using System.Globalization; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public TokenParser(ReadOnlySpan s) + { + _s = s; + Position = 0; + } + + public void SkipWhitespace() + { + while (true) + { + if (_s.Length > 0 && char.IsWhiteSpace(_s[0])) + Advance(1); + else + return; + } + } + + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z'); + + public bool TryConsume(char c) + { + SkipWhitespace(); + if (_s.Length == 0 || _s[0] != c) + return false; + + Advance(1); + return true; + } + public bool TryConsume(string s) + { + SkipWhitespace(); + if (_s.Length < s.Length) + return false; + for (var c = 0; c < s.Length; c++) + { + if (_s[c] != s[c]) + return false; + } + + Advance(s.Length); + return true; + } + + public bool TryConsumeAny(ReadOnlySpan chars, out char token) + { + SkipWhitespace(); + token = default; + if (_s.Length == 0) + return false; + + foreach (var c in chars) + { + if (c == _s[0]) + { + token = c; + Advance(1); + return true; + } + } + + return false; + } + + + public bool TryParseKeyword(string keyword) + { + SkipWhitespace(); + if (keyword.Length > _s.Length) + return false; + for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) + return false; + + Advance(keyword.Length); + return true; + } + + public bool TryParseKeywordLowerCase(string keywordInLowerCase) + { + SkipWhitespace(); + if (keywordInLowerCase.Length > _s.Length) + return false; + for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) + return false; + + Advance(keywordInLowerCase.Length); + return true; + } + + public void Advance(int c) + { + _s = _s.Slice(c); + Position += c; + } + + public int Length => _s.Length; + + public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + { + var found = false; + foreach(var vc in extraValidChars) + if (vc == ch) + { + found = true; + break; + } + + if (found) + len++; + else + break; + } + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseIdentifier(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + break; + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseCall(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') + len++; + else + break; + } + + res = _s.Slice(0, len); + + // Find '(' + for (var c = len; c < _s.Length; c++) + { + if(char.IsWhiteSpace(_s[c])) + continue; + if(_s[c]=='(') + { + Advance(c + 1); + return true; + } + + return false; + + } + + return false; + + } + + + public bool TryParseFloat(out float res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + + public override string ToString() => _s.ToString(); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs new file mode 100644 index 0000000000..9ef31c30e0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs @@ -0,0 +1,9 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public interface ICompositionSurface + { + internal ServerCompositionSurface Server { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs new file mode 100644 index 0000000000..5e91bcb3d4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -0,0 +1,46 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + static class MatrixUtils + { + public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, + Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) + { + // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work + + var anchor = size * anchorPoint; + var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0); + + var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z); + + if (!transformMatrix.IsIdentity) + mat = transformMatrix * mat; + + + if (scale != new Vector3(1, 1, 1)) + mat *= Matrix4x4.CreateScale(scale, center); + + //TODO: RotationAxis support + if (rotationAngle != 0) + mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); + + if (orientation != Quaternion.Identity) + { + if (centerPoint != default) + { + mat *= Matrix4x4.CreateTranslation(-center) + * Matrix4x4.CreateFromQuaternion(orientation) + * Matrix4x4.CreateTranslation(center); + } + else + mat *= Matrix4x4.CreateFromQuaternion(orientation); + } + + if (offset != default) + mat *= Matrix4x4.CreateTranslation(offset); + + return mat; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs new file mode 100644 index 0000000000..f5dfa92897 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -0,0 +1,142 @@ +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositorDrawingContextProxy : IDrawingContextImpl +{ + private IDrawingContextImpl _impl; + + public CompositorDrawingContextProxy(IDrawingContextImpl impl) + { + _impl = impl; + } + + public Matrix PreTransform { get; set; } = Matrix.Identity; + + public void Dispose() + { + _impl.Dispose(); + } + + Matrix _transform; + public Matrix Transform + { + get => _transform; + set => _impl.Transform = PreTransform * (_transform = value); + } + + public void Clear(Color color) + { + _impl.Clear(color); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + _impl.DrawLine(pen, p1, p2); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + _impl.DrawGeometry(brush, pen, geometry); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + _impl.DrawRectangle(brush, pen, rect, boxShadows); + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + _impl.DrawEllipse(brush, pen, rect); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + _impl.DrawGlyphRun(foreground, glyphRun); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return _impl.CreateLayer(size); + } + + public void PushClip(Rect clip) + { + _impl.PushClip(clip); + } + + public void PushClip(RoundedRect clip) + { + _impl.PushClip(clip); + } + + public void PopClip() + { + _impl.PopClip(); + } + + public void PushOpacity(double opacity) + { + _impl.PushOpacity(opacity); + } + + public void PopOpacity() + { + _impl.PopOpacity(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + _impl.PushOpacityMask(mask, bounds); + } + + public void PopOpacityMask() + { + _impl.PopOpacityMask(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + _impl.PushGeometryClip(clip); + } + + public void PopGeometryClip() + { + _impl.PopGeometryClip(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + _impl.PushBitmapBlendMode(blendingMode); + } + + public void PopBitmapBlendMode() + { + _impl.PopBitmapBlendMode(); + } + + public void Custom(ICustomDrawOperation custom) + { + _impl.Custom(custom); + } + + public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, + transform.M22, transform.M41, + transform.M42); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs new file mode 100644 index 0000000000..372fa4d9ce --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal class ReadbackIndices + { + private readonly object _lock = new object(); + public int ReadIndex { get; private set; } = 0; + public int WriteIndex { get; private set; } = -1; + public ulong ReadRevision { get; private set; } + public ulong WriteRevision { get; private set; } + private ulong[] _revisions = new ulong[3]; + + + public void NextRead() + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) + { + ReadIndex = c; + ReadRevision = _revisions[c]; + return; + } + } + } + } + + public void NextWrite(ulong revision) + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex) + { + WriteIndex = c; + WriteRevision = revision; + _revisions[c] = revision; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs new file mode 100644 index 0000000000..eb041aaf88 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionBrush : ServerObject + { + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs new file mode 100644 index 0000000000..397c968d04 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using Avalonia.Collections.Pooled; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +{ + private CompositionDrawList? _renderCommands; + + public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + { + } + + protected override void ApplyCore(ChangeSet changes) + { + var ch = (DrawListVisualChanges)changes; + if (ch.DrawCommandsIsSet) + { + _renderCommands?.Dispose(); + _renderCommands = ch.AcquireDrawCommands(); + } + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (_renderCommands != null) + { + foreach (var cmd in _renderCommands) + { + cmd.Item.Render(canvas); + } + } + base.RenderCore(canvas, transform); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs new file mode 100644 index 0000000000..0948b9692f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionGradientBrush : ServerCompositionBrush + { + public ServerCompositionGradientStopCollection Stops { get; } + public ServerCompositionGradientBrush(ServerCompositor compositor) : base(compositor) + { + Stops = new ServerCompositionGradientStopCollection(compositor); + } + + public override long LastChangedBy => Math.Max(base.LastChangedBy, (long)Stops.LastChangedBy); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs new file mode 100644 index 0000000000..c421cdcfb0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs @@ -0,0 +1,22 @@ +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionLinearGradientBrush + { + /* + protected override void UpdateBackendBrush(ICbBrush brush) + { + var stopColors = new Color[Stops.List.Count]; + var offsets = new float[Stops.List.Count]; + for (var c = 0; c < Stops.List.Count; c++) + { + stopColors[c] = Stops.List[c].Color; + offsets[c] = Stops.List[c].Offset; + } + + ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); + }*/ + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs new file mode 100644 index 0000000000..462a193a86 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerCompositionSurface : ServerObject + { + protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs new file mode 100644 index 0000000000..493529e111 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -0,0 +1,53 @@ +using System; +using System.Numerics; +using System.Threading; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionTarget + { + private readonly ServerCompositor _compositor; + private readonly Func _renderTargetFactory; + private static long s_nextId = 1; + public long Id { get; } + private ulong _frame = 1; + private IRenderTarget? _renderTarget; + + public ReadbackIndices Readback { get; } = new(); + + public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + base(compositor) + { + _compositor = compositor; + _renderTargetFactory = renderTargetFactory; + Id = Interlocked.Increment(ref s_nextId); + } + + partial void OnIsEnabledChanged() + { + if (IsEnabled) + _compositor.AddCompositionTarget(this); + else + _compositor.RemoveCompositionTarget(this); + } + + public void Render() + { + if (Root == null) + return; + _renderTarget ??= _renderTargetFactory(); + + Compositor.UpdateServerTime(); + using (var context = _renderTarget.CreateDrawingContext(null)) + { + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + } + + Readback.NextWrite(_frame); + _frame++; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs new file mode 100644 index 0000000000..e56f85acdf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositor : IRenderLoopTask + { + private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); + public long LastBatchId { get; private set; } + public Stopwatch Clock { get; } = Stopwatch.StartNew(); + public TimeSpan ServerNow { get; private set; } + private List _activeTargets = new(); + + public ServerCompositor(IRenderLoop renderLoop) + { + _renderLoop = renderLoop; + _renderLoop.Add(this); + } + + public void EnqueueBatch(Batch batch) + { + lock (_batches) + _batches.Enqueue(batch); + } + + internal void UpdateServerTime() => ServerNow = Clock.Elapsed; + + List _reusableToCompleteList = new(); + void ApplyPendingBatches() + { + while (true) + { + Batch batch; + lock (_batches) + { + if(_batches.Count == 0) + break; + batch = _batches.Dequeue(); + } + + foreach (var change in batch.Changes) + { + if (change.Dispose) + { + //TODO + } + change.Target!.Apply(change); + change.Reset(); + } + + _reusableToCompleteList.Add(batch); + LastBatchId = batch.SequenceId; + } + } + + void CompletePendingBatches() + { + foreach(var batch in _reusableToCompleteList) + batch.Complete(); + _reusableToCompleteList.Clear(); + } + + bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) + { + } + + void IRenderLoopTask.Render() + { + ApplyPendingBatches(); + foreach (var t in _activeTargets) + t.Render(); + + CompletePendingBatches(); + } + + public void AddCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Add(target); + } + + public void RemoveCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Remove(target); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs new file mode 100644 index 0000000000..ac112b846f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositionContainerVisual : ServerCompositionVisual + { + public ServerCompositionVisualCollection Children { get; } + + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + base.RenderCore(canvas, transform); + + foreach (var ch in Children) + { + var t = transform; + + t = ch.CombinedTransformMatrix * t; + ch.Render(canvas, t); + } + } + + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + { + Children = new ServerCompositionVisualCollection(compositor); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs new file mode 100644 index 0000000000..5f3eb051a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerCustomDrawVisual : ServerCompositionContainerVisual + { + private readonly ICustomDrawVisualRenderer _renderer; + private TData? _data; + public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) + { + _renderer = renderer; + } + + protected override void ApplyCore(ChangeSet changes) + { + var c = (CustomDrawVisualChanges) changes; + if (c.Data.IsSet) + _data = c.Data.Value; + + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + _renderer.Render(canvas, _data); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs new file mode 100644 index 0000000000..09ef119e6b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerList : ServerObject where T : ServerObject + { + public List List { get; } = new List(); + protected override void ApplyCore(ChangeSet changes) + { + var c = (ListChangeSet) changes; + if (c.HasListChanges) + { + foreach (var lc in c.ListChanges) + { + if(lc.Action == ListChangeAction.Clear) + List.Clear(); + if(lc.Action == ListChangeAction.RemoveAt) + List.RemoveAt(lc.Index); + if(lc.Action == ListChangeAction.InsertAt) + List.Insert(lc.Index, lc.Added!); + if (lc.Action == ListChangeAction.ReplaceAt) + List[lc.Index] = lc.Added!; + } + } + } + + public override long LastChangedBy + { + get + { + var seq = base.LastChangedBy; + foreach (var i in List) + seq = Math.Max(i.LastChangedBy, seq); + return seq; + } + } + + public List.Enumerator GetEnumerator() => List.GetEnumerator(); + + public ServerList(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs new file mode 100644 index 0000000000..072377cd7e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerObject : IExpressionObject + { + public ServerCompositor Compositor { get; } + + public virtual long LastChangedBy => ItselfLastChangedBy; + public long ItselfLastChangedBy { get; private set; } + + public ServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + protected virtual void ApplyCore(ChangeSet changes) + { + + } + + public void Apply(ChangeSet changes) + { + ApplyCore(changes); + ItselfLastChangedBy = changes.Batch!.SequenceId; + } + + public virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs new file mode 100644 index 0000000000..60569867de --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -0,0 +1,16 @@ +using System.Numerics; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSolidColorVisual + { + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + canvas.Transform = canvas.CutTransform(transform); + canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs new file mode 100644 index 0000000000..c658dc8ae3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSpriteVisual + { + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (Brush != null) + { + //SetTransform(canvas, transform); + //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); + } + + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs new file mode 100644 index 0000000000..37e188fb47 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + unsafe partial class ServerCompositionVisual : ServerObject + { + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + + } + + public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if(Visible == false) + return; + if(Opacity == 0) + return; + canvas.PreTransform = canvas.CutTransform(transform); + canvas.Transform = Matrix.Identity; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + if(ClipToBounds) + canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); + if (Clip != null) + canvas.PushGeometryClip(Clip); + + RenderCore(canvas, transform); + + if (Clip != null) + canvas.PopGeometryClip(); + if (ClipToBounds) + canvas.PopClip(); + if(Opacity != 1) + canvas.PopOpacity(); + } + + private ReadbackData _readback0, _readback1, _readback2; + + + public ref ReadbackData GetReadback(int idx) + { + if (idx == 0) + return ref _readback0; + if (idx == 1) + return ref _readback1; + return ref _readback2; + } + + public Matrix4x4 CombinedTransformMatrix + { + get + { + if (Root == null) + return default; + + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + + return res; + } + } + + public struct ReadbackData + { + public Matrix4x4 Matrix; + public bool Visible; + public ulong Revision; + public long TargetId; + } + + partial void ApplyChangesExtra(CompositionVisualChanges c) + { + if (c.Parent.IsSet) + Parent = c.Parent.Value; + if (c.Root.IsSet) + Root = c.Root.Value; + } + + public ServerCompositionTarget? Root { get; private set; } + + public ServerCompositionVisual? Parent { get; private set; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs new file mode 100644 index 0000000000..7b64c01d09 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class Batch + { + private static long _nextSequenceId = 1; + private static ConcurrentBag> _pool = new ConcurrentBag>(); + public long SequenceId { get; } + + public Batch() + { + SequenceId = Interlocked.Increment(ref _nextSequenceId); + if (!_pool.TryTake(out var lst)) + lst = new List(); + Changes = lst; + } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public List Changes { get; private set; } + public TimeSpan CommitedAt { get; set; } + + public void Complete() + { + Changes.Clear(); + _pool.Add(Changes); + Changes = null!; + + _tcs.TrySetResult(0); + } + + public Task Completed => _tcs.Task; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs new file mode 100644 index 0000000000..cbee350ab3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Animations; + +namespace Avalonia.Rendering.Composition.Transport +{ + struct Change + { + private T? _value; + + public bool IsSet { get; private set; } + + public T? Value + { + get + { + if(!IsSet) + throw new InvalidOperationException(); + return _value; + } + set + { + IsSet = true; + _value = value; + } + } + + public void Reset() + { + _value = default; + IsSet = false; + } + } + + struct AnimatedChange + { + private T? _value; + private IAnimationInstance? _animation; + + public bool IsValue { get; private set; } + public bool IsAnimation { get; private set; } + + public T Value + { + get + { + if(!IsValue) + throw new InvalidOperationException(); + return _value!; + } + set + { + IsAnimation = false; + _animation = null; + IsValue = true; + _value = value; + } + } + + public IAnimationInstance Animation + { + get + { + if(!IsAnimation) + throw new InvalidOperationException(); + return _animation!; + } + set + { + IsValue = false; + _value = default; + IsAnimation = true; + _animation = value; + } + } + + public void Reset() + { + this = default; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs new file mode 100644 index 0000000000..898885dce6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal abstract class ChangeSet + { + private readonly IChangeSetPool _pool; + public Batch Batch = null!; + public ServerObject? Target; + public bool Dispose; + + public ChangeSet(IChangeSetPool pool) + { + _pool = pool; + } + + public virtual void Reset() + { + Batch = null!; + Target = null; + Dispose = false; + } + + public void Return() + { + _pool.Return(this); + } + } + + internal class CompositionObjectChanges : ChangeSet + { + public CompositionObjectChanges(IChangeSetPool pool) : base(pool) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs new file mode 100644 index 0000000000..ea97cd7d44 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + interface IChangeSetPool + { + void Return(ChangeSet changes); + ChangeSet Get(ServerObject target, Batch batch); + } + + class ChangeSetPool : IChangeSetPool where T : ChangeSet + { + private readonly Func _factory; + private readonly ConcurrentBag _pool = new ConcurrentBag(); + + public ChangeSetPool(Func factory) + { + _factory = factory; + } + + public void Return(T changes) + { + changes.Reset(); + _pool.Add(changes); + } + + void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); + ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); + + public T Get(ServerObject target, Batch batch) + { + if (!_pool.TryTake(out var res)) + res = _factory(this); + res.Target = target; + res.Batch = batch; + res.Dispose = false; + return res; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs new file mode 100644 index 0000000000..aed041b62e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Rendering.Composition.Transport +{ + class CustomDrawVisualChanges : CompositionVisualChanges + { + public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public Change Data; + + public override void Reset() + { + Data.Reset(); + base.Reset(); + } + + public new static ChangeSetPool> Pool { get; } = + new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs new file mode 100644 index 0000000000..215c03b229 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class DrawListVisualChanges : CompositionVisualChanges +{ + private CompositionDrawList? _drawCommands; + + public DrawListVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public CompositionDrawList? DrawCommands + { + get => _drawCommands; + set + { + _drawCommands?.Dispose(); + _drawCommands = value; + DrawCommandsIsSet = true; + } + } + + public bool DrawCommandsIsSet { get; private set; } + + public CompositionDrawList? AcquireDrawCommands() + { + var rv = _drawCommands; + _drawCommands = null; + DrawCommandsIsSet = false; + return rv; + } + + public override void Reset() + { + _drawCommands?.Dispose(); + _drawCommands = null; + DrawCommandsIsSet = false; + base.Reset(); + } + + public new static ChangeSetPool Pool { get; } = + new ChangeSetPool(pool => new(pool)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs new file mode 100644 index 0000000000..ee6e4231f8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class ListChange where T : ServerObject + { + public int Index; + public ListChangeAction Action; + public T? Added; + } + + internal enum ListChangeAction + { + InsertAt, + RemoveAt, + Clear, + ReplaceAt + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs new file mode 100644 index 0000000000..9bb101a080 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ListChangeSet : ChangeSet where T : ServerObject + { + private List>? _listChanges; + public List> ListChanges => _listChanges ??= new List>(); + public bool HasListChanges => _listChanges != null; + + public override void Reset() + { + _listChanges?.Clear(); + base.Reset(); + } + + public ListChangeSet(IChangeSetPool pool) : base(pool) + { + } + + public static readonly ChangeSetPool> Pool = + new ChangeSetPool>(pool => new ListChangeSet(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs new file mode 100644 index 0000000000..1add3aa990 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ServerListProxyHelper : IList + where TServer : ServerObject + where TClient : CompositionObject + { + private readonly IGetChanges _parent; + private readonly List _list = new List(); + + public interface IGetChanges + { + ListChangeSet GetChanges(); + } + + public ServerListProxyHelper(IGetChanges parent) + { + _parent = parent; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TClient item) => Insert(_list.Count, item); + + public void Clear() + { + _list.Clear(); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.Clear + }); + } + + public bool Contains(TClient item) => _list.Contains(item); + + public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(TClient item) + { + var idx = _list.IndexOf(item); + if (idx == -1) + return false; + RemoveAt(idx); + return true; + } + + public int Count => _list.Count; + public bool IsReadOnly => false; + public int IndexOf(TClient item) => _list.IndexOf(item); + + public void Insert(int index, TClient item) + { + _list.Insert(index, item); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.InsertAt, + Index = index, + Added = (TServer) item.Server + }); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.RemoveAt, + Index = index + }); + } + + public TClient this[int index] + { + get => _list[index]; + set + { + _list[index] = value; + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.ReplaceAt, + Index = index, + Added = (TServer) value.Server + }); + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs new file mode 100644 index 0000000000..c87fb96967 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + partial class CompositionVisualChanges + { + public Change Parent; + public Change Root; + + partial void ResetExtra() + { + Parent.Reset(); + Root.Reset(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs new file mode 100644 index 0000000000..8c85d7978b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs @@ -0,0 +1,302 @@ +// ReSharper disable InconsistentNaming +// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc + +using System; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable CommentTypo +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable TooWideLocalVariableScope +// ReSharper disable UnusedMember.Global +#pragma warning disable 649 + +namespace Avalonia.Rendering.Composition.Utils +{ + internal unsafe struct CubicBezier + { + const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; + double ax_; + double bx_; + double cx_; + + double ay_; + double by_; + double cy_; + + double start_gradient_; + double end_gradient_; + + double range_min_; + double range_max_; + private bool monotonically_increasing_; + + fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; + + public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() + { + InitCoefficients(p1x, p1y, p2x, p2y); + InitGradients(p1x, p1y, p2x, p2y); + InitRange(p1y, p2y); + InitSpline(); + } + + public readonly double SampleCurveX(double t) + { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax_ * t + bx_) * t + cx_) * t; + } + + readonly double SampleCurveY(double t) + { + return ((ay_ * t + by_) * t + cy_) * t; + } + + readonly double SampleCurveDerivativeX(double t) + { + return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; + } + + readonly double SampleCurveDerivativeY(double t) + { + return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; + } + + public readonly double SolveWithEpsilon(double x, double epsilon) + { + if (x < 0.0) + return 0.0 + start_gradient_ * x; + if (x > 1.0) + return 1.0 + end_gradient_ * (x - 1.0); + return SampleCurveY(SolveCurveX(x, epsilon)); + } + + void InitCoefficients(double p1x, + double p1y, + double p2x, + double p2y) + { + // Calculate the polynomial coefficients, implicit first and last control + // points are (0,0) and (1,1). + cx_ = 3.0 * p1x; + bx_ = 3.0 * (p2x - p1x) - cx_; + ax_ = 1.0 - cx_ - bx_; + + cy_ = 3.0 * p1y; + by_ = 3.0 * (p2y - p1y) - cy_; + ay_ = 1.0 - cy_ - by_; + +#if DEBUG + // Bezier curves with x-coordinates outside the range [0,1] for internal + // control points may have multiple values for t for a given value of x. + // In this case, calls to SolveCurveX may produce ambiguous results. + monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; +#endif + } + + void InitGradients(double p1x, + double p1y, + double p2x, + double p2y) + { + // End-point gradients are used to calculate timing function results + // outside the range [0, 1]. + // + // There are four possibilities for the gradient at each end: + // (1) the closest control point is not horizontally coincident with regard to + // (0, 0) or (1, 1). In this case the line between the end point and + // the control point is tangent to the bezier at the end point. + // (2) the closest control point is coincident with the end point. In + // this case the line between the end point and the far control + // point is tangent to the bezier at the end point. + // (3) both internal control points are coincident with an endpoint. There + // are two special case that fall into this category: + // CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are + // equivalent to linear. + // (4) the closest control point is horizontally coincident with the end + // point, but vertically distinct. In this case the gradient at the + // end point is Infinite. However, this causes issues when + // interpolating. As a result, we break down to a simple case of + // 0 gradient under these conditions. + + if (p1x > 0) + start_gradient_ = p1y / p1x; + else if (p1y == 0 && p2x > 0) + start_gradient_ = p2y / p2x; + else if (p1y == 0 && p2y == 0) + start_gradient_ = 1; + else + start_gradient_ = 0; + + if (p2x < 1) + end_gradient_ = (p2y - 1) / (p2x - 1); + else if (p2y == 1 && p1x < 1) + end_gradient_ = (p1y - 1) / (p1x - 1); + else if (p2y == 1 && p1y == 1) + end_gradient_ = 1; + else + end_gradient_ = 0; + } + + const double kBezierEpsilon = 1e-7; + + void InitRange(double p1y, double p2y) + { + range_min_ = 0; + range_max_ = 1; + if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) + return; + + double epsilon = kBezierEpsilon; + + // Represent the function's derivative in the form at^2 + bt + c + // as in sampleCurveDerivativeY. + // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros + // but does not actually give the slope of the curve.) + double a = 3.0 * ay_; + double b = 2.0 * by_; + double c = cy_; + + // Check if the derivative is constant. + if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) + return; + + // Zeros of the function's derivative. + double t1; + double t2 = 0; + + if (Math.Abs(a) < epsilon) + { + // The function's derivative is linear. + t1 = -c / b; + } + else + { + // The function's derivative is a quadratic. We find the zeros of this + // quadratic using the quadratic formula. + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) + return; + double discriminant_sqrt = Math.Sqrt(discriminant); + t1 = (-b + discriminant_sqrt) / (2 * a); + t2 = (-b - discriminant_sqrt) / (2 * a); + } + + double sol1 = 0; + double sol2 = 0; + + // If the solution is in the range [0,1] then we include it, otherwise we + // ignore it. + + // An interesting fact about these beziers is that they are only + // actually evaluated in [0,1]. After that we take the tangent at that point + // and linearly project it out. + if (0 < t1 && t1 < 1) + sol1 = SampleCurveY(t1); + + if (0 < t2 && t2 < 1) + sol2 = SampleCurveY(t2); + + range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); + range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); + } + + void InitSpline() + { + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + spline_samples_[i] = SampleCurveX(i * delta_t); + } + } + + const int kMaxNewtonIterations = 4; + + + public readonly double SolveCurveX(double x, double epsilon) + { + if (x < 0 || x > 1) + throw new ArgumentException(); + + double t0 = 0; + double t1 = 0; + double t2 = x; + double x2 = 0; + double d2; + int i; + +#if DEBUG + if (!monotonically_increasing_) + throw new InvalidOperationException(); +#endif + + // Linear interpolation of spline curve for initial guess. + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + if (x <= spline_samples_[i]) + { + t1 = delta_t * i; + t0 = t1 - delta_t; + t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / + (spline_samples_[i] - spline_samples_[i - 1]); + break; + } + } + + // Perform a few iterations of Newton's method -- normally very fast. + // See https://en.wikipedia.org/wiki/Newton%27s_method. + double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); + for (i = 0; i < kMaxNewtonIterations; i++) + { + x2 = SampleCurveX(t2) - x; + if (Math.Abs(x2) < newton_epsilon) + return t2; + d2 = SampleCurveDerivativeX(t2); + if (Math.Abs(d2) < kBezierEpsilon) + break; + t2 = t2 - x2 / d2; + } + + if (Math.Abs(x2) < epsilon) + return t2; + + // Fall back to the bisection method for reliability. + while (t0 < t1) + { + x2 = SampleCurveX(t2); + if (Math.Abs(x2 - x) < epsilon) + return t2; + if (x > x2) + t0 = t2; + else + t1 = t2; + t2 = (t1 + t0) * .5; + } + + // Failure. + return t2; + } + + public readonly double Solve(double x) + { + return SolveWithEpsilon(x, kBezierEpsilon); + } + + public readonly double SlopeWithEpsilon(double x, double epsilon) + { + x = MathExt.Clamp(x, 0.0, 1.0); + double t = SolveCurveX(x, epsilon); + double dx = SampleCurveDerivativeX(t); + double dy = SampleCurveDerivativeY(t); + return dy / dx; + } + + public readonly double Slope(double x) + { + return SlopeWithEpsilon(x, kBezierEpsilon); + } + + public readonly double RangeMin => range_min_; + public readonly double RangeMax => range_max_; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs new file mode 100644 index 0000000000..0be19a8e9d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Rendering.Composition.Utils +{ + static class MathExt + { + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + public static double Clamp(double value, double min, double max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs new file mode 100644 index 0000000000..1e6d7f8abb --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -0,0 +1,64 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + public abstract partial class CompositionVisual + { + private CompositionVisual? _parent; + private CompositionTarget? _root; + + public CompositionVisual? Parent + { + get => _parent; + internal set + { + if (_parent == value) + return; + _parent = value; + Changes.Parent.Value = value?.Server; + Root = _parent?.Root; + } + } + + // TODO: hide behind private-ish API + public CompositionTarget? Root + { + get => _root; + internal set + { + var changed = _root != value; + _root = value; + Changes.Root.Value = value?.Server; + if (changed) + OnRootChanged(); + } + } + + private protected virtual void OnRootChanged() + { + } + + + internal Matrix4x4? TryGetServerTransform() + { + if (Root == null) + return null; + var i = Root.Server.Readback; + ref var readback = ref Server.GetReadback(i.ReadIndex); + + // CompositionVisual wasn't visible + if (readback.Revision < i.ReadRevision) + return null; + + // CompositionVisual was reparented (potential race here) + if (readback.TargetId != Root.Server.Id) + return null; + + return readback.Matrix; + } + + internal object? Tag { get; set; } + + internal virtual bool HitTest(Vector2 point) => true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs new file mode 100644 index 0000000000..35f33c3b38 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -0,0 +1,64 @@ +using System; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionVisualCollection : CompositionObject + { + private CompositionVisual _owner; + internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server) + { + _owner = parent; + InitializeDefaults(); + } + + public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + + Insert(idx + 1, newChild); + } + + public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + Insert(idx, newChild); + } + + public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild); + + public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild); + + public void RemoveAll() => Clear(); + + partial void OnAdded(CompositionVisual item) => item.Parent = _owner; + + partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + OnBeforeAdded(newItem); + } + + partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + { + OnRemoved(oldItem); + OnAdded(newItem); + } + } + + partial void OnRemoved(CompositionVisual item) => item.Parent = null; + + + partial void OnBeforeAdded(CompositionVisual item) + { + if (item.Parent != null) + throw new InvalidOperationException("Visual already has a parent"); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index e998f78d5c..8d6aabf440 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using Avalonia.Rendering.Composition; namespace Avalonia.Rendering { @@ -87,4 +88,9 @@ namespace Avalonia.Rendering /// void Stop(); } + + public interface IRendererWithCompositor : IRenderer + { + Compositor Compositor { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5225b85020..3f495c619c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 69c3ae7319..5f20206200 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -52,6 +52,17 @@ namespace Avalonia _width = width; _height = height; } + +#if !BUILDTASK + /// + /// Initializes a new instance of the structure. + /// + /// The vector to take values from. + public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y) + { + + } +#endif /// /// Gets the aspect ratio of the size. diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a93e4f406d..b4bf603f74 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -62,10 +62,20 @@ namespace Avalonia.Threading /// public static readonly DispatcherPriority Render = new(5); + /// + /// The job will be processed with the same priority as composition batch commit. + /// + public static readonly DispatcherPriority CompositionBatch = new(6); + + /// + /// The job will be processed with the same priority as composition updates. + /// + public static readonly DispatcherPriority Composition = new(7); + /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(6); + public static readonly DispatcherPriority Layout = new(8); /// /// The job will be processed with the same priority as data binding. @@ -75,7 +85,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(7); + public static readonly DispatcherPriority Send = new(9); /// /// Maximum possible priority diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 4fd21f02f9..64341be0c7 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -1,3 +1,7 @@ + + +#nullable enable + using System; using System.Collections; using System.Collections.Specialized; @@ -8,11 +12,10 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; using Avalonia.Utilities; using Avalonia.VisualTree; - -#nullable enable - namespace Avalonia { /// @@ -288,6 +291,8 @@ namespace Avalonia /// protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + internal CompositionDrawListVisual? CompositionVisual { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -432,6 +437,10 @@ namespace Avalonia } EnableTransitions(); + if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + { + AttachToCompositor(compositingRenderer.Compositor); + } OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -452,6 +461,14 @@ namespace Avalonia } } + internal CompositionVisual AttachToCompositor(Compositor compositor) + { + if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + CompositionVisual = new CompositionDrawListVisual(compositor, + new ServerCompositionDrawListVisual(compositor.Server), this); + return CompositionVisual; + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -564,7 +581,7 @@ namespace Avalonia { newValue.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml new file mode 100644 index 0000000000..cf864dac2d --- /dev/null +++ b/src/Avalonia.Base/composition-schema.xml @@ -0,0 +1,70 @@ + + + System.Numerics + Avalonia.Rendering.Composition.Server + Avalonia.Rendering.Composition.Transport + Avalonia.Rendering.Composition.Animations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..984f76adb6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Platform; @@ -344,6 +345,7 @@ namespace Avalonia.Headless } public Matrix Transform { get; set; } + public void Clear(Color color) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs new file mode 100644 index 0000000000..f079a339df --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +[Generator(LanguageNames.CSharp)] +public class CompositionRoslynGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(spc, config); + generator.Generate(); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs new file mode 100644 index 0000000000..8b6aca33cd --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -0,0 +1,116 @@ +#nullable disable +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + + + [XmlRoot("NComposition")] + public class GConfig + { + [XmlElement("Using")] + public List Usings { get; set; } = new List(); + + [XmlElement(typeof(GManualClass), ElementName = "Manual")] + public List ManualClasses { get; set; } = new List(); + + [XmlElement(typeof(GClass), ElementName = "Object")] + [XmlElement(typeof(GBrush), ElementName = "Brush")] + [XmlElement(typeof(GList), ElementName = "List")] + public List Classes { get; set; } = new List(); + + [XmlElement(typeof(GAnimationType), ElementName = "KeyFrameAnimation")] + public List KeyFrameAnimations { get; set; } = new List(); + } + + public class GUsing + { + [XmlText] + public string Name { get; set; } + } + + public class GManualClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GImplements + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Inherits { get; set; } + + [XmlAttribute] + public string ChangesBase { get; set; } + + [XmlAttribute] + public string ServerBase { get; set; } + + [XmlAttribute] + public bool CustomCtor { get; set; } + + [XmlAttribute] + public bool CustomServerCtor { get; set; } + + [XmlElement(typeof(GImplements), ElementName = "Implements")] + public List Implements { get; set; } = new List(); + + [XmlAttribute] + public bool Abstract { get; set; } + + [XmlElement(typeof(GProperty), ElementName = "Property")] + public List Properties { get; set; } = new List(); + } + + public class GBrush : GClass + { + [XmlAttribute] + public bool CustomUpdate { get; set; } + + public GBrush() + { + Inherits = "CompositionBrush"; + } + } + + public class GList : GClass + { + [XmlAttribute] + public string ItemType { get; set; } + } + + public class GProperty + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + [XmlAttribute] + public string DefaultValue { get; set; } + [XmlAttribute] + public bool Animated { get; set; } + } + + public class GAnimationType + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs new file mode 100644 index 0000000000..43a4a4afa7 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ExpressionSyntax ConditionalMemberAccess(ExpressionSyntax expr, string member, bool checkNull) + { + if (checkNull) + return ConditionalAccessExpression(expr, MemberBindingExpression(IdentifierName(member))); + return MemberAccess(expr, member); + } + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs new file mode 100644 index 0000000000..7d5146c5f5 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -0,0 +1,59 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + void GenerateAnimations() + { + var code = $@"using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition +{{ +"; + + foreach (var a in _config.KeyFrameAnimations) + { + var name = a.Name ?? a.Type; + + code += $@" + public class {name}KeyFrameAnimation : KeyFrameAnimation + {{ + public {name}KeyFrameAnimation(Compositor compositor) : base(compositor) + {{ + }} + + internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) + {{ + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + finalValue?.CastOrDefault<{a.Type}>(), targetObject, + DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, + IterationCount, StopBehavior); + }} + + private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); + private protected override IKeyFrames KeyFrames => _keyFrames; + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, CompositionEasingFunction easingFunction) + {{ + _keyFrames.Insert(normalizedProgressKey, value, easingFunction); + }} + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value) + {{ + _keyFrames.Insert(normalizedProgressKey, value, Compositor.DefaultEasing); + }} + }} + + public partial class Compositor + {{ + public {name}KeyFrameAnimation Create{name}KeyFrameAnimation() => new {name}KeyFrameAnimation(this); + }} +"; + } + + code += "}"; + _output.AddSource("CompositionAnimations.cs", code); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs new file mode 100644 index 0000000000..593386f713 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private const string ListProxyTemplate = @" +class Template +{ + private ServerListProxyHelper _list = null!; + + ListChangeSet + ServerListProxyHelper.IGetChanges. + GetChanges() => Changes; + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _list).GetEnumerator(); + + public void Add(ItemTypeName item) + { + OnBeforeAdded(item); + _list.Add(item); + OnAdded(item); + } + + public void Clear() + { + OnBeforeClear(); + _list.Clear(); + OnClear(); + } + + public bool Contains(ItemTypeName item) => _list.Contains(item); + + public void CopyTo(ItemTypeName[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(ItemTypeName item) + { + var removed = _list.Remove(item); + if(removed) + OnRemoved(item); + return removed; + } + + public int Count => _list.Count; + + public bool IsReadOnly => _list.IsReadOnly; + + public int IndexOf(ItemTypeName item) => _list.IndexOf(item); + + public void Insert(int index, ItemTypeName item) + { + OnBeforeAdded(item); + _list.Insert(index, item); + OnAdded(item); + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + OnRemoved(item); + } + + public ItemTypeName this[int index] + { + get => _list[index]; + set + { + var old = _list[index]; + OnBeforeReplace(old, value); + _list[index] = value; + OnReplace(old, value); + } + } + + partial void OnBeforeAdded(ItemTypeName item); + partial void OnAdded(ItemTypeName item); + partial void OnRemoved(ItemTypeName item); + partial void OnBeforeClear(); + partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnClear(); +} +"; + + private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) + { + + var itemType = list.ItemType; + var serverItemType = ServerName(itemType); + + cl = cl.AddBaseListTypes(SimpleBaseType( + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) + ); + var code = ListProxyTemplate.Replace("ListTypeName", list.Name) + .Replace("ItemTypeName", itemType); + + var parsed = ParseCompilationUnit(code); + var parsedClass = (ClassDeclarationSyntax)parsed.Members.First(); + + cl = cl.AddMembers(parsedClass.Members.ToArray()); + + var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); + + cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + + ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); + + return cl; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs new file mode 100644 index 0000000000..b53c247991 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + static void CleanDirectory(string path) + { + Directory.CreateDirectory(path); + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic" + } + .Concat(_config.Usings + .Select(x => x.Name)).Select(u => UsingDirective(IdentifierName(u))))); + + void SaveTo(CompilationUnitSyntax unit, params string[] path) + { + var text = @" +#nullable enable +#pragma warning disable CS0108, CS0114 + +" + + + unit.NormalizeWhitespace().ToFullString(); + _output.AddSource(string.Join("_", path), text); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs new file mode 100644 index 0000000000..43ef4a96e8 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -0,0 +1,504 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + partial class Generator + { + private readonly SourceProductionContext _output; + private readonly GConfig _config; + private readonly HashSet _objects; + private readonly HashSet _brushes; + private readonly Dictionary _manuals; + public Generator(SourceProductionContext output, GConfig config) + { + _output = output; + _config = config; + _manuals = _config.ManualClasses.ToDictionary(x => x.Name); + _objects = new HashSet(_config.ManualClasses.Select(x => x.Name) + .Concat(_config.Classes.Select(x => x.Name))); + _brushes = new HashSet(_config.Classes.OfType().Select(x => x.Name)) {"CompositionBrush"}; + } + + + + public void Generate() + { + foreach (var cl in _config.Classes) + GenerateClass(cl); + + GenerateAnimations(); + } + + + + string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + + void GenerateClass(GClass cl) + { + var list = cl as GList; + + var unit = Unit(); + + var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition")); + var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server")); + var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport")); + + var inherits = cl.Inherits ?? "CompositionObject"; + var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + + var client = ClassDeclaration(cl.Name) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(inherits); + + var serverName = ServerName(cl.Name); + var serverBase = cl.ServerBase ?? ServerName(cl.Inherits); + if (list != null) + serverBase = "ServerList<" + ServerName(list.ItemType) + ">"; + + var server = ClassDeclaration(serverName) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(serverBase); + + string changesName = ChangesName(cl.Name); + var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + + if (list != null) + changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; + + var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; + var transport = ClassDeclaration(changesName) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(changesBase) + .AddMembers(DeclareField(changeSetPoolType, "Pool", + EqualsValueClause( + ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") + ), + SyntaxKind.PublicKeyword, + SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) + .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); + + client = client + .AddMembers( + PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, + SyntaxKind.OverrideKeyword) + .WithExpressionBody( + ArrowExpressionClause(MemberAccess(changesName, "Pool"))) + .WithSemicolonToken(Semicolon())) + .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) + .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), + MemberAccess(BaseExpression(), "Changes")))) + .WithSemicolonToken(Semicolon())); + + if (!cl.CustomCtor) + { + client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server") + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) + .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Semicolon()))); + client = client.AddMembers( + ConstructorDeclaration(cl.Name) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")), + Parameter(Identifier("server")).WithType(ParseTypeName(serverName)), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + Argument(IdentifierName("server")), + })))).WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Server"), + CastExpression(ParseTypeName(serverName), IdentifierName("server")))), + ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults"))) + ))); + } + + if (!cl.CustomServerCtor) + { + server = server.AddMembers( + ConstructorDeclaration(serverName) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + })))).WithBody(Block())); + } + + + var changesVarName = "c"; + var changesVar = IdentifierName(changesVarName); + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + transport = transport.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ResetExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + var applyMethodBody = Block( + ExpressionStatement(InvocationExpression(MemberAccess(IdentifierName("base"), "ApplyCore"), + ArgumentList(SeparatedList(new[] {Argument(IdentifierName("changes"))})))), + LocalDeclarationStatement(VariableDeclaration(ParseTypeName("var")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(changesVarName) + .WithInitializer(EqualsValueClause(CastExpression(ParseTypeName(changesName), + IdentifierName("changes"))))))), + ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) + .AddArgumentListArguments(Argument(IdentifierName("c")))) + ); + + var resetBody = Block(); + var startAnimationBody = Block(); + var getPropertyBody = Block(); + var serverGetPropertyBody = Block(); + + var defaultsMethodBody = Block(); + + foreach (var prop in cl.Properties) + { + var fieldName = "_" + prop.Name.WithLowerFirst(); + var propType = ParseTypeName(prop.Type); + var filteredPropertyType = prop.Type.TrimEnd('?'); + var isObject = _objects.Contains(filteredPropertyType); + var isNullable = prop.Type.EndsWith("?"); + + + + + client = client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + + var animatedServer = prop.Animated; + + var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); + if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + + + transport = transport + .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", + prop.Name, SyntaxKind.PublicKeyword)); + + if (animatedServer) + server = server.AddMembers( + DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess(fieldName, "GetAnimated"), + ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + .WithSemicolonToken(Semicolon()) + ); + else + { + server = server + .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement($"changed = true;")) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + if (animatedServer) + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(changesVar), + Argument(MemberAccess(changesVar, prop.Name, "Animation")) + }))))) + ); + else + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsSet"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) + + ); + + + resetBody = resetBody.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); + + if (animatedServer) + startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + + if (prop.DefaultValue != null) + { + defaultsMethodBody = defaultsMethodBody.AddStatements( + ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); + } + } + + if (cl is GBrush brush && !cl.Abstract) + { + var brushName = brush.Name.StripPrefix("Composition"); + /* + server = server.AddMembers( + MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) + )).WithSemicolonToken(Semicolon()) + ); + if (!brush.CustomUpdate) + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters(Parameter(Identifier("brush")) + .WithType(ParseTypeName("ICbBrush"))) + .AddBodyStatements( + ExpressionStatement( + InvocationExpression( + MemberAccess( + ParenthesizedExpression( + CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), + ArgumentList(SeparatedList(cl.Properties.Select(x => + { + if(x.Type.TrimEnd('?') == "ICompositionSurface") + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("BackendSurface"))) + ); + if (_brushes.Contains(x.Type)) + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("Brush"))) + ); + return Argument(IdentifierName(x.Name)); + })))) + ))); + +*/ + } + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyCore") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters( + Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) + .WithBody(applyMethodBody)); + + client = client.AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); + + transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .WithBody(resetBody.AddStatements( + ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), + ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); + + if (list != null) + client = AppendListProxy(list, client); + + if (startAnimationBody.Statements.Count != 0) + client = WithStartAnimation(client, startAnimationBody); + + client = WithGetProperty(client, getPropertyBody, false); + server = WithGetProperty(server, serverGetPropertyBody, true); + + if(cl.Implements.Count > 0) + foreach (var impl in cl.Implements) + { + client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + if (impl.ServerName != null) + server = server.WithBaseList( + server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + + client = client.AddMembers( + ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + } + + + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(serverNs.AddMembers(server)), + "Server", "Server" + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), + "Transport", cl.Name + "Changes.generated.cs"); + } + + StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + { + var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, + "Value"), + isObject + ? + ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) + : IdentifierName("value"))); + if (!prop.Animated) + return normalChangesAssignment; + + var code = $@" +{{ + if(animation is CompositionAnimation a) + Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); + else + {{ + var saved = Changes.{prop.Name}; + if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) + Changes.{prop.Name}.Value = value; + }} +}} + +"; + + return IfStatement( + ParseExpression( + $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), + ParseStatement(code), + ElseClause(normalChangesAssignment) + ); + } + + BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + { + var code = $@" +if (propertyName == ""{prop.Name}"") +{{ +var current = {fieldName}; +var server = animation.CreateInstance(this.Server, finalValue); +Changes.{prop.Name}.Animation = server; +return; +}} +"; + return body.AddStatements(ParseStatement(code)); + } + + private static HashSet VariantPropertyTypes = new HashSet + { + "bool", + "float", + "Vector2", + "Vector3", + "Vector4", + "Matrix3x2", + "Matrix4x4", + "Quaternion", + "CompositionColor" + }; + + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + { + if (VariantPropertyTypes.Contains(prop.Type)) + return body.AddStatements( + ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ); + + return body; + } + + ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetPropertyForAnimation(name);")); + var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( + $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + body = body.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("propertyName")), + Argument(IdentifierName("animation")), + Argument(IdentifierName("finalValue")), + })))) + ); + return cl.AddMembers( + ((MethodDeclarationSyntax) ParseMemberDeclaration( + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + .WithBody(body)); + + + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..7a64b39575 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -13,6 +13,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; @@ -29,6 +30,7 @@ namespace Avalonia.X11 public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } + public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -101,7 +103,9 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } - + if (options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + } public IntPtr DeferredDisplay { get; set; } @@ -222,6 +226,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Determines whether to use IME. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 066156a652..37cfeb0624 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -18,6 +18,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -360,13 +361,15 @@ namespace Avalonia.X11 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - - return _platform.Options.UseDeferredRendering ? - new DeferredRenderer(root, loop) - { - RenderOnlyOnRenderThread = true - } : - (IRenderer)new X11ImmediateRendererProxy(root, loop); + + return _platform.Options.UseDeferredRendering + ? _platform.Options.UseCompositor + ? new CompositingRenderer(root, this._platform.Compositor) + : new DeferredRenderer(root, loop) + { + RenderOnlyOnRenderThread = true + } + : (IRenderer)new X11ImmediateRendererProxy(root, loop); } void OnEvent(ref XEvent ev) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a259d8fab9..1243c90214 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -73,6 +74,12 @@ namespace Avalonia.Direct2D1.Media set { _deviceContext.Transform = value.ToDirect2D(); } } + public Matrix4x4 Transform4x4 + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + /// public void Clear(Color color) { From 4274e78a321b37412c77915638535e1d12304c6e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 21:43:22 +0300 Subject: [PATCH 036/481] Win32 --- samples/ControlCatalog.NetCore/Program.cs | 3 ++- .../Rendering/Composition/CompositingRenderer.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 ++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 2a0755b900..6196aac153 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,7 +115,8 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true + EnableMultitouch = true, + UseCompositor = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..65ed0d17ad 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -177,7 +177,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() @@ -191,4 +191,4 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public Compositor Compositor => _compositor; -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index dc5e5324c4..32705a2cc6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -14,6 +14,7 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; @@ -48,6 +49,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. @@ -132,6 +135,8 @@ namespace Avalonia.Win32 public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } + + internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size( UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), @@ -181,6 +186,9 @@ namespace Avalonia.Win32 if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + if (Options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f0036236ec..e61121f23e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -14,6 +14,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; @@ -500,6 +501,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); + if (Win32Platform.Compositor != null) + return new CompositingRenderer(root, Win32Platform.Compositor); + return Win32Platform.UseDeferredRendering ? _isUsingComposition ? new DeferredRenderer(root, loop) From 7a3b5f051f02e2e0689df3232ffac5b76f6d986b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:20:39 +0300 Subject: [PATCH 037/481] Make ThreadSafeObjectPool actually thread safe --- src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index c6845485dc..05995f2069 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -5,7 +5,6 @@ namespace Avalonia.Threading public class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); - private object _lock = new object(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); public T Get() @@ -20,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_stack) + lock (_lock) { _stack.Push(obj); } From 6d46006d075e05a4de377e8f85b1b93199e348f3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:30:02 +0300 Subject: [PATCH 038/481] Removed some matrix operations --- .../Composition/Expressions/ExpressionVariant.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 8c6af5cb0c..086c8ce276 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -290,9 +290,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 + right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix + right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 + right.Matrix4x4; @@ -325,9 +322,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 - right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix - right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 - right.Matrix4x4; @@ -407,12 +401,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) return left.AvaloniaMatrix * right.AvaloniaMatrix; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) - return left.AvaloniaMatrix * (double)right.Scalar; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) - return left.AvaloniaMatrix * right.Double; if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) return left.Matrix4x4 * right.Matrix4x4; From 16dde385d0d8c6b9f7711a801b67a8f4440b3a57 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 00:59:00 +0300 Subject: [PATCH 039/481] WIP: Matrix3x3, some invalidation support, dirty rects drawing --- samples/RenderDemo/App.xaml.cs | 4 + .../Composition/CompositingRenderer.cs | 26 ++++-- .../Composition/CompositionDrawListVisual.cs | 3 +- .../Composition/CompositionTarget.cs | 24 +++--- .../Rendering/Composition/Compositor.cs | 1 + .../Rendering/Composition/MatrixUtils.cs | 20 +++++ .../Composition/Server/DrawingContextProxy.cs | 4 - .../Composition/Server/FpsCounter.cs | 73 ++++++++++++++++ .../Server/ServerCompositionDrawListVisual.cs | 20 +++++ .../Server/ServerCompositionTarget.cs | 83 ++++++++++++++++++- .../Server/ServerContainerVisual.cs | 7 ++ .../Server/ServerSolidColorVisual.cs | 2 +- .../Composition/Server/ServerVisual.cs | 52 ++++++++---- .../Transport/CompositionTargetChanges.cs | 11 +++ .../Rendering/Composition/Visual.cs | 2 +- .../Rendering/Composition/VisualCollection.cs | 5 ++ .../Threading/ThreadSafeObjectPool.cs | 4 +- src/Avalonia.Base/composition-schema.xml | 4 + 18 files changed, 297 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 8054b06964..8f4e02df01 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -29,6 +29,10 @@ namespace RenderDemo .With(new Win32PlatformOptions { OverlayPopups = true, + }) + .With(new X11PlatformOptions + { + UseCompositor = true }) .UsePlatformDetect() .LogToTrace(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..22fc6a77d4 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -37,9 +37,19 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; } - - public bool DrawFps { get; set; } - public bool DrawDirtyRects { get; set; } + + public bool DrawFps + { + get => _target.DrawFps; + set => _target.DrawFps = value; + } + + public bool DrawDirtyRects + { + get => _target.DrawDirtyRects; + set => _target.DrawDirtyRects = value; + } + public event EventHandler? SceneInvalidated; void QueueUpdate() @@ -57,7 +67,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public IEnumerable HitTest(Point p, IVisual root, Func filter) { - var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + var res = _target.TryHitTest(p); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -146,7 +156,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor renderTransform *= mirrorMatrix; } - comp.TransformMatrix = renderTransform; + comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); _recorder.BeginUpdate(comp.DrawList ?? new CompositionDrawList()); visual.Render(_recordingContext); @@ -159,6 +169,8 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor SyncChildren(v); _dirty.Clear(); _recalculateChildren.Clear(); + _target.Size = _root.ClientSize; + _target.Scaling = _root.RenderScaling; } public void Resized(Size size) @@ -169,7 +181,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { // We render only on the render thread for now Update(); - + _target.RequestRedraw(); Compositor.RequestCommitAsync().Wait(); } @@ -177,7 +189,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 9f02055412..b19c311663 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -29,11 +29,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Vector2 point) + internal override bool HitTest(Point pt) { if (DrawList == null) return false; - var pt = new Point(point.X, point.Y); if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index a8835ca668..8d052389c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Vector2 point) + public PooledList? TryHitTest(Point point) { Server.Readback.NextRead(); if (Root == null) @@ -28,12 +28,12 @@ namespace Avalonia.Rendering.Composition return res; } - public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + public Point? TryTransformToVisual(CompositionVisual visual, Point point) { if (visual.Root != this) return null; var v = visual; - var m = Matrix3x2.Identity; + var m = Matrix.Identity; while (v != null) { if (!TryGetInvertedTransform(v, out var cm)) @@ -42,10 +42,10 @@ namespace Avalonia.Rendering.Composition v = v.Parent; } - return Vector2.Transform(point, m); + return point * m; } - bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) { var m = visual.TryGetServerTransform(); if (m == null) @@ -54,24 +54,22 @@ namespace Avalonia.Rendering.Composition return false; } - // TODO: Use Matrix3x3 - var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); - - return Matrix3x2.Invert(m32, out matrix); + var m33 = MatrixUtils.ToMatrix(m.Value); + return m33.TryInvert(out matrix); } - bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + bool TryTransformTo(CompositionVisual visual, ref Point v) { if (TryGetInvertedTransform(visual, out var m)) { - v = Vector2.Transform(v, m); + v = v * m; return true; } return false; } - bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result) { //TODO: Check readback too if (visual.Visible == false) @@ -103,5 +101,7 @@ namespace Avalonia.Rendering.Composition return false; } + + public void RequestRedraw() => Changes.RedrawRequested.Value = true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 217d8dd803..14d779dbc4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Threading.Tasks; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 5e91bcb3d4..2cb500cae4 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -42,5 +42,25 @@ namespace Avalonia.Rendering.Composition return mat; } + + public static Matrix4x4 ToMatrix4x4(Matrix matrix) => + new Matrix4x4( + (float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13, + (float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23, + 0, 0, 1, 0, + (float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33 + ); + + public static Matrix ToMatrix(Matrix4x4 matrix44) => + new Matrix( + matrix44.M11, + matrix44.M12, + matrix44.M14, + matrix44.M21, + matrix44.M22, + matrix44.M24, + matrix44.M41, + matrix44.M42, + matrix44.M44); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index f5dfa92897..7d061e86a9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -135,8 +135,4 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } - - public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, - transform.M22, transform.M41, - transform.M42); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs new file mode 100644 index 0000000000..8c2e6e774a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class FpsCounter +{ + private readonly GlyphTypeface _typeface; + private readonly bool _useManualFpsCounting; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _framesThisSecond; + private int _fps; + private TimeSpan _lastFpsUpdate; + private GlyphRun[] _runs = new GlyphRun[10]; + + public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + { + for (var c = 0; c <= 9; c++) + { + var s = c.ToString(); + var glyph = typeface.GetGlyph((uint)(s[0])); + _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + } + _typeface = typeface; + _useManualFpsCounting = useManualFpsCounting; + } + + public void FpsTick() => _framesThisSecond++; + + public void RenderFps(IDrawingContextImpl context) + { + var now = _stopwatch.Elapsed; + var elapsed = now - _lastFpsUpdate; + + if (!_useManualFpsCounting) + ++_framesThisSecond; + + if (elapsed.TotalSeconds > 1) + { + _fps = (int)(_framesThisSecond / elapsed.TotalSeconds); + _framesThisSecond = 0; + _lastFpsUpdate = now; + } + + var fpsLine = _fps.ToString("000"); + double width = 0; + double height = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + var rect = new Rect(0, 0, width + 3, height + 3); + + context.DrawRectangle(Brushes.Black, null, rect); + + double offset = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + context.Transform = Matrix.CreateTranslation(offset, 0); + context.DrawGlyphRun(Brushes.White, run); + offset += run.Size.Width; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 397c968d04..f0384c36fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -16,6 +16,25 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { } + Rect? _contentBounds; + + public override Rect ContentBounds + { + get + { + if (_contentBounds == null) + { + var rect = Rect.Empty; + if(_renderCommands!=null) + foreach (var cmd in _renderCommands) + rect = rect.Union(cmd.Item.Bounds); + _contentBounds = rect; + } + + return _contentBounds.Value; + } + } + protected override void ApplyCore(ChangeSet changes) { var ch = (DrawListVisualChanges)changes; @@ -23,6 +42,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { _renderCommands?.Dispose(); _renderCommands = ch.AcquireDrawCommands(); + _contentBounds = null; } base.ApplyCore(changes); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 493529e111..04ec711455 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -2,7 +2,10 @@ using System; using System.Numerics; using System.Threading; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -14,6 +17,13 @@ namespace Avalonia.Rendering.Composition.Server public long Id { get; } private ulong _frame = 1; private IRenderTarget? _renderTarget; + private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private Rect _dirtyRect; + private Random _random = new(); + private Size _layerSize; + private IDrawingContextLayerImpl? _layer; + private bool _redrawRequested; + public ReadbackIndices Readback { get; } = new(); @@ -33,6 +43,11 @@ namespace Avalonia.Rendering.Composition.Server _compositor.RemoveCompositionTarget(this); } + partial void ApplyChangesExtra(CompositionTargetChanges c) + { + _redrawRequested = true; + } + public void Render() { if (Root == null) @@ -40,14 +55,76 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget ??= _renderTargetFactory(); Compositor.UpdateServerTime(); - using (var context = _renderTarget.CreateDrawingContext(null)) + + Root.Update(this, Matrix4x4.Identity); + + if(_dirtyRect.IsEmpty && !_redrawRequested) + return; + _redrawRequested = false; + using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + var layerSize = Size * Scaling; + if (layerSize != _layerSize || _layer == null) + { + _layer?.Dispose(); + _layer = null; + _layer = targetContext.CreateLayer(layerSize); + _layerSize = layerSize; + } + + if (!_dirtyRect.IsEmpty) + { + using (var context = _layer.CreateDrawingContext(null)) + { + context.PushClip(_dirtyRect); + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + context.PopClip(); + } + } + + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), + new Rect(_layerSize)); + + + if (DrawDirtyRects) + { + targetContext.DrawRectangle(new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), + (byte)_random.Next(255))) + , null, _dirtyRect); + } + + if(DrawFps) + _fpsCounter.RenderFps(targetContext); + _dirtyRect = Rect.Empty; + } Readback.NextWrite(_frame); _frame++; } + + private static Rect SnapToDevicePixels(Rect rect, double scale) + { + return new Rect( + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); + } + + public void AddDirtyRect(Rect rect) + { + var snapped = SnapToDevicePixels(rect, Scaling); + _dirtyRect = _dirtyRect.Union(snapped); + } + + public void Invalidate() + { + _redrawRequested = true; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index ac112b846f..3f0995b257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -21,6 +21,13 @@ namespace Avalonia.Rendering.Composition.Server } } + public override void Update(ServerCompositionTarget root, Matrix4x4 transform) + { + base.Update(root, transform); + foreach (var child in Children) + child.Update(root, GlobalTransformMatrix); + } + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) { Children = new ServerCompositionVisualCollection(compositor); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 60569867de..786779bb2d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { - canvas.Transform = canvas.CutTransform(transform); + canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 37e188fb47..90b580bfa7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -6,6 +6,7 @@ namespace Avalonia.Rendering.Composition.Server { unsafe partial class ServerCompositionVisual : ServerObject { + private bool _isDirty; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -17,7 +18,7 @@ namespace Avalonia.Rendering.Composition.Server return; if(Opacity == 0) return; - canvas.PreTransform = canvas.CutTransform(transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -25,7 +26,8 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); if (Clip != null) canvas.PushGeometryClip(Clip); - + + //TODO: Check clip RenderCore(canvas, transform); if (Clip != null) @@ -48,22 +50,31 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix + public Matrix4x4 CombinedTransformMatrix { get; private set; } + public Matrix4x4 GlobalTransformMatrix { get; private set; } + + public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - get + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0; + CombinedTransformMatrix = res; + GlobalTransformMatrix = res * transform; + //TODO: Cache + TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + + if (!IsVisibleInFrame) + _isDirty = false; + else if (_isDirty) { - if (Root == null) - return default; - - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, - Scale, RotationAngle, Orientation, Offset); - var i = Root.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; - readback.Matrix = res; - readback.TargetId = Root.Id; - - return res; + Root.AddDirtyRect(TransformedBounds); + _isDirty = false; } } @@ -81,11 +92,20 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; + _isDirty = true; + + if (IsVisibleInFrame) + Root?.AddDirtyRect(TransformedBounds); + else + Root?.Invalidate(); } public ServerCompositionTarget? Root { get; private set; } public ServerCompositionVisual? Parent { get; private set; } + public bool IsVisibleInFrame { get; set; } + public Rect TransformedBounds { get; set; } + public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs new file mode 100644 index 0000000000..014adc7bbe --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Rendering.Composition.Transport; + +partial class CompositionTargetChanges +{ + public Change RedrawRequested; + + partial void ResetExtra() + { + RedrawRequested.Reset(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 1e6d7f8abb..25fce01de3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -59,6 +59,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Vector2 point) => true; + internal virtual bool HitTest(Point point) => true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 35f33c3b38..fef4caf675 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -54,6 +54,11 @@ namespace Avalonia.Rendering.Composition partial void OnRemoved(CompositionVisual item) => item.Parent = null; + partial void OnBeforeClear() + { + foreach (var i in this) + i.Parent = null; + } partial void OnBeforeAdded(CompositionVisual item) { diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 05995f2069..827a02334a 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -9,7 +9,7 @@ namespace Avalonia.Threading public T Get() { - lock (_lock) + lock (_stack) { if(_stack.Count == 0) return new T(); @@ -19,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_lock) + lock (_stack) { _stack.Push(obj); } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index cf864dac2d..eb1ffe1922 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -25,6 +25,10 @@ + + + + From 0e8672017b4ba40c78413dc73a1e2ba0dcad971a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 20:08:14 +0300 Subject: [PATCH 040/481] Workaround for #8118 --- src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 90b580bfa7..bf5b8bf292 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -30,6 +30,9 @@ namespace Avalonia.Rendering.Composition.Server //TODO: Check clip RenderCore(canvas, transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.Transform = Matrix.Identity; + if (Clip != null) canvas.PopGeometryClip(); if (ClipToBounds) From 9ac37065dca2a23006617dc649debd3392e23b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 24 May 2022 19:49:43 +0300 Subject: [PATCH 041/481] Animations now work... more or less? --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/CompositionPage.axaml | 45 +++++ .../Pages/CompositionPage.axaml.cs | 157 ++++++++++++++++++ .../Animations/AnimatedValueStore.cs | 88 +++++++++- .../Animations/AnimationInstanceBase.cs | 77 +++++++++ .../Animations/CompositionAnimation.cs | 2 +- .../Animations/ExpressionAnimationInstance.cs | 25 +-- .../Animations/IAnimationInstance.cs | 7 +- .../Animations/KeyFrameAnimationInstance.cs | 59 +++++-- .../Composition/Animations/KeyFrames.cs | 2 +- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/ElementCompositionPreview.cs | 6 + .../Composition/Expressions/Expression.cs | 45 ++++- .../ExpressionEvaluationContext.cs | 1 + .../Expressions/ExpressionTrackedValues.cs | 57 +++++++ .../Composition/Server/FpsCounter.cs | 27 ++- .../Server/ServerCompositionTarget.cs | 5 +- .../Composition/Server/ServerCompositor.cs | 20 +++ .../Composition/Server/ServerObject.cs | 82 ++++++++- .../Composition/Server/ServerVisual.cs | 22 ++- .../Rendering/Composition/Visual.cs | 1 - .../Utilities/RefTrackingDictionary.cs | 67 ++++++++ .../Avalonia.SourceGenerator.csproj | 1 + .../CompositionRoslynGenerator.cs | 26 +-- .../CompositionGenerator/Generator.cs | 102 ++++++++++-- .../ICompositionGeneratorSink.cs | 6 + .../RoslynCompositionGeneratorSink.cs | 15 ++ 27 files changed, 873 insertions(+), 87 deletions(-) create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs create mode 100644 src/Avalonia.Base/Utilities/RefTrackingDictionary.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 59d724db69..2ce5ab3934 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -13,6 +13,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml new file mode 100644 index 0000000000..592290fde5 --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -0,0 +1,45 @@ + + + Implicit animations + + + + + + + + + + + + + + + + + + + + + + + Resize me + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs new file mode 100644 index 0000000000..b37231243d --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class CompositionPage : UserControl +{ + private ImplicitAnimationCollection _implicitAnimations; + + public CompositionPage() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + this.FindControl("Items").Items = CreateColorItems(); + } + + private List CreateColorItems() + { + var list = new List(); + + list.Add(new ColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new ColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new ColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new ColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new ColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new ColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new ColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new ColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new ColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new ColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new ColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new ColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new ColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new ColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new ColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new ColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new ColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new ColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new ColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new ColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new ColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new ColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new ColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new ColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new ColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new ColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new ColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new ColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new ColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new ColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new ColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new ColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 126, 115, 95))); + + return list; + } + + public class ColorItem + { + public Color Color { get; private set; } + + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + + public ColorItem(Color color) + { + Color = color; + } + } + + private void EnsureImplicitAnimations() + { + if (_implicitAnimations == null) + { + var compositor = ElementCompositionPreview.GetElementVisual(this)!.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngle"; + rotationAnimation.InsertKeyFrame(.5f, 0.160f); + rotationAnimation.InsertKeyFrame(1f, 0f); + rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(offsetAnimation); + animationGroup.Add(rotationAnimation); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = animationGroup; + } + } + + public static void SetEnableAnimations(Border border, bool value) + { + + var page = border.FindAncestorOfType(); + if (page == null) + { + border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; + return; + } + + if (ElementCompositionPreview.GetElementVisual(page) == null) + return; + + page.EnsureImplicitAnimations(); + ElementCompositionPreview.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = + page._implicitAnimations; + } + + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index 180c45022f..e877b50b20 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,19 +1,62 @@ +using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Animations { - internal struct AnimatedValueStore where T : struct + internal struct ServerObjectSubscriptionStore { + public bool IsValid; + public RefTrackingDictionary Subscribers; + + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerValueStore + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private T _value; + public T Value + { + set + { + _value = value; + Subscriptions.Invalidate(); + } + get + { + Subscriptions.IsValid = true; + return _value; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerAnimatedValueStore where T : struct + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private IAnimationInstance? _animation; private T _direct; - private IAnimationInstance _animation; private T? _lastAnimated; public T Direct => _direct; public T GetAnimated(ServerCompositor compositor) { + Subscriptions.IsValid = true; if (_animation == null) return _direct; var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) @@ -22,19 +65,50 @@ namespace Avalonia.Rendering.Composition.Animations return v; } + public void Activate(ServerObject parent) + { + if (_animation != null) + _animation.Activate(); + } + + public void Deactivate(ServerObject parent) + { + if (_animation != null) + _animation.Deactivate(); + } + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; public bool IsAnimation => _animation != null; - public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) { + _direct = default; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + _animation = animation; - _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + if (target.IsActive) + _animation.Activate(); + + Subscriptions.Invalidate(); } - public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + public void SetValue(ServerObject target, T value) { - _direct = value - }; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + + _animation = null; + _direct = value; + Subscriptions.Invalidate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs new file mode 100644 index 0000000000..212237049f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations; + +internal abstract class AnimationInstanceBase : IAnimationInstance +{ + private List<(ServerObject obj, int member)>? _trackedObjects; + protected PropertySetSnapshot Parameters { get; } + public ServerObject TargetObject { get; } + protected int StoreOffset { get; private set; } + private bool _invalidated; + + public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) + { + Parameters = parameters; + TargetObject = target; + } + + protected void Initialize(int storeOffset, HashSet<(string name, string member)> trackedObjects) + { + if (trackedObjects.Count > 0) + { + _trackedObjects = new (); + foreach (var t in trackedObjects) + { + var obj = Parameters.GetObjectParameter(t.name); + if (obj is ServerObject tracked) + { + var off = tracked.GetFieldOffset(t.member); + if (off == null) +#if DEBUG + throw new InvalidCastException("Attempting to subscribe to unknown field"); +#else + continue; +#endif + _trackedObjects.Add((tracked, off.Value)); + } + } + } + + StoreOffset = storeOffset; + } + + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + _invalidated = false; + return EvaluateCore(now, currentValue); + } + + public virtual void Activate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.SubscribeToInvalidation(tracked.member, this); + } + + public virtual void Deactivate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); + } + + public void Invalidate() + { + if (_invalidated) + return; + _invalidated = true; + TargetObject.NotifyAnimatedValueChanged(StoreOffset); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 9375faaaae..fe20115b38 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering.Composition.Animations ExpressionVariant? finalValue); internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server, 1); + => _propertySet.Snapshot(server); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs index 47b947b2e9..7944fe7990 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -1,22 +1,22 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - internal class ExpressionAnimationInstance : IAnimationInstance + internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance { private readonly Expression _expression; - private readonly IExpressionObject _target; private ExpressionVariant _startingValue; private readonly ExpressionVariant? _finalValue; - private readonly PropertySetSnapshot _parameters; - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { var ctx = new ExpressionEvaluationContext { - Parameters = _parameters, - Target = _target, + Parameters = Parameters, + Target = TargetObject, ForeignFunctionInterface = BuiltInExpressionFfi.Instance, StartingValue = _startingValue, FinalValue = _finalValue ?? _startingValue, @@ -25,20 +25,21 @@ namespace Avalonia.Rendering.Composition.Animations return _expression.Evaluate(ref ctx); } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startingValue = startingValue; + var hs = new HashSet<(string, string)>(); + _expression.CollectReferences(hs); + base.Initialize(storeOffset, hs); } - + public ExpressionAnimationInstance(Expression expression, - IExpressionObject target, + ServerObject target, ExpressionVariant? finalValue, - PropertySetSnapshot parameters) + PropertySetSnapshot parameters) : base(target, parameters) { _expression = expression; - _target = target; _finalValue = finalValue; - _parameters = parameters; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index a0b066ae0c..05d1b50953 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -1,11 +1,16 @@ using System; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { internal interface IAnimationInstance { + ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); - void Start(TimeSpan startedAt, ExpressionVariant startingValue); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + void Activate(); + void Deactivate(); + void Invalidate(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index b90a02148d..9571cef0b4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -1,15 +1,15 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - class KeyFrameAnimationInstance : IAnimationInstance where T : struct + class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct { private readonly IInterpolator _interpolator; private readonly ServerKeyFrame[] _keyFrames; - private readonly PropertySetSnapshot _snapshot; private readonly ExpressionVariant? _finalValue; - private readonly IExpressionObject _target; private readonly AnimationDelayBehavior _delayBehavior; private readonly TimeSpan _delayTime; private readonly AnimationDirection _direction; @@ -19,21 +19,21 @@ namespace Avalonia.Rendering.Composition.Animations private readonly AnimationStopBehavior _stopBehavior; private TimeSpan _startedAt; private T _startingValue; + private readonly TimeSpan _totalDuration; + private bool _finished; public KeyFrameAnimationInstance( IInterpolator interpolator, ServerKeyFrame[] keyFrames, PropertySetSnapshot snapshot, ExpressionVariant? finalValue, - IExpressionObject target, + ServerObject target, AnimationDelayBehavior delayBehavior, TimeSpan delayTime, AnimationDirection direction, TimeSpan duration, AnimationIterationBehavior iterationBehavior, - int iterationCount, AnimationStopBehavior stopBehavior) + int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) { _interpolator = interpolator; _keyFrames = keyFrames; - _snapshot = snapshot; _finalValue = finalValue; - _target = target; _delayBehavior = delayBehavior; _delayTime = delayTime; _direction = direction; @@ -41,26 +41,43 @@ namespace Avalonia.Rendering.Composition.Animations _iterationBehavior = iterationBehavior; _iterationCount = iterationCount; _stopBehavior = stopBehavior; + if (_iterationBehavior == AnimationIterationBehavior.Count) + _totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks)); if (_keyFrames.Length == 0) throw new InvalidOperationException("Animation has no key frames"); if(_duration.Ticks <= 0) throw new InvalidOperationException("Invalid animation duration"); } - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { - var elapsed = now - _startedAt; var starting = ExpressionVariant.Create(_startingValue); var ctx = new ExpressionEvaluationContext { - Parameters = _snapshot, - Target = _target, + Parameters = Parameters, + Target = TargetObject, CurrentValue = currentValue, FinalValue = _finalValue ?? starting, StartingValue = starting, ForeignFunctionInterface = BuiltInExpressionFfi.Instance }; + var elapsed = now - _startedAt; + var res = EvaluateImpl(elapsed, currentValue, ref ctx); + if (_iterationBehavior == AnimationIterationBehavior.Count + && !_finished + && elapsed > _totalDuration) + { + // Active check? + TargetObject.Compositor.RemoveFromClock(this); + _finished = true; + } + return res; + } + + private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx) + { if (elapsed < _delayTime) { if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) @@ -130,10 +147,28 @@ namespace Avalonia.Rendering.Composition.Animations return f.Value; } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startedAt = startedAt; _startingValue = startingValue.CastOrDefault(); + var hs = new HashSet<(string, string)>(); + + // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them + foreach (var frame in _keyFrames) + frame.Expression?.CollectReferences(hs); + Initialize(storeOffset, hs); + } + + public override void Activate() + { + TargetObject.Compositor.AddToClock(this); + base.Activate(); + } + + public override void Deactivate() + { + TargetObject.Compositor.RemoveFromClock(this); + base.Deactivate(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs index d7f2504061..26ba35409d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -66,7 +66,7 @@ namespace Avalonia.Rendering.Composition.Animations struct ServerKeyFrame { public T Value; - public Expression Expression; + public Expression? Expression; public IEasingFunction EasingFunction; public float Key; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 004c2676ff..bc0ce804dc 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -24,11 +24,16 @@ namespace Avalonia.Rendering.Composition _variants[key] = value; } + /* + For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression + paths like SomeParam.SomePropertyObject.SomeValue + */ internal void Set(string key, CompositionObject obj) { _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); _variants.Remove(key); } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); @@ -99,7 +104,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + internal PropertySetSnapshot Snapshot(bool server) => + SnapshotCore(server, 1); + + private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -108,7 +116,7 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs new file mode 100644 index 0000000000..afda314276 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Rendering.Composition; + +public static class ElementCompositionPreview +{ + public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 5577d2b52a..088771e1ba 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { @@ -15,6 +16,11 @@ namespace Avalonia.Rendering.Composition.Expressions public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + public virtual void CollectReferences(HashSet<(string parameter, string property)> references) + { + + } + protected abstract string Print(); public override string ToString() => Print(); @@ -114,6 +120,13 @@ namespace Avalonia.Rendering.Composition.Expressions return FalsePart.Evaluate(ref context); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Condition.CollectReferences(references); + TruePart.CollectReferences(references); + FalsePart.CollectReferences(references); + } + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; } @@ -128,6 +141,7 @@ namespace Avalonia.Rendering.Composition.Expressions } public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); } @@ -155,6 +169,12 @@ namespace Avalonia.Rendering.Composition.Expressions return res; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + foreach(var arg in Parameters) + arg.CollectReferences(references); + } + protected override string Print() { return Name + "( (" + string.Join("), (", Parameters) + ") )"; @@ -173,18 +193,30 @@ namespace Avalonia.Rendering.Composition.Expressions Member = string.Intern(member); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Target.CollectReferences(references); + if (Target is ParameterExpression pe) + references.Add((pe.Name, Member)); + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { if (Target is KeywordExpression ke && ke.Keyword == ExpressionKeyword.Target) + { return context.Target.GetProperty(Member); + } + if (Target is ParameterExpression pe) { var obj = context.Parameters?.GetObjectParameter(pe.Name); if (obj != null) + { return obj.GetProperty(Member); + } } - + // Those are considered immutable return Target.Evaluate(ref context).GetProperty(Member); } @@ -263,6 +295,11 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Parameter.CollectReferences(references); + } + protected override string Print() { return OperatorName(Type) + Parameter; @@ -313,6 +350,12 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Left.CollectReferences(references); + Right.CollectReferences(references); + } + protected override string Print() { return "(" + Left + OperatorName(Type) + Right + ")"; diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index a7ddabd70d..9d23551e43 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs new file mode 100644 index 0000000000..334f975aa0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions; + +internal class ExpressionTrackedObjects : IEnumerable +{ + private List _list = new(); + private HashSet _hashSet = new(); + + public void Add(IExpressionObject obj, string member) + { + if (_hashSet.Add(obj)) + _list.Add(obj); + } + + public void Clear() + { + _list.Clear(); + _hashSet.Clear(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + public struct Pool + { + private Stack _stack = new(); + + public Pool() + { + } + + public ExpressionTrackedObjects Get() + { + if (_stack.Count > 0) + return _stack.Pop(); + return new ExpressionTrackedObjects(); + } + + public void Return(ExpressionTrackedObjects obj) + { + _stack.Clear(); + _stack.Push(obj); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 8c2e6e774a..a60084d8f3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -10,24 +10,23 @@ namespace Avalonia.Rendering.Composition.Server; internal class FpsCounter { - private readonly GlyphTypeface _typeface; - private readonly bool _useManualFpsCounting; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; + private int _totalFrames; private int _fps; private TimeSpan _lastFpsUpdate; - private GlyphRun[] _runs = new GlyphRun[10]; + const int FirstChar = 32; + const int LastChar = 126; + private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + public FpsCounter(GlyphTypeface typeface) { - for (var c = 0; c <= 9; c++) + for (var c = FirstChar; c <= LastChar; c++) { - var s = c.ToString(); + var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); } - _typeface = typeface; - _useManualFpsCounting = useManualFpsCounting; } public void FpsTick() => _framesThisSecond++; @@ -37,8 +36,8 @@ internal class FpsCounter var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (!_useManualFpsCounting) - ++_framesThisSecond; + ++_framesThisSecond; + ++_totalFrames; if (elapsed.TotalSeconds > 1) { @@ -47,12 +46,12 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = _fps.ToString("000"); + var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000}"; double width = 0; double height = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; width += run.Size.Width; height = Math.Max(height, run.Size.Height); } @@ -64,7 +63,7 @@ internal class FpsCounter double offset = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; context.Transform = Matrix.CreateTranslation(offset, 0); context.DrawGlyphRun(Brushes.White, run); offset += run.Size.Width; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 04ec711455..dab65fc8ed 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -56,10 +56,11 @@ namespace Avalonia.Rendering.Composition.Server Compositor.UpdateServerTime(); - Root.Update(this, Matrix4x4.Identity); - if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Root.Update(this, Matrix4x4.Identity); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e56f85acdf..5dbe9cfb17 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server @@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } private List _activeTargets = new(); + private HashSet _activeAnimations = new(); + private List _animationsToUpdate = new(); public ServerCompositor(IRenderLoop renderLoop) { @@ -64,6 +68,7 @@ namespace Avalonia.Rendering.Composition.Server } bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) { } @@ -71,6 +76,15 @@ namespace Avalonia.Rendering.Composition.Server void IRenderLoopTask.Render() { ApplyPendingBatches(); + + foreach(var animation in _activeAnimations) + _animationsToUpdate.Add(animation); + + foreach(var animation in _animationsToUpdate) + animation.Invalidate(); + + _animationsToUpdate.Clear(); + foreach (var t in _activeTargets) t.Render(); @@ -86,5 +100,11 @@ namespace Avalonia.Rendering.Composition.Server { _activeTargets.Remove(target); } + + public void AddToClock(IAnimationInstance animationInstance) => + _activeAnimations.Add(animationInstance); + + public void RemoveFromClock(IAnimationInstance animationInstance) => + _activeAnimations.Remove(animationInstance); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 072377cd7e..5b2f58b186 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -1,5 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -9,7 +15,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual long LastChangedBy => ItselfLastChangedBy; public long ItselfLastChangedBy { get; private set; } - + private uint _activationCount; + public bool IsActive => _activationCount != 0; + public ServerObject(ServerCompositor compositor) { Compositor = compositor; @@ -23,6 +31,7 @@ namespace Avalonia.Rendering.Composition.Server public void Apply(ChangeSet changes) { ApplyCore(changes); + ValuesInvalidated(); ItselfLastChangedBy = changes.Batch!.SequenceId; } @@ -32,5 +41,76 @@ namespace Avalonia.Rendering.Composition.Server } ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void Activate() + { + _activationCount++; + if (_activationCount == 1) + Activated(); + } + + public void Deactivate() + { +#if DEBUG + if (_activationCount == 0) + throw new InvalidOperationException(); +#endif + _activationCount--; + if (_activationCount == 0) + Deactivated(); + } + + protected virtual void Activated() + { + + } + + protected virtual void Deactivated() + { + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int GetOffset(ref T field) where T : struct + { + return Unsafe.ByteOffset(ref Unsafe.As(ref _activationCount), + ref Unsafe.As(ref field)) + .ToInt32(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + { + return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, + new IntPtr(offset))); + } + + public void NotifyAnimatedValueChanged(int offset) + { + ref var store = ref GetStoreFromOffset(offset); + store.Invalidate(); + ValuesInvalidated(); + } + + protected virtual void ValuesInvalidated() + { + + } + + public void SubscribeToInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.AddRef(animation)) + Activate(); + } + + public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.ReleaseRef(animation)) + Deactivate(); + } + + public virtual int? GetFieldOffset(string fieldName) => null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index bf5b8bf292..05b63a7a73 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,6 +7,7 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; + private ServerCompositionTarget? _root; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -95,16 +96,31 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; - _isDirty = true; + ValuesInvalidated(); + } + + public ServerCompositionTarget? Root + { + get => _root; + private set + { + if(_root != null) + Deactivate(); + _root = value; + if (_root != null) + Activate(); + } + } + protected override void ValuesInvalidated() + { + _isDirty = true; if (IsVisibleInFrame) Root?.AddDirtyRect(TransformedBounds); else Root?.Invalidate(); } - public ServerCompositionTarget? Root { get; private set; } - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 25fce01de3..f7c8078073 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -38,7 +38,6 @@ namespace Avalonia.Rendering.Composition { } - internal Matrix4x4? TryGetServerTransform() { if (Root == null) diff --git a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs new file mode 100644 index 0000000000..71305a8305 --- /dev/null +++ b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Utilities; + +internal class RefTrackingDictionary : Dictionary where TKey : class +{ + /// + /// Increase reference count for a key by 1. + /// + /// true if key was added to the dictionary, false otherwise + public bool AddRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(this, key, out var _); + count++; +#else + TryGetValue(key, out var count); + count++; + this[key] = count; +#endif + return count == 1; + } + + /// + /// Decrease reference count for a key by 1. + /// + /// true if key was removed to the dictionary, false otherwise + public bool ReleaseRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrNullRef(this, key); + if (Unsafe.IsNullRef(ref count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + return false; +#else + if (!TryGetValue(key, out var count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + this[key] = count; + return false; +#endif + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..3312f7a619 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + false diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs index f079a339df..72311b4d18 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -2,20 +2,22 @@ using System.IO; using System.Xml.Serialization; using Microsoft.CodeAnalysis; -namespace Avalonia.SourceGenerator.CompositionGenerator; - -[Generator(LanguageNames.CSharp)] -public class CompositionRoslynGenerator : IIncrementalGenerator +namespace Avalonia.SourceGenerator.CompositionGenerator { - public void Initialize(IncrementalGeneratorInitializationContext context) + [Generator(LanguageNames.CSharp)] + public class CompositionRoslynGenerator : IIncrementalGenerator { - var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); - var configs = schema.Select((t, _) => - (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); - context.RegisterSourceOutput(configs, (spc, config) => + public void Initialize(IncrementalGeneratorInitializationContext context) { - var generator = new Generator(spc, config); - generator.Generate(); - }); + var schema = + context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config); + generator.Generate(); + }); + } } } \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 43ef4a96e8..5a514a4eff 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -7,14 +7,14 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; namespace Avalonia.SourceGenerator.CompositionGenerator { - partial class Generator + public partial class Generator { - private readonly SourceProductionContext _output; + private readonly ICompositionGeneratorSink _output; private readonly GConfig _config; private readonly HashSet _objects; private readonly HashSet _brushes; private readonly Dictionary _manuals; - public Generator(SourceProductionContext output, GConfig config) + public Generator(ICompositionGeneratorSink output, GConfig config) { _output = output; _config = config; @@ -168,17 +168,35 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); + + var uninitializedObjectName = "dummy"; + var serverStaticCtorBody = cl.Abstract + ? Block() + : Block( + ParseStatement( + $"var dummy = ({serverName})System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof({serverName}));"), + ParseStatement($"System.GC.SuppressFinalize(dummy);"), + ParseStatement("InitializeFieldOffsets(dummy);") + ); + + var initializeFieldOffsetsBody = cl.ServerBase == null + ? Block() + : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); var getPropertyBody = Block(); var serverGetPropertyBody = Block(); + var serverGetFieldOffsetBody = Block(); + var activatedBody = Block(ParseStatement("base.Activated();")); + var deactivatedBody = Block(ParseStatement("base.Deactivated();")); var defaultsMethodBody = Block(); foreach (var prop in cl.Properties) { var fieldName = "_" + prop.Name.WithLowerFirst(); + var fieldOffsetName = "s_OffsetOf" + fieldName; var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); @@ -229,7 +247,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) server = server.AddMembers( - DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .WithExpressionBody(ArrowExpressionClause( @@ -240,24 +258,24 @@ namespace Avalonia.SourceGenerator.CompositionGenerator else { server = server - .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), + Block(ReturnStatement(MemberAccess(IdentifierName(fieldName), "Value")))), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, Block( ParseStatement("var changed = false;"), IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value")), Block( ParseStatement("On" + prop.Name + "Changing();"), ParseStatement($"changed = true;")) ), ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), ParseStatement($"if(changed) On" + prop.Name + "Changed();") )) )) @@ -270,15 +288,22 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) applyMethodBody = applyMethodBody.AddStatements( IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetValue"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("this")), + Argument(MemberAccess(changesVar, prop.Name, "Value")), + }))))), IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), ExpressionStatement( InvocationExpression(MemberAccess(fieldName, "SetAnimation"), ArgumentList(SeparatedList(new[] { + Argument(IdentifierName("this")), Argument(changesVar), - Argument(MemberAccess(changesVar, prop.Name, "Animation")) + Argument(MemberAccess(changesVar, prop.Name, "Animation")), + Argument(IdentifierName(fieldOffsetName)) }))))) ); else @@ -288,16 +313,28 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) ); - resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); if (animatedServer) + { startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); + deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); + } + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); - serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); + serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); + + server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); + initializeFieldOffsetsBody = initializeFieldOffsetsBody.AddStatements(ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), + InvocationExpression(MemberAccess(IdentifierName(uninitializedObjectName), "GetOffset"), + ArgumentList(SingletonSeparatedList(Argument( + RefExpression(MemberAccess(IdentifierName(uninitializedObjectName), fieldName))))))))); if (prop.DefaultValue != null) { @@ -357,6 +394,21 @@ namespace Avalonia.SourceGenerator.CompositionGenerator Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) .WithBody(applyMethodBody)); + server = server.AddMembers(ConstructorDeclaration(serverName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithBody(serverStaticCtorBody)); + + server = server.AddMembers( + ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected static void InitializeFieldOffsets({serverName} dummy){{}}")!) + .WithBody(initializeFieldOffsetsBody)); + + server = server + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Activated(){{}}")!).WithBody(activatedBody)) + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + client = client.AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); @@ -374,7 +426,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator client = WithGetProperty(client, getPropertyBody, false); server = WithGetProperty(server, serverGetPropertyBody, true); - + server = WithGetFieldOffset(server, serverGetFieldOffsetBody); + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -452,17 +505,19 @@ return; "Vector2", "Vector3", "Vector4", + "Matrix", "Matrix3x2", "Matrix4x4", "Quaternion", - "CompositionColor" + "Color", + "Avalonia.Media.Color" }; - BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop, string? expr = null) { if (VariantPropertyTypes.Contains(prop.Type)) return body.AddStatements( - ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ParseStatement($"if(name == \"{prop.Name}\")\n return {expr ?? prop.Name};\n") ); return body; @@ -480,6 +535,19 @@ return; return cl.AddMembers(method); } + + ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetFieldOffset(name);")); + var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"public override int? GetFieldOffset(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs new file mode 100644 index 0000000000..085a4041be --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs @@ -0,0 +1,6 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator; + +public interface ICompositionGeneratorSink +{ + void AddSource(string name, string code); +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs new file mode 100644 index 0000000000..6fec3faf93 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +class RoslynCompositionGeneratorSink : ICompositionGeneratorSink +{ + private readonly SourceProductionContext _ctx; + + public RoslynCompositionGeneratorSink(SourceProductionContext ctx) + { + _ctx = ctx; + } + + public void AddSource(string name, string code) => _ctx.AddSource(name, code); +} \ No newline at end of file From bcbd86eca3bda0b3e6aeee03fb063a2ba6811c28 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:15:45 +0300 Subject: [PATCH 042/481] Added VisualBrush support --- .../Composition/CompositionDrawListVisual.cs | 2 +- .../Drawing/CompositionDrawList.cs | 16 +++++++++- .../Drawing/CompositionDrawingContext.cs | 27 ++++++++++------- .../Composition/Server/DrawingContextProxy.cs | 30 ++++++++++++++++++- .../Server/ServerCompositionDrawListVisual.cs | 5 +--- .../Server/ServerCompositionTarget.cs | 5 ++-- .../Rendering/DeferredRenderer.cs | 6 ++-- .../SceneGraph/BrushDrawOperation.cs | 16 +++++++--- .../SceneGraph/DeferredDrawingContextImpl.cs | 4 +-- .../Rendering/SceneGraph/EllipseNode.cs | 7 ++--- .../Rendering/SceneGraph/GeometryNode.cs | 11 +++---- .../Rendering/SceneGraph/GlyphRunNode.cs | 11 +++---- .../Rendering/SceneGraph/LineNode.cs | 8 ++--- .../Rendering/SceneGraph/OpacityMaskNode.cs | 12 ++++---- .../Rendering/SceneGraph/RectangleNode.cs | 11 +++---- 15 files changed, 104 insertions(+), 67 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index b19c311663..e4ed0abd29 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -24,7 +24,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; - internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { Visual = visual; } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index aca8ef7c46..1d416f5a8a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -7,6 +8,8 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class CompositionDrawList : PooledList> { + public Size? Size { get; set; } + public CompositionDrawList() { @@ -26,11 +29,22 @@ internal class CompositionDrawList : PooledList> public CompositionDrawList Clone() { - var clone = new CompositionDrawList(Count); + var clone = new CompositionDrawList(Count) { Size = Size }; foreach (var r in this) clone.Add(r.Clone()); return clone; } + + public void Render(CompositorDrawingContextProxy canvas) + { + foreach (var cmd in this) + { + canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + cmd.Item.Render(canvas); + } + + canvas.VisualBrushDrawList = null; + } } internal class CompositionDrawListBuilder diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index c8e5d9e064..96c0e22d56 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -359,25 +359,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl ? _builder.DrawOperations[_drawOperationIndex] as IRef : null; } - - private IDictionary? CreateChildScene(IBrush? brush) + + private IDisposable? CreateChildScene(IBrush? brush) { - /* - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) + if (brush is VisualBrush visualBrush) { var visual = visualBrush.Visual; if (visual != null) { + // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer + // We should directly reference the corresponding CompositionVisual (which should + // be attached to the same composition target) like UWP does. + // Render-able visuals shouldn't be dangling unattached (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; - } - }*/ + var drawList = new CompositionDrawList() { Size = visual.Bounds.Size }; + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(drawList); + ImmediateRenderer.Render(visual, new DrawingContext(recorder)); + recorder.EndUpdate(); + + return drawList; + } + } return null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7d061e86a9..8b6ac5b0c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -2,6 +2,7 @@ using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -10,12 +11,21 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl { private IDrawingContextImpl _impl; + private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl) + public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) { _impl = impl; + _visualBrushRenderer = visualBrushRenderer; } + // This is a hack to make it work with the current way of handling visual brushes + public CompositionDrawList? VisualBrushDrawList + { + get => _visualBrushRenderer.VisualBrushDrawList; + set => _visualBrushRenderer.VisualBrushDrawList = value; + } + public Matrix PreTransform { get; set; } = Matrix.Identity; public void Dispose() @@ -135,4 +145,22 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } + + public class VisualBrushRenderer : IVisualBrushRenderer + { + public CompositionDrawList? VisualBrushDrawList { get; set; } + public Size GetRenderTargetSize(IVisualBrush brush) + { + return VisualBrushDrawList?.Size ?? Size.Empty; + } + + public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) + { + if (VisualBrushDrawList != null) + { + foreach (var cmd in VisualBrushDrawList) + cmd.Item.Render(context); + } + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f0384c36fc..ba18211459 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -51,10 +51,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_renderCommands != null) { - foreach (var cmd in _renderCommands) - { - cmd.Item.Render(canvas); - } + _renderCommands.Render(canvas); } base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index dab65fc8ed..d8a5de4f54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -75,11 +75,12 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsEmpty) { - using (var context = _layer.CreateDrawingContext(null)) + var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); + using (var context = _layer.CreateDrawingContext(visualBrushHelper)) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 82be0a1a0f..4236763e3b 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -272,16 +272,18 @@ namespace Avalonia.Rendering } } + Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = TryGetChildScene(_currentDraw); if (childScene != null) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index cd3dac699a..e81966ce81 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.VisualTree; @@ -9,14 +10,21 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform) + public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) : base(bounds, transform) { + Aux = aux; } /// - /// Gets a collection of child scenes that are needed to draw visual brushes. + /// Auxiliary data required to draw the brush /// - public abstract IDictionary? ChildScenes { get; } + public IDisposable? Aux { get; } + + public override void Dispose() + { + Aux?.Dispose(); + base.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3f495c619c..07082e4ac3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -457,7 +457,7 @@ namespace Avalonia.Rendering.SceneGraph return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; } - private IDictionary? CreateChildScene(IBrush? brush) + private IDisposable? CreateChildScene(IBrush? brush) { var visualBrush = brush as VisualBrush; @@ -470,7 +470,7 @@ namespace Avalonia.Rendering.SceneGraph (visual as IVisualBrushInitialize)?.EnsureInitialized(); var scene = new Scene(visual); _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; + return scene; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index c1fc6a81f6..4600653b9d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -17,14 +17,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) + IDisposable? aux = null) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; } /// @@ -47,8 +46,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect Rect { get; } - public override IDictionary? ChildScenes { get; } - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) { return transform == Transform && diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 70748989d6..4b43f93aee 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -23,14 +24,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform) + IDisposable? aux) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; - ChildScenes = childScenes; } /// @@ -53,9 +53,6 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index d6da087120..9199611ed6 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -23,13 +24,12 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IBrush foreground, GlyphRun glyphRun, - IDictionary? childScenes = null) - : base(new Rect(glyphRun.Size), transform) + IDisposable? aux = null) + : base(new Rect(glyphRun.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); GlyphRun = glyphRun; - ChildScenes = childScenes; } /// @@ -47,9 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - public override IDictionary? ChildScenes { get; } - /// public override void Render(IDrawingContextImpl context) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index a9e1ce8ed7..ee5ec0a5fc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -25,14 +25,13 @@ namespace Avalonia.Rendering.SceneGraph IPen pen, Point p1, Point p2, - IDictionary? childScenes = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) + IDisposable? aux = null) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) { Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; - ChildScenes = childScenes; } /// @@ -55,9 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Point P2 { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 4b6e7d2254..549c1fd7de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,12 +18,11 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Child scenes for drawing visual brushes. - public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary? childScenes = null) - : base(Rect.Empty, Matrix.Identity) + public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) + : base(Rect.Empty, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; - ChildScenes = childScenes; } /// @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity) + : base(Rect.Empty, Matrix.Identity, null) { } @@ -44,8 +44,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect? MaskBounds { get; } - /// - public override IDictionary? ChildScenes { get; } /// public override bool HitTest(Point p) => false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 3279c3a549..7b79c446f9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -26,14 +27,13 @@ namespace Avalonia.Rendering.SceneGraph IPen? pen, RoundedRect rect, BoxShadows boxShadows, - IDictionary? childScenes = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) + IDisposable? aux = null) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; BoxShadows = boxShadows; } @@ -62,9 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BoxShadows BoxShadows { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// From 7e4fa1d84b7a8eac22fa72fb2d0c9c612e743bf3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:46:52 +0300 Subject: [PATCH 043/481] Fixed hit-testing for the first frame --- .../Composition/Server/ReadbackIndices.cs | 25 ++++++++----------- .../Server/ServerCompositionTarget.cs | 10 +++++--- .../Composition/Server/ServerVisual.cs | 3 +-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs index 372fa4d9ce..1971451811 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -4,29 +4,24 @@ namespace Avalonia.Rendering.Composition.Server { private readonly object _lock = new object(); public int ReadIndex { get; private set; } = 0; - public int WriteIndex { get; private set; } = -1; + public int WriteIndex { get; private set; } = 1; + public int WrittenIndex { get; private set; } = 0; public ulong ReadRevision { get; private set; } - public ulong WriteRevision { get; private set; } - private ulong[] _revisions = new ulong[3]; - - + public ulong LastWrittenRevision { get; private set; } + public void NextRead() { lock (_lock) { - for (var c = 0; c < 3; c++) + if (ReadRevision < LastWrittenRevision) { - if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) - { - ReadIndex = c; - ReadRevision = _revisions[c]; - return; - } + ReadIndex = WrittenIndex; + ReadRevision = LastWrittenRevision; } } } - public void NextWrite(ulong revision) + public void CompleteWrite(ulong writtenRevision) { lock (_lock) { @@ -34,9 +29,9 @@ namespace Avalonia.Rendering.Composition.Server { if (c != WriteIndex && c != ReadIndex) { + WrittenIndex = WriteIndex; + LastWrittenRevision = writtenRevision; WriteIndex = c; - WriteRevision = revision; - _revisions[c] = revision; return; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index d8a5de4f54..a50562eabc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -15,7 +15,7 @@ namespace Avalonia.Rendering.Composition.Server private readonly Func _renderTargetFactory; private static long s_nextId = 1; public long Id { get; } - private ulong _frame = 1; + public ulong Revision { get; private set; } private IRenderTarget? _renderTarget; private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); private Rect _dirtyRect; @@ -58,9 +58,14 @@ namespace Avalonia.Rendering.Composition.Server if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Revision++; + // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + Readback.CompleteWrite(Revision); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { @@ -102,9 +107,6 @@ namespace Avalonia.Rendering.Composition.Server _dirtyRect = Rect.Empty; } - - Readback.NextWrite(_frame); - _frame++; } private static Rect SnapToDevicePixels(Rect rect, double scale) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 05b63a7a73..4e320c34be 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -63,7 +63,7 @@ namespace Avalonia.Rendering.Composition.Server Scale, RotationAngle, Orientation, Offset); var i = Root!.Readback; ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; + readback.Revision = root.Revision; readback.Matrix = res; readback.TargetId = Root.Id; //TODO: check effective opacity too @@ -85,7 +85,6 @@ namespace Avalonia.Rendering.Composition.Server public struct ReadbackData { public Matrix4x4 Matrix; - public bool Visible; public ulong Revision; public long TargetId; } From 7a9d9ea304e3e96f0b857afb9b3c14bb864fd6b9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 22:17:35 +0300 Subject: [PATCH 044/481] Invalidate visual's rect if it's moved in the global coordinate space --- .../Rendering/Composition/Server/ServerVisual.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 4e320c34be..9bfc909fe4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -69,7 +69,14 @@ namespace Avalonia.Rendering.Composition.Server //TODO: check effective opacity too IsVisibleInFrame = Visible && Opacity > 0; CombinedTransformMatrix = res; - GlobalTransformMatrix = res * transform; + var newTransform = res * transform; + if (GlobalTransformMatrix != newTransform) + { + // Visual was moved alongside with its parent + _isDirty = true; + Root.AddDirtyRect(TransformedBounds); + } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); From f0989357a01a1df28ad5b522aad180e08d5e539f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:29:56 +0300 Subject: [PATCH 045/481] Visiblity check fix --- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index f7c8078073..fa8d5d8f3b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -45,8 +45,8 @@ namespace Avalonia.Rendering.Composition var i = Root.Server.Readback; ref var readback = ref Server.GetReadback(i.ReadIndex); - // CompositionVisual wasn't visible - if (readback.Revision < i.ReadRevision) + // CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame + if (!readback.Visible || readback.Revision < i.ReadRevision) return null; // CompositionVisual was reparented (potential race here) From f974859323c06b493226dc8d85de66349a5ac4a9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:30:12 +0300 Subject: [PATCH 046/481] Change stream WIP --- .../Composition/Server/ServerVisual.cs | 51 ++++-- .../Composition/Transport/BatchStream.cs | 173 ++++++++++++++++++ .../Transport/BatchStreamArrayPool.cs | 144 +++++++++++++++ 3 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 9bfc909fe4..801bfb2f65 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,6 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private ServerCompositionTarget? _root; + private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -59,23 +60,35 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + // Calculate new parent-relative transform + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale, RotationAngle, Orientation, Offset); - var i = Root!.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = root.Revision; - readback.Matrix = res; - readback.TargetId = Root.Id; - //TODO: check effective opacity too - IsVisibleInFrame = Visible && Opacity > 0; - CombinedTransformMatrix = res; - var newTransform = res * transform; + + var newTransform = CombinedTransformMatrix * transform; + + // Check if visual was moved and recalculate face orientation + var positionChanged = false; if (GlobalTransformMatrix != newTransform) { - // Visual was moved alongside with its parent - _isDirty = true; - Root.AddDirtyRect(TransformedBounds); + _isBackface = Vector3.Transform( + new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; + positionChanged = true; + } + + var wasVisible = IsVisibleInFrame; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0 && !_isBackface; + + // Invalidate previous rect and queue new rect based on visibility + if (positionChanged) + { + if(wasVisible) + Root!.AddDirtyRect(TransformedBounds); + + if (IsVisibleInFrame) + _isDirty = true; } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); @@ -84,9 +97,18 @@ namespace Avalonia.Rendering.Composition.Server _isDirty = false; else if (_isDirty) { - Root.AddDirtyRect(TransformedBounds); + Root!.AddDirtyRect(TransformedBounds); _isDirty = false; } + + // Update readback indices + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = root.Revision; + readback.Matrix = CombinedTransformMatrix; + readback.TargetId = Root.Id; + readback.Visible = IsVisibleInFrame; + } public struct ReadbackData @@ -94,6 +116,7 @@ namespace Avalonia.Rendering.Composition.Server public Matrix4x4 Matrix; public ulong Revision; public long TargetId; + public bool Visible; } partial void ApplyChangesExtra(CompositionVisualChanges c) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs new file mode 100644 index 0000000000..feb892d134 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamData +{ + public Queue> Objects { get; } = new(); + public Queue> Structs { get; } = new(); +} + +public struct BatchStreamSegment +{ + public TData Data { get; set; } + public int ElementCount { get; set; } +} + +internal class BatchStreamWriter : IDisposable +{ + private readonly BatchStreamData _output; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + _output = output; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + void CommitDataSegment() + { + if (_currentDataSegment.Data != IntPtr.Zero) + _output.Structs.Enqueue(_currentDataSegment); + _currentDataSegment = new (); + } + + void NextDataSegment() + { + CommitDataSegment(); + _currentDataSegment.Data = _memoryPool.Get(); + } + + void CommitObjectSegment() + { + if (_currentObjectSegment.Data != null) + _output.Objects.Enqueue(_currentObjectSegment!); + _currentObjectSegment = new(); + } + + void NextObjectSegment() + { + CommitObjectSegment(); + _currentObjectSegment.Data = _objectPool.Get(); + } + + public unsafe void Write(T item) where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) + NextDataSegment(); + *(T*)((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount) = item; + _currentDataSegment.ElementCount += size; + } + + public void Write(ServerObject item) + { + if (_currentObjectSegment.Data == null || + _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) + NextObjectSegment(); + _currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item; + _currentObjectSegment.ElementCount++; + } + + public void Dispose() + { + CommitDataSegment(); + CommitObjectSegment(); + } +} + +internal class BatchStreamReader : IDisposable +{ + private readonly BatchStreamData _input; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + private int _memoryOffset, _objectOffset; + + public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + this._input = _input; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + public unsafe T Read() where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero) + { + if (_input.Structs.Count == 0) + throw new EndOfStreamException(); + _currentDataSegment = _input.Structs.Dequeue(); + _memoryOffset = 0; + } + + if (_memoryOffset + size > _currentDataSegment.ElementCount) + throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); + + var rv = *(T*)((byte*)_currentDataSegment.Data + size); + _memoryOffset += size; + if (_memoryOffset == _currentDataSegment.ElementCount) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + return rv; + } + + public ServerObject ReadObject() + { + if (_currentObjectSegment.Data == null) + { + if (_input.Objects.Count == 0) + throw new EndOfStreamException(); + _currentObjectSegment = _input.Objects.Dequeue()!; + _objectOffset = 0; + } + + var rv = _currentObjectSegment.Data![_objectOffset]; + _objectOffset++; + if (_objectOffset == _currentObjectSegment.ElementCount) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + return rv; + } + + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; + + public void Dispose() + { + if (_currentDataSegment.Data != IntPtr.Zero) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + while (_input.Structs.Count > 0) + _memoryPool.Return(_input.Structs.Dequeue().Data); + + if (_currentObjectSegment.Data != null) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + while (_input.Objects.Count > 0) + _objectPool.Return(_input.Objects.Dequeue().Data); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs new file mode 100644 index 0000000000..913958765a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition.Transport; + +/// +/// A pool that keeps a number of elements that was used in the last 10 seconds +/// +internal abstract class BatchStreamPoolBase : IDisposable +{ + readonly Stack _pool = new(); + bool _disposed; + int _usage; + readonly int[] _usageStatistics = new int[10]; + int _usageStatisticsSlot; + + public BatchStreamPoolBase(bool needsFinalize = false) + { + if(!needsFinalize) + GC.SuppressFinalize(needsFinalize); + + var updateRef = new WeakReference>(this); + StartUpdateTimer(updateRef); + } + + static void StartUpdateTimer(WeakReference> updateRef) + { + DispatcherTimer.Run(() => + { + if (updateRef.TryGetTarget(out var target)) + { + target.UpdateStatistics(); + return true; + } + return false; + + }, TimeSpan.FromSeconds(1)); + } + + private void UpdateStatistics() + { + lock (_pool) + { + var maximumUsage = _usageStatistics.Max(); + var recentlyUsedPooledSlots = maximumUsage - _usage; + while (recentlyUsedPooledSlots < _pool.Count) + DestroyItem(_pool.Pop()); + + _usageStatistics[_usage] = 0; + _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + } + } + + protected abstract T CreateItem(); + + protected virtual void DestroyItem(T item) + { + + } + + public T Get() + { + lock (_pool) + { + _usage++; + if (_usageStatistics[_usageStatisticsSlot] < _usage) + _usageStatistics[_usageStatisticsSlot] = _usage; + + if (_pool.Count != 0) + return _pool.Pop(); + } + + return CreateItem(); + } + + public void Return(T item) + { + lock (_pool) + { + _usage--; + if (!_disposed) + { + _pool.Push(item); + return; + } + } + + DestroyItem(item); + } + + public void Dispose() + { + lock (_pool) + { + _disposed = true; + foreach (var item in _pool) + DestroyItem(item); + _pool.Clear(); + } + } + + ~BatchStreamPoolBase() + { + Dispose(); + } +} + +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +{ + private readonly int _arraySize; + + public BatchStreamObjectPool(int arraySize = 1024) + { + _arraySize = arraySize; + } + + protected override T[] CreateItem() + { + return new T[_arraySize]; + } + + protected override void DestroyItem(T[] item) + { + Array.Clear(item, 0, item.Length); + } +} + +internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase +{ + public int BufferSize { get; } + + public BatchStreamMemoryPool(int bufferSize = 16384) + { + BufferSize = bufferSize; + } + + protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize); + + protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item); +} \ No newline at end of file From 4991d4f370acd7503260258722bcad328e619d65 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:35:50 +0300 Subject: [PATCH 047/481] Switched to byte-stream based transport --- .../Animations/AnimatedValueStore.cs | 5 +- .../Animations/CompositionAnimation.cs | 5 +- .../Animations/CompositionAnimationGroup.cs | 2 - .../Animations/ExpressionAnimation.cs | 2 +- .../Animations/ImplicitAnimationCollection.cs | 2 - .../Composition/CompositionDrawListVisual.cs | 16 +- .../Composition/CompositionEasingFunction.cs | 4 +- .../Composition/CompositionObject.cs | 57 ++- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/CompositionTarget.cs | 2 +- .../Rendering/Composition/Compositor.cs | 50 ++- .../Rendering/Composition/ContainerVisual.cs | 13 +- .../Rendering/Composition/CustomDrawVisual.cs | 56 --- .../Server/ServerCompositionDrawListVisual.cs | 10 +- .../Server/ServerCompositionTarget.cs | 4 +- .../Composition/Server/ServerCompositor.cs | 23 +- .../Server/ServerContainerVisual.cs | 9 +- .../Server/ServerCustomDrawVisual.cs | 31 -- .../Composition/Server/ServerList.cs | 24 +- .../Composition/Server/ServerObject.cs | 28 +- .../Composition/Server/ServerVisual.cs | 31 +- .../Rendering/Composition/Transport/Batch.cs | 7 +- .../Composition/Transport/BatchStream.cs | 27 +- .../Transport/BatchStreamArrayPool.cs | 24 +- .../Transport/BatchStreamDebugMarker.cs | 9 + .../Rendering/Composition/Transport/Change.cs | 82 ---- .../Composition/Transport/ChangeSet.cs | 36 -- .../Composition/Transport/ChangeSetPool.cs | 42 -- .../Transport/CompositionTargetChanges.cs | 11 - .../Transport/CustomDrawVisualChanges.cs | 20 - .../Transport/DrawListVisualChanges.cs | 48 -- .../Composition/Transport/ListChange.cs | 19 - .../Composition/Transport/ListChangeSet.cs | 25 -- .../Transport/ServerListProxyHelper.cs | 55 +-- .../Composition/Transport/VisualChanges.cs | 16 - .../Rendering/Composition/Visual.cs | 35 +- .../Rendering/Composition/VisualCollection.cs | 1 + src/Avalonia.Base/composition-schema.xml | 7 +- .../CompositionGenerator/Config.cs | 6 + .../CompositionGenerator/Extensions.cs | 9 + .../Generator.KeyFrameAnimation.cs | 2 +- .../Generator.ListProxy.cs | 12 +- .../CompositionGenerator/Generator.cs | 413 ++++++++++-------- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../Composition/BatchStreamTests.cs | 45 ++ 46 files changed, 532 insertions(+), 809 deletions(-) delete mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index e877b50b20..95bc384743 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; @@ -81,7 +82,7 @@ namespace Avalonia.Rendering.Composition.Animations public bool IsAnimation => _animation != null; - public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) + public void SetAnimation(ServerObject target, TimeSpan commitedAt, IAnimationInstance animation, int storeOffset) { _direct = default; if (_animation != null) @@ -91,7 +92,7 @@ namespace Avalonia.Rendering.Composition.Animations } _animation = animation; - _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + _animation.Initialize(commitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); if (target.IsActive) _animation.Activate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index fe20115b38..cf81c6e656 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -18,8 +18,6 @@ namespace Avalonia.Rendering.Composition.Animations _propertySet = new CompositionPropertySet(compositor); } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public void ClearAllParameters() => _propertySet.ClearAll(); public void ClearParameter(string key) => _propertySet.Clear(key); @@ -52,8 +50,7 @@ namespace Avalonia.Rendering.Composition.Animations internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, ExpressionVariant? finalValue); - internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server); + internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index 833f7e498c..89f8ba411d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -20,7 +20,5 @@ namespace Avalonia.Rendering.Composition.Animations public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index a6f24c2e35..6a2c07e6ef 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -29,6 +29,6 @@ namespace Avalonia.Rendering.Composition.Animations internal override IAnimationInstance CreateInstance( ServerObject targetObject, ExpressionVariant? finalValue) => new ExpressionAnimationInstance(ParsedExpression, - targetObject, finalValue, CreateSnapshot(true)); + targetObject, finalValue, CreateSnapshot()); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index be91352527..fa5b69dae9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -15,8 +15,6 @@ namespace Avalonia.Rendering.Composition.Animations _innerface = _inner; } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index e4ed0abd29..069d888fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -9,7 +9,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { public Visual Visual { get; } - private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private bool _drawListChanged; private CompositionDrawList? _drawList; public CompositionDrawList? DrawList { @@ -18,11 +18,21 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { _drawList?.Dispose(); _drawList = value; - Changes.DrawCommands = value?.Clone(); + _drawListChanged = true; + RegisterForSerialization(); } } - private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + writer.Write((byte)(_drawListChanged ? 1 : 0)); + if (_drawListChanged) + { + writer.WriteObject(DrawList?.Clone()); + _drawListChanged = false; + } + base.SerializeChangesCore(writer); + } internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs index 73db243e93..90b2bec268 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -10,9 +10,7 @@ namespace Avalonia.Rendering.Composition internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - + internal abstract IEasingFunction Snapshot(); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 2417ecaba8..d561338a36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -6,7 +6,7 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition { - public abstract class CompositionObject : IDisposable, IExpressionObject + public abstract class CompositionObject : IDisposable { public ImplicitAnimationCollection? ImplicitAnimations { get; set; } internal CompositionObject(Compositor compositor, ServerObject server) @@ -18,46 +18,17 @@ namespace Avalonia.Rendering.Composition public Compositor Compositor { get; } internal ServerObject Server { get; } public bool IsDisposed { get; private set; } - private ChangeSet? _changes; + private bool _registeredForSerialization; private static void ThrowInvalidOperation() => throw new InvalidOperationException("There is no server-side counterpart for this object"); - - private protected ChangeSet Changes - { - get - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (Server == null) ThrowInvalidOperation(); - var currentBatch = Compositor.CurrentBatch; - if (_changes != null && _changes.Batch != currentBatch) - _changes = null; - if (_changes == null) - { - _changes = ChangeSetPool.Get(Server!, currentBatch); - currentBatch.Changes!.Add(_changes); - Compositor.QueueImplicitBatchCommit(); - } - - return _changes; - } - } - - private protected abstract IChangeSetPool ChangeSetPool { get; } public void Dispose() { - Changes.Dispose = true; + //Changes.Dispose = true; IsDisposed = true; } - internal virtual ExpressionVariant GetPropertyForAnimation(string name) - { - return default; - } - - ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); - public void StartAnimation(string propertyName, CompositionAnimation animation) => StartAnimation(propertyName, animation, null); @@ -121,5 +92,27 @@ namespace Avalonia.Rendering.Composition throw new ArgumentException(); } + + protected void RegisterForSerialization() + { + if (Server == null) + throw new InvalidOperationException("The object doesn't have an associated server counterpart"); + + if(_registeredForSerialization) + return; + _registeredForSerialization = true; + Compositor.RegisterForSerialization(this); + } + + internal void SerializeChanges(BatchStreamWriter writer) + { + _registeredForSerialization = false; + SerializeChangesCore(writer); + } + + private protected virtual void SerializeChangesCore(BatchStreamWriter writer) + { + + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index bc0ce804dc..584969cbc0 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -16,8 +16,6 @@ namespace Avalonia.Rendering.Composition { } - private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); - internal void Set(string key, ExpressionVariant value) { _objects.Remove(key); @@ -104,10 +102,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server) => - SnapshotCore(server, 1); + internal PropertySetSnapshot Snapshot() => + SnapshotCore(1); - private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) + private PropertySetSnapshot SnapshotCore(int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -116,12 +114,12 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); else - dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); } foreach (var v in _variants) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 8d052389c2..c5cfaeacce 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -102,6 +102,6 @@ namespace Avalonia.Rendering.Composition return false; } - public void RequestRedraw() => Changes.RedrawRequested.Value = true; + public void RequestRedraw() => RegisterForSerialization(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 14d779dbc4..96564f0800 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -16,18 +16,17 @@ namespace Avalonia.Rendering.Composition public partial class Compositor { private ServerCompositor _server; - private Batch _currentBatch; private bool _implicitBatchCommitQueued; private Action _implicitBatchCommit; - - internal Batch CurrentBatch => _currentBatch; + private BatchStreamObjectPool _batchObjectPool = new(); + private BatchStreamMemoryPool _batchMemoryPool = new(); + private List _objectsForSerialization = new(); internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - - private Compositor(ServerCompositor server) + + public Compositor(IRenderLoop loop) { - _server = server; - _currentBatch = new Batch(); + _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); @@ -40,18 +39,27 @@ namespace Avalonia.Rendering.Composition public Task RequestCommitAsync() { - var batch = CurrentBatch; - _currentBatch = new Batch(); + var batch = new Batch(); + + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + foreach (var obj in _objectsForSerialization) + { + writer.WriteObject(obj.Server); + obj.SerializeChanges(writer); +#if DEBUG_COMPOSITOR_SERIALIZATION + writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); + writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); +#endif + } + _objectsForSerialization.Clear(); + } + batch.CommitedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); return batch.Completed; } - public static Compositor Create(IRenderLoop timer) - { - return new Compositor(new ServerCompositor(timer)); - } - public void Dispose() { @@ -122,12 +130,8 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); - - internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => - new CustomDrawVisual(this, renderer, hitTest); - - public void QueueImplicitBatchCommit() + + private void QueueImplicitBatchCommit() { if(_implicitBatchCommitQueued) return; @@ -140,5 +144,11 @@ namespace Avalonia.Rendering.Composition _implicitBatchCommitQueued = false; RequestCommitAsync(); } + + internal void RegisterForSerialization(CompositionObject compositionObject) + { + _objectsForSerialization.Add(compositionObject); + QueueImplicitBatchCommit(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index f650d3e995..5b2a4be1bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -2,19 +2,20 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition { - public class CompositionContainerVisual : CompositionVisual + public partial class CompositionContainerVisual : CompositionVisual { - public CompositionVisualCollection Children { get; } - internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + public CompositionVisualCollection Children { get; private set; } = null!; + + partial void InitializeDefaultsExtra() { - Children = new CompositionVisualCollection(this, server.Children); + Children = new CompositionVisualCollection(this, Server.Children); } - private protected override void OnRootChanged() + private protected override void OnRootChangedCore() { foreach (var ch in Children) ch.Root = Root; - base.OnRootChanged(); + base.OnRootChangedCore(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs deleted file mode 100644 index 0505d6a46c..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition -{ - internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable - { - private readonly ICustomDrawVisualHitTest? _hitTest; - - internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest) : base(compositor, - new ServerCustomDrawVisual(compositor.Server, renderer)) - { - _hitTest = hitTest; - } - - private TData? _data; - - static bool Eq(TData? left, TData? right) - { - if (left == null && right == null) - return true; - if (left == null) - return false; - return left.Equals(right); - } - - public TData? Data - { - get => _data; - set - { - if (!Eq(_data, value)) - { - ((CustomDrawVisualChanges) Changes).Data.Value = value; - _data = value; - } - } - } - - private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; - } - - public interface ICustomDrawVisualRenderer - { - void Render(IDrawingContextImpl canvas, TData? data); - } - - public interface ICustomDrawVisualHitTest - { - bool HitTest(TData data, Vector2 vector2); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index ba18211459..f1b5032cd3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Collections.Pooled; using Avalonia.Platform; @@ -35,16 +36,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua } } - protected override void ApplyCore(ChangeSet changes) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var ch = (DrawListVisualChanges)changes; - if (ch.DrawCommandsIsSet) + if (reader.Read() == 1) { _renderCommands?.Dispose(); - _renderCommands = ch.AcquireDrawCommands(); + _renderCommands = reader.ReadObject(); _contentBounds = null; } - base.ApplyCore(changes); + base.DeserializeChangesCore(reader, commitedAt); } protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index a50562eabc..7567eba534 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -42,8 +42,8 @@ namespace Avalonia.Rendering.Composition.Server else _compositor.RemoveCompositionTarget(this); } - - partial void ApplyChangesExtra(CompositionTargetChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 5dbe9cfb17..f7de704b23 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -17,10 +17,14 @@ namespace Avalonia.Rendering.Composition.Server private List _activeTargets = new(); private HashSet _activeAnimations = new(); private List _animationsToUpdate = new(); + private BatchStreamObjectPool _batchObjectPool; + private BatchStreamMemoryPool _batchMemoryPool; - public ServerCompositor(IRenderLoop renderLoop) + public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { _renderLoop = renderLoop; + _batchObjectPool = batchObjectPool; + _batchMemoryPool = batchMemoryPool; _renderLoop.Add(this); } @@ -45,14 +49,21 @@ namespace Avalonia.Rendering.Composition.Server batch = _batches.Dequeue(); } - foreach (var change in batch.Changes) + using (var stream = new BatchStreamReader(batch.Changes, _batchMemoryPool, _batchObjectPool)) { - if (change.Dispose) + while (!stream.IsObjectEof) { - //TODO + var target = (ServerObject)stream.ReadObject()!; + target.DeserializeChanges(stream, batch); +#if DEBUG_COMPOSITOR_SERIALIZATION + if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on object stream"); + if(stream.Read() != BatchStreamDebugMarkers.ObjectEndMagic) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on data stream"); +#endif } - change.Target!.Apply(change); - change.Reset(); } _reusableToCompleteList.Add(batch); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index 3f0995b257..a277450214 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -3,10 +3,9 @@ using Avalonia.Platform; namespace Avalonia.Rendering.Composition.Server { - internal class ServerCompositionContainerVisual : ServerCompositionVisual + internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { - public ServerCompositionVisualCollection Children { get; } - + public ServerCompositionVisualCollection Children { get; private set; } = null!; protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -28,9 +27,9 @@ namespace Avalonia.Rendering.Composition.Server child.Update(root, GlobalTransformMatrix); } - public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + partial void Initialize() { - Children = new ServerCompositionVisualCollection(compositor); + Children = new ServerCompositionVisualCollection(Compositor); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs deleted file mode 100644 index 5f3eb051a4..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition.Server -{ - class ServerCustomDrawVisual : ServerCompositionContainerVisual - { - private readonly ICustomDrawVisualRenderer _renderer; - private TData? _data; - public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) - { - _renderer = renderer; - } - - protected override void ApplyCore(ChangeSet changes) - { - var c = (CustomDrawVisualChanges) changes; - if (c.Data.IsSet) - _data = c.Data.Value; - - base.ApplyCore(changes); - } - - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) - { - _renderer.Render(canvas, _data); - base.RenderCore(canvas, transform); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 09ef119e6b..4beea4715b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -7,25 +7,19 @@ namespace Avalonia.Rendering.Composition.Server class ServerList : ServerObject where T : ServerObject { public List List { get; } = new List(); - protected override void ApplyCore(ChangeSet changes) + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var c = (ListChangeSet) changes; - if (c.HasListChanges) + if (reader.Read() == 1) { - foreach (var lc in c.ListChanges) - { - if(lc.Action == ListChangeAction.Clear) - List.Clear(); - if(lc.Action == ListChangeAction.RemoveAt) - List.RemoveAt(lc.Index); - if(lc.Action == ListChangeAction.InsertAt) - List.Insert(lc.Index, lc.Added!); - if (lc.Action == ListChangeAction.ReplaceAt) - List[lc.Index] = lc.Added!; - } + List.Clear(); + var count = reader.Read(); + for (var c = 0; c < count; c++) + List.Add(reader.ReadObject()); } + base.DeserializeChangesCore(reader, commitedAt); } - + public override long LastChangedBy { get diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 5b2f58b186..16f57d9059 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -23,18 +23,6 @@ namespace Avalonia.Rendering.Composition.Server Compositor = compositor; } - protected virtual void ApplyCore(ChangeSet changes) - { - - } - - public void Apply(ChangeSet changes) - { - ApplyCore(changes); - ValuesInvalidated(); - ItselfLastChangedBy = changes.Batch!.SequenceId; - } - public virtual ExpressionVariant GetPropertyForAnimation(string name) { return default; @@ -81,6 +69,10 @@ namespace Avalonia.Rendering.Composition.Server [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) { +#if DEBUG + if (offset == 0) + throw new InvalidOperationException(); +#endif return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, new IntPtr(offset))); } @@ -112,5 +104,17 @@ namespace Avalonia.Rendering.Composition.Server } public virtual int? GetFieldOffset(string fieldName) => null; + + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + + } + + public void DeserializeChanges(BatchStreamReader reader, Batch batch) + { + DeserializeChangesCore(reader, batch.CommitedAt); + ValuesInvalidated(); + ItselfLastChangedBy = batch.SequenceId; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 801bfb2f65..5717ab2f8c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; - private ServerCompositionTarget? _root; private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -119,28 +118,24 @@ namespace Avalonia.Rendering.Composition.Server public bool Visible; } - partial void ApplyChangesExtra(CompositionVisualChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { - if (c.Parent.IsSet) - Parent = c.Parent.Value; - if (c.Root.IsSet) - Root = c.Root.Value; ValuesInvalidated(); } - public ServerCompositionTarget? Root + partial void OnRootChanging() { - get => _root; - private set - { - if(_root != null) - Deactivate(); - _root = value; - if (_root != null) - Activate(); - } + if(Root != null) + Deactivate(); } - + + partial void OnRootChanged() + { + if (Root != null) + Activate(); + } + protected override void ValuesInvalidated() { _isDirty = true; @@ -149,8 +144,6 @@ namespace Avalonia.Rendering.Composition.Server else Root?.Invalidate(); } - - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index 7b64c01d09..0714db5781 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -9,23 +9,22 @@ namespace Avalonia.Rendering.Composition.Transport internal class Batch { private static long _nextSequenceId = 1; - private static ConcurrentBag> _pool = new ConcurrentBag>(); + private static ConcurrentBag _pool = new(); public long SequenceId { get; } public Batch() { SequenceId = Interlocked.Increment(ref _nextSequenceId); if (!_pool.TryTake(out var lst)) - lst = new List(); + lst = new BatchStreamData(); Changes = lst; } private TaskCompletionSource _tcs = new TaskCompletionSource(); - public List Changes { get; private set; } + public BatchStreamData Changes { get; private set; } public TimeSpan CommitedAt { get; set; } public void Complete() { - Changes.Clear(); _pool.Add(Changes); Changes = null!; diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index feb892d134..9e9ed739fb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -2,13 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Transport; internal class BatchStreamData { - public Queue> Objects { get; } = new(); + public Queue> Objects { get; } = new(); public Queue> Structs { get; } = new(); } @@ -22,12 +23,12 @@ internal class BatchStreamWriter : IDisposable { private readonly BatchStreamData _output; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; - public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { _output = output; _memoryPool = memoryPool; @@ -69,7 +70,7 @@ internal class BatchStreamWriter : IDisposable _currentDataSegment.ElementCount += size; } - public void Write(ServerObject item) + public void WriteObject(object? item) { if (_currentObjectSegment.Data == null || _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) @@ -89,15 +90,15 @@ internal class BatchStreamReader : IDisposable { private readonly BatchStreamData _input; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; private int _memoryOffset, _objectOffset; - public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { - this._input = _input; + _input = input; _memoryPool = memoryPool; _objectPool = objectPool; } @@ -116,7 +117,7 @@ internal class BatchStreamReader : IDisposable if (_memoryOffset + size > _currentDataSegment.ElementCount) throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); - var rv = *(T*)((byte*)_currentDataSegment.Data + size); + var rv = *(T*)((byte*)_currentDataSegment.Data + _memoryOffset); _memoryOffset += size; if (_memoryOffset == _currentDataSegment.ElementCount) { @@ -127,7 +128,9 @@ internal class BatchStreamReader : IDisposable return rv; } - public ServerObject ReadObject() + public T ReadObject() where T : class? => (T)ReadObject()!; + + public object? ReadObject() { if (_currentObjectSegment.Data == null) { @@ -148,6 +151,8 @@ internal class BatchStreamReader : IDisposable return rv; } + public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0; + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 913958765a..d76a9c609e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -18,27 +18,31 @@ internal abstract class BatchStreamPoolBase : IDisposable readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - public BatchStreamPoolBase(bool needsFinalize = false) + public BatchStreamPoolBase(bool needsFinalize, Action>? startTimer = null) { if(!needsFinalize) GC.SuppressFinalize(needsFinalize); var updateRef = new WeakReference>(this); - StartUpdateTimer(updateRef); + StartUpdateTimer(startTimer, updateRef); } - static void StartUpdateTimer(WeakReference> updateRef) + static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) { - DispatcherTimer.Run(() => + Func timerProc = () => { if (updateRef.TryGetTarget(out var target)) { target.UpdateStatistics(); return true; } - return false; - }, TimeSpan.FromSeconds(1)); + return false; + }; + if (startTimer != null) + startTimer(timerProc); + else + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); } private void UpdateStatistics() @@ -50,7 +54,7 @@ internal abstract class BatchStreamPoolBase : IDisposable while (recentlyUsedPooledSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usage] = 0; + _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; } } @@ -109,11 +113,11 @@ internal abstract class BatchStreamPoolBase : IDisposable } } -internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class? { private readonly int _arraySize; - public BatchStreamObjectPool(int arraySize = 1024) + public BatchStreamObjectPool(int arraySize = 1024, Action>? startTimer = null) : base(false, startTimer) { _arraySize = arraySize; } @@ -133,7 +137,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384) + public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs new file mode 100644 index 0000000000..7d21b03f24 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamDebugMarkers +{ + public static object ObjectEndMarker = new object(); + public static Guid ObjectEndMagic = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs deleted file mode 100644 index cbee350ab3..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Rendering.Composition.Animations; - -namespace Avalonia.Rendering.Composition.Transport -{ - struct Change - { - private T? _value; - - public bool IsSet { get; private set; } - - public T? Value - { - get - { - if(!IsSet) - throw new InvalidOperationException(); - return _value; - } - set - { - IsSet = true; - _value = value; - } - } - - public void Reset() - { - _value = default; - IsSet = false; - } - } - - struct AnimatedChange - { - private T? _value; - private IAnimationInstance? _animation; - - public bool IsValue { get; private set; } - public bool IsAnimation { get; private set; } - - public T Value - { - get - { - if(!IsValue) - throw new InvalidOperationException(); - return _value!; - } - set - { - IsAnimation = false; - _animation = null; - IsValue = true; - _value = value; - } - } - - public IAnimationInstance Animation - { - get - { - if(!IsAnimation) - throw new InvalidOperationException(); - return _animation!; - } - set - { - IsValue = false; - _value = default; - IsAnimation = true; - _animation = value; - } - } - - public void Reset() - { - this = default; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs deleted file mode 100644 index 898885dce6..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal abstract class ChangeSet - { - private readonly IChangeSetPool _pool; - public Batch Batch = null!; - public ServerObject? Target; - public bool Dispose; - - public ChangeSet(IChangeSetPool pool) - { - _pool = pool; - } - - public virtual void Reset() - { - Batch = null!; - Target = null; - Dispose = false; - } - - public void Return() - { - _pool.Return(this); - } - } - - internal class CompositionObjectChanges : ChangeSet - { - public CompositionObjectChanges(IChangeSetPool pool) : base(pool) - { - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs deleted file mode 100644 index ea97cd7d44..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - interface IChangeSetPool - { - void Return(ChangeSet changes); - ChangeSet Get(ServerObject target, Batch batch); - } - - class ChangeSetPool : IChangeSetPool where T : ChangeSet - { - private readonly Func _factory; - private readonly ConcurrentBag _pool = new ConcurrentBag(); - - public ChangeSetPool(Func factory) - { - _factory = factory; - } - - public void Return(T changes) - { - changes.Reset(); - _pool.Add(changes); - } - - void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); - ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); - - public T Get(ServerObject target, Batch batch) - { - if (!_pool.TryTake(out var res)) - res = _factory(this); - res.Target = target; - res.Batch = batch; - res.Dispose = false; - return res; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs deleted file mode 100644 index 014adc7bbe..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport; - -partial class CompositionTargetChanges -{ - public Change RedrawRequested; - - partial void ResetExtra() - { - RedrawRequested.Reset(); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs deleted file mode 100644 index aed041b62e..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport -{ - class CustomDrawVisualChanges : CompositionVisualChanges - { - public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public Change Data; - - public override void Reset() - { - Data.Reset(); - base.Reset(); - } - - public new static ChangeSetPool> Pool { get; } = - new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs deleted file mode 100644 index 215c03b229..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Collections.Pooled; -using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Transport; - -internal class DrawListVisualChanges : CompositionVisualChanges -{ - private CompositionDrawList? _drawCommands; - - public DrawListVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public CompositionDrawList? DrawCommands - { - get => _drawCommands; - set - { - _drawCommands?.Dispose(); - _drawCommands = value; - DrawCommandsIsSet = true; - } - } - - public bool DrawCommandsIsSet { get; private set; } - - public CompositionDrawList? AcquireDrawCommands() - { - var rv = _drawCommands; - _drawCommands = null; - DrawCommandsIsSet = false; - return rv; - } - - public override void Reset() - { - _drawCommands?.Dispose(); - _drawCommands = null; - DrawCommandsIsSet = false; - base.Reset(); - } - - public new static ChangeSetPool Pool { get; } = - new ChangeSetPool(pool => new(pool)); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs deleted file mode 100644 index ee6e4231f8..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal class ListChange where T : ServerObject - { - public int Index; - public ListChangeAction Action; - public T? Added; - } - - internal enum ListChangeAction - { - InsertAt, - RemoveAt, - Clear, - ReplaceAt - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs deleted file mode 100644 index 9bb101a080..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - class ListChangeSet : ChangeSet where T : ServerObject - { - private List>? _listChanges; - public List> ListChanges => _listChanges ??= new List>(); - public bool HasListChanges => _listChanges != null; - - public override void Reset() - { - _listChanges?.Clear(); - base.Reset(); - } - - public ListChangeSet(IChangeSetPool pool) : base(pool) - { - } - - public static readonly ChangeSetPool> Pool = - new ChangeSetPool>(pool => new ListChangeSet(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs index 1add3aa990..2399bd71d7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -8,19 +8,21 @@ namespace Avalonia.Rendering.Composition.Transport where TServer : ServerObject where TClient : CompositionObject { - private readonly IGetChanges _parent; - private readonly List _list = new List(); + private readonly IRegisterForSerialization _parent; + private bool _changed; - public interface IGetChanges + public interface IRegisterForSerialization { - ListChangeSet GetChanges(); + void RegisterForSerialization(); } - public ServerListProxyHelper(IGetChanges parent) + public ServerListProxyHelper(IRegisterForSerialization parent) { _parent = parent; } - + + private readonly List _list = new List(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -30,10 +32,8 @@ namespace Avalonia.Rendering.Composition.Transport public void Clear() { _list.Clear(); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.Clear - }); + _changed = true; + _parent.RegisterForSerialization(); } public bool Contains(TClient item) => _list.Contains(item); @@ -56,22 +56,15 @@ namespace Avalonia.Rendering.Composition.Transport public void Insert(int index, TClient item) { _list.Insert(index, item); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.InsertAt, - Index = index, - Added = (TServer) item.Server - }); + _changed = true; + _parent.RegisterForSerialization(); } public void RemoveAt(int index) { _list.RemoveAt(index); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.RemoveAt, - Index = index - }); + _changed = true; + _parent.RegisterForSerialization(); } public TClient this[int index] @@ -80,13 +73,21 @@ namespace Avalonia.Rendering.Composition.Transport set { _list[index] = value; - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.ReplaceAt, - Index = index, - Added = (TServer) value.Server - }); + _changed = true; + _parent.RegisterForSerialization(); + } + } + + public void Serialize(BatchStreamWriter writer) + { + writer.Write((byte)(_changed ? 1 : 0)); + if (_changed) + { + writer.Write(_list.Count); + foreach (var el in _list) + writer.WriteObject(el.Server); } + _changed = false; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs deleted file mode 100644 index c87fb96967..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - partial class CompositionVisualChanges - { - public Change Parent; - public Change Root; - - partial void ResetExtra() - { - Parent.Reset(); - Root.Reset(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index fa8d5d8f3b..5bf5dcee74 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -4,39 +4,14 @@ namespace Avalonia.Rendering.Composition { public abstract partial class CompositionVisual { - private CompositionVisual? _parent; - private CompositionTarget? _root; - - public CompositionVisual? Parent - { - get => _parent; - internal set - { - if (_parent == value) - return; - _parent = value; - Changes.Parent.Value = value?.Server; - Root = _parent?.Root; - } - } - - // TODO: hide behind private-ish API - public CompositionTarget? Root + private protected virtual void OnRootChangedCore() { - get => _root; - internal set - { - var changed = _root != value; - _root = value; - Changes.Root.Value = value?.Server; - if (changed) - OnRootChanged(); - } } - private protected virtual void OnRootChanged() - { - } + partial void OnRootChanged() => OnRootChangedCore(); + + partial void OnParentChanged() => Root = Parent?.Root; + internal Matrix4x4? TryGetServerTransform() { diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index fef4caf675..42226a8b4d 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -64,6 +64,7 @@ namespace Avalonia.Rendering.Composition { if (item.Parent != null) throw new InvalidOperationException("Visual already has a parent"); + item.Parent = item; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index eb1ffe1922..a7ae341bb3 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -4,10 +4,12 @@ Avalonia.Rendering.Composition.Server Avalonia.Rendering.Composition.Transport Avalonia.Rendering.Composition.Animations - - + + + + @@ -21,6 +23,7 @@ + diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 8b6aca33cd..096864e52a 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -35,6 +35,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator [XmlAttribute] public string Name { get; set; } + + [XmlAttribute] + public bool Passthrough { get; set; } + [XmlAttribute] public string ServerName { get; set; } } @@ -104,6 +108,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public string DefaultValue { get; set; } [XmlAttribute] public bool Animated { get; set; } + [XmlAttribute] + public bool InternalSet { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs index 43a4a4afa7..d88e9b4600 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -41,6 +41,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } + + public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs index 7d5146c5f5..314ac1acbf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) {{ - return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(), finalValue?.CastOrDefault<{a.Type}>(), targetObject, DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, IterationCount, StopBehavior); diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs index 593386f713..e0ea5b20ae 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -11,9 +11,7 @@ class Template { private ServerListProxyHelper _list = null!; - ListChangeSet - ServerListProxyHelper.IGetChanges. - GetChanges() => Changes; + void ServerListProxyHelper.IRegisterForSerialization.RegisterForSerialization() => RegisterForSerialization(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); @@ -87,7 +85,11 @@ class Template partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnClear(); -} + private protected override void SerializeChangesCore(BatchStreamWriter writer) + {{ + _list.Serialize(writer); + base.SerializeChangesCore(writer); + }} "; private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) @@ -97,7 +99,7 @@ class Template var serverItemType = ServerName(itemType); cl = cl.AddBaseListTypes(SimpleBaseType( - ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IRegisterForSerialization")), SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) ); var code = ListProxyTemplate.Replace("ListTypeName", list.Name) diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 5a514a4eff..3c38c0331e 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; @@ -38,7 +39,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; - + string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; + string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; + string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); + string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); + string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name; + void GenerateClass(GClass cl) { var list = cl as GList; @@ -68,36 +74,13 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .WithBaseType(serverBase); string changesName = ChangesName(cl.Name); - var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + string changedFieldsTypeName = ChangedFieldsTypeName(cl); + string changedFieldsName = ChangedFieldsFieldName(cl); - if (list != null) - changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; - - var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; - var transport = ClassDeclaration(changesName) - .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) - .WithBaseType(changesBase) - .AddMembers(DeclareField(changeSetPoolType, "Pool", - EqualsValueClause( - ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") - ), - SyntaxKind.PublicKeyword, - SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) - .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); - - client = client - .AddMembers( - PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, - SyntaxKind.OverrideKeyword) - .WithExpressionBody( - ArrowExpressionClause(MemberAccess(changesName, "Pool"))) - .WithSemicolonToken(Semicolon())) - .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) - .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), - MemberAccess(BaseExpression(), "Changes")))) - .WithSemicolonToken(Semicolon())); + if (cl.Properties.Count > 0) + client = client + .AddMembers(DeclareField(changedFieldsTypeName, changedFieldsName)); + if (!cl.CustomCtor) { @@ -105,7 +88,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Semicolon()))); - client = client.AddMembers( + client = client.AddMembers( ConstructorDeclaration(cl.Name) .AddModifiers(SyntaxKind.InternalKeyword) .WithParameterList(ParameterList(SeparatedList(new[] @@ -141,20 +124,20 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("compositor")), - })))).WithBody(Block())); + })))).WithBody(Block(ParseStatement("Initialize();")))); } + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "Initialize") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + var changesVarName = "c"; var changesVar = IdentifierName(changesVarName); server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") - .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - - transport = transport.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ResetExtra") + MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); var applyMethodBody = Block( @@ -168,7 +151,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); - + var uninitializedObjectName = "dummy"; var serverStaticCtorBody = cl.Abstract ? Block() @@ -179,72 +162,51 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ParseStatement("InitializeFieldOffsets(dummy);") ); - var initializeFieldOffsetsBody = cl.ServerBase == null + var initializeFieldOffsetsBody = cl.Inherits == null ? Block() - : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); + : Block(ParseStatement($"Server{cl.Inherits}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); - var getPropertyBody = Block(); var serverGetPropertyBody = Block(); var serverGetFieldOffsetBody = Block(); var activatedBody = Block(ParseStatement("base.Activated();")); var deactivatedBody = Block(ParseStatement("base.Deactivated();")); + var serializeMethodBody = SerializeChangesPrologue(cl); + var deserializeMethodBody = DeserializeChangesPrologue(cl); - var defaultsMethodBody = Block(); + var defaultsMethodBody = Block(ParseStatement("InitializeDefaultsExtra();")); foreach (var prop in cl.Properties) { - var fieldName = "_" + prop.Name.WithLowerFirst(); - var fieldOffsetName = "s_OffsetOf" + fieldName; + var fieldName = PropertyBackingFieldName(prop); + var animatedFieldName = PropertyPendingAnimationFieldName(prop); + var fieldOffsetName = ServerPropertyOffsetFieldName(prop); var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); var isNullable = prop.Type.EndsWith("?"); - - - - - client = client - .AddMembers(DeclareField(prop.Type, fieldName)) - .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), - AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, - Block( - ParseStatement("var changed = false;"), - IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), - IdentifierName("value")), - Block( - ParseStatement("On" + prop.Name + "Changing();"), - ParseStatement("changed = true;"), - GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) - ), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), - ParseStatement($"if(changed) On" + prop.Name + "Changed();") - )) - )) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + bool isPassthrough = false; + if (prop.Animated) + client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName)); + client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); var animatedServer = prop.Animated; var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); - if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) - serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); - - - transport = transport - .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", - prop.Name, SyntaxKind.PublicKeyword)); + if (_manuals.TryGetValue(filteredPropertyType, out var manual)) + { + if (manual.Passthrough) + { + isPassthrough = true; + serverPropertyType = prop.Type; + } + if (manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + } + if (animatedServer) server = server.AddMembers( DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), @@ -301,7 +263,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("this")), - Argument(changesVar), + Argument(ParseExpression("c.Batch.CommitedAt")), Argument(MemberAccess(changesVar, prop.Name, "Animation")), Argument(IdentifierName(fieldOffsetName)) }))))) @@ -316,16 +278,18 @@ namespace Avalonia.SourceGenerator.CompositionGenerator resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); - + + serializeMethodBody = ApplySerializeField(serializeMethodBody,cl, prop, isObject, isPassthrough); + deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject); + if (animatedServer) { - startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); } - - getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); @@ -345,55 +309,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } } - if (cl is GBrush brush && !cl.Abstract) - { - var brushName = brush.Name.StripPrefix("Composition"); - /* - server = server.AddMembers( - MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) - )).WithSemicolonToken(Semicolon()) - ); - if (!brush.CustomUpdate) - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters(Parameter(Identifier("brush")) - .WithType(ParseTypeName("ICbBrush"))) - .AddBodyStatements( - ExpressionStatement( - InvocationExpression( - MemberAccess( - ParenthesizedExpression( - CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), - ArgumentList(SeparatedList(cl.Properties.Select(x => - { - if(x.Type.TrimEnd('?') == "ICompositionSurface") - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("BackendSurface"))) - ); - if (_brushes.Contains(x.Type)) - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("Brush"))) - ); - return Argument(IdentifierName(x.Name)); - })))) - ))); - -*/ - } - - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyCore") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters( - Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) - .WithBody(applyMethodBody)); - server = server.AddMembers(ConstructorDeclaration(serverName) .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) .WithBody(serverStaticCtorBody)); @@ -408,26 +323,32 @@ namespace Avalonia.SourceGenerator.CompositionGenerator $"protected override void Activated(){{}}")!).WithBody(activatedBody)) .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + if (cl.Properties.Count > 0) + server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") + !) + .WithBody(deserializeMethodBody)); client = client.AddMembers( - MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); - - transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") - .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) - .WithBody(resetBody.AddStatements( - ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), - ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); - + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) + .AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + if (cl.Properties.Count > 0) + client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) + .WithBody(serializeMethodBody)); + if (list != null) client = AppendListProxy(list, client); if (startAnimationBody.Statements.Count != 0) client = WithStartAnimation(client, startAnimationBody); - - client = WithGetProperty(client, getPropertyBody, false); - server = WithGetProperty(server, serverGetPropertyBody, true); + + server = WithGetPropertyForAnimation(server, serverGetPropertyBody); server = WithGetFieldOffset(server, serverGetFieldOffsetBody); - + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -441,57 +362,121 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } + SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport", + ChangedFieldsTypeName(cl) + ".generated.cs"); + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), cl.Name + ".generated.cs"); SaveTo(unit.AddMembers(serverNs.AddMembers(server)), "Server", "Server" + cl.Name + ".generated.cs"); - SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), - "Transport", cl.Name + "Changes.generated.cs"); + } + + private ClassDeclarationSyntax GenerateClientProperty(ClassDeclarationSyntax client, GClass cl, GProperty prop, + TypeSyntax propType, bool isObject, bool isNullable) + { + var fieldName = PropertyBackingFieldName(prop); + return client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(cl, prop, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty())) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); } - StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + EnumDeclarationSyntax GenerateChangedFieldsEnum(GClass cl) { - var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, - "Value"), - isObject - ? - ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) - : IdentifierName("value"))); - if (!prop.Animated) - return normalChangesAssignment; + var changedFieldsEnum = EnumDeclaration(Identifier(ChangedFieldsTypeName(cl))); + int count = 0; - var code = $@" -{{ - if(animation is CompositionAnimation a) - Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); - else + void AddValue(string name) + { + var value = 1ul << count; + changedFieldsEnum = changedFieldsEnum.AddMembers( + EnumMemberDeclaration(name) + .WithEqualsValue(EqualsValueClause(ParseExpression(value.ToString())))); + count++; + } + + foreach (var prop in cl.Properties) + { + AddValue(prop.Name); + + if (prop.Animated) + AddValue(prop.Name + "Animated"); + } + + var baseType = count <= 8 ? "byte" : count <= 16 ? "ushort" : count <= 32 ? "uint" : "ulong"; + return changedFieldsEnum.AddBaseListTypes(SimpleBaseType(ParseTypeName(baseType))) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("System.Flags"))))); + } + + StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) + { + var pendingAnimationField = PropertyPendingAnimationFieldName(prop); + + var code = @$" + // Update the backing value + {PropertyBackingFieldName(prop)} = value; + + // Register object for serialization in the next batch + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}; + RegisterForSerialization(); +"; + if (prop.Animated) + { + code += @$" + // Reset previous animation if any + {pendingAnimationField} = null; + {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + // Check for implicit animations + if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) {{ - var saved = Changes.{prop.Name}; - if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) - Changes.{prop.Name}.Value = value; + // Animation affects only current property + if(animation is CompositionAnimation a) + {{ + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + {pendingAnimationField} = a.CreateInstance(this.Server, value); + }} + // Animation is triggered by the current field, but does not necessary affects it + StartAnimationGroup(animation, ""{prop.Name}"", value); }} -}} - "; - - return IfStatement( - ParseExpression( - $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), - ParseStatement(code), - ElseClause(normalChangesAssignment) - ); + } + + return ParseStatement("{\n" + code + "\n}"); } - BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + BlockSyntax ApplyStartAnimation(BlockSyntax body, GClass cl, GProperty prop) { var code = $@" if (propertyName == ""{prop.Name}"") {{ -var current = {fieldName}; +var current = {PropertyBackingFieldName(prop)}; var server = animation.CreateInstance(this.Server, finalValue); -Changes.{prop.Name}.Animation = server; +{PropertyPendingAnimationFieldName(prop)} = server; +{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; +RegisterForSerialization(); return; }} "; @@ -522,15 +507,75 @@ return; return body; } + + private BlockSyntax SerializeChangesPrologue(GClass cl) + { + return Block( + ParseStatement("base.SerializeChangesCore(writer);"), + ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") + ); + } + + BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) + { + var changedFields = ChangedFieldsFieldName(cl); + var changedFieldsType = ChangedFieldsTypeName(cl); + + var code = ""; + if (prop.Animated) + { + code = $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); + else "; + } + + code += $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) + writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")}); +"; + return body.AddStatements(ParseStatement(code)); + } + + private BlockSyntax DeserializeChangesPrologue(GClass cl) + { + return Block(ParseStatement($@" +base.DeserializeChangesCore(reader, commitedAt); +DeserializeChangesExtra(reader); +var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); +")); + } + + BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) + { + var changedFieldsType = ChangedFieldsTypeName(cl); + var code = ""; + if (prop.Animated) + { + code = $@" + if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + {PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject(), {ServerPropertyOffsetFieldName(prop)}); + else "; + } + + var readValueCode = $"reader.Read{(isObject ? "Object" : "")}<{serverType}>()"; + code += $@" + if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) +"; + if (prop.Animated) + code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; + else code += $"{prop.Name} = {readValueCode};"; + return body.AddStatements(ParseStatement(code)); + } - ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) return cl; body = body.AddStatements( ParseStatement("return base.GetPropertyForAnimation(name);")); var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( - $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) .WithBody(body); return cl.AddMembers(method); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7a64b39575..9c82288c8e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -104,7 +104,7 @@ namespace Avalonia.X11 } if (options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 32705a2cc6..4b0350f40f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -188,7 +188,7 @@ namespace Avalonia.Win32 AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs new file mode 100644 index 0000000000..a1b55257e6 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks.Dataflow; +using Avalonia.Rendering.Composition.Transport; +using Xunit; + +namespace Avalonia.Base.UnitTests.Composition; + +public class BatchStreamTests +{ + [Fact] + public void BatchStreamCorrectlyWritesAndReadsData() + { + var data = new BatchStreamData(); + var memPool = new BatchStreamMemoryPool(100, _ => { }); + var objPool = new BatchStreamObjectPool(10, _ => { }); + + var guids = new List(); + var objects = new List(); + for (var c = 0; c < 453; c++) + { + guids.Add(Guid.NewGuid()); + objects.Add(new object()); + } + + using (var writer = new BatchStreamWriter(data, memPool, objPool)) + { + foreach(var guid in guids) + writer.Write(guid); + foreach (var obj in objects) + writer.WriteObject(obj); + } + + using (var reader = new BatchStreamReader(data, memPool, objPool)) + { + foreach (var guid in guids) + Assert.Equal(guid, reader.Read()); + foreach (var obj in objects) + Assert.Equal(obj, reader.ReadObject()); + } + + + + } +} \ No newline at end of file From 2d8be0dff68dc4036bc6e8b5c40894f0367e6271 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:51:40 +0300 Subject: [PATCH 048/481] Fixes --- .../Composition/Transport/BatchStreamArrayPool.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index d76a9c609e..97d05704af 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -51,11 +51,12 @@ internal abstract class BatchStreamPoolBase : IDisposable { var maximumUsage = _usageStatistics.Max(); var recentlyUsedPooledSlots = maximumUsage - _usage; - while (recentlyUsedPooledSlots < _pool.Count) + var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); + while (keepSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + _usageStatistics[_usageStatisticsSlot] = 0; } } @@ -137,7 +138,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) + public BatchStreamMemoryPool(int bufferSize = 1024, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } From dbbed2c70bcb7abff176edffa7a7b045a0dd914d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:14:13 +0300 Subject: [PATCH 049/481] Added Dispose for CompositionTarget --- .../Composition/CompositionObject.cs | 5 ++-- .../Server/ServerCompositionTarget.cs | 27 ++++++++++++++++++- .../Composition/Server/ServerCompositor.cs | 2 ++ .../Composition/Server/ServerObject.cs | 4 ++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index d561338a36..baf1bfcddf 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition public void Dispose() { - //Changes.Dispose = true; + RegisterForSerialization(); IsDisposed = true; } @@ -112,7 +112,8 @@ namespace Avalonia.Rendering.Composition private protected virtual void SerializeChangesCore(BatchStreamWriter writer) { - + if (Server is IDisposable) + writer.Write((byte)(IsDisposed ? 1 : 0)); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 7567eba534..9513fb58fa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -9,7 +9,7 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { - internal partial class ServerCompositionTarget + internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; private readonly Func _renderTargetFactory; @@ -23,6 +23,7 @@ namespace Avalonia.Rendering.Composition.Server private Size _layerSize; private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; + private bool _disposed; public ReadbackIndices Readback { get; } = new(); @@ -50,6 +51,12 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + if (_disposed) + { + Compositor.RemoveCompositionTarget(this); + return; + } + if (Root == null) return; _renderTarget ??= _renderTargetFactory(); @@ -69,6 +76,12 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { + // This is a hack to safely dispose layer created by some other render target + // because we can only dispose layers with the corresponding GPU context being + // active on the current thread + while (Compositor.LayersToDispose.Count > 0) + Compositor.LayersToDispose.Dequeue().Dispose(); + var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -130,5 +143,17 @@ namespace Avalonia.Rendering.Composition.Server { _redrawRequested = true; } + + public void Dispose() + { + _disposed = true; + if (_layer != null) + { + Compositor.LayersToDispose.Enqueue(_layer); + _layer = null; + } + _renderTarget?.Dispose(); + _renderTarget = null; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index f7de704b23..241be479ff 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; @@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; + public Queue LayersToDispose { get; } = new(); public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 16f57d9059..dde711c3b5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -107,7 +107,9 @@ namespace Avalonia.Rendering.Composition.Server protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - + if (this is IDisposable disp + && reader.Read() == 1) + disp.Dispose(); } public void DeserializeChanges(BatchStreamReader reader, Batch batch) From 7790a513065b0e91676ba675bea4587ff428d402 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:15:39 +0300 Subject: [PATCH 050/481] Fixed hittest filtering --- .../Rendering/Composition/CompositingRenderer.cs | 6 +++--- .../Composition/CompositionDrawListVisual.cs | 6 +++++- .../Rendering/Composition/CompositionTarget.cs | 13 ++++++++----- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 +++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 88c6948e48..b83f804a8f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -65,9 +65,9 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor QueueUpdate(); } - public IEnumerable HitTest(Point p, IVisual root, Func filter) + public IEnumerable HitTest(Point p, IVisual root, Func? filter) { - var res = _target.TryHitTest(p); + var res = _target.TryHitTest(p, filter); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -81,7 +81,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor } } - public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + public IVisual? HitTestFirst(Point p, IVisual root, Func? filter) { // TODO: Optimize return HitTest(p, root, filter).FirstOrDefault(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 069d888fbb..cc2b411822 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -1,7 +1,9 @@ +using System; using System.Numerics; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; @@ -39,10 +41,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Point pt) + internal override bool HitTest(Point pt, Func? filter) { if (DrawList == null) return false; + if (filter != null && !filter(Visual)) + return false; if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index c5cfaeacce..3243934932 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Numerics; using Avalonia.Collections.Pooled; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -18,13 +20,13 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Point point) + public PooledList? TryHitTest(Point point, Func? filter) { Server.Readback.NextRead(); if (Root == null) return null; var res = new PooledList(); - HitTestCore(Root, point, res); + HitTestCore(Root, point, res, filter); return res; } @@ -69,7 +71,8 @@ namespace Avalonia.Rendering.Composition return false; } - bool HitTestCore(CompositionVisual visual, Point point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result, + Func? filter) { //TODO: Check readback too if (visual.Visible == false) @@ -80,7 +83,7 @@ namespace Avalonia.Rendering.Composition { bool success = false; // Hit-test the current node - if (visual.HitTest(point)) + if (visual.HitTest(point, filter)) { result.Add(visual); success = true; @@ -91,7 +94,7 @@ namespace Avalonia.Rendering.Composition for (var c = cv.Children.Count - 1; c >= 0; c--) { var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result); + var hit = HitTestCore(ch, point, result, filter); if (hit) return true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 5bf5dcee74..3d6e3fdaeb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -1,4 +1,6 @@ +using System; using System.Numerics; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -33,6 +35,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Point point) => true; + internal virtual bool HitTest(Point point, Func? filter) => true; } } \ No newline at end of file From fcb7d254f9d0f147dd85c38200a3ebd896c65b3c Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:21 -0400 Subject: [PATCH 051/481] Add IColorPalette --- .../ColorPalette/IColorPalette.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs new file mode 100644 index 0000000000..7c6ebc3f6a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs @@ -0,0 +1,38 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Interface to define a color palette. + /// + public interface IColorPalette + { + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// + /// + /// Represents total columns in a table. + /// + int ColorCount { get; } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// + /// + /// Represents total rows in a table. + /// + int ShadeCount { get; } + + /// + /// Gets a color in the palette by index. + /// + /// The index of the color in the palette. + /// The index must be between zero and . + /// The index of the color shade in the palette. + /// The index must be between zero and . + /// The color at the specified index or an exception. + Color GetColor(int colorIndex, int shadeIndex); + } +} From fa5a47b4426d32a00b01233540e25342f2013646 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:44 -0400 Subject: [PATCH 052/481] Add initial ColorView.Properties --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 2 +- .../ColorView/ColorView.Properties.cs | 362 ++++++++++++++++++ .../ColorView/ColorView.cs | 21 + 3 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 587a89ee38..00d84f5dd3 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives /// Gets or sets the currently selected color in the RGB color model. /// /// - /// For control authors use instead to avoid loss + /// For control authors, use instead to avoid loss /// of precision and color drifting. /// public Color Color diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs new file mode 100644 index 0000000000..aa5dfb5fc4 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -0,0 +1,362 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public partial class ColorView + { + // SelectedColorModel ActiveColorModel? + // SelectedTab + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumComponentsProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumComponents), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumShapeProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumShape), + ColorSpectrumShape.Box); + + /// + /// Defines the property. + /// + public static readonly DirectProperty> CustomPaletteColorsProperty = + AvaloniaProperty.RegisterDirect>( + nameof(CustomPaletteColors), + o => o.CustomPaletteColors); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomPaletteColumnCountProperty = + AvaloniaProperty.Register( + nameof(CustomPaletteColumnCount), + 4); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomPaletteProperty = + AvaloniaProperty.Register( + nameof(CustomPalette), + null); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAlphaEnabled), + false); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorChannelTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorChannelTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPaletteVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPaletteVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPreviewVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPreviewVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSpectrumVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSpectrumVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHexInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsHexInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register( + nameof(MaxHue), + 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register( + nameof(MaxSaturation), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register( + nameof(MinHue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register( + nameof(MinSaturation), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register( + nameof(MinValue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + public ColorSpectrumComponents ColorSpectrumComponents + { + get => GetValue(ColorSpectrumComponentsProperty); + set => SetValue(ColorSpectrumComponentsProperty, value); + } + + /// + public ColorSpectrumShape ColorSpectrumShape + { + get => GetValue(ColorSpectrumShapeProperty); + set => SetValue(ColorSpectrumShapeProperty, value); + } + + /// + /// Gets the list of custom palette colors. + /// + public ObservableCollection CustomPaletteColors + { + get => _customPaletteColors; + } + + /// + /// Gets or sets the number of colors in each row (section) of the custom color palette. + /// Within a standard palette, rows are shades and columns are colors. + /// + public int CustomPaletteColumnCount + { + get => GetValue(CustomPaletteColumnCountProperty); + set => SetValue(CustomPaletteColumnCountProperty, value); + } + + /// + /// Gets or sets the custom color palette. + /// This will automatically set and + /// overwriting any existing values. + /// + public IColorPalette? CustomPalette + { + get => GetValue(CustomPaletteProperty); + set => SetValue(CustomPaletteProperty, value); + } + + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + public bool IsAlphaEnabled + { + get => GetValue(IsAlphaEnabledProperty); + set => SetValue(IsAlphaEnabledProperty, value); + } + + public bool IsAlphaSliderVisible + { + get => GetValue(IsAlphaSliderVisibleProperty); + set => SetValue(IsAlphaSliderVisibleProperty, value); + } + + public bool IsAlphaTextInputVisible + { + get => GetValue(IsAlphaTextInputVisibleProperty); + set => SetValue(IsAlphaTextInputVisibleProperty, value); + } + + public bool IsColorChannelTextInputVisible // TODO: Component + { + get => GetValue(IsColorChannelTextInputVisibleProperty); + set => SetValue(IsColorChannelTextInputVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color palette is visible. + /// + public bool IsColorPaletteVisible + { + get => GetValue(IsColorPaletteVisibleProperty); + set => SetValue(IsColorPaletteVisibleProperty, value); + } + + public bool IsColorPreviewVisible + { + get => GetValue(IsColorPreviewVisibleProperty); + set => SetValue(IsColorPreviewVisibleProperty, value); + } + + public bool IsColorSliderVisible + { + get => GetValue(IsColorSliderVisibleProperty); + set => SetValue(IsColorSliderVisibleProperty, value); + } + + public bool IsColorSpectrumVisible + { + get => GetValue(IsColorSpectrumVisibleProperty); + set => SetValue(IsColorSpectrumVisibleProperty, value); + } + + public bool IsHexInputVisible + { + get => GetValue(IsHexInputVisibleProperty); + set => SetValue(IsHexInputVisibleProperty, value); + } + + /// + public int MaxHue + { + get => GetValue(MaxHueProperty); + set => SetValue(MaxHueProperty, value); + } + + /// + public int MaxSaturation + { + get => GetValue(MaxSaturationProperty); + set => SetValue(MaxSaturationProperty, value); + } + + /// + public int MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// + public int MinHue + { + get => GetValue(MinHueProperty); + set => SetValue(MinHueProperty, value); + } + + /// + public int MinSaturation + { + get => GetValue(MinSaturationProperty); + set => SetValue(MinSaturationProperty, value); + } + + /// + public int MinValue + { + get => GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// + public bool ShowAccentColors + { + get => GetValue(ShowAccentColorsProperty); + set => SetValue(ShowAccentColorsProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs new file mode 100644 index 0000000000..3aff4614a5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Presents a color for user editing using a spectrum, palette and component sliders. + /// + public partial class ColorView : TemplatedControl + { + private ObservableCollection _customPaletteColors = new ObservableCollection(); + + + } +} From d3bad7bd1d5612901ff0f3f74080ca5349091228 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:55 -0400 Subject: [PATCH 053/481] Add ColorViewTab enum --- .../ColorView/ColorViewTab.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs new file mode 100644 index 0000000000..d8c7d5163c --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific tab (subview) within the . + /// + public enum ColorViewTab + { + /// + /// The components view with sliders and numeric input boxes. + /// + Components, + + /// + /// The color palette view with a grid of colors and shades. + /// + Palette, + + /// + /// The color spectrum view with a box/ring spectrum and sliders. + /// + Spectrum, + } +} From 77e36914d4c5bb49e12c8a79bb7d5de30a71f19c Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:06:58 -0400 Subject: [PATCH 054/481] Add FluentColorPalette --- .../ColorPalette/FluentColorPalette.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs new file mode 100644 index 0000000000..89400280a9 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -0,0 +1,142 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard Windows 10 color palette. + /// + public class FluentColorPalette : IColorPalette + { + // Values were taken from the Settings App, Personalization > Colors which match with + // https://docs.microsoft.com/en-us/windows/uwp/whats-new/windows-docs-december-2017 + // + // The default ordering and grouping of colors was undesirable so was modified. + // Colors were transposed: the colors in rows within the Settings app became columns here. + // This is because columns in an IColorPalette generally should contain different shades of + // the same color. In the settings app this concept is somewhat loosely reversed. + // The first 'column' ordering, after being transposed, was then reversed so 'red' colors + // were near to each other. + // + // This new ordering most closely follows the Windows standard while: + // + // 1. Keeping colors in a 'spectrum' order + // 2. Keeping like colors next to each both in rows and columns + // (which is unique for the windows palette). + // For example, similar red colors are next to each other in both + // rows within the same column and rows within the column next to it. + // This follows a 'snake-like' pattern as illustrated below. + // 3. A downside of this ordering is colors don't follow strict 'shades' + // as in other palettes. + // + // The colors will be displayed in the below pattern. + // This pattern follows a spectrum while keeping like-colors near to one + // another across both rows and columns. + // + // ┌Red───┐ ┌Blue──┐ ┌Gray──┐ + // │ │ │ │ │ | + // │ │ │ │ │ | + // Yellow └Violet┘ └Green─┘ Brown + + private static Color[,] colorChart = new Color[,] + { + { + // Ordering reversed for this section only + Color.FromArgb(255, 255, 67, 67), /* #FF4343 */ + Color.FromArgb(255, 209, 52, 56), /* #D13438 */ + Color.FromArgb(255, 239, 105, 80), /* #EF6950 */ + Color.FromArgb(255, 218, 59, 1), /* #DA3B01 */ + Color.FromArgb(255, 202, 80, 16), /* #CA5010 */ + Color.FromArgb(255, 247, 99, 12), /* #F7630C */ + Color.FromArgb(255, 255, 140, 0), /* #FF8C00 */ + Color.FromArgb(255, 255, 185, 0), /* #FFB900 */ + }, + { + Color.FromArgb(255, 231, 72, 86), /* #E74856 */ + Color.FromArgb(255, 232, 17, 35), /* #E81123 */ + Color.FromArgb(255, 234, 0, 94), /* #EA005E */ + Color.FromArgb(255, 195, 0, 82), /* #C30052 */ + Color.FromArgb(255, 227, 0, 140), /* #E3008C */ + Color.FromArgb(255, 191, 0, 119), /* #BF0077 */ + Color.FromArgb(255, 194, 57, 179), /* #C239B3 */ + Color.FromArgb(255, 154, 0, 137), /* #9A0089 */ + }, + { + Color.FromArgb(255, 0, 120, 215), /* #0078D7 */ + Color.FromArgb(255, 0, 99, 177), /* #0063B1 */ + Color.FromArgb(255, 142, 140, 216), /* #8E8CD8 */ + Color.FromArgb(255, 107, 105, 214), /* #6B69D6 */ + Color.FromArgb(255, 135, 100, 184), /* #8764B8 */ + Color.FromArgb(255, 116, 77, 169), /* #744DA9 */ + Color.FromArgb(255, 177, 70, 194), /* #B146C2 */ + Color.FromArgb(255, 136, 23, 152), /* #881798 */ + }, + { + Color.FromArgb(255, 0, 153, 188), /* #0099BC */ + Color.FromArgb(255, 45, 125, 154), /* #2D7D9A */ + Color.FromArgb(255, 0, 183, 195), /* #00B7C3 */ + Color.FromArgb(255, 3, 131, 135), /* #038387 */ + Color.FromArgb(255, 0, 178, 148), /* #00B294 */ + Color.FromArgb(255, 1, 133, 116), /* #018574 */ + Color.FromArgb(255, 0, 204, 106), /* #00CC6A */ + Color.FromArgb(255, 16, 137, 62), /* #10893E */ + }, + { + Color.FromArgb(255, 122, 117, 116), /* #7A7574 */ + Color.FromArgb(255, 93, 90, 80), /* #5D5A58 */ + Color.FromArgb(255, 104, 118, 138), /* #68768A */ + Color.FromArgb(255, 81, 92, 107), /* #515C6B */ + Color.FromArgb(255, 86, 124, 115), /* #567C73 */ + Color.FromArgb(255, 72, 104, 96), /* #486860 */ + Color.FromArgb(255, 73, 130, 5), /* #498205 */ + Color.FromArgb(255, 16, 124, 16), /* #107C10 */ + }, + { + Color.FromArgb(255, 118, 118, 118), /* #767676 */ + Color.FromArgb(255, 76, 74, 72), /* #4C4A48 */ + Color.FromArgb(255, 105, 121, 126), /* #69797E */ + Color.FromArgb(255, 74, 84, 89), /* #4A5459 */ + Color.FromArgb(255, 100, 124, 100), /* #647C64 */ + Color.FromArgb(255, 82, 94, 84), /* #525E54 */ + Color.FromArgb(255, 132, 117, 69), /* #847545 */ + Color.FromArgb(255, 126, 115, 95), /* #7E735F */ + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + public const int DefaultShadeIndex = 0; + + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// This has little meaning in this palette as colors are not strictly separated. + /// + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0)), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1))]; + } + } +} From 33637f651a3456f7791dbe01f3e16c79fd0725d6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:07:09 -0400 Subject: [PATCH 055/481] Fix ColorToHexConverter --- .../Converters/ColorToHexConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 9b09073d9d..8d5f2332be 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToString(); + string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); if (includeSymbol == false) { From 2835767b42ddfc23d40e64c34ea0719312ab8586 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:09:28 -0400 Subject: [PATCH 056/481] Add incomplete ColorView control template --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 9 +- .../ColorView/ColorView.Properties.cs | 4 +- .../ColorView/ColorView.cs | 86 ++++++ .../ColorView/ColorViewTab.cs | 6 +- .../Themes/Fluent/ColorView.xaml | 259 ++++++++++++++++++ .../Themes/Fluent/Fluent.xaml | 3 + 6 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index c0c83d6a35..47a407821c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,8 +13,11 @@ - - + + - diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index aa5dfb5fc4..eddbeaf112 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -292,7 +292,9 @@ namespace Avalonia.Controls set => SetValue(IsColorPreviewVisibleProperty, value); } - public bool IsColorSliderVisible + // IsColorComponentsVisible + + public bool IsColorSliderVisible // ColorSpectrumSlider { get => GetValue(IsColorSliderVisibleProperty); set => SetValue(IsColorSliderVisibleProperty, value); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 3aff4614a5..76e3cfe3e1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -14,8 +14,94 @@ namespace Avalonia.Controls /// public partial class ColorView : TemplatedControl { + /// + /// Event for when the selected color changes within the slider. + /// + public event EventHandler? ColorChanged; + + private bool disableUpdates = false; + private ObservableCollection _customPaletteColors = new ObservableCollection(); + /// + /// Initializes a new instance of the class. + /// + public ColorView() : base() + { + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + this.CustomPalette = new FluentColorPalette(); + + base.OnApplyTemplate(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + disableUpdates = true; + + HsvColor = Color.ToHsv(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue(), + change.GetNewValue())); + + disableUpdates = false; + } + else if (change.Property == HsvColorProperty) + { + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + + disableUpdates = false; + } + else if (change.Property == CustomPaletteProperty) + { + IColorPalette? palette = CustomPalette; + + // Any custom palette change must be automatically synced with the + // bound properties controlling the palette grid + if (palette != null) + { + CustomPaletteColumnCount = palette.ColorCount; + CustomPaletteColors.Clear(); + + for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) + { + for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++) + { + CustomPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex)); + } + } + } + } + + base.OnPropertyChanged(change); + } + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs index d8c7d5163c..677cdb8674 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -6,17 +6,17 @@ public enum ColorViewTab { /// - /// The components view with sliders and numeric input boxes. + /// The components subview with sliders and numeric input boxes. /// Components, /// - /// The color palette view with a grid of colors and shades. + /// The color palette subview with a grid of selectable colors. /// Palette, /// - /// The color spectrum view with a box/ring spectrum and sliders. + /// The color spectrum subview with a box/ring spectrum and sliders. /// Spectrum, } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml new file mode 100644 index 0000000000..9f4324594d --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c25d79727f..c55766e07c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -25,4 +25,7 @@ + + + From 197ab047a510c58b30a32216fb96c8cfd2239c3a Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 27 May 2022 22:45:56 -0400 Subject: [PATCH 057/481] Add braces --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..7c8c2f882f 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -291,7 +291,9 @@ namespace Avalonia.Controls.Primitives // Existing code kinda expect to see a NameScope even if it's empty if (nameScope == null) + { nameScope = new NameScope(); + } var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); From 5ed084123540fae249cc1881a21e67dad42ad0c1 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 27 May 2022 22:46:12 -0400 Subject: [PATCH 058/481] Fix index clamping --- .../ColorPalette/FluentColorPalette.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs index 89400280a9..b6f9a244b1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -135,8 +135,8 @@ namespace Avalonia.Controls public Color GetColor(int colorIndex, int shadeIndex) { return colorChart[ - MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0)), - MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1))]; + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; } } } From e5b18d0b9c263b65b9121a25e19b6103e21d1d07 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 01:25:59 -0400 Subject: [PATCH 059/481] Further work on ColorView --- .../Converters/ContrastBrushConverter.cs | 84 +++++++++ .../Themes/Fluent/ColorView.xaml | 165 +++++++++++++++++- 2 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs new file mode 100644 index 0000000000..574f23dfae --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Gets a , either black or white, depending on the luminance of the supplied color. + /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. + /// + public class ContrastBrushConverter : IValueConverter + { + private ToColorConverter toColorConverter = new ToColorConverter(); + + /// + /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. + /// + public byte AlphaThreshold { get; set; } = 128; + + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + Color comparisonColor; + Color? defaultColor = null; + + // Get the changing color to compare against + var convertedValue = toColorConverter.Convert(value, targetType, parameter, culture); + if (convertedValue is Color valueColor) + { + comparisonColor = valueColor; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // Get the default color when transparency is high + var convertedParameter = toColorConverter.Convert(parameter, targetType, parameter, culture); + if (convertedParameter is Color parameterColor) + { + defaultColor = parameterColor; + } + + if (comparisonColor.A < AlphaThreshold && + defaultColor.HasValue) + { + // If the transparency is less than the threshold, just use the default brush + // This can commonly be something like the TextControlForeground brush + return new SolidColorBrush(defaultColor.Value); + } + else + { + // Chose a white/black brush based on contrast to the base color + if (ColorHelper.GetRelativeLuminance(comparisonColor) <= 0.5) + { + // Dark color, return light for contrast + return new SolidColorBrush(Colors.White); + } + else + { + // Bright color, return dark for contrast + return new SolidColorBrush(Colors.Black); + } + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 9f4324594d..5d3642a3b1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -3,12 +3,15 @@ xmlns:converters="using:Avalonia.Controls.Converters" xmlns:primitives="using:Avalonia.Controls.Primitives" xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" + xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib" x:CompileBindings="False"> + + + + + + + @@ -195,11 +230,11 @@ - + + + + + + + + + + + + + + + + From 53a08f126382f9a7eae4d5eda331efd1a7080bd5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:04:26 -0400 Subject: [PATCH 060/481] Add new EnumToBooleanConverter --- .../Converters/EnumToBooleanConverter.cs | 57 +++++++++++++++++++ .../Converters/EnumValueEqualsConverter.cs | 12 +++- 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs diff --git a/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs b/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000000..ba1c4cab3e --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter to convert an enum value to bool by comparing to the given parameter. + /// Both value and parameter must be of the same enum type. + /// + /// + /// This converter is useful to enable binding of radio buttons with a selected enum value. + /// + public class EnumToBooleanConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value == null && + parameter == null) + { + return true; + } + else if (value == null || + parameter == null) + { + return false; + } + else + { + return value!.Equals(parameter); + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? parameter : BindingOperations.DoNothing; + } + else + { + return BindingOperations.DoNothing; + } + } + } +} diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs index 1a33a82ca4..abd0fe1dfd 100644 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs @@ -10,7 +10,11 @@ namespace Avalonia.Controls.Converters public class EnumValueEqualsConverter : IValueConverter { /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { // Note: Unlike string comparisons, null/empty is not supported // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal @@ -46,7 +50,11 @@ namespace Avalonia.Controls.Converters } /// - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new System.NotImplementedException(); } From 13b82a0d1d4f70eba999e6e29ae075aed3b6cd1b Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:08:16 -0400 Subject: [PATCH 061/481] Implement ColorModel switching in ColorView --- .../ColorSlider/ColorSlider.cs | 14 ++ .../ColorView/ColorView.Properties.cs | 20 +- .../Themes/Fluent/ColorView.xaml | 172 +++++++++++++----- 3 files changed, 163 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 3c38c6ed1b..78a796e93a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -332,6 +332,20 @@ namespace Avalonia.Controls.Primitives disableUpdates = false; } + else if (change.Property == ColorModelProperty) + { + disableUpdates = true; + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + UpdatePseudoClasses(); + + disableUpdates = false; + } else if (change.Property == HsvColorProperty) { disableUpdates = true; diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index eddbeaf112..c56811b8a3 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -7,7 +7,6 @@ namespace Avalonia.Controls { public partial class ColorView { - // SelectedColorModel ActiveColorModel? // SelectedTab /// @@ -19,6 +18,14 @@ namespace Avalonia.Controls Colors.White, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + /// /// Defines the property. /// @@ -203,6 +210,17 @@ namespace Avalonia.Controls set => SetValue(ColorProperty, value); } + /// + /// + /// This property is only applicable to the components tab. + /// The spectrum tab must always be in HSV and the palette tab is pre-defined colors. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + /// public ColorSpectrumComponents ColorSpectrumComponents { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 5d3642a3b1..b037d14956 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -1,5 +1,6 @@  + @@ -156,6 +158,11 @@ + + + + + + + + + + + + + + + + + + + + Grid.Column="2"> @@ -260,12 +333,17 @@ BorderThickness="1,1,0,1" CornerRadius="4,0,0,4" VerticalAlignment="Center"> - + + + + - + + + + - + + + + Date: Sat, 28 May 2022 12:08:28 -0400 Subject: [PATCH 062/481] Improve comments --- .../Converters/AccentColorConverter.cs | 4 +++- .../Converters/ContrastBrushConverter.cs | 3 +++ .../Converters/ThirdComponentConverter.cs | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs index 4d05222e31..2c8e09adc9 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters { /// /// Creates an accent color for a given base color value and step parameter. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class AccentColorConverter : IValueConverter { /// diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs index 574f23dfae..8b66b1a4e5 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -10,6 +10,9 @@ namespace Avalonia.Controls.Primitives.Converters /// Gets a , either black or white, depending on the luminance of the supplied color. /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ContrastBrushConverter : IValueConverter { private ToColorConverter toColorConverter = new ToColorConverter(); diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs index 220a993f99..11e33c74f0 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters /// /// Gets the third corresponding with a given /// that represents the other two components. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ThirdComponentConverter : IValueConverter { /// From 4a267b69d61dbe8d784a3c7046c3af652632b9c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:15:04 -0400 Subject: [PATCH 063/481] Set MaxHue of ColorSlider to the same value as ColorSpectrum --- .../ColorSlider/ColorSlider.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 78a796e93a..4c7df0fda7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -20,7 +20,15 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler? ColorChanged; - private const double MaxHue = 359.99999999999999999; // 17 decimal places + /// + /// Defines the maximum hue component value + /// (other components are always 0..100 or 0.255). + /// + /// + /// This should match the default property. + /// + private const double MaxHue = 359; + private bool disableUpdates = false; /// From 36f85325bc7242487f20eeb3fb2fe3142aa0872a Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:28:23 -0400 Subject: [PATCH 064/481] Remove EnumValueEqualsConverter replaced by EnumToBooleanConverter --- .../Themes/Default/ColorSpectrum.xaml | 15 ++--- .../Themes/Fluent/ColorSpectrum.xaml | 15 ++--- .../Converters/EnumValueEqualsConverter.cs | 62 ------------------- 3 files changed, 16 insertions(+), 76 deletions(-) delete mode 100644 src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 78e6da8aa3..891e040e9f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,10 +1,11 @@  - + @@ -24,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index ac8e2a9c06..779f228b97 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -1,10 +1,11 @@  - + @@ -24,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs deleted file mode 100644 index abd0fe1dfd..0000000000 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Converters -{ - /// - /// Converter that checks if an enum value is equal to the given parameter enum value. - /// - public class EnumValueEqualsConverter : IValueConverter - { - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - // Note: Unlike string comparisons, null/empty is not supported - // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal - if (value != null && - parameter != null) - { - Type type = value.GetType(); - - if (type.IsEnum) - { - var valueStr = value?.ToString(); - var paramStr = parameter?.ToString(); - - if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - /* - // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below - // This is a little more type safe - if (type.IsEnum && - Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && - Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) - { - return valueEnum == paramEnum; - } - */ - } - - return false; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - throw new System.NotImplementedException(); - } - } -} From 043b56c7da836dc6e8976c91957ce82626ca5b26 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 13:46:40 -0400 Subject: [PATCH 065/481] Remove IsAutoUpdatingEnabled property from ColorSlider This was unused and is an unnecessary complexity --- .../ColorSlider/ColorSlider.Properties.cs | 21 ------------- .../ColorSlider/ColorSlider.cs | 31 ++++++------------- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index 31bd296288..b1be794794 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -48,14 +48,6 @@ namespace Avalonia.Controls.Primitives nameof(IsAlphaMaxForced), true); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAutoUpdatingEnabledProperty = - AvaloniaProperty.Register( - nameof(IsAutoUpdatingEnabled), - true); - /// /// Defines the property. /// @@ -119,19 +111,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } - /// - /// Gets or sets a value indicating whether automatic background and foreground updates will be - /// calculated when the set color changes. - /// - /// - /// This can be disabled for performance reasons when working with multiple sliders. - /// - public bool IsAutoUpdatingEnabled - { - get => GetValue(IsAutoUpdatingEnabledProperty); - set => SetValue(IsAutoUpdatingEnabledProperty, value); - } - /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 4c7df0fda7..641516c474 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -327,13 +327,10 @@ namespace Avalonia.Controls.Primitives HsvColor = Color.ToHsv(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), change.GetNewValue())); @@ -344,12 +341,8 @@ namespace Avalonia.Controls.Primitives { disableUpdates = true; - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); disableUpdates = false; @@ -360,13 +353,10 @@ namespace Avalonia.Controls.Primitives Color = HsvColor.ToRgb(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); @@ -375,10 +365,7 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == BoundsProperty) { - if (IsAutoUpdatingEnabled) - { - UpdateBackground(); - } + UpdateBackground(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || From 8ef440ceda8237a746c409a3eaa9e230b820cdb9 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 14:24:00 -0400 Subject: [PATCH 066/481] Finish ColorView design/style --- .../Themes/Fluent/ColorView.xaml | 215 +++++++++++------- 1 file changed, 136 insertions(+), 79 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index b037d14956..09feeb38f9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -14,35 +14,125 @@ + 48 + + + M3 2C3.27614 2 3.5 2.22386 3.5 2.5V5.5C3.5 5.77614 3.72386 6 4 6H16C16.2761 6 16.5 5.77614 + 16.5 5.5V2.5C16.5 2.22386 16.7239 2 17 2C17.2761 2 17.5 2.22386 17.5 2.5V5.5C17.5 6.32843 + 16.8284 7 16 7H15.809L12.2236 14.1708C12.0615 14.4951 11.7914 14.7431 11.4695 + 14.8802C11.4905 15.0808 11.5 15.2891 11.5 15.5C11.5 16.0818 11.4278 16.6623 11.2268 + 17.1165C11.019 17.5862 10.6266 18 10 18C9.37343 18 8.98105 17.5862 8.77323 17.1165C8.57222 + 16.6623 8.5 16.0818 8.5 15.5C8.5 15.2891 8.50952 15.0808 8.53051 14.8802C8.20863 14.7431 + 7.93851 14.4951 7.77639 14.1708L4.19098 7H4C3.17157 7 2.5 6.32843 2.5 5.5V2.5C2.5 2.22386 + 2.72386 2 3 2ZM9.11803 14H10.882C11.0714 14 11.2445 13.893 11.3292 13.7236L14.691 + 7H5.30902L8.67082 13.7236C8.75552 13.893 8.92865 14 9.11803 14ZM9.52346 15C9.50787 15.1549 + 9.5 15.3225 9.5 15.5C9.5 16.0228 9.56841 16.4423 9.6877 16.7119C9.8002 16.9661 9.90782 17 + 10 17C10.0922 17 10.1998 16.9661 10.3123 16.7119C10.4316 16.4423 10.5 16.0228 10.5 + 15.5C10.5 15.3225 10.4921 15.1549 10.4765 15H9.52346Z + + + + M9.75003 6.5C10.1642 6.5 10.5 6.16421 10.5 5.75C10.5 5.33579 10.1642 5 9.75003 5C9.33582 + 5 9.00003 5.33579 9.00003 5.75C9.00003 6.16421 9.33582 6.5 9.75003 6.5ZM12.75 7.5C13.1642 + 7.5 13.5 7.16421 13.5 6.75C13.5 6.33579 13.1642 6 12.75 6C12.3358 6 12 6.33579 12 6.75C12 + 7.16421 12.3358 7.5 12.75 7.5ZM15.25 9C15.25 9.41421 14.9142 9.75 14.5 9.75C14.0858 9.75 + 13.75 9.41421 13.75 9C13.75 8.58579 14.0858 8.25 14.5 8.25C14.9142 8.25 15.25 8.58579 + 15.25 9ZM14.5 12.75C14.9142 12.75 15.25 12.4142 15.25 12C15.25 11.5858 14.9142 11.25 14.5 + 11.25C14.0858 11.25 13.75 11.5858 13.75 12C13.75 12.4142 14.0858 12.75 14.5 12.75ZM13.25 + 14C13.25 14.4142 12.9142 14.75 12.5 14.75C12.0858 14.75 11.75 14.4142 11.75 14C11.75 + 13.5858 12.0858 13.25 12.5 13.25C12.9142 13.25 13.25 13.5858 13.25 14ZM13.6972 + 2.99169C10.9426 1.57663 8.1432 1.7124 5.77007 3.16636C4.55909 3.9083 3.25331 5.46925 + 2.51605 7.05899C2.14542 7.85816 1.89915 8.70492 1.90238 9.49318C1.90566 10.2941 2.16983 + 11.0587 2.84039 11.6053C3.45058 12.1026 3.98165 12.353 4.49574 12.3784C5.01375 12.404 + 5.41804 12.1942 5.73429 12.0076C5.80382 11.9666 5.86891 11.927 5.93113 11.8892C6.17332 + 11.7421 6.37205 11.6214 6.62049 11.5426C6.90191 11.4534 7.2582 11.4205 7.77579 + 11.5787C7.96661 11.637 8.08161 11.7235 8.16212 11.8229C8.24792 11.9289 8.31662 12.0774 + 8.36788 12.2886C8.41955 12.5016 8.44767 12.7527 8.46868 13.0491C8.47651 13.1594 8.48379 + 13.2855 8.49142 13.4176C8.50252 13.6098 8.51437 13.8149 8.52974 14.0037C8.58435 14.6744 + 8.69971 15.4401 9.10362 16.1357C9.51764 16.8488 10.2047 17.439 11.307 17.8158C12.9093 + 18.3636 14.3731 17.9191 15.5126 17.0169C16.6391 16.125 17.4691 14.7761 17.8842 + 13.4272C19.1991 9.15377 17.6728 5.03394 13.6972 2.99169ZM6.29249 4.01905C8.35686 2.75426 + 10.7844 2.61959 13.2403 3.88119C16.7473 5.68275 18.1135 9.28161 16.9284 13.1332C16.5624 + 14.3227 15.8338 15.4871 14.8919 16.2329C13.963 16.9684 12.8486 17.286 11.6305 + 16.8696C10.7269 16.5607 10.2467 16.1129 9.96842 15.6336C9.68001 15.1369 9.57799 14.5556 + 9.52644 13.9225C9.51101 13.733 9.50132 13.5621 9.49147 13.3884C9.48399 13.2564 9.47642 + 13.1229 9.46618 12.9783C9.44424 12.669 9.41175 12.3499 9.33968 12.0529C9.26719 11.7541 + 9.14902 11.4527 8.93935 11.1937C8.72439 10.9282 8.43532 10.7346 8.06801 10.6223C7.36648 + 10.408 6.80266 10.4359 6.31839 10.5893C5.94331 10.7082 5.62016 10.9061 5.37179 + 11.0582C5.31992 11.0899 5.2713 11.1197 5.22616 11.1463C4.94094 11.3146 4.75357 11.39 + 4.54514 11.3796C4.33279 11.3691 4.00262 11.2625 3.47218 10.8301C3.0866 10.5158 2.90473 + 10.0668 2.90237 9.48908C2.89995 8.89865 3.08843 8.20165 3.42324 7.47971C4.09686 6.0272 + 5.28471 4.63649 6.29249 4.01905Z + + + + M14.95 5C14.7184 3.85888 13.7095 3 12.5 3C11.2905 3 10.2816 3.85888 10.05 5H2.5C2.22386 + 5 2 5.22386 2 5.5C2 5.77614 2.22386 6 2.5 6H10.05C10.2816 7.14112 11.2905 8 12.5 8C13.7297 + 8 14.752 7.11217 14.961 5.94254C14.9575 5.96177 14.9539 5.98093 14.95 6H17.5C17.7761 6 18 + 5.77614 18 5.5C18 5.22386 17.7761 5 17.5 5H14.95ZM12.5 7C11.6716 7 11 6.32843 11 5.5C11 + 4.67157 11.6716 4 12.5 4C13.3284 4 14 4.67157 14 5.5C14 6.32843 13.3284 7 12.5 7ZM9.94999 + 14C9.71836 12.8589 8.70948 12 7.5 12C6.29052 12 5.28164 12.8589 5.05001 14H2.5C2.22386 + 14 2 14.2239 2 14.5C2 14.7761 2.22386 15 2.5 15H5.05001C5.28164 16.1411 6.29052 17 7.5 + 17C8.70948 17 9.71836 16.1411 9.94999 15H17.5C17.7761 15 18 14.7761 18 14.5C18 14.2239 + 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 + 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z + + - - + + + + RowDefinitions="Auto,*" + Margin="12"> - - + + + + SelectedItem="{Binding Color, ElementName=ColorSpectrum}" + UseLayoutRounding="False" + Margin="12"> @@ -281,17 +343,11 @@ @@ -497,7 +553,8 @@ + HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + Margin="12,0,12,12" /> From 12a8ecb9239f315767448bc5d434b7652b7987f8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 15:17:40 -0400 Subject: [PATCH 067/481] Implement the hex input TextBox in ColorView --- .../ColorView/ColorView.cs | 85 +++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 19 ++--- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 76e3cfe3e1..0363d9c182 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; @@ -12,6 +11,7 @@ namespace Avalonia.Controls /// /// Presents a color for user editing using a spectrum, palette and component sliders. /// + [TemplatePart("PART_HexTextBox", typeof(TextBox))] public partial class ColorView : TemplatedControl { /// @@ -19,20 +19,70 @@ namespace Avalonia.Controls /// public event EventHandler? ColorChanged; - private bool disableUpdates = false; + // XAML template parts + private TextBox? _hexTextBox; private ObservableCollection _customPaletteColors = new ObservableCollection(); + private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); + private bool disableUpdates = false; /// /// Initializes a new instance of the class. /// public ColorView() : base() { + this.CustomPalette = new FluentColorPalette(); + } + + /// + /// Gets the value of the hex TextBox and sets it as the current . + /// If invalid, the TextBox hex text will revert back to the last valid color. + /// + private void GetColorFromHexTextBox() + { + if (_hexTextBox != null) + { + var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture); + + if (convertedColor is Color color) + { + Color = color; + } + + // Re-apply the hex value + // This ensure the hex color value is always valid and formatted correctly + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } + } + + /// + /// Sets the current to the hex TextBox. + /// + private void SetColorToHexTextBox() + { + if (_hexTextBox != null) + { + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - this.CustomPalette = new FluentColorPalette(); + if (_hexTextBox != null) + { + _hexTextBox.KeyDown -= HexTextBox_KeyDown; + _hexTextBox.LostFocus -= HexTextBox_LostFocus; + } + + _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + SetColorToHexTextBox(); + + if (_hexTextBox != null) + { + _hexTextBox.KeyDown += HexTextBox_KeyDown; + _hexTextBox.LostFocus += HexTextBox_LostFocus; + } base.OnApplyTemplate(e); } @@ -52,6 +102,7 @@ namespace Avalonia.Controls disableUpdates = true; HsvColor = Color.ToHsv(); + SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), @@ -64,6 +115,7 @@ namespace Avalonia.Controls disableUpdates = true; Color = HsvColor.ToRgb(); + SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), @@ -103,5 +155,26 @@ namespace Avalonia.Controls { ColorChanged?.Invoke(this, e); } + + /// + /// Event handler for when a key is pressed within the Hex RGB value TextBox. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_KeyDown(object? sender, Input.KeyEventArgs e) + { + if (e.Key == Input.Key.Enter) + { + GetColorFromHexTextBox(); + } + } + + /// + /// Event handler for when the Hex RGB value TextBox looses focus. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_LostFocus(object? sender, Interactivity.RoutedEventArgs e) + { + GetColorFromHexTextBox(); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 09feeb38f9..9800b1b3a5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -152,8 +152,7 @@ Grid.Row="0" Grid.RowSpan="2" Components="{TemplateBinding ColorSpectrumComponents}" - Color="{TemplateBinding Color}" - HsvColor="{TemplateBinding HsvColor}" + HsvColor="{Binding $parent[ColorView].HsvColor}" MinHue="{TemplateBinding MinHue}" MaxHue="{TemplateBinding MaxHue}" MinSaturation="{TemplateBinding MinSaturation}" @@ -188,7 +187,7 @@ @@ -369,12 +368,12 @@ HorizontalAlignment="Center" VerticalAlignment="Center" /> - + @@ -419,7 +418,7 @@ Orientation="Horizontal" ColorComponent="Component1" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -462,7 +461,7 @@ Orientation="Horizontal" ColorComponent="Component2" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -505,7 +504,7 @@ Orientation="Horizontal" ColorComponent="Component3" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -543,7 +542,7 @@ Orientation="Horizontal" ColorComponent="Alpha" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -553,7 +552,7 @@ From a5b3bb9cb6c80b9d83c38ea372c9a64cc663acb6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 15:58:48 -0400 Subject: [PATCH 068/481] Implement ColorSlider component value rounding --- .../ColorSlider/ColorSlider.Properties.cs | 21 +++++++++++ .../ColorSlider/ColorSlider.cs | 35 +++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 12 +++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index b1be794794..e2a34a7f90 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -48,6 +48,14 @@ namespace Avalonia.Controls.Primitives nameof(IsAlphaMaxForced), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsRoundingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsRoundingEnabled), + false); + /// /// Defines the property. /// @@ -111,6 +119,19 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } + /// + /// Gets or sets a value indicating whether rounding of color component values is enabled. + /// + /// + /// This is applicable for the HSV color model only. The struct uses double + /// values while the struct uses byte. Only double types need rounding. + /// + public bool IsRoundingEnabled + { + get => GetValue(IsRoundingEnabledProperty); + set => SetValue(IsRoundingEnabledProperty, value); + } + /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 641516c474..957e5e7b77 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -115,6 +115,21 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Rounds the component values of the given . + /// This is useful for user-display and to ensure a color matches user selection exactly. + /// + /// The to round component values for. + /// A new with rounded component values. + private HsvColor RoundComponentValues(HsvColor hsvColor) + { + return new HsvColor( + Math.Round(hsvColor.A, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.H, 0, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.S, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.V, 2, MidpointRounding.AwayFromZero)); + } + /// /// Updates the slider property values by applying the current color. /// @@ -130,6 +145,11 @@ namespace Avalonia.Controls.Primitives if (ColorModel == ColorModel.Hsva) { + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + // Note: Components converted into a usable range for the user switch (component) { @@ -222,7 +242,7 @@ namespace Avalonia.Controls.Primitives } } - return (hsvColor.ToRgb(), hsvColor); + rgbColor = hsvColor.ToRgb(); } else { @@ -244,8 +264,15 @@ namespace Avalonia.Controls.Primitives break; } - return (rgbColor, rgbColor.ToHsv()); + hsvColor = rgbColor.ToHsv(); } + + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + + return (rgbColor, hsvColor); } /// @@ -363,6 +390,10 @@ namespace Avalonia.Controls.Primitives disableUpdates = false; } + else if (change.Property == IsRoundingEnabledProperty) + { + SetColorToSliderValues(); + } else if (change.Property == BoundsProperty) { UpdateBackground(); diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 9800b1b3a5..1edc2768a5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -416,6 +416,9 @@ Grid.Column="2" Grid.Row="2" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component1" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -459,6 +462,9 @@ Grid.Column="2" Grid.Row="3" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component2" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -502,6 +508,9 @@ Grid.Column="2" Grid.Row="4" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component3" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -540,6 +549,9 @@ Grid.Column="2" Grid.Row="5" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Alpha" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" From 0824c874fc7c5bbc8b98f758e47f5e6194f7b595 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 16:42:58 -0400 Subject: [PATCH 069/481] Add initial ColorPicker control --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 5 +- .../ColorPicker/ColorPicker.cs | 15 ++++ .../Themes/Fluent/ColorPicker.xaml | 71 +++++++++++++++++++ .../Themes/Fluent/Fluent.xaml | 1 + 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 47a407821c..eca52e796a 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,7 +13,10 @@ - + + diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs new file mode 100644 index 0000000000..cb84c77d20 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Controls +{ + /// + /// + /// + public class ColorPicker : ColorView + { + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml new file mode 100644 index 0000000000..3fa3ab5ead --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c55766e07c..03e8238a44 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -26,6 +26,7 @@ + From 5cd0ea68053394bb85f7db96c65e7203448dd1cf Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 30 May 2022 20:39:17 -0400 Subject: [PATCH 070/481] Complete initial ColorPicker --- .../ColorPicker/ColorPicker.cs | 3 +- .../Themes/Fluent/ColorPicker.xaml | 31 +++++++++++++------ .../Themes/Fluent/ColorView.xaml | 16 +++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index cb84c77d20..140a24d6a1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -7,7 +7,8 @@ using System.Threading.Tasks; namespace Avalonia.Controls { /// - /// + /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. + /// Editing is available when the drop down flyout is opened; otherwise, only the preview color is shown. /// public class ColorPicker : ColorView { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 3fa3ab5ead..a4f52f111c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -2,10 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Avalonia.Controls" xmlns:converters="using:Avalonia.Controls.Converters" - x:CompileBindings="True"> + x:CompileBindings="False"> + - + @@ -150,7 +150,8 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="0,0,12,0" /> + Margin="0,0,12,0" + IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}"/> - + @@ -254,7 +255,7 @@ - + @@ -590,7 +591,7 @@ From adfac7b69dbcdc79a676b04aaebe1db4d4d63b40 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 01:22:39 -0400 Subject: [PATCH 074/481] Implement tab selection validation and automatic width --- .../ColorView/ColorView.cs | 93 ++++++++++++++++++- .../Themes/Fluent/ColorView.xaml | 23 +++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 0363d9c182..96de734cc7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; +using Avalonia.Threading; namespace Avalonia.Controls { @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// Presents a color for user editing using a spectrum, palette and component sliders. /// [TemplatePart("PART_HexTextBox", typeof(TextBox))] + [TemplatePart("PART_TabControl", typeof(TabControl))] public partial class ColorView : TemplatedControl { /// @@ -20,7 +22,8 @@ namespace Avalonia.Controls public event EventHandler? ColorChanged; // XAML template parts - private TextBox? _hexTextBox; + private TextBox? _hexTextBox; + private TabControl? _tabControl; private ObservableCollection _customPaletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); @@ -31,7 +34,7 @@ namespace Avalonia.Controls /// public ColorView() : base() { - this.CustomPalette = new FluentColorPalette(); + CustomPalette = new FluentColorPalette(); } /// @@ -66,6 +69,77 @@ namespace Avalonia.Controls } } + /// + /// Validates the selected subview/tab taking into account the visibility of each subview/tab + /// as well as the current selection. + /// + private void ValidateSelectedTab() + { + if (_tabControl != null && + _tabControl.Items != null) + { + // Determine if any item is visible + bool isAnyItemVisible = false; + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + isAnyItemVisible = true; + break; + } + } + + if (isAnyItemVisible) + { + object? selectedItem = null; + + if (_tabControl.SelectedItem == null && + _tabControl.ItemCount > 0) + { + // As a failsafe, forcefully select the first item + foreach (var item in _tabControl.Items) + { + selectedItem = item; + break; + } + } + else + { + selectedItem = _tabControl.SelectedItem; + } + + if (selectedItem is Control selectedControl && + selectedControl.IsVisible == false) + { + // Select the first visible item instead + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + selectedItem = item; + break; + } + } + } + + _tabControl.SelectedItem = selectedItem; + _tabControl.IsVisible = true; + } + else + { + // Special case when all items are hidden + // If TabControl ever properly supports no selected item / + // all items hidden this can be removed + _tabControl.SelectedItem = null; + _tabControl.IsVisible = false; + } + } + + return; + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -76,6 +150,8 @@ namespace Avalonia.Controls } _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + _tabControl = e.NameScope.Find("PART_TabControl"); + SetColorToHexTextBox(); if (_hexTextBox != null) @@ -85,6 +161,7 @@ namespace Avalonia.Controls } base.OnApplyTemplate(e); + ValidateSelectedTab(); } /// @@ -143,6 +220,18 @@ namespace Avalonia.Controls } } } + else if (change.Property == IsColorComponentsVisibleProperty || + change.Property == IsColorPaletteVisibleProperty || + change.Property == IsColorSpectrumVisibleProperty) + { + // When the property changed notification is received here the visibility + // of individual tab items has not yet been updated though the bindings. + // Therefore, the validation is delayed until after bindings update. + Dispatcher.UIThread.Post(() => + { + ValidateSelectedTab(); + }, DispatcherPriority.Background); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index afc8682a66..420e3b2ae9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -109,22 +109,29 @@ BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" />--> - 0,0,0,0 - - + @@ -181,8 +188,7 @@ - + @@ -257,8 +263,7 @@ - + From c02439aaaf5c1984f8aae75c777173c775c89257 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 May 2022 17:10:09 +0200 Subject: [PATCH 075/481] Refactored most of Style into StyleBase. Ready for `ControlTheme` class, which is a style without a selector. --- src/Avalonia.Base/Styling/Style.cs | 148 ++------------------- src/Avalonia.Base/Styling/StyleBase.cs | 137 +++++++++++++++++++ src/Avalonia.Base/Styling/StyleChildren.cs | 10 +- 3 files changed, 151 insertions(+), 144 deletions(-) create mode 100644 src/Avalonia.Base/Styling/StyleBase.cs diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 000e588bad..c85c85fe21 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -1,23 +1,12 @@ using System; -using System.Collections.Generic; -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Metadata; namespace Avalonia.Styling { /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, IResourceProvider + public class Style : StyleBase { - private IResourceHost? _owner; - private StyleChildren? _children; - private IResourceDictionary? _resources; - private List? _setters; - private List? _animations; - private StyleCache? _childCache; - /// /// Initializes a new instance of the class. /// @@ -34,114 +23,11 @@ namespace Avalonia.Styling Selector = selector(null); } - /// - /// Gets the children of the style. - /// - public IList Children => _children ??= new(this); - - /// - /// Gets the or Application that hosts the style. - /// - public IResourceHost? Owner - { - get => _owner; - private set - { - if (_owner != value) - { - _owner = value; - OwnerChanged?.Invoke(this, EventArgs.Empty); - } - } - } - - /// - /// Gets the parent style if this style is hosted in a collection. - /// - public Style? Parent { get; private set; } - - /// - /// Gets or sets a dictionary of style resources. - /// - public IResourceDictionary Resources - { - get => _resources ?? (Resources = new ResourceDictionary()); - set - { - value = value ?? throw new ArgumentNullException(nameof(value)); - - var hadResources = _resources?.HasResources ?? false; - - _resources = value; - - if (Owner is object) - { - _resources.AddOwner(Owner); - - if (hadResources || _resources.HasResources) - { - Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } - } - } - } - /// /// Gets or sets the style's selector. /// public Selector? Selector { get; set; } - /// - /// Gets the style's setters. - /// - public IList Setters => _setters ??= new List(); - - /// - /// Gets the style's animations. - /// - public IList Animations => _animations ??= new List(); - - bool IResourceNode.HasResources => _resources?.Count > 0; - IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); - - public event EventHandler? OwnerChanged; - - public void Add(ISetter setter) => Setters.Add(setter); - public void Add(IStyle style) => Children.Add(style); - - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - var match = Selector is object ? Selector.Match(target, Parent) : - target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - - if (match.IsMatch && (_setters is object || _animations is object)) - { - var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); - target.StyleApplied(instance); - instance.Start(); - } - - var result = match.Result; - - if (_children is not null) - { - _childCache ??= new StyleCache(); - var childResult = _childCache.TryAttach(_children, target, host); - if (childResult > result) - result = childResult; - } - - return result; - } - - public bool TryGetResource(object key, out object? result) - { - result = null; - return _resources?.TryGetResource(key, out result) ?? false; - } - /// /// Returns a string representation of the style. /// @@ -158,33 +44,17 @@ namespace Avalonia.Styling } } - void IResourceProvider.AddOwner(IResourceHost owner) - { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner != null) - { - throw new InvalidOperationException("The Style already has a parent."); - } - - Owner = owner; - _resources?.AddOwner(owner); - } - - void IResourceProvider.RemoveOwner(IResourceHost owner) + protected override SelectorMatch Matches(IStyleable target, IStyleHost? host) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner == owner) - { - Owner = null; - _resources?.RemoveOwner(owner); - } + return Selector?.Match(target, Parent) ?? + (target == host ? + SelectorMatch.AlwaysThisInstance : + SelectorMatch.NeverThisInstance); } - internal void SetParent(Style? parent) + internal override void SetParent(StyleBase? parent) { - if (parent?.Selector is not null) + if (parent is Style parentStyle && parentStyle.Selector is not null) { if (Selector is null) throw new InvalidOperationException("Child styles must have a selector."); @@ -192,7 +62,7 @@ namespace Avalonia.Styling throw new InvalidOperationException("Child styles must have a nesting selector."); } - Parent = parent; + base.SetParent(parent); } } } diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs new file mode 100644 index 0000000000..0fc57da728 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling +{ + /// + /// Base class for and . + /// + public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider + { + private IResourceHost? _owner; + private StyleChildren? _children; + private IResourceDictionary? _resources; + private List? _setters; + private List? _animations; + private StyleCache? _childCache; + + public IList Children => _children ??= new(this); + + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + public IStyle? Parent { get; private set; } + + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + value = value ?? throw new ArgumentNullException(nameof(value)); + + var hadResources = _resources?.HasResources ?? false; + + _resources = value; + + if (Owner is object) + { + _resources.AddOwner(Owner); + + if (hadResources || _resources.HasResources) + { + Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + } + } + + public IList Setters => _setters ??= new List(); + public IList Animations => _animations ??= new List(); + + bool IResourceNode.HasResources => _resources?.Count > 0; + IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); + + public void Add(ISetter setter) => Setters.Add(setter); + public void Add(IStyle style) => Children.Add(style); + + public event EventHandler? OwnerChanged; + + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + var result = SelectorMatchResult.NeverThisType; + + if (_setters?.Count > 0 || _animations?.Count > 0) + { + var match = Matches(target, host); + + if (match.IsMatch) + { + var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); + target.StyleApplied(instance); + instance.Start(); + } + + result = match.Result; + } + + if (_children is not null) + { + _childCache ??= new StyleCache(); + var childResult = _childCache.TryAttach(_children, target, host); + if (childResult > result) + result = childResult; + } + + return result; + } + + public bool TryGetResource(object key, out object? result) + { + result = null; + return _resources?.TryGetResource(key, out result) ?? false; + } + + protected abstract SelectorMatch Matches(IStyleable target, IStyleHost? host); + + internal virtual void SetParent(StyleBase? parent) => Parent = parent; + + void IResourceProvider.AddOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + Owner = owner; + _resources?.AddOwner(owner); + } + + void IResourceProvider.RemoveOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner == owner) + { + Owner = null; + _resources?.RemoveOwner(owner); + } + } + } +} diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs index 5f8635f155..42b0a331ee 100644 --- a/src/Avalonia.Base/Styling/StyleChildren.cs +++ b/src/Avalonia.Base/Styling/StyleChildren.cs @@ -5,20 +5,20 @@ namespace Avalonia.Styling { internal class StyleChildren : Collection { - private readonly Style _owner; + private readonly StyleBase _owner; - public StyleChildren(Style owner) => _owner = owner; + public StyleChildren(StyleBase owner) => _owner = owner; protected override void InsertItem(int index, IStyle item) { - (item as Style)?.SetParent(_owner); + (item as StyleBase)?.SetParent(_owner); base.InsertItem(index, item); } protected override void RemoveItem(int index) { var item = Items[index]; - (item as Style)?.SetParent(null); + (item as StyleBase)?.SetParent(null); if (_owner.Owner is IResourceHost host) (item as IResourceProvider)?.RemoveOwner(host); base.RemoveItem(index); @@ -26,7 +26,7 @@ namespace Avalonia.Styling protected override void SetItem(int index, IStyle item) { - (item as Style)?.SetParent(_owner); + (item as StyleBase)?.SetParent(_owner); base.SetItem(index, item); if (_owner.Owner is IResourceHost host) (item as IResourceProvider)?.AddOwner(host); From 088d8cfc5c147da723e7641cf77c8dc67646e786 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 13:21:52 +0200 Subject: [PATCH 076/481] Initial implementation of control themes. --- src/Avalonia.Base/Styling/ControlTheme.cs | 27 ++++ src/Avalonia.Base/Styling/IStyle.cs | 2 +- src/Avalonia.Base/Styling/IThemed.cs | 13 ++ src/Avalonia.Base/Styling/NestingSelector.cs | 4 +- src/Avalonia.Base/Styling/Style.cs | 8 +- src/Avalonia.Base/Styling/StyleBase.cs | 20 ++- src/Avalonia.Base/Styling/StyleCache.cs | 2 +- src/Avalonia.Base/Styling/Styler.cs | 14 +++ src/Avalonia.Base/Styling/Styles.cs | 2 +- .../Primitives/TemplatedControl.cs | 25 +++- src/Avalonia.Themes.Default/SimpleTheme.cs | 2 +- src/Avalonia.Themes.Fluent/FluentTheme.cs | 2 +- .../Styling/StyleInclude.cs | 2 +- .../TemplatedControlTests_Theming.cs | 119 ++++++++++++++++++ 14 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Base/Styling/ControlTheme.cs create mode 100644 src/Avalonia.Base/Styling/IThemed.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs new file mode 100644 index 0000000000..54fc972c31 --- /dev/null +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -0,0 +1,27 @@ +using System; + +namespace Avalonia.Styling +{ + /// + /// Defines a switchable theme for a control. + /// + public class ControlTheme : StyleBase + { + /// + /// Gets or sets the type for which this control theme is intended. + /// + public Type? TargetType { get; set; } + + internal override bool HasSelector => TargetType is not null; + + internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) + { + if (TargetType is null) + throw new InvalidOperationException("ControlTheme has no TargetType."); + + return control.StyleKey == TargetType ? + SelectorMatch.AlwaysThisType : + SelectorMatch.NeverThisType; + } + } +} diff --git a/src/Avalonia.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs index e9faf82c07..417739fb28 100644 --- a/src/Avalonia.Base/Styling/IStyle.cs +++ b/src/Avalonia.Base/Styling/IStyle.cs @@ -23,6 +23,6 @@ namespace Avalonia.Styling /// /// A describing how the style matches the control. /// - SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); + SelectorMatchResult TryAttach(IStyleable target, object? host); } } diff --git a/src/Avalonia.Base/Styling/IThemed.cs b/src/Avalonia.Base/Styling/IThemed.cs new file mode 100644 index 0000000000..32ae515bcb --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemed.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Styling +{ + /// + /// Represents a themed element. + /// + public interface IThemed + { + /// + /// Gets the theme style for the element. + /// + public ControlTheme? Theme { get; } + } +} diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 481a937867..6d31f7cb18 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -15,9 +15,9 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { - if (parent is Style s && s.Selector is Selector selector) + if (parent is StyleBase s && s.HasSelector) { - return selector.Match(control, (parent as Style)?.Parent, subscribe); + return s.Match(control, null, subscribe); } throw new InvalidOperationException( diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index c85c85fe21..ca20ff2b4b 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -28,6 +28,8 @@ namespace Avalonia.Styling /// public Selector? Selector { get; set; } + internal override bool HasSelector => Selector is not null; + /// /// Returns a string representation of the style. /// @@ -44,10 +46,10 @@ namespace Avalonia.Styling } } - protected override SelectorMatch Matches(IStyleable target, IStyleHost? host) + internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) { - return Selector?.Match(target, Parent) ?? - (target == host ? + return Selector?.Match(control, Parent, subscribe) ?? + (control == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance); } diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index 0fc57da728..b6bfec62bd 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -64,12 +64,14 @@ namespace Avalonia.Styling bool IResourceNode.HasResources => _resources?.Count > 0; IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); + internal abstract bool HasSelector { get; } + public void Add(ISetter setter) => Setters.Add(setter); public void Add(IStyle style) => Children.Add(style); public event EventHandler? OwnerChanged; - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) + public SelectorMatchResult TryAttach(IStyleable target, object? host) { target = target ?? throw new ArgumentNullException(nameof(target)); @@ -77,7 +79,7 @@ namespace Avalonia.Styling if (_setters?.Count > 0 || _animations?.Count > 0) { - var match = Matches(target, host); + var match = Match(target, host, subscribe: true); if (match.IsMatch) { @@ -106,7 +108,19 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - protected abstract SelectorMatch Matches(IStyleable target, IStyleHost? host); + /// + /// Evaluates the style's selector against the specified target element. + /// + /// The control. + /// The element that hosts the style. + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an immediate result. + /// + /// + /// A describing how the style matches the control. + /// + internal abstract SelectorMatch Match(IStyleable control, object? host, bool subscribe); internal virtual void SetParent(StyleBase? parent) => Parent = parent; diff --git a/src/Avalonia.Base/Styling/StyleCache.cs b/src/Avalonia.Base/Styling/StyleCache.cs index 3285476880..81196f6a27 100644 --- a/src/Avalonia.Base/Styling/StyleCache.cs +++ b/src/Avalonia.Base/Styling/StyleCache.cs @@ -12,7 +12,7 @@ namespace Avalonia.Styling /// internal class StyleCache : Dictionary?> { - public SelectorMatchResult TryAttach(IList styles, IStyleable target, IStyleHost? host) + public SelectorMatchResult TryAttach(IList styles, IStyleable target, object? host) { if (TryGetValue(target.StyleKey, out var cached)) { diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index 74cf77ea40..b9359b3329 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -10,6 +10,20 @@ namespace Avalonia.Styling { target = target ?? throw new ArgumentNullException(nameof(target)); + // If the control has a themed templated parent then first apply the styles from + // the templated parent theme. + if (target.TemplatedParent is IThemed themedTemplatedParent) + { + themedTemplatedParent.Theme?.TryAttach(target, themedTemplatedParent); + } + + // If the control itself is themed, then next apply the control theme. + if (target is IThemed themed) + { + themed.Theme?.TryAttach(target, target); + } + + // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) { ApplyStyles(target, styleHost); diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 7c0bc4ad7f..4c011f1b0d 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -109,7 +109,7 @@ namespace Avalonia.Styling set => _styles[index] = value; } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) + public SelectorMatchResult TryAttach(IStyleable target, object? host) { _cache ??= new StyleCache(); return _cache.TryAttach(this, target, host); diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..e1f42b6eb0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives /// /// A lookless control whose visual appearance is defined by its . /// - public class TemplatedControl : Control, ITemplatedControl + public class TemplatedControl : Control, IThemed, ITemplatedControl { /// /// Defines the property. @@ -86,6 +86,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TemplateProperty = AvaloniaProperty.Register(nameof(Template)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ThemeProperty = + AvaloniaProperty.Register(nameof(Theme)); + /// /// Defines the IsTemplateFocusTarget attached property. /// @@ -228,6 +234,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(TemplateProperty, value); } } + /// + /// Gets or sets the theme to be applied to the control. + /// + public ControlTheme? Theme + { + get { return GetValue(ThemeProperty); } + set { SetValue(ThemeProperty, value); } + } + /// /// Gets the value of the IsTemplateFocusTargetProperty attached property on a control. /// @@ -365,6 +380,14 @@ namespace Avalonia.Controls.Primitives { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThemeProperty) + InvalidateStyles(); + } + /// /// Called when the control's template is applied. /// diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs index 6929660757..d7939a68c1 100644 --- a/src/Avalonia.Themes.Default/SimpleTheme.cs +++ b/src/Avalonia.Themes.Default/SimpleTheme.cs @@ -103,7 +103,7 @@ namespace Avalonia.Themes.Default void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); public bool TryGetResource(object key, out object? value) { diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index f6b47a5466..befe669029 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -164,7 +164,7 @@ namespace Avalonia.Themes.Fluent } } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); public bool TryGetResource(object key, out object? value) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index fa4a27fc50..109e85f1a4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -82,7 +82,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); public bool TryGetResource(object key, out object? value) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs new file mode 100644 index 0000000000..b24adfe7ab --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs @@ -0,0 +1,119 @@ +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +#nullable enable + +namespace Avalonia.Controls.UnitTests.Primitives +{ + public class TemplatedControlTests_Theming + { + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Template); + + var root = CreateRoot(target); + + Assert.NotNull(target.Template); + var border = Assert.IsType(target.VisualChild); + + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + [Fact] + public void Theme_Is_Detached_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(target.Template); + + target.Theme = null; + Assert.Null(target.Template); + } + + [Fact] + public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new ThemedControl(); + var root = CreateRoot(target); + + Assert.Null(target.Template); + + target.Theme = CreateTheme(); + Assert.Null(target.Template); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.NotNull(target.Template); + Assert.Equal(border.Background, Brushes.Red); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl + { + Theme = CreateTheme(), + }; + } + + private static ControlTheme CreateTheme() + { + var template = new FuncControlTemplate((o, n) => + new Border { Name = "PART_Border" }); + + return new ControlTheme + { + TargetType = typeof(ThemedControl), + Setters = + { + new Setter(ThemedControl.TemplateProperty, template), + }, + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Green), + } + }, + } + }; + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(child); + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + + private class ThemedControl : TemplatedControl + { + public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); + } + } +} From dee353bb9640278ab2364b1d9b4624d5cbe7a215 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 15:24:15 +0200 Subject: [PATCH 077/481] Support ControlTheme in XAML compiler. --- .../AvaloniaXamlIlCompiler.cs | 1 + .../AvaloniaXamlIlControlThemeTransformer.cs | 39 ++++++++++ .../AvaloniaXamlIlSetterTransformer.cs | 75 +++++++++++++------ .../Xaml/ControlThemeTests.cs | 53 +++++++++++++ .../Xaml/TestTemplatedControl.cs | 8 ++ 5 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 1ca7be67a7..20e035f8ff 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -48,6 +48,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlBindingPathParser(), + new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlPropertyPathTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs new file mode 100644 index 0000000000..1338dc7248 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs @@ -0,0 +1,39 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlControlThemeTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme")) + return node; + + // Check if we've already transformed this node. + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode) + return node; + + var targetTypeNode = on.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType") ?? + throw new XamlParseException("ControlTheme must have a TargetType.", node); + + IXamlType targetType; + + if (targetTypeNode.Values[0] is XamlTypeExtensionNode extension) + targetType = extension.Value.GetClrType(); + else if (targetTypeNode.Values[0] is XamlAstTextNode text) + targetType = TypeReferenceResolver.ResolveType(context, text.Text, false, text, true).GetClrType(); + else + throw new XamlParseException("Could not determine TargetType for ControlTheme.", targetTypeNode); + + return new AvaloniaXamlIlTargetTypeMetadataNode(on, + new XamlAstClrTypeReference(targetTypeNode, targetType, false), + AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index e816265422..06e34a85a2 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,19 +1,14 @@ -using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Data.Core; -using XamlX; using XamlX.Ast; using XamlX.Emit; using XamlX.IL; using XamlX.Transform; -using XamlX.Transform.Transformers; using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { using XamlParseException = XamlX.XamlParseException; - using XamlLoadException = XamlX.XamlLoadException; class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer { public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) @@ -22,21 +17,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) return node; - var parent = context.ParentNodes().OfType() - .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); - - if (parent == null) - throw new XamlParseException( - "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); - var selectorProperty = parent.Children.OfType() - .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); - if (selectorProperty == null) - throw new XamlParseException( - "Can not find parent Style Selector", node); - var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; - if (selector?.TargetType == null) - throw new XamlParseException( - "Can not resolve parent Style Selector type", node); + var targetTypeNode = context.ParentNodes() + .OfType() + .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ?? + throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node); IXamlType propType = null; var property = @on.Children.OfType() @@ -50,7 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + new XamlAstClrTypeReference(targetTypeNode, targetTypeNode.TargetType.GetClrType(), false), property.Values[0]); property.Values = new List {avaloniaPropertyNode}; propType = avaloniaPropertyNode.AvaloniaPropertyType; } @@ -84,6 +68,55 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } + private (IXamlLineInfo, IXamlType) GetTargetType(AstTransformationContext context, IXamlAstNode node) + { + foreach (var n in context.ParentNodes()) + { + if (n is XamlAstObjectNode parent) + { + switch (parent.Type.GetClrType().FullName) + { + case "Avalonia.Styling.Style": + var selectorProperty = parent.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); + if (selectorProperty == null) + throw new XamlParseException("Can not find parent Style Selector.", node); + var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; + if (selector?.TargetType != null) + return (selector, selector.TargetType); + throw new XamlParseException( + "Can not resolve parent Style Selector type", node); + + case "Avalonia.Styling.ControlTheme": + var targetTypeProperty = parent.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType"); + if (targetTypeProperty == null) + throw new XamlParseException("ControlTemplate has no TargetType.", parent); + break; + } + } + } + + throw new XamlParseException("'Setter' is only valid inside a 'Style' or 'ControlTheme'.", node); + //var parent = context.ParentNodes().OfType() + // .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style" || + // p.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme"); + + //if (parent == null) + // throw new XamlParseException( + // "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); + //var selectorProperty = parent.Children.OfType() + // .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector" || + // p.Property.GetClrProperty().Name == "TargetType"); + //if (selectorProperty == null) + // throw new XamlParseException( + // "Can not find parent Style Selector or ControlTemplate TargetType", node); + //var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; + //if (selector?.TargetType == null) + // throw new XamlParseException( + // "Can not resolve parent Style Selector type", node); + } + class SetterValueProperty : XamlAstClrProperty { public SetterValueProperty(IXamlLineInfo line, IXamlType setterType, IXamlType targetType, diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs new file mode 100644 index 0000000000..05083537cd --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ControlThemeTests : XamlTestBase + { + [Fact] + public void ControlTheme_Can_Be_StaticResource() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = $@" + + + {ControlThemeXaml} + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = Assert.IsType(window.Content); + + window.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.NotNull(button.Template); + + var child = Assert.Single(button.GetVisualChildren()); + var border = Assert.IsType(child); + + Assert.Equal(Brushes.Red, border.Background); + } + } + + private const string ControlThemeXaml = @" + + + + + + + +"; + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs new file mode 100644 index 0000000000..0c862bb66a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class TestTemplatedControl : TemplatedControl + { + } +} From a6dc6b1c887c8a5139be7bf1abba1315c26af0d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 21:45:45 +0200 Subject: [PATCH 078/481] Prevent ControlTheme as a nested style. --- src/Avalonia.Base/Styling/ControlTheme.cs | 16 +++++++++++ .../Styling/ControlThemeTests.cs | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 54fc972c31..9dcbd7d2c4 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -7,6 +7,17 @@ namespace Avalonia.Styling /// public class ControlTheme : StyleBase { + /// + /// Initializes a new instance of the class. + /// + public ControlTheme() { } + + /// + /// Initializes a new instance of the class. + /// + /// The value for . + public ControlTheme(Type targetType) => TargetType = targetType; + /// /// Gets or sets the type for which this control theme is intended. /// @@ -23,5 +34,10 @@ namespace Avalonia.Styling SelectorMatch.AlwaysThisType : SelectorMatch.NeverThisType; } + + internal override void SetParent(StyleBase? parent) + { + throw new InvalidOperationException("ControlThemes cannot be added as a nested style."); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs new file mode 100644 index 0000000000..93a0e6c2fd --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Controls; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Base.UnitTests.Styling +{ + public class ControlThemeTests + { + [Fact] + public void ControlTheme_Cannot_Be_Added_To_Style_Children() + { + var target = new ControlTheme(typeof(Button)); + var style = new Style(); + + Assert.Throws(() => style.Children.Add(target)); + } + + [Fact] + public void ControlTheme_Cannot_Be_Added_To_ControlTheme_Children() + { + var target = new ControlTheme(typeof(Button)); + var other = new ControlTheme(typeof(CheckBox)); + + Assert.Throws(() => other.Children.Add(target)); + } + } +} From fc3c036b02afce41d8faca7e4c1e8219fb0c4ceb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 22:48:34 +0200 Subject: [PATCH 079/481] Move Theme to StyledElement. The WPF equivalent (`Style`) is in `FrameworkElement` so it would make sense. Will also make stuff a lot easier and removes the need for an `IThemed` interface. --- src/Avalonia.Base/StyledElement.cs | 25 +++++++++++++++++-- src/Avalonia.Base/Styling/IStyleable.cs | 7 ++++-- src/Avalonia.Base/Styling/IThemed.cs | 13 ---------- src/Avalonia.Base/Styling/Styler.cs | 23 ++++------------- .../Primitives/TemplatedControl.cs | 25 +------------------ .../AvaloniaPropertyConverterTest.cs | 5 ++++ 6 files changed, 39 insertions(+), 59 deletions(-) delete mode 100644 src/Avalonia.Base/Styling/IThemed.cs diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index f98d2cdbcc..4ead2470d7 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -12,8 +12,6 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Styling; -#nullable enable - namespace Avalonia { /// @@ -55,6 +53,12 @@ namespace Avalonia nameof(TemplatedParent), o => o.TemplatedParent, (o ,v) => o.TemplatedParent = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThemeProperty = + AvaloniaProperty.Register(nameof(Theme)); private int _initCount; private string? _name; @@ -230,6 +234,15 @@ namespace Avalonia internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); } + /// + /// Gets or sets the theme to be applied to the element. + /// + public ControlTheme? Theme + { + get { return GetValue(ThemeProperty); } + set { SetValue(ThemeProperty, value); } + } + /// /// Gets the styled element's logical children. /// @@ -590,6 +603,14 @@ namespace Avalonia { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThemeProperty) + InvalidateStyles(); + } + private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) { if (o is StyledElement element) diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 5bc972e7ab..61fcbdf850 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Metadata; -#nullable enable - namespace Avalonia.Styling { /// @@ -28,6 +26,11 @@ namespace Avalonia.Styling /// ITemplatedControl? TemplatedParent { get; } + /// + /// Gets the theme to be applied to the control. + /// + public ControlTheme? Theme { get; } + /// /// Notifies the element that a style has been applied. /// diff --git a/src/Avalonia.Base/Styling/IThemed.cs b/src/Avalonia.Base/Styling/IThemed.cs deleted file mode 100644 index 32ae515bcb..0000000000 --- a/src/Avalonia.Base/Styling/IThemed.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Avalonia.Styling -{ - /// - /// Represents a themed element. - /// - public interface IThemed - { - /// - /// Gets the theme style for the element. - /// - public ControlTheme? Theme { get; } - } -} diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index b9359b3329..c9ea123bdc 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -1,33 +1,24 @@ using System; -#nullable enable - namespace Avalonia.Styling { public class Styler : IStyler { public void ApplyStyles(IStyleable target) { - target = target ?? throw new ArgumentNullException(nameof(target)); + _ = target ?? throw new ArgumentNullException(nameof(target)); // If the control has a themed templated parent then first apply the styles from // the templated parent theme. - if (target.TemplatedParent is IThemed themedTemplatedParent) - { - themedTemplatedParent.Theme?.TryAttach(target, themedTemplatedParent); - } + if (target.TemplatedParent is IStyleable styleableParent) + styleableParent.Theme?.TryAttach(target, styleableParent); - // If the control itself is themed, then next apply the control theme. - if (target is IThemed themed) - { - themed.Theme?.TryAttach(target, target); - } + // Next apply the control theme. + target.Theme?.TryAttach(target, target); // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) - { ApplyStyles(target, styleHost); - } } private void ApplyStyles(IStyleable target, IStyleHost host) @@ -35,14 +26,10 @@ namespace Avalonia.Styling var parent = host.StylingParent; if (parent != null) - { ApplyStyles(target, parent); - } if (host.IsStylesInitialized) - { host.Styles.TryAttach(target, host); - } } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index e1f42b6eb0..db029d38c0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives /// /// A lookless control whose visual appearance is defined by its . /// - public class TemplatedControl : Control, IThemed, ITemplatedControl + public class TemplatedControl : Control, ITemplatedControl { /// /// Defines the property. @@ -86,12 +86,6 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TemplateProperty = AvaloniaProperty.Register(nameof(Template)); - /// - /// Defines the property. - /// - public static readonly StyledProperty ThemeProperty = - AvaloniaProperty.Register(nameof(Theme)); - /// /// Defines the IsTemplateFocusTarget attached property. /// @@ -234,15 +228,6 @@ namespace Avalonia.Controls.Primitives set { SetValue(TemplateProperty, value); } } - /// - /// Gets or sets the theme to be applied to the control. - /// - public ControlTheme? Theme - { - get { return GetValue(ThemeProperty); } - set { SetValue(ThemeProperty, value); } - } - /// /// Gets the value of the IsTemplateFocusTargetProperty attached property on a control. /// @@ -380,14 +365,6 @@ namespace Avalonia.Controls.Primitives { } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == ThemeProperty) - InvalidateStyles(); - } - /// /// Called when the control's template is applied. /// diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index 33bf72014c..ca59fe8480 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -137,6 +137,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } + public ControlTheme Theme + { + get { throw new NotImplementedException(); } + } + public void DetachStyles() { throw new NotImplementedException(); From 8c61f25188afe50b0785150e2e1fbf605c4652c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2022 09:23:17 +0200 Subject: [PATCH 080/481] Promote theme to LocalValue if applied from style. --- src/Avalonia.Base/StyledElement.cs | 25 ++- .../TemplatedControlTests_Theming.cs | 146 ++++++++++++------ .../Xaml/ControlThemeTests.cs | 36 +++++ 3 files changed, 157 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 4ead2470d7..75c4b94174 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -71,6 +71,7 @@ namespace Avalonia private List? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; + private bool _hasPromotedTheme; /// /// Initializes static members of the class. @@ -239,8 +240,8 @@ namespace Avalonia /// public ControlTheme? Theme { - get { return GetValue(ThemeProperty); } - set { SetValue(ThemeProperty, value); } + get => GetValue(ThemeProperty); + set => SetValue(ThemeProperty, value); } /// @@ -315,6 +316,7 @@ namespace Avalonia /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; + /// public virtual void BeginInit() { @@ -354,10 +356,15 @@ namespace Avalonia } finally { + _styled = true; EndBatchUpdate(); } - _styled = true; + if (_hasPromotedTheme) + { + _hasPromotedTheme = false; + ClearValue(ThemeProperty); + } } return _styled; @@ -608,7 +615,19 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { + // Changing the theme detaches all styles, meaning that if the theme property was + // set via a style, it will get cleared! To work around this, if the value was + // applied at less than local value priority then promote the value to local value + // priority until styling is re-applied. + if (change.Priority > BindingPriority.LocalValue) + { + Theme = change.GetNewValue(); + _hasPromotedTheme = true; + } + InvalidateStyles(); + } } private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs index b24adfe7ab..74d75ff056 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs @@ -13,63 +13,122 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class TemplatedControlTests_Theming { - [Fact] - public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + public class InlineTheme { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); - Assert.Null(target.Template); + Assert.Null(target.Template); - var root = CreateRoot(target); + var root = CreateRoot(target); + Assert.NotNull(target.Template); - Assert.NotNull(target.Template); - var border = Assert.IsType(target.VisualChild); - - Assert.Equal(border.Background, Brushes.Red); + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); - target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); - } + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } - [Fact] - public void Theme_Is_Detached_When_Theme_Property_Cleared() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - var root = CreateRoot(target); + [Fact] + public void Theme_Is_Detached_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); - Assert.NotNull(target.Template); + Assert.NotNull(target.Template); - target.Theme = null; - Assert.Null(target.Template); - } + target.Theme = null; + Assert.Null(target.Template); + } - [Fact] - public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = new ThemedControl(); - var root = CreateRoot(target); + [Fact] + public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new ThemedControl(); + var root = CreateRoot(target); + + Assert.Null(target.Template); - Assert.Null(target.Template); + target.Theme = CreateTheme(); + Assert.Null(target.Template); - target.Theme = CreateTheme(); - Assert.Null(target.Template); + root.LayoutManager.ExecuteLayoutPass(); - root.LayoutManager.ExecuteLayoutPass(); + var border = Assert.IsType(target.VisualChild); + Assert.NotNull(target.Template); + Assert.Equal(border.Background, Brushes.Red); + } - var border = Assert.IsType(target.VisualChild); - Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); + private static ThemedControl CreateTarget() + { + return new ThemedControl + { + Theme = CreateTheme(), + }; + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(child); + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } } - private static ThemedControl CreateTarget() + public class ThemeFromStyle { - return new ThemedControl + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() { - Theme = CreateTheme(), - }; + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl(); + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot() + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TemplatedControl.ThemeProperty, CreateTheme()) + } + } + } + }; + + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } } private static ControlTheme CreateTheme() @@ -104,13 +163,6 @@ namespace Avalonia.Controls.UnitTests.Primitives }; } - private static TestRoot CreateRoot(IControl child) - { - var result = new TestRoot(child); - result.LayoutManager.ExecuteInitialLayoutPass(); - return result; - } - private class ThemedControl : TemplatedControl { public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs index 05083537cd..9eb48311df 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs @@ -38,6 +38,42 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void ControlTheme_Can_Be_Set_In_Style() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = $@" + + + {ControlThemeXaml} + + + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = Assert.IsType(window.Content); + + window.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.NotNull(button.Template); + + var child = Assert.Single(button.GetVisualChildren()); + var border = Assert.IsType(child); + + Assert.Equal(Brushes.Red, border.Background); + } + } + private const string ControlThemeXaml = @" From 5cd95320128fa97fff7af8223cf55b9d043086f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2022 09:36:53 +0200 Subject: [PATCH 081/481] Move tests to correct place. --- .../Styling/StyledElementTests_Theming.cs | 169 +++++++++++++++++ .../TemplatedControlTests_Theming.cs | 171 ------------------ 2 files changed, 169 insertions(+), 171 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs new file mode 100644 index 0000000000..539f9e6576 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -0,0 +1,169 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Base.UnitTests.Styling; + +public class StyledElementTests_Theming +{ + public class InlineTheme + { + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Template); + + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + [Fact] + public void Theme_Is_Detached_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(target.Template); + + target.Theme = null; + Assert.Null(target.Template); + } + + [Fact] + public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new ThemedControl(); + var root = CreateRoot(target); + + Assert.Null(target.Template); + + target.Theme = CreateTheme(); + Assert.Null(target.Template); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.NotNull(target.Template); + Assert.Equal(border.Background, Brushes.Red); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl + { + Theme = CreateTheme(), + }; + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(child); + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + + public class ThemeFromStyle + { + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl(); + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot() + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TemplatedControl.ThemeProperty, CreateTheme()) + } + } + } + }; + + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + + private static ControlTheme CreateTheme() + { + var template = new FuncControlTemplate((o, n) => + new Border { Name = "PART_Border" }); + + return new ControlTheme + { + TargetType = typeof(ThemedControl), + Setters = + { + new Setter(ThemedControl.TemplateProperty, template), + }, + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Green), + } + }, + } + }; + } + + private class ThemedControl : TemplatedControl + { + public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs deleted file mode 100644 index 74d75ff056..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Media; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Avalonia.VisualTree; -using Xunit; - -#nullable enable - -namespace Avalonia.Controls.UnitTests.Primitives -{ - public class TemplatedControlTests_Theming - { - public class InlineTheme - { - [Fact] - public void Theme_Is_Applied_When_Attached_To_Logical_Tree() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - - Assert.Null(target.Template); - - var root = CreateRoot(target); - Assert.NotNull(target.Template); - - var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); - - target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); - } - - [Fact] - public void Theme_Is_Detached_When_Theme_Property_Cleared() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - var root = CreateRoot(target); - - Assert.NotNull(target.Template); - - target.Theme = null; - Assert.Null(target.Template); - } - - [Fact] - public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = new ThemedControl(); - var root = CreateRoot(target); - - Assert.Null(target.Template); - - target.Theme = CreateTheme(); - Assert.Null(target.Template); - - root.LayoutManager.ExecuteLayoutPass(); - - var border = Assert.IsType(target.VisualChild); - Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); - } - - private static ThemedControl CreateTarget() - { - return new ThemedControl - { - Theme = CreateTheme(), - }; - } - - private static TestRoot CreateRoot(IControl child) - { - var result = new TestRoot(child); - result.LayoutManager.ExecuteInitialLayoutPass(); - return result; - } - } - - public class ThemeFromStyle - { - [Fact] - public void Theme_Is_Applied_When_Attached_To_Logical_Tree() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - - Assert.Null(target.Theme); - Assert.Null(target.Template); - - var root = CreateRoot(target); - - Assert.NotNull(target.Theme); - Assert.NotNull(target.Template); - - var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); - - target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); - } - - private static ThemedControl CreateTarget() - { - return new ThemedControl(); - } - - private static TestRoot CreateRoot(IControl child) - { - var result = new TestRoot() - { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(TemplatedControl.ThemeProperty, CreateTheme()) - } - } - } - }; - - result.Child = child; - result.LayoutManager.ExecuteInitialLayoutPass(); - return result; - } - } - - private static ControlTheme CreateTheme() - { - var template = new FuncControlTemplate((o, n) => - new Border { Name = "PART_Border" }); - - return new ControlTheme - { - TargetType = typeof(ThemedControl), - Setters = - { - new Setter(ThemedControl.TemplateProperty, template), - }, - Children = - { - new Style(x => x.Nesting().Template().OfType()) - { - Setters = - { - new Setter(Border.BackgroundProperty, Brushes.Red), - } - }, - new Style(x => x.Nesting().Class("foo").Template().OfType()) - { - Setters = - { - new Setter(Border.BackgroundProperty, Brushes.Green), - } - }, - } - }; - } - - private class ThemedControl : TemplatedControl - { - public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); - } - } -} From 4bdcb8eeeaab2f790245ddb70a5cfca3df7886f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2022 10:34:34 +0200 Subject: [PATCH 082/481] Invalidate template control styles when Theme changes. --- .../Primitives/TemplatedControl.cs | 11 +++ .../Styling/StyledElementTests_Theming.cs | 75 ++++++++++++++----- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..a07dd9ae27 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -365,6 +365,17 @@ namespace Avalonia.Controls.Primitives { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThemeProperty) + { + foreach (var child in this.GetTemplateChildren()) + child.InvalidateStyles(); + } + } + /// /// Called when the control's template is applied. /// diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 539f9e6576..0c0808987a 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -8,6 +8,8 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests.Styling; public class StyledElementTests_Theming @@ -45,6 +47,40 @@ public class StyledElementTests_Theming Assert.Null(target.Template); } + [Fact] + public void Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + + var theme = new ControlTheme + { + TargetType = typeof(ThemedControl), + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Canvas.BackgroundProperty, Brushes.Red), + } + }, + } + }; + + var target = CreateTarget(theme); + target.Template = new FuncControlTemplate((o, n) => new Canvas()); + + var root = CreateRoot(target); + + var canvas = Assert.IsType(target.VisualChild); + Assert.Equal(canvas.Background, Brushes.Red); + + target.Theme = null; + + Assert.IsType(target.VisualChild); + Assert.Null(canvas.Background); + } + [Fact] public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() { @@ -64,11 +100,11 @@ public class StyledElementTests_Theming Assert.Equal(border.Background, Brushes.Red); } - private static ThemedControl CreateTarget() + private static ThemedControl CreateTarget(ControlTheme? theme = null) { return new ThemedControl { - Theme = CreateTheme(), + Theme = theme ?? CreateTheme(), }; } @@ -132,33 +168,32 @@ public class StyledElementTests_Theming private static ControlTheme CreateTheme() { - var template = new FuncControlTemplate((o, n) => - new Border { Name = "PART_Border" }); + var template = new FuncControlTemplate((o, n) => new Border()); return new ControlTheme { TargetType = typeof(ThemedControl), Setters = - { - new Setter(ThemedControl.TemplateProperty, template), - }, - Children = - { - new Style(x => x.Nesting().Template().OfType()) { - Setters = - { - new Setter(Border.BackgroundProperty, Brushes.Red), - } + new Setter(ThemedControl.TemplateProperty, template), }, - new Style(x => x.Nesting().Class("foo").Template().OfType()) + Children = { - Setters = + new Style(x => x.Nesting().Template().OfType()) { - new Setter(Border.BackgroundProperty, Brushes.Green), - } - }, - } + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Green), + } + }, + } }; } From 55de1523c42ae438ef8997c0a72cc89b338ff2db Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 20:20:43 -0400 Subject: [PATCH 083/481] Support ignoring property changes in derived Color controls --- .../ColorPicker/ColorPicker.cs | 9 ++++++++ .../ColorSlider/ColorSlider.cs | 22 +++++++++---------- .../ColorView/ColorView.cs | 12 +++++----- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 140a24d6a1..d34a91d1bb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -12,5 +12,14 @@ namespace Avalonia.Controls /// public class ColorPicker : ColorView { + /// + /// Initializes a new instance of the class. + /// + public ColorPicker() : base() + { + // Completely ignore property changes here + // The ColorView in the control template is responsible to manage this + base.ignorePropertyChanged = true; + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 957e5e7b77..a9ba5a20fa 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives /// private const double MaxHue = 359; - private bool disableUpdates = false; + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -135,7 +135,7 @@ namespace Avalonia.Controls.Primitives /// /// /// Warning: This will trigger property changed updates. - /// Consider using externally. + /// Consider using externally. /// private void SetColorToSliderValues() { @@ -341,7 +341,7 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -350,7 +350,7 @@ namespace Avalonia.Controls.Primitives // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); @@ -362,21 +362,21 @@ namespace Avalonia.Controls.Primitives change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == ColorModelProperty) { - disableUpdates = true; + ignorePropertyChanged = true; SetColorToSliderValues(); UpdateBackground(); UpdatePseudoClasses(); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); @@ -388,7 +388,7 @@ namespace Avalonia.Controls.Primitives change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == IsRoundingEnabledProperty) { @@ -402,7 +402,7 @@ namespace Avalonia.Controls.Primitives change.Property == MinimumProperty || change.Property == MaximumProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color oldColor = Color; (var color, var hsvColor) = GetColorFromSliderValues(); @@ -421,7 +421,7 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); OnColorChanged(new ColorChangedEventArgs(oldColor, Color)); - disableUpdates = false; + ignorePropertyChanged = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 96de734cc7..c19daf5f40 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls private ObservableCollection _customPaletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); - private bool disableUpdates = false; + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -167,7 +167,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -176,7 +176,7 @@ namespace Avalonia.Controls // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); SetColorToHexTextBox(); @@ -185,11 +185,11 @@ namespace Avalonia.Controls change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); SetColorToHexTextBox(); @@ -198,7 +198,7 @@ namespace Avalonia.Controls change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == CustomPaletteProperty) { From 36225e9132719d209d727cd4a6f7ef42c5b7f054 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 20:21:06 -0400 Subject: [PATCH 084/481] Use more standard binding in ColorPicker This doesn't currently work for some reason --- .../Themes/Fluent/ColorPicker.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b3424d2cc1..6e5228039d 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -36,7 +36,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="1,1,0,1" /> - Date: Thu, 2 Jun 2022 20:21:16 -0400 Subject: [PATCH 085/481] Small improvements --- src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index c19daf5f40..d0ee5f9acd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls // Re-apply the hex value // This ensure the hex color value is always valid and formatted correctly - _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + SetColorToHexTextBox(); } } @@ -225,7 +225,7 @@ namespace Avalonia.Controls change.Property == IsColorSpectrumVisibleProperty) { // When the property changed notification is received here the visibility - // of individual tab items has not yet been updated though the bindings. + // of individual tab items has not yet been updated through the bindings. // Therefore, the validation is delayed until after bindings update. Dispatcher.UIThread.Post(() => { From b8be7ba4cb59eb1c406d794d76a6ad74d1d21a3a Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 21:05:47 -0400 Subject: [PATCH 086/481] Implement SelectedTabIndex in ColorView --- .../ColorView/ColorView.Properties.cs | 18 +++++++++++++++++- .../ColorView/ColorView.cs | 16 +++++++++++++++- .../ColorView/ColorViewTab.cs | 15 +++++++++------ .../Themes/Fluent/ColorView.xaml | 3 ++- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index a3267d88f2..a4897c99a2 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -8,7 +8,6 @@ namespace Avalonia.Controls /// public partial class ColorView { - // SelectedTabIndex // IsColorModelSelectorVisible // IsComponentSliderVisible @@ -198,6 +197,14 @@ namespace Avalonia.Controls nameof(MinValue), 0); + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedTabIndexProperty = + AvaloniaProperty.Register( + nameof(SelectedTabIndex), + (int)ColorViewTab.Spectrum); + /// /// Defines the property. /// @@ -413,6 +420,15 @@ namespace Avalonia.Controls set => SetValue(MinValueProperty, value); } + /// + /// Gets or sets the index of the selected subview/tab. + /// + public int SelectedTabIndex + { + get => GetValue(SelectedTabIndexProperty); + set => SetValue(SelectedTabIndexProperty, value); + } + /// public bool ShowAccentColors { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index d0ee5f9acd..9809f1312b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -73,7 +73,10 @@ namespace Avalonia.Controls /// Validates the selected subview/tab taking into account the visibility of each subview/tab /// as well as the current selection. /// - private void ValidateSelectedTab() + /// + /// Derived controls may re-implement this based on their default style / control template. + /// + protected virtual void ValidateSelectedTab() { if (_tabControl != null && _tabControl.Items != null) @@ -135,6 +138,8 @@ namespace Avalonia.Controls _tabControl.SelectedItem = null; _tabControl.IsVisible = false; } + + SelectedTabIndex = _tabControl.SelectedIndex; } return; @@ -232,6 +237,15 @@ namespace Avalonia.Controls ValidateSelectedTab(); }, DispatcherPriority.Background); } + else if (change.Property == SelectedTabIndexProperty) + { + // Again, it is necessary to wait for the SelectedTabIndex value to + // be applied to the TabControl through binding before validation occurs. + Dispatcher.UIThread.Post(() => + { + ValidateSelectedTab(); + }, DispatcherPriority.Background); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs index 534e833631..8e3433d819 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -1,23 +1,26 @@ namespace Avalonia.Controls { /// - /// Defines a specific subview (tab) within the . + /// Defines a specific subview/tab within the . /// + /// + /// This is indexed to match the default control template ordering. + /// public enum ColorViewTab { /// - /// The components subview with sliders and numeric input boxes. + /// The color spectrum subview with a box/ring spectrum and sliders. /// - Components, + Spectrum = 0, /// /// The color palette subview with a grid of selectable colors. /// - Palette, + Palette = 1, /// - /// The color spectrum subview with a box/ring spectrum and sliders. + /// The components subview with sliders and numeric input boxes. /// - Spectrum, + Components = 2, } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 420e3b2ae9..d6bf83ff44 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -112,7 +112,8 @@ + Width="350" + SelectedIndex="{Binding SelectedTabIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> 0,0,0,0 From 10bc7b38cbb4795ef4d7a2ad8a32e13e4194d125 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:20:44 -0400 Subject: [PATCH 087/481] Rename 'EnumToBooleanConverter' to 'EnumToBoolConverter' Bool is shorter and is also slightly more commonly used in other converters. Bool matches the type as written in C# as well. --- .../Themes/Default/ColorSpectrum.xaml | 14 +++++++------- .../Themes/Fluent/ColorSpectrum.xaml | 14 +++++++------- .../Themes/Fluent/ColorView.xaml | 18 +++++++++--------- ...leanConverter.cs => EnumToBoolConverter.cs} | 13 ++++++------- 4 files changed, 29 insertions(+), 30 deletions(-) rename src/Avalonia.Controls/Converters/{EnumToBooleanConverter.cs => EnumToBoolConverter.cs} (82%) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 891e040e9f..c29f8f51e5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -5,7 +5,7 @@ x:CompileBindings="True"> - + @@ -25,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index 779f228b97..6dd7ddd373 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -5,7 +5,7 @@ x:CompileBindings="True"> - + @@ -25,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index d6bf83ff44..4b01de15a0 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -12,7 +12,7 @@ - + 48 @@ -351,12 +351,12 @@ Grid.Column="0" Content="RGB" CornerRadius="4,0,0,4" - IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=TwoWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}"/> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> /// This converter is useful to enable binding of radio buttons with a selected enum value. /// - public class EnumToBooleanConverter : IValueConverter + public class EnumToBoolConverter : IValueConverter { /// public object? Convert( @@ -44,14 +44,13 @@ namespace Avalonia.Controls.Converters object? parameter, CultureInfo culture) { - if (value is bool boolValue) + if (value is bool boolValue && + boolValue == true) { - return boolValue ? parameter : BindingOperations.DoNothing; - } - else - { - return BindingOperations.DoNothing; + return parameter; } + + return BindingOperations.DoNothing; } } } From e0c936dbb4e2d48bc3367e0f13e3e2ac1d15002e Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:37:54 -0400 Subject: [PATCH 088/481] Add FlatColorPalette --- .../ColorPalette/FlatColorPalette.cs | 284 ++++++++++++++++++ .../ColorPalette/FluentColorPalette.cs | 6 - 2 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs new file mode 100644 index 0000000000..130d7e0edd --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs @@ -0,0 +1,284 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a reduced flat design or flat UI color palette. + /// + /// + /// See: + /// - https://htmlcolorcodes.com/color-chart/ + /// - https://htmlcolorcodes.com/color-chart/flat-design-color-chart/ + /// - http://designmodo.github.io/Flat-UI/ + /// + /// The GitHub project is licensed as MIT: https://github.com/designmodo/Flat-UI. + /// + /// + public class FlatColorPalette : IColorPalette + { + // The full Flat UI color chart has 10 rows and 20 columns + // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png + // This is a reduced palette for usability + private static Color[,] colorChart = new Color[,] + { + // Pomegranate + { + Color.FromArgb(0xFF, 0xF9, 0xEB, 0xEA), + Color.FromArgb(0xFF, 0xE6, 0xB0, 0xAA), + Color.FromArgb(0xFF, 0xCD, 0x61, 0x55), + Color.FromArgb(0xFF, 0xA9, 0x32, 0x26), + Color.FromArgb(0xFF, 0x7B, 0x24, 0x1C), + }, + + // Amethyst + { + Color.FromArgb(0xFF, 0xF5, 0xEE, 0xF8), + Color.FromArgb(0xFF, 0xD7, 0xBD, 0xE2), + Color.FromArgb(0xFF, 0xAF, 0x7A, 0xC5), + Color.FromArgb(0xFF, 0x88, 0x4E, 0xA0), + Color.FromArgb(0xFF, 0x63, 0x39, 0x74), + }, + + // Belize Hole + { + Color.FromArgb(0xFF, 0xEA, 0xF2, 0xF8), + Color.FromArgb(0xFF, 0xA9, 0xCC, 0xE3), + Color.FromArgb(0xFF, 0x54, 0x99, 0xC7), + Color.FromArgb(0xFF, 0x24, 0x71, 0xA3), + Color.FromArgb(0xFF, 0x1A, 0x52, 0x76), + }, + + // Turquoise + { + Color.FromArgb(0xFF, 0xE8, 0xF8, 0xF5), + Color.FromArgb(0xFF, 0xA3, 0xE4, 0xD7), + Color.FromArgb(0xFF, 0x48, 0xC9, 0xB0), + Color.FromArgb(0xFF, 0x17, 0xA5, 0x89), + Color.FromArgb(0xFF, 0x11, 0x78, 0x64), + }, + + // Nephritis + { + Color.FromArgb(0xFF, 0xE9, 0xF7, 0xEF), + Color.FromArgb(0xFF, 0xA9, 0xDF, 0xBF), + Color.FromArgb(0xFF, 0x52, 0xBE, 0x80), + Color.FromArgb(0xFF, 0x22, 0x99, 0x54), + Color.FromArgb(0xFF, 0x19, 0x6F, 0x3D), + }, + + // Sunflower + { + Color.FromArgb(0xFF, 0xFE, 0xF9, 0xE7), + Color.FromArgb(0xFF, 0xF9, 0xE7, 0x9F), + Color.FromArgb(0xFF, 0xF4, 0xD0, 0x3F), + Color.FromArgb(0xFF, 0xD4, 0xAC, 0x0D), + Color.FromArgb(0xFF, 0x9A, 0x7D, 0x0A), + }, + + // Carrot + { + Color.FromArgb(0xFF, 0xFD, 0xF2, 0xE9), + Color.FromArgb(0xFF, 0xF5, 0xCB, 0xA7), + Color.FromArgb(0xFF, 0xEB, 0x98, 0x4E), + Color.FromArgb(0xFF, 0xCA, 0x6F, 0x1E), + Color.FromArgb(0xFF, 0x93, 0x51, 0x16), + }, + + // Clouds + { + Color.FromArgb(0xFF, 0xFD, 0xFE, 0xFE), + Color.FromArgb(0xFF, 0xF7, 0xF9, 0xF9), + Color.FromArgb(0xFF, 0xF0, 0xF3, 0xF4), + Color.FromArgb(0xFF, 0xD0, 0xD3, 0xD4), + Color.FromArgb(0xFF, 0x97, 0x9A, 0x9A), + }, + + // Concrete + { + Color.FromArgb(0xFF, 0xF4, 0xF6, 0xF6), + Color.FromArgb(0xFF, 0xD5, 0xDB, 0xDB), + Color.FromArgb(0xFF, 0xAA, 0xB7, 0xB8), + Color.FromArgb(0xFF, 0x83, 0x91, 0x92), + Color.FromArgb(0xFF, 0x5F, 0x6A, 0x6A), + }, + + // Wet Asphalt + { + Color.FromArgb(0xFF, 0xEB, 0xED, 0xEF), + Color.FromArgb(0xFF, 0xAE, 0xB6, 0xBF), + Color.FromArgb(0xFF, 0x5D, 0x6D, 0x7E), + Color.FromArgb(0xFF, 0x2E, 0x40, 0x53), + Color.FromArgb(0xFF, 0x21, 0x2F, 0x3C), + }, + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 2; + + /// + /// The index in the color palette of the 'Pomegranate' color. + /// This index can correspond to multiple color shades. + /// + public const int PomegranateIndex = 0; + + /// + /// The index in the color palette of the 'Amethyst' color. + /// This index can correspond to multiple color shades. + /// + public const int AmethystIndex = 1; + + /// + /// The index in the color palette of the 'BelizeHole' color. + /// This index can correspond to multiple color shades. + /// + public const int BelizeHoleIndex = 2; + + /// + /// The index in the color palette of the 'Turquoise' color. + /// This index can correspond to multiple color shades. + /// + public const int TurquoiseIndex = 3; + + /// + /// The index in the color palette of the 'Nephritis' color. + /// This index can correspond to multiple color shades. + /// + public const int NephritisIndex = 4; + + /// + /// The index in the color palette of the 'Sunflower' color. + /// This index can correspond to multiple color shades. + /// + public const int SunflowerIndex = 5; + + /// + /// The index in the color palette of the 'Carrot' color. + /// This index can correspond to multiple color shades. + /// + public const int CarrotIndex = 6; + + /// + /// The index in the color palette of the 'Clouds' color. + /// This index can correspond to multiple color shades. + /// + public const int CloudsIndex = 7; + + /// + /// The index in the color palette of the 'Concrete' color. + /// This index can correspond to multiple color shades. + /// + public const int ConcreteIndex = 8; + + /// + /// The index in the color palette of the 'WetAsphalt' color. + /// This index can correspond to multiple color shades. + /// + public const int WetAsphaltIndex = 9; + + /// + public int ColorCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0392B. + /// + public static Color Pomegranate + { + get => colorChart[PomegranateIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF9B59B6. + /// + public static Color Amethyst + { + get => colorChart[AmethystIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF2980B9. + /// + public static Color BelizeHole + { + get => colorChart[BelizeHoleIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF1ABC9C. + /// + public static Color Turquoise + { + get => colorChart[TurquoiseIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF27AE60. + /// + public static Color Nephritis + { + get => colorChart[NephritisIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFF1C40F. + /// + public static Color Sunflower + { + get => colorChart[SunflowerIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFE67E22. + /// + public static Color Carrot + { + get => colorChart[CarrotIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFECF0F1. + /// + public static Color Clouds + { + get => colorChart[CloudsIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF95A5A6. + /// + public static Color Concrete + { + get => colorChart[ConcreteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF34495E. + /// + public static Color WetAsphalt + { + get => colorChart[WetAsphaltIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + // Table is transposed compared to the reference chart + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs index b6f9a244b1..013e69ce20 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -103,12 +103,6 @@ namespace Avalonia.Controls } }; - /// - /// Gets the index of the default shade of colors in this palette. - /// This has little meaning in this palette as colors are not strictly separated by shade. - /// - public const int DefaultShadeIndex = 0; - /// /// Gets the total number of colors in this palette. /// A color is not necessarily a single value and may be composed of several shades. From fe2d51b111696298e35c3db629751006f8e5ceec Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:42:56 -0400 Subject: [PATCH 089/481] Add SixteenColorPalette --- .../ColorPalette/SixteenColorPalette.cs | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs new file mode 100644 index 0000000000..f3abfdfd7f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs @@ -0,0 +1,302 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard sixteen color palette from the HTML 4.01 specification. + /// + /// + /// See https://en.wikipedia.org/wiki/Web_colors#HTML_color_names. + /// + public class SixteenColorPalette : IColorPalette + { + // The 16 standard colors from HTML and early Windows computers + // https://en.wikipedia.org/wiki/List_of_software_palettes + // https://en.wikipedia.org/wiki/Web_colors#HTML_color_names + private static Color[,] colorChart = new Color[,] + { + { + Colors.White, + Colors.Silver + }, + { + Colors.Gray, + Colors.Black + }, + { + Colors.Red, + Colors.Maroon + }, + { + Colors.Yellow, + Colors.Olive + }, + { + Colors.Lime, + Colors.Green + }, + { + Colors.Aqua, + Colors.Teal + }, + { + Colors.Blue, + Colors.Navy + }, + { + Colors.Fuchsia, + Colors.Purple + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 0; + + /// + /// The index in the color palette of the 'White' color. + /// This index can correspond to multiple color shades. + /// + public const int WhiteIndex = 0; + + /// + /// The index in the color palette of the 'Silver' color. + /// This index can correspond to multiple color shades. + /// + public const int SilverIndex = 1; + + /// + /// The index in the color palette of the 'Gray' color. + /// This index can correspond to multiple color shades. + /// + public const int GrayIndex = 2; + + /// + /// The index in the color palette of the 'Black' color. + /// This index can correspond to multiple color shades. + /// + public const int BlackIndex = 3; + + /// + /// The index in the color palette of the 'Red' color. + /// This index can correspond to multiple color shades. + /// + public const int RedIndex = 4; + + /// + /// The index in the color palette of the 'Maroon' color. + /// This index can correspond to multiple color shades. + /// + public const int MaroonIndex = 5; + + /// + /// The index in the color palette of the 'Yellow' color. + /// This index can correspond to multiple color shades. + /// + public const int YellowIndex = 6; + + /// + /// The index in the color palette of the 'Olive' color. + /// This index can correspond to multiple color shades. + /// + public const int OliveIndex = 7; + + /// + /// The index in the color palette of the 'Lime' color. + /// This index can correspond to multiple color shades. + /// + public const int LimeIndex = 8; + + /// + /// The index in the color palette of the 'Green' color. + /// This index can correspond to multiple color shades. + /// + public const int GreenIndex = 9; + + /// + /// The index in the color palette of the 'Aqua' color. + /// This index can correspond to multiple color shades. + /// + public const int AquaIndex = 10; + + /// + /// The index in the color palette of the 'Teal' color. + /// This index can correspond to multiple color shades. + /// + public const int TealIndex = 11; + + /// + /// The index in the color palette of the 'Blue' color. + /// This index can correspond to multiple color shades. + /// + public const int BlueIndex = 12; + + /// + /// The index in the color palette of the 'Navy' color. + /// This index can correspond to multiple color shades. + /// + public const int NavyIndex = 13; + + /// + /// The index in the color palette of the 'Fuchsia' color. + /// This index can correspond to multiple color shades. + /// + public const int FuchsiaIndex = 14; + + /// + /// The index in the color palette of the 'Purple' color. + /// This index can correspond to multiple color shades. + /// + public const int PurpleIndex = 15; + + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFFFF. + /// + public static Color White + { + get => colorChart[WhiteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0C0C0. + /// + public static Color Silver + { + get => colorChart[SilverIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808080. + /// + public static Color Gray + { + get => colorChart[GrayIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000000. + /// + public static Color Black + { + get => colorChart[BlackIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF0000. + /// + public static Color Red + { + get => colorChart[RedIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800000. + /// + public static Color Maroon + { + get => colorChart[MaroonIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFF00. + /// + public static Color Yellow + { + get => colorChart[YellowIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808000. + /// + public static Color Olive + { + get => colorChart[OliveIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FF00. + /// + public static Color Lime + { + get => colorChart[LimeIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008000. + /// + public static Color Green + { + get => colorChart[GreenIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FFFF. + /// + public static Color Aqua + { + get => colorChart[AquaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008080. + /// + public static Color Teal + { + get => colorChart[TealIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF0000FF. + /// + public static Color Blue + { + get => colorChart[BlueIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000080. + /// + public static Color Navy + { + get => colorChart[NavyIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF00FF. + /// + public static Color Fuchsia + { + get => colorChart[FuchsiaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800080. + /// + public static Color Purple + { + get => colorChart[PurpleIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} From 5e8f0fb70119e88335a555c80082311c9ef26664 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:00:08 -0400 Subject: [PATCH 090/481] Add comment explaining rounding value selection --- src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 32a898ee71..7dc340ea16 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -64,6 +64,10 @@ namespace Avalonia.Controls.Primitives // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to // reduce the color space into something more useful. + // + // The rounding value of 5 is specially chosen. + // It is a factor of 255 and therefore evenly divisible which improves + // the quality of the calculations. double rounding = 5; var roundedColor = new Color( 0xFF, From 4dcf13623d7e78698141f5f64742ec949e62afbb Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:02:18 -0400 Subject: [PATCH 091/481] Make ColorPalettes directory plural --- .../{ColorPalette => ColorPalettes}/FlatColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/FluentColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/IColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/SixteenColorPalette.cs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/FlatColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/FluentColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/IColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/SixteenColorPalette.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs From 63d1bdec3c3b05964b4b3f158d3dfde6ddbc4bcf Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:08:24 -0400 Subject: [PATCH 092/481] Add resources to control accent color section size in ColorPreviewer --- .../Themes/Default/ColorPreviewer.xaml | 10 ++++++---- .../Themes/Fluent/ColorPreviewer.xaml | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 15e5ca1655..ac1531fe7a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -9,6 +9,8 @@ + 80 + 40 + Date: Fri, 3 Jun 2022 14:32:19 +0200 Subject: [PATCH 096/481] Fix nested :not selector. --- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- .../Styling/SelectorTests_Nesting.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index cdc3254d38..76a0690e96 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -67,6 +67,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector(); + internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index d49fcf03a2..1520dc329d 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -257,6 +257,30 @@ namespace Avalonia.Base.UnitTests.Styling parent.Children.Add(child); } + + [Fact] + public void Nesting_Not_Class_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => x.Nesting().Not(y => y.Class("foo")))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + + var sink = new ActivatorSink(match.Activator); + + Assert.False(sink.Active); + control.Classes.Clear(); + Assert.True(sink.Active); + } + public class Control1 : Control { } From 0d7adc70d9e69334d2109b04a8d2ebade8c6461b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 15:47:01 +0300 Subject: [PATCH 097/481] Implemented ZIndex support --- .../Composition/CompositingRenderer.cs | 69 ++++++++++++++++--- .../Rendering/ImmediateRenderer.cs | 6 +- .../Rendering/SceneGraph/SceneBuilder.cs | 44 +++++++----- src/Avalonia.Base/Visual.cs | 8 +++ src/Avalonia.Base/VisualTree/IVisual.cs | 5 ++ 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b83f804a8f..5aded281b3 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; @@ -100,26 +101,74 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor return; var compositionChildren = v.CompositionVisual.Children; var visualChildren = (AvaloniaList)v.GetVisualChildren(); + + PooledList<(IVisual visual, int index)>? sortedChildren = null; + if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1) + { + sortedChildren = new (visualChildren.Count); + for (var c = 0; c < visualChildren.Count; c++) + sortedChildren.Add((visualChildren[c], c)); + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + sortedChildren.Sort(static (lhs, rhs) => + { + var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex); + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + } + if (compositionChildren.Count == visualChildren.Count) { bool mismatch = false; - for(var c=0; c x, ZIndexComparer.Instance)) + var childrenEnumerable = visual.HasNonUniformZIndexChildren + ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) + : (IEnumerable)visual.VisualChildren; + + foreach (var child in childrenEnumerable) { var childBounds = GetTransformedBounds(child); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index 019c3e0e9b..cc18400a4c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -272,26 +272,36 @@ namespace Avalonia.Rendering.SceneGraph else if (visualChildren.Count > 1) { var count = visualChildren.Count; - var sortedChildren = new (IVisual visual, int index)[count]; - for (var i = 0; i < count; i++) + if (visual.HasNonUniformZIndexChildren) { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + var sortedChildren = new (IVisual visual, int index)[count]; + + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + + foreach (var child in sortedChildren) + { + var childNode = GetOrCreateChildNode(scene, child.Item1, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } + else + foreach (var child in visualChildren) + { + var childNode = GetOrCreateChildNode(scene, child, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } node.SubTreeUpdated = true; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 64341be0c7..c11ea2c40a 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -293,6 +293,8 @@ namespace Avalonia internal CompositionDrawListVisual? CompositionVisual { get; private set; } + public bool HasNonUniformZIndexChildren { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -445,6 +447,9 @@ namespace Avalonia AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); + if (ZIndex != 0 && this.GetVisualParent() is Visual parent) + parent.HasNonUniformZIndexChildren = true; + var visualChildren = VisualChildren; if (visualChildren != null) @@ -611,6 +616,9 @@ namespace Avalonia { var sender = e.Sender as IVisual; var parent = sender?.VisualParent; + if (sender?.ZIndex != 0 && parent is Visual parentVisual) + parentVisual.HasNonUniformZIndexChildren = true; + sender?.InvalidateVisual(); parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); } diff --git a/src/Avalonia.Base/VisualTree/IVisual.cs b/src/Avalonia.Base/VisualTree/IVisual.cs index 3b053fab38..fdd2d187b8 100644 --- a/src/Avalonia.Base/VisualTree/IVisual.cs +++ b/src/Avalonia.Base/VisualTree/IVisual.cs @@ -79,6 +79,11 @@ namespace Avalonia.VisualTree /// Gets a value indicating whether to apply mirror transform on this control. /// bool HasMirrorTransform { get; } + + /// + /// Gets a value indicating whether to sort children when rendering this control + /// + bool HasNonUniformZIndexChildren { get; } /// /// Gets or sets the render transform of the control. From 32c1eb41e44a097898355893ce53aa6f23e2319d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 15:58:21 +0300 Subject: [PATCH 098/481] Hit-testing clip --- .../Composition/CompositionTarget.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 3243934932..31431a4e36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -79,30 +79,33 @@ namespace Avalonia.Rendering.Composition return false; if (!TryTransformTo(visual, ref point)) return false; - if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + + if (visual.ClipToBounds + && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) + return false; + if (visual.Clip?.FillContains(point) == false) + return false; + + bool success = false; + // Hit-test the current node + if (visual.HitTest(point, filter)) { - bool success = false; - // Hit-test the current node - if (visual.HitTest(point, filter)) + result.Add(visual); + success = true; + } + + // Inspect children too + if (visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) { - result.Add(visual); - success = true; + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result, filter); + if (hit) + return true; } - - // Inspect children too - if(visual is CompositionContainerVisual cv) - for (var c = cv.Children.Count - 1; c >= 0; c--) - { - var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result, filter); - if (hit) - return true; - } - return success; - } + return success; - return false; } public void RequestRedraw() => RegisterForSerialization(); From 10247f3dc2f80e00584e098964715e09a8d832f0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 16:03:05 +0300 Subject: [PATCH 099/481] Stop animations from ticking when window is hidden --- .../Server/ServerCompositionTarget.cs | 22 +++++++++++++++++++ .../Composition/Server/ServerVisual.cs | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 9513fb58fa..e8c417a98d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using System.Threading; using Avalonia.Media; @@ -24,6 +25,7 @@ namespace Avalonia.Rendering.Composition.Server private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; private bool _disposed; + private HashSet _attachedVisuals = new(); public ReadbackIndices Readback { get; } = new(); @@ -39,9 +41,17 @@ namespace Avalonia.Rendering.Composition.Server partial void OnIsEnabledChanged() { if (IsEnabled) + { _compositor.AddCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Activate(); + } else + { _compositor.RemoveCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Deactivate(); + } } partial void DeserializeChangesExtra(BatchStreamReader c) @@ -155,5 +165,17 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget?.Dispose(); _renderTarget = null; } + + public void AddVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Add(visual) && IsEnabled) + visual.Activate(); + } + + public void RemoveVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Remove(visual) && IsEnabled) + visual.Deactivate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 5717ab2f8c..5c7067bfb1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -126,14 +126,14 @@ namespace Avalonia.Rendering.Composition.Server partial void OnRootChanging() { - if(Root != null) - Deactivate(); + if (Root != null) + Root.RemoveVisual(this); } partial void OnRootChanged() { if (Root != null) - Activate(); + Root.AddVisual(this); } protected override void ValuesInvalidated() From 6e2214b6d42e6bf8f1913d9dd5fc509098769094 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 17:22:02 +0300 Subject: [PATCH 100/481] Added adorner support --- .../Server/ServerCompositionDrawListVisual.cs | 19 +++++++++++++++--- .../Server/ServerCompositionTarget.cs | 14 ++++++++++++- .../Server/ServerContainerVisual.cs | 18 +++++++++-------- .../Server/ServerSolidColorVisual.cs | 5 ++--- .../Composition/Server/ServerSpriteVisual.cs | 4 ++-- .../Composition/Server/ServerVisual.cs | 12 +++++++---- src/Avalonia.Base/Visual.cs | 2 +- src/Avalonia.Base/composition-schema.xml | 1 + .../Primitives/AdornerLayer.cs | 20 ++++++++++++++----- .../CompositionGenerator/Config.cs | 2 ++ .../CompositionGenerator/Generator.cs | 2 +- 11 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f1b5032cd3..45062632c7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -11,10 +11,16 @@ namespace Avalonia.Rendering.Composition.Server; internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual { +#if DEBUG + public readonly Visual UiVisual; +#endif private CompositionDrawList? _renderCommands; - public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) { +#if DEBUG + UiVisual = v; +#endif } Rect? _contentBounds; @@ -47,12 +53,19 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, commitedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { if (_renderCommands != null) { _renderCommands.Render(canvas); } - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } + +#if DEBUG + public override string ToString() + { + return UiVisual.GetType().ToString(); + } +#endif } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e8c417a98d..fb0d6fce60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -26,6 +26,7 @@ namespace Avalonia.Rendering.Composition.Server private bool _redrawRequested; private bool _disposed; private HashSet _attachedVisuals = new(); + private Queue _adornerUpdateQueue = new(); public ReadbackIndices Readback { get; } = new(); @@ -80,6 +81,12 @@ namespace Avalonia.Rendering.Composition.Server // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.Update(this, adorner.AdornedVisual?.GlobalTransformMatrix ?? Matrix4x4.Identity); + } Readback.CompleteWrite(Revision); @@ -108,7 +115,7 @@ namespace Avalonia.Rendering.Composition.Server { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper)); context.PopClip(); } } @@ -147,6 +154,7 @@ namespace Avalonia.Rendering.Composition.Server { var snapped = SnapToDevicePixels(rect, Scaling); _dirtyRect = _dirtyRect.Union(snapped); + _redrawRequested = true; } public void Invalidate() @@ -176,6 +184,10 @@ namespace Avalonia.Rendering.Composition.Server { if (_attachedVisuals.Remove(visual) && IsEnabled) visual.Deactivate(); + if(visual.IsVisibleInFrame) + AddDirtyRect(visual.TransformedBounds); } + + public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index a277450214..bcfcfbe4f2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -7,24 +7,26 @@ namespace Avalonia.Rendering.Composition.Server { public ServerCompositionVisualCollection Children { get; private set; } = null!; - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { - base.RenderCore(canvas, transform); + base.RenderCore(canvas); foreach (var ch in Children) { - var t = transform; - - t = ch.CombinedTransformMatrix * t; - ch.Render(canvas, t); + ch.Render(canvas); } } public override void Update(ServerCompositionTarget root, Matrix4x4 transform) { base.Update(root, transform); - foreach (var child in Children) - child.Update(root, GlobalTransformMatrix); + foreach (var child in Children) + { + if (child.AdornedVisual != null) + root.EnqueueAdornerUpdate(child); + else + child.Update(root, GlobalTransformMatrix); + } } partial void Initialize() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 786779bb2d..5720d80304 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -6,11 +6,10 @@ namespace Avalonia.Rendering.Composition.Server { internal partial class ServerCompositionSolidColorVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { - canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs index c658dc8ae3..2f4c446cfa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionSpriteVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { if (Brush != null) { @@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); } - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 5c7067bfb1..0b2ba6922b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,17 +8,18 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private bool _isBackface; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected virtual void RenderCore(CompositorDrawingContextProxy canvas) { } - public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + public void Render(CompositorDrawingContextProxy canvas) { if(Visible == false) return; if(Opacity == 0) return; + var transform = GlobalTransformMatrix; canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) @@ -29,8 +30,9 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushGeometryClip(Clip); //TODO: Check clip - RenderCore(canvas, transform); + RenderCore(canvas); + // Hack to force invalidation of SKMatrix canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; @@ -60,7 +62,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { // Calculate new parent-relative transform - CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, + // HACK: Ignore RenderTransform set by the adorner layer + AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, Scale, RotationAngle, Orientation, Offset); var newTransform = CombinedTransformMatrix * transform; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index c11ea2c40a..2f4fbbd3f0 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -470,7 +470,7 @@ namespace Avalonia { if (CompositionVisual == null || CompositionVisual.Compositor != compositor) CompositionVisual = new CompositionDrawListVisual(compositor, - new ServerCompositionDrawListVisual(compositor.Server), this); + new ServerCompositionDrawListVisual(compositor.Server, this), this); return CompositionVisual; } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index a7ae341bb3..b86ce35d5f 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 5ad4e39baf..57fb7226e8 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -164,6 +164,9 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { + if (adorner.CompositionVisual != null) + adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) @@ -184,11 +187,18 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(s_adornedElementInfoProperty, info); } - info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => - { - info.Bounds = x; - InvalidateMeasure(); - }); + if (adorner.CompositionVisual != null) + info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x => + { + info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity); + InvalidateMeasure(); + }); + else + info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => + { + info.Bounds = x; + InvalidateMeasure(); + }); } } diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 096864e52a..d1fc691a8b 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -110,6 +110,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public bool Animated { get; set; } [XmlAttribute] public bool InternalSet { get; set; } + [XmlAttribute] + public bool Internal { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 3c38c0331e..f8f86265cf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -378,7 +378,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return client .AddMembers(DeclareField(prop.Type, fieldName)) .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) + .AddModifiers(prop.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, Block(ReturnStatement(IdentifierName(fieldName)))), From 223a67543335365de9e7746ad5e9ad568a64f319 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 18:50:32 +0300 Subject: [PATCH 101/481] IPlatformGpu --- src/Avalonia.Base/Platform/IPlatformGpu.cs | 16 +++++ .../Rendering/Composition/Compositor.cs | 4 +- .../Server/ServerCompositionTarget.cs | 22 +++---- .../Composition/Server/ServerCompositor.cs | 6 +- .../AvaloniaNativePlatformOpenGlInterface.cs | 2 + .../Egl/EglPlatformOpenGlInterface.cs | 2 + src/Avalonia.OpenGL/IGlContext.cs | 3 +- .../IPlatformOpenGlInterface.cs | 6 +- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 2 + src/Avalonia.X11/X11Platform.cs | 6 +- .../OpenGl/WglPlatformOpenGlInterface.cs | 2 + src/Windows/Avalonia.Win32/Win32GlManager.cs | 61 +++++++++++-------- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 +- src/iOS/Avalonia.iOS/EaglDisplay.cs | 2 + 14 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Platform/IPlatformGpu.cs diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs new file mode 100644 index 0000000000..0507dea1d7 --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +[Unstable] +public interface IPlatformGpu +{ + IPlatformGpuContext PrimaryContext { get; } +} + +[Unstable] +public interface IPlatformGpuContext : IDisposable +{ + IDisposable EnsureCurrent(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 96564f0800..03aebba907 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - public Compositor(IRenderLoop loop) + public Compositor(IRenderLoop loop, IPlatformGpu? gpu) { - _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); + _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index fb0d6fce60..d1d7e421a4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -93,12 +93,6 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - // This is a hack to safely dispose layer created by some other render target - // because we can only dispose layers with the corresponding GPU context being - // active on the current thread - while (Compositor.LayersToDispose.Count > 0) - Compositor.LayersToDispose.Dequeue().Dispose(); - var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -164,14 +158,20 @@ namespace Avalonia.Rendering.Composition.Server public void Dispose() { + if(_disposed) + return; _disposed = true; - if (_layer != null) + using (_compositor.GpuContext?.EnsureCurrent()) { - Compositor.LayersToDispose.Enqueue(_layer); - _layer = null; + if (_layer != null) + { + _layer.Dispose(); + _layer = null; + } + + _renderTarget?.Dispose(); + _renderTarget = null; } - _renderTarget?.Dispose(); - _renderTarget = null; } public void AddVisual(ServerCompositionVisual visual) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 241be479ff..c44150f756 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -20,10 +20,12 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; - public Queue LayersToDispose { get; } = new(); + public IPlatformGpuContext? GpuContext { get; } - public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) + public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { + GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; _batchObjectPool = batchObjectPool; _batchMemoryPool = batchMemoryPool; diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index 3b3d8836fd..14d27a90e9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Native @@ -37,6 +38,7 @@ namespace Avalonia.Native internal GlContext MainContext { get; } public IGlContext PrimaryContext => MainContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => true; public bool CanCreateContexts => true; diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs index 476f65a774..a6d8c1e98d 100644 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Logging; +using Avalonia.Platform; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -12,6 +13,7 @@ namespace Avalonia.OpenGL.Egl public EglContext PrimaryEglContext { get; } public IGlContext PrimaryContext => PrimaryEglContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public EglPlatformOpenGlInterface(EglDisplay display) { diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index 50868db873..a52a6535da 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,8 +1,9 @@ using System; +using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IDisposable + public interface IGlContext : IPlatformGpuContext { GlVersion Version { get; } GlInterface GlInterface { get; } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs index 5ee5df1e85..4ff7997b03 100644 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -1,8 +1,10 @@ +using Avalonia.Platform; + namespace Avalonia.OpenGL { - public interface IPlatformOpenGlInterface + public interface IPlatformOpenGlInterface : IPlatformGpu { - IGlContext PrimaryContext { get; } + new IGlContext PrimaryContext { get; } IGlContext CreateSharedContext(); bool CanShareContexts { get; } bool CanCreateContexts { get; } diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 6735a32ffe..0968adc799 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.X11.Glx { @@ -14,6 +15,7 @@ namespace Avalonia.X11.Glx public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } public IGlContext PrimaryContext => DeferredContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public static bool TryInitialize(X11Info x11, IList glProfiles) { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 9c82288c8e..d882450259 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,8 +103,12 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } + var gl = AvaloniaLocator.Current.GetService(); + if (gl != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + if (options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index b948495b99..1d0880a468 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { class WglPlatformOpenGlInterface : IPlatformOpenGlInterface { public WglContext PrimaryContext { get; } + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 2a3f4a3384..39a742d1ac 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,6 +1,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; +using Avalonia.Platform; using Avalonia.Win32.OpenGl; using Avalonia.Win32.WinRT.Composition; @@ -9,45 +10,53 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static void Initialize() + public static IPlatformOpenGlInterface Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => + var gl = InitializeCore(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + return gl; + } + + static IPlatformOpenGlInterface InitializeCore() + { + + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { - var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); - if (opts.UseWgl) - { - var wgl = WglPlatformOpenGlInterface.TryCreate(); - return wgl; - } + var wgl = WglPlatformOpenGlInterface.TryCreate(); + return wgl; + } - if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) - { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + { + var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null) + if (egl != null) + { + if (opts.EglRendererBlacklist != null) { - if (opts.EglRendererBlacklist != null) + foreach (var item in opts.EglRendererBlacklist) { - foreach (var item in opts.EglRendererBlacklist) + if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } + return null; } } - - if (opts.UseWindowsUIComposition) - { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); - } } - return egl; + if (opts.UseWindowsUIComposition) + { + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + } } - return null; - }); + return egl; + } + + return null; } + + } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 4b0350f40f..af38a92e26 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -180,15 +180,15 @@ namespace Avalonia.Win32 .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - Win32GlManager.Initialize(); + var gl = Win32GlManager.Initialize(); _uiThread = Thread.CurrentThread; if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - + if (Options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); } public bool HasMessages() diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index bd1969081d..906bbc29e7 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.Platform; using OpenGLES; namespace Avalonia.iOS @@ -9,6 +10,7 @@ namespace Avalonia.iOS { public IGlContext PrimaryContext => Context; public IGlContext CreateSharedContext() => throw new NotSupportedException(); + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => false; public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); From 05fdc0446416a285c9067b56d858c018e4f7104d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Jun 2022 23:43:55 +0200 Subject: [PATCH 102/481] Add ControlTheme.BasedOn. --- src/Avalonia.Base/Styling/ControlTheme.cs | 26 ++++++-- src/Avalonia.Base/Styling/NestingSelector.cs | 12 +++- src/Avalonia.Base/Styling/Style.cs | 35 ++++++++--- src/Avalonia.Base/Styling/StyleBase.cs | 61 ++++++------------- .../Styling/StyledElementTests_Theming.cs | 59 ++++++++++++++++-- 5 files changed, 129 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 9dcbd7d2c4..aff6fad990 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -23,16 +23,32 @@ namespace Avalonia.Styling /// public Type? TargetType { get; set; } - internal override bool HasSelector => TargetType is not null; + /// + /// Gets or sets a control theme that is the basis of the current theme. + /// + public ControlTheme? BasedOn { get; set; } - internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) + public override SelectorMatchResult TryAttach(IStyleable target, object? host) { + _ = target ?? throw new ArgumentNullException(nameof(target)); + if (TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); - return control.StyleKey == TargetType ? - SelectorMatch.AlwaysThisType : - SelectorMatch.NeverThisType; + var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; + + if (HasSettersOrAnimations && target.StyleKey == TargetType) + { + Attach(target, null); + result = SelectorMatchResult.AlwaysThisType; + } + + var childResult = TryAttachChildren(target, host); + + if (childResult > result) + result = childResult; + + return result; } internal override void SetParent(StyleBase? parent) diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 6d31f7cb18..c8945a713d 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -15,9 +15,17 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { - if (parent is StyleBase s && s.HasSelector) + if (parent is Style s && s.Selector is not null) { - return s.Match(control, null, subscribe); + return s.Selector.Match(control, s.Parent, subscribe); + } + else if (parent is ControlTheme theme) + { + if (theme.TargetType is null) + throw new InvalidOperationException("ControlTheme has no TargetType."); + return control.StyleKey == theme.TargetType ? + SelectorMatch.AlwaysThisType : + SelectorMatch.NeverThisType; } throw new InvalidOperationException( diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index ca20ff2b4b..7a6b746488 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -28,7 +28,32 @@ namespace Avalonia.Styling /// public Selector? Selector { get; set; } - internal override bool HasSelector => Selector is not null; + public override SelectorMatchResult TryAttach(IStyleable target, object? host) + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + + var result = SelectorMatchResult.NeverThisType; + + if (HasSettersOrAnimations) + { + var match = Selector?.Match(target, Parent, true) ?? + (target == host ? + SelectorMatch.AlwaysThisInstance : + SelectorMatch.NeverThisInstance); + + if (match.IsMatch) + Attach(target, match.Activator); + + result = match.Result; + } + + var childResult = TryAttachChildren(target, host); + + if (childResult > result) + result = childResult; + + return result; + } /// /// Returns a string representation of the style. @@ -46,14 +71,6 @@ namespace Avalonia.Styling } } - internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) - { - return Selector?.Match(control, Parent, subscribe) ?? - (control == host ? - SelectorMatch.AlwaysThisInstance : - SelectorMatch.NeverThisInstance); - } - internal override void SetParent(StyleBase? parent) { if (parent is Style parentStyle && parentStyle.Selector is not null) diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index b6bfec62bd..306a4cf010 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +using Avalonia.Styling.Activators; namespace Avalonia.Styling { @@ -64,43 +65,14 @@ namespace Avalonia.Styling bool IResourceNode.HasResources => _resources?.Count > 0; IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); - internal abstract bool HasSelector { get; } + internal bool HasSettersOrAnimations => _setters?.Count > 0 || _animations?.Count > 0; public void Add(ISetter setter) => Setters.Add(setter); public void Add(IStyle style) => Children.Add(style); public event EventHandler? OwnerChanged; - public SelectorMatchResult TryAttach(IStyleable target, object? host) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - var result = SelectorMatchResult.NeverThisType; - - if (_setters?.Count > 0 || _animations?.Count > 0) - { - var match = Match(target, host, subscribe: true); - - if (match.IsMatch) - { - var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); - target.StyleApplied(instance); - instance.Start(); - } - - result = match.Result; - } - - if (_children is not null) - { - _childCache ??= new StyleCache(); - var childResult = _childCache.TryAttach(_children, target, host); - if (childResult > result) - result = childResult; - } - - return result; - } + public abstract SelectorMatchResult TryAttach(IStyleable target, object? host); public bool TryGetResource(object key, out object? result) { @@ -108,19 +80,20 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - /// - /// Evaluates the style's selector against the specified target element. - /// - /// The control. - /// The element that hosts the style. - /// - /// Whether the match should subscribe to changes in order to track the match over time, - /// or simply return an immediate result. - /// - /// - /// A describing how the style matches the control. - /// - internal abstract SelectorMatch Match(IStyleable control, object? host, bool subscribe); + internal void Attach(IStyleable target, IStyleActivator? activator) + { + var instance = new StyleInstance(this, target, _setters, _animations, activator); + target.StyleApplied(instance); + instance.Start(); + } + + internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host) + { + if (_children is null || _children.Count == 0) + return SelectorMatchResult.NeverThisType; + _childCache ??= new StyleCache(); + return _childCache.TryAttach(_children, target, host); + } internal virtual void SetParent(StyleBase? parent) => Parent = parent; diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 0c0808987a..737cf1e048 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -28,10 +28,10 @@ public class StyledElementTests_Theming Assert.NotNull(target.Template); var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); + Assert.Equal(Brushes.Red, border.Background); target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); + Assert.Equal(Brushes.Green, border.Background); } [Fact] @@ -73,7 +73,7 @@ public class StyledElementTests_Theming var root = CreateRoot(target); var canvas = Assert.IsType(target.VisualChild); - Assert.Equal(canvas.Background, Brushes.Red); + Assert.Equal(Brushes.Red, canvas.Background); target.Theme = null; @@ -97,7 +97,28 @@ public class StyledElementTests_Theming var border = Assert.IsType(target.VisualChild); Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); + Assert.Equal(Brushes.Red, border.Background); + } + + [Fact] + public void BasedOn_Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(CreateDerivedTheme()); + + Assert.Null(target.Template); + + var root = CreateRoot(target); + Assert.NotNull(target.Template); + Assert.Equal(Brushes.Blue, target.BorderBrush); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Yellow, border.BorderBrush); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + Assert.Equal(Brushes.Cyan, border.BorderBrush); } private static ThemedControl CreateTarget(ControlTheme? theme = null) @@ -197,6 +218,36 @@ public class StyledElementTests_Theming }; } + private static ControlTheme CreateDerivedTheme() + { + return new ControlTheme + { + TargetType = typeof(ThemedControl), + BasedOn = CreateTheme(), + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Blue), + }, + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Yellow), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Cyan), + } + }, + } + }; + } + private class ThemedControl : TemplatedControl { public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); From f772e5fb5a09c0fdb30a916a0eb21b04a1a73524 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 4 Jun 2022 01:19:31 +0300 Subject: [PATCH 103/481] Don't transitively activate server objects, only objects directly attached to a composition target should receive updates --- .../Rendering/Composition/Server/ServerObject.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index dde711c3b5..f55c9439e4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -92,15 +92,13 @@ namespace Avalonia.Rendering.Composition.Server public void SubscribeToInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - if (store.Subscribers.AddRef(animation)) - Activate(); + store.Subscribers.AddRef(animation); } public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - if (store.Subscribers.ReleaseRef(animation)) - Deactivate(); + store.Subscribers.ReleaseRef(animation); } public virtual int? GetFieldOffset(string fieldName) => null; From 1d1ef5ca9fdb42d2fbf029f122530dcca9b51a59 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2022 12:45:19 +0200 Subject: [PATCH 104/481] Display control themes in devtools. --- src/Avalonia.Base/Styling/ControlTheme.cs | 8 ++++++++ .../Diagnostics/ViewModels/ControlDetailsViewModel.cs | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index aff6fad990..399eb9ae59 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -51,6 +51,14 @@ namespace Avalonia.Styling return result; } + public override string ToString() + { + if (TargetType is not null) + return "ControlTheme: " + TargetType.Name; + else + return "ControlTheme"; + } + internal override void SetParent(StyleBase? parent) { throw new InvalidOperationException("ControlThemes cannot be added as a nested style."); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index e383c160e3..795826e4f6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -67,8 +67,15 @@ namespace Avalonia.Diagnostics.ViewModels var setters = new List(); - if (styleSource is Style style) + if (styleSource is StyleBase style) { + var selector = style switch + { + Style s => s.Selector?.ToString(), + ControlTheme t => t.TargetType?.Name.ToString(), + _ => null, + }; + foreach (var setter in style.Setters) { if (setter is Setter regularSetter @@ -105,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); + AppliedStyles.Add(new StyleViewModel(appliedStyle, selector ?? "No selector", setters)); } } From 95f70143ca319e5e6f49601d1e2de60bdbb39759 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2022 14:53:31 +0200 Subject: [PATCH 105/481] Can apply control theme to derived types. --- src/Avalonia.Base/Styling/ControlTheme.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- .../Styling/StyledElementTests_Theming.cs | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 399eb9ae59..644e8b32d4 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -37,7 +37,7 @@ namespace Avalonia.Styling var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; - if (HasSettersOrAnimations && target.StyleKey == TargetType) + if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey)) { Attach(target, null); result = SelectorMatchResult.AlwaysThisType; diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index c8945a713d..4393d3239f 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -23,7 +23,7 @@ namespace Avalonia.Styling { if (theme.TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); - return control.StyleKey == theme.TargetType ? + return theme.TargetType.IsAssignableFrom(control.StyleKey) ? SelectorMatch.AlwaysThisType : SelectorMatch.NeverThisType; } diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 737cf1e048..ab6c239393 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -34,6 +34,27 @@ public class StyledElementTests_Theming Assert.Equal(Brushes.Green, border.Background); } + [Fact] + public void Theme_Is_Applied_To_Derived_Class_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new DerivedThemedControl + { + Theme = CreateTheme(), + }; + + Assert.Null(target.Template); + + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + } + [Fact] public void Theme_Is_Detached_When_Theme_Property_Cleared() { @@ -252,4 +273,8 @@ public class StyledElementTests_Theming { public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); } + + private class DerivedThemedControl : ThemedControl + { + } } From d21e634ab308c1b63d1e2f2105de29c8af236b2a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2022 12:00:14 +0200 Subject: [PATCH 106/481] Added support for implicit themes. If no `Theme` property is provided, try to look up a resource keyed with the control's `StyleKey`. --- src/Avalonia.Base/StyledElement.cs | 28 ++++++++++++ src/Avalonia.Base/Styling/IStyleable.cs | 4 +- src/Avalonia.Base/Styling/Styler.cs | 4 +- .../Styling/StyledElementTests_Theming.cs | 43 +++++++++++++++++++ .../AvaloniaPropertyConverterTest.cs | 4 +- .../DynamicResourceExtensionTests.cs | 6 ++- 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 75c4b94174..f377eb848c 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -60,6 +60,7 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); + private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; private readonly Classes _classes = new Classes(); @@ -72,6 +73,7 @@ namespace Avalonia private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; private bool _hasPromotedTheme; + private ControlTheme? _implicitTheme; /// /// Initializes static members of the class. @@ -495,6 +497,31 @@ namespace Avalonia }; } + ControlTheme? IStyleable.GetEffectiveTheme() + { + var theme = Theme; + + // Explitly set Theme property takes precedence. + if (theme is not null) + return theme; + + // If the Theme property is not set, try to find a ControlTheme resource with our StyleKey. + if (_implicitTheme is null) + { + var key = ((IStyleable)this).StyleKey; + + if (this.TryFindResource(key, out var value) && value is ControlTheme t) + _implicitTheme = t; + else + _implicitTheme = s_invalidTheme; + } + + if (_implicitTheme != s_invalidTheme) + return _implicitTheme; + + return null; + } + void IStyleable.StyleApplied(IStyleInstance instance) { instance = instance ?? throw new ArgumentNullException(nameof(instance)); @@ -736,6 +763,7 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; + _implicitTheme = null; DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 61fcbdf850..254da4d85c 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -27,9 +27,9 @@ namespace Avalonia.Styling ITemplatedControl? TemplatedParent { get; } /// - /// Gets the theme to be applied to the control. + /// Gets the effective theme for the control as used by the syling system. /// - public ControlTheme? Theme { get; } + ControlTheme? GetEffectiveTheme(); /// /// Notifies the element that a style has been applied. diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index c9ea123bdc..6ac2e8d372 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -11,10 +11,10 @@ namespace Avalonia.Styling // If the control has a themed templated parent then first apply the styles from // the templated parent theme. if (target.TemplatedParent is IStyleable styleableParent) - styleableParent.Theme?.TryAttach(target, styleableParent); + styleableParent.GetEffectiveTheme()?.TryAttach(target, styleableParent); // Next apply the control theme. - target.Theme?.TryAttach(target, target); + target.GetEffectiveTheme()?.TryAttach(target, target); // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index ab6c239393..522937b669 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -158,6 +158,49 @@ public class StyledElementTests_Theming } } + public class ImplicitTheme + { + [Fact] + public void Implicit_Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + } + + [Fact] + public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(((IStyleable)target).GetEffectiveTheme()); + + root.Child = null; + + Assert.Null(((IStyleable)target).GetEffectiveTheme()); + } + + private static ThemedControl CreateTarget() => new ThemedControl(); + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(); + result.Resources.Add(typeof(ThemedControl), CreateTheme()); + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + public class ThemeFromStyle { [Fact] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index ca59fe8480..75e21a7138 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -137,9 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } - public ControlTheme Theme + public ControlTheme GetEffectiveTheme() { - get { throw new NotImplementedException(); } + throw new NotImplementedException(); } public void DetachStyles() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 592dbfc0d1..987725c314 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -845,7 +845,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar", border.Tag); var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; - Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + Assert.Contains("bar", resourceProvider.RequestedResources); + Assert.DoesNotContain("foo", resourceProvider.RequestedResources); } [Fact] @@ -883,7 +884,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar", border.Tag); var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; - Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + Assert.Contains("bar", resourceProvider.RequestedResources); + Assert.DoesNotContain("foo", resourceProvider.RequestedResources); } private IDisposable StyledWindow(params (string, string)[] assets) From 35440b0f2119a527c442a91afdbb254e425c70b2 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 8 Jun 2022 22:53:53 -0400 Subject: [PATCH 107/481] Remove unused field to avoid warnings --- .../ColorSpectrum/ColorSpectrum.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 245592207e..77994aa0c8 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -44,7 +44,6 @@ namespace Avalonia.Controls.Primitives private bool _updatingColor = false; private bool _updatingHsvColor = false; - private bool _isPointerOver = false; private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); @@ -851,7 +850,6 @@ namespace Avalonia.Controls.Primitives /// private void InputTarget_PointerEnter(object? sender, PointerEventArgs args) { - _isPointerOver = true; UpdatePseudoClasses(); args.Handled = true; } @@ -859,7 +857,6 @@ namespace Avalonia.Controls.Primitives /// private void InputTarget_PointerLeave(object? sender, PointerEventArgs args) { - _isPointerOver = false; UpdatePseudoClasses(); args.Handled = true; } From 8b4cf63be3ffbf29427bb16d15ff514c595d348b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2022 11:18:22 +0200 Subject: [PATCH 108/481] Additional validation for ControlTheme children. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- .../Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 12 +--- .../Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 31 ++++++++- src/Avalonia.Base/Styling/Style.cs | 9 ++- src/Avalonia.Base/Styling/Styles.cs | 5 ++ src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- .../Styling/ControlThemeTests.cs | 64 +++++++++++++++++++ 13 files changed, 117 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 34f3a76b61..9512dc34df 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -65,6 +65,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 4ffaff6861..677a924189 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -70,6 +70,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 4393d3239f..77c5b719c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -33,6 +33,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => true; + protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index 76a0690e96..c7727bb6b8 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -67,6 +67,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index 047bf434da..f473791664 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -105,7 +105,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() { diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 913c27bf0c..af9249864f 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -103,18 +103,12 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; + protected override Selector? MovePreviousOrParent() => null; - internal override bool HasValidNestingSelector() + internal override void ValidateNestingSelector(bool inControlTheme) { foreach (var selector in _selectors) - { - if (!selector.HasValidNestingSelector()) - { - return false; - } - } - - return true; + selector.ValidateNestingSelector(inControlTheme); } private Type? EvaluateTargetType() diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 7a37daf087..48136ba2de 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) { diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 1e06f3d375..7ce17518dd 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -86,7 +86,36 @@ namespace Avalonia.Styling /// protected abstract Selector? MovePrevious(); - internal abstract bool HasValidNestingSelector(); + /// + /// Moves to the previous selector or the parent selector. + /// + protected abstract Selector? MovePreviousOrParent(); + + internal virtual void ValidateNestingSelector(bool inControlTheme) + { + var s = this; + var templateCount = 0; + + do + { + if (inControlTheme) + { + if (!s.InTemplate && s.IsCombinator) + throw new InvalidOperationException( + "ControlTheme style may not directly contain a child or descendent selector."); + if (s is TemplateSelector && templateCount++ > 0) + throw new InvalidOperationException( + "ControlTemplate styles cannot contain multiple template selectors."); + } + + var previous = s.MovePreviousOrParent(); + + if (previous is null && s is not NestingSelector) + throw new InvalidOperationException("Child styles must have a nesting selector."); + + s = previous; + } while (s is not null); + } private static SelectorMatch MatchUntilCombinator( IStyleable control, diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 7a6b746488..77c4e62d29 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -77,8 +77,13 @@ namespace Avalonia.Styling { if (Selector is null) throw new InvalidOperationException("Child styles must have a selector."); - if (!Selector.HasValidNestingSelector()) - throw new InvalidOperationException("Child styles must have a nesting selector."); + Selector.ValidateNestingSelector(false); + } + else if (parent is ControlTheme) + { + if (Selector is null) + throw new InvalidOperationException("Child styles must have a selector."); + Selector.ValidateNestingSelector(true); } base.SetParent(parent); diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 4c011f1b0d..3a27275438 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -26,6 +26,11 @@ namespace Avalonia.Styling { _styles.ResetBehavior = ResetBehavior.Remove; _styles.CollectionChanged += OnCollectionChanged; + _styles.Validate = i => + { + if (i is ControlTheme) + throw new InvalidOperationException("ControlThemes cannot be added to a Styles collection."); + }; } public Styles(IResourceHost owner) diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index b0a2dae8d6..278e24a203 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -49,6 +49,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 24d5d6bbbf..6681a7da36 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -140,7 +140,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() { diff --git a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs index 93a0e6c2fd..7a27a02fc4 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Styling; using Xunit; @@ -7,6 +8,15 @@ namespace Avalonia.Base.UnitTests.Styling { public class ControlThemeTests { + [Fact] + public void ControlTheme_Cannot_Be_Added_To_Styles() + { + var target = new ControlTheme(typeof(Button)); + var styles = new Styles(); + + Assert.Throws(() => styles.Add(target)); + } + [Fact] public void ControlTheme_Cannot_Be_Added_To_Style_Children() { @@ -24,5 +34,59 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Throws(() => other.Children.Add(target)); } + + [Fact] + public void Style_Without_Selector_Cannot_Be_Added_To_Children() + { + var target = new ControlTheme(typeof(Button)); + var child = new Style(); + + Assert.Throws(() => target.Children.Add(child)); + } + + [Fact] + public void Style_Without_Nesting_Selector_Cannot_Be_Added_To_Children() + { + var target = new ControlTheme(typeof(Button)); + var child = new Style(x => x.OfType public ColorView() : base() { - CustomPalette = new FluentColorPalette(); } /// diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 4b01de15a0..cf58b882e7 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -81,6 +81,9 @@ - + - + - + + @@ -316,40 +316,40 @@ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> - - + - + - + - + - + - From 58f9b443d9a40aff415d1021835794f003b5c025 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 27 Jun 2022 00:45:54 -0400 Subject: [PATCH 155/481] Implement IsAlphaEnabled using coercion --- .../ColorView/ColorView.Properties.cs | 16 +++-- .../ColorView/ColorView.cs | 66 +++++++++++++++++++ .../Themes/Fluent/ColorView.xaml | 18 ++++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index c7652833bf..e1da427542 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -15,7 +15,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(Color), Colors.White, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceColor) ; /// /// Defines the property. @@ -48,7 +49,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(HsvColor), Colors.White.ToHsv(), - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceHsvColor); /// /// Defines the property. @@ -267,7 +269,8 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component is enabled. - /// When disabled (set to false) the alpha component will be fixed to maximum. + /// When disabled (set to false) the alpha component will be fixed to maximum and + /// editing controls hidden. /// public bool IsAlphaEnabled { @@ -277,7 +280,8 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component editing controls - /// (both Slider and TextBox) are visible. + /// (Slider(s) and TextBox) are visible. When hidden, the existing alpha component + /// value is maintained. /// /// /// Note that also controls the alpha @@ -353,7 +357,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with . + /// controlled with and . /// public bool IsComponentSliderVisible { @@ -366,7 +370,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with . + /// controlled with and . /// public bool IsComponentTextInputVisible { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 4f95b4acfe..8fb80980d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -237,6 +237,12 @@ namespace Avalonia.Controls PaletteColors = newPaletteColors; } } + else if (change.Property == IsAlphaEnabledProperty) + { + // Manually coerce the HsvColor value + // (Color will be coerced automatically if HsvColor changes) + HsvColor = OnCoerceHsvColor(HsvColor); + } else if (change.Property == IsColorComponentsVisibleProperty || change.Property == IsColorPaletteVisibleProperty || change.Property == IsColorSpectrumVisibleProperty) @@ -271,6 +277,66 @@ namespace Avalonia.Controls ColorChanged?.Invoke(this, e); } + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual Color OnCoerceColor(Color value) + { + if (IsAlphaEnabled == false) + { + return new Color(255, value.R, value.G, value.B); + } + + return value; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual HsvColor OnCoerceHsvColor(HsvColor value) + { + if (IsAlphaEnabled == false) + { + return new HsvColor(1.0, value.H, value.S, value.V); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static Color CoerceColor(IAvaloniaObject instance, Color value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceColor(value); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static HsvColor CoerceHsvColor(IAvaloniaObject instance, HsvColor value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceHsvColor(value); + } + + return value; + } + /// /// Event handler for when a key is pressed within the Hex RGB value TextBox. /// This is used to trigger re-evaluation of the color based on the TextBox value. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index af2c8e061f..97760ba94a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -187,8 +187,16 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="12,0,0,0" - IsVisible="{TemplateBinding IsAlphaVisible}" /> + Margin="12,0,0,0"> + + + + + + + @@ -565,6 +573,8 @@ VerticalAlignment="Center" /> + + + Date: Mon, 27 Jun 2022 00:49:59 -0400 Subject: [PATCH 156/481] Only set slider IsVisible once --- src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 97760ba94a..d43bc7e004 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -618,8 +618,7 @@ ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" HorizontalAlignment="Stretch" - VerticalAlignment="Center" - IsVisible="{TemplateBinding IsAlphaVisible}"> + VerticalAlignment="Center"> Date: Mon, 27 Jun 2022 00:53:49 -0400 Subject: [PATCH 157/481] Only disable alpha editing controls when IsAlphaEnabled is false Rely on IsAlphaVisible for visibility --- .../ColorView/ColorView.Properties.cs | 6 +++--- .../Themes/Fluent/ColorView.xaml | 20 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index e1da427542..d59c4d544a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -270,7 +270,7 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component is enabled. /// When disabled (set to false) the alpha component will be fixed to maximum and - /// editing controls hidden. + /// editing controls disabled. /// public bool IsAlphaEnabled { @@ -357,7 +357,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with and . + /// controlled with . /// public bool IsComponentSliderVisible { @@ -370,7 +370,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with and . + /// controlled with . /// public bool IsComponentTextInputVisible { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index d43bc7e004..563f230cc9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -187,11 +187,10 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="12,0,0,0"> + Margin="12,0,0,0" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - @@ -564,7 +563,8 @@ BorderBrush="{DynamicResource TextControlBorderBrush}" BorderThickness="1,1,0,1" CornerRadius="4,0,0,4" - VerticalAlignment="Center"> + VerticalAlignment="Center" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - + Value="{Binding Value, ElementName=AlphaComponentSlider}" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - + VerticalAlignment="Center" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - Date: Mon, 27 Jun 2022 00:56:56 -0400 Subject: [PATCH 158/481] Remove unused ValueConverterGroup --- .../Converters/ValueConverterGroup.cs | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs deleted file mode 100644 index 2710c220f4..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Primitives.Converters -{ - /// - /// Converter to chain together multiple converters. - /// - public class ValueConverterGroup : List, IValueConverter - { - /// - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = 0; i < Count; i++) - { - curValue = this[i].Convert(curValue, targetType, parameter, culture); - } - - return curValue; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = (Count - 1); i >= 0; i--) - { - curValue = this[i].ConvertBack(curValue, targetType, parameter, culture); - } - - return curValue; - } - } -} From f4a38437314b00f99122c2b36d14a016c26a8c52 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 27 Jun 2022 09:49:21 +0200 Subject: [PATCH 159/481] Fix unit test --- tests/Avalonia.Controls.UnitTests/TextBlockTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 6da011f062..37dde9fbac 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -20,13 +20,11 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Default_Text_Value_Should_Be_EmptyString() + public void Default_Text_Value_Should_Be_Null() { var textBlock = new TextBlock(); - Assert.Equal( - "", - textBlock.Text); + Assert.Equal(null, textBlock.Text); } [Fact] From bf7f50690f6d0b18516daaba971a44bd17570c33 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Jun 2022 19:06:32 +0200 Subject: [PATCH 160/481] Don't promote layout property values to local values. When DevTools was opened, all layout property values were being promoted to `LocalValue`s because the `_updatingFromControl` flag was not being set during initialization, causing the initial values to be written back out to the control as local values. --- .../ViewModels/ControlLayoutViewModel.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 0c0c005122..a453fef212 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -30,20 +30,28 @@ namespace Avalonia.Diagnostics.ViewModels if (control is AvaloniaObject ao) { - MarginThickness = ao.GetValue(Layoutable.MarginProperty); - - if (HasPadding) + try { - PaddingThickness = ao.GetValue(Decorator.PaddingProperty); - } + _updatingFromControl = true; + MarginThickness = ao.GetValue(Layoutable.MarginProperty); + + if (HasPadding) + { + PaddingThickness = ao.GetValue(Decorator.PaddingProperty); + } - if (HasBorder) + if (HasBorder) + { + BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + } + + HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); + VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); + } + finally { - BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + _updatingFromControl = false; } - - HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); - VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); } UpdateSize(); From 1849ca4caa660a172fa16a3e616a58be84d12340 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 Jun 2022 23:50:22 +0300 Subject: [PATCH 161/481] Fixed matrix multiplication order --- .../Rendering/Composition/Server/DrawingContextProxy.cs | 4 ++-- .../Rendering/Composition/Server/ServerVisual.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 2fd87f6620..e261507f60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -33,7 +33,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont set => _visualBrushRenderer.VisualBrushDrawList = value; } - public Matrix PreTransform { get; set; } = Matrix.Identity; + public Matrix PostTransform { get; set; } = Matrix.Identity; public void Dispose() { @@ -44,7 +44,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public Matrix Transform { get => _transform; - set => _impl.Transform = PreTransform * (_transform = value); + set => _impl.Transform = (_transform = value) * PostTransform; } public void Clear(Color color) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 06f087258b..3b36dfb87e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -41,7 +41,7 @@ namespace Avalonia.Rendering.Composition.Server Root!.RenderedVisuals++; var transform = GlobalTransformMatrix; - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -56,7 +56,7 @@ namespace Avalonia.Rendering.Composition.Server RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (OpacityMaskBrush != null) From f6506f19e83d6f8456a86d749324b18bb1cba4ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 Jun 2022 23:57:03 +0300 Subject: [PATCH 162/481] Fixed Ref finalizer --- src/Avalonia.Base/Utilities/Ref.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index 7209f02720..95a1c23883 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -159,7 +159,7 @@ namespace Avalonia.Utilities ~Ref() { - _counter?.Release(); + Dispose(); } public T Item From b4512e0da8f2334b1a1b2826e600f2386a30a4af Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Jun 2022 00:05:10 +0300 Subject: [PATCH 163/481] Clear the draw list before letting go of the visual --- src/Avalonia.Base/Visual.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7db218d2df..716b5f261d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -495,7 +495,12 @@ namespace Avalonia DisableTransitions(); OnDetachedFromVisualTree(e); - CompositionVisual = null; + if (CompositionVisual != null) + { + CompositionVisual.DrawList = null; + CompositionVisual = null; + } + DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); From 2edc2ec425c4b1373e08204d7ab01448ca48bca7 Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Thu, 23 Jun 2022 02:42:15 +0700 Subject: [PATCH 164/481] Prevent input focus leaving the TextBox on first click --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor | 3 ++- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor index 68662bd931..b501db16d3 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor @@ -5,7 +5,8 @@ @onpointerdown="OnPointerDown" @onpointerup="OnPointerUp" @onpointermove="OnPointerMove" - @onpointercancel="OnPointerCancel"> + @onpointercancel="OnPointerCancel" + @onfocus="OnFocus"> diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index b575bc6dbb..d42529985b 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -37,6 +37,7 @@ namespace Avalonia.Web.Blazor private const SKColorType ColorType = SKColorType.Rgba8888; private bool _initialised; + private bool _inputElementFocused; [Inject] private IJSRuntime Js { get; set; } = null!; @@ -221,6 +222,16 @@ namespace Avalonia.Web.Blazor _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e)); } + private void OnFocus(FocusEventArgs e) + { + // if focus has unexpectedly moved from the input element to the container element, + // shift it back to the input element + if (_inputElementFocused && _inputHelper is not null) + { + _inputHelper.Focus(); + } + } + private void OnInput(ChangeEventArgs e) { if (e.Value != null) @@ -374,10 +385,12 @@ namespace Avalonia.Web.Blazor if (active) { _inputHelper.Show(); + _inputElementFocused = true; _inputHelper.Focus(); } else { + _inputElementFocused = false; _inputHelper.Hide(); } } From 508fad9d2c333fdca885dbb652665fd994f07d2f Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Wed, 15 Jun 2022 17:27:16 +0700 Subject: [PATCH 165/481] Enable use of Skia Raster backend for HTML canvas in Blazor To enable the raster backend set CustomGpuFactory to null in the existing SkiaOptions, by default Avalonia will use the GPU/GL Skia backend. --- samples/ControlCatalog.Web/App.razor.cs | 1 + .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 57 ++++++++---- .../BlazorSkiaRasterSurface.cs | 91 +++++++++++++++++++ .../Avalonia.Web.Blazor/BlazorSkiaSurface.cs | 2 +- .../Avalonia.Web.Blazor/IBlazorSkiaSurface.cs | 9 ++ .../RazorViewTopLevelImpl.cs | 7 +- 6 files changed, 149 insertions(+), 18 deletions(-) create mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs create mode 100644 src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs index c0b7ddbe1e..560e8079a6 100644 --- a/samples/ControlCatalog.Web/App.razor.cs +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -11,6 +11,7 @@ public partial class App { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); }) + //.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering .SetupWithSingleViewLifetime(); base.OnParametersSet(); diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index b575bc6dbb..0e5580ebe4 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -37,6 +37,7 @@ namespace Avalonia.Web.Blazor private const SKColorType ColorType = SKColorType.Rgba8888; private bool _initialised; + private bool _useGL; [Inject] private IJSRuntime Js { get; set; } = null!; @@ -261,25 +262,44 @@ namespace Avalonia.Web.Blazor _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); Console.WriteLine("Interop created"); - _jsGlInfo = _interop.InitGL(); - - Console.WriteLine("jsglinfo created - init gl"); + + var skiaOptions = AvaloniaLocator.Current.GetService(); + _useGL = skiaOptions?.CustomGpuFactory != null; - // create the SkiaSharp context - if (_context == null) + if (_useGL) + { + _jsGlInfo = _interop.InitGL(); + Console.WriteLine("jsglinfo created - init gl"); + } + else { - Console.WriteLine("create glcontext"); - _glInterface = GRGlInterface.Create(); - _context = GRContext.CreateGl(_glInterface); - - var options = AvaloniaLocator.Current.GetService(); - // bump the default resource cache limit - _context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); - Console.WriteLine("glcontext created and resource limit set"); + var rasterInitialized = _interop.InitRaster(); + Console.WriteLine("raster initialized: {0}", rasterInitialized); } - _topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, - new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + if (_useGL) + { + // create the SkiaSharp context + if (_context == null) + { + Console.WriteLine("create glcontext"); + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + + // bump the default resource cache limit + _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); + Console.WriteLine("glcontext created and resource limit set"); + } + + _topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + } + else + { + _topLevelImpl.SetSurface(ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + } _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); @@ -301,7 +321,12 @@ namespace Avalonia.Web.Blazor private void OnRenderFrame() { - if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) { Console.WriteLine("nothing to render"); return; diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs new file mode 100644 index 0000000000..a286affe04 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs @@ -0,0 +1,91 @@ +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + { + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + + private FramebufferData? _fbData; + private readonly Action _blitCallback; + private readonly Action _onDisposeAction; + + public BlazorSkiaRasterSurface( + SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + ColorType = colorType; + Size = size; + Scaling = scaling; + _blitCallback = blitCallback; + _onDisposeAction = Blit; + } + + public void Dispose() + { + _fbData?.Dispose(); + _fbData = null; + } + + public ILockedFramebuffer Lock() + { + var bytesPerPixel = 4; // TODO: derive from ColorType + var dpi = Scaling * 96.0; + var width = (int)(Size.Width * Scaling); + var height = (int)(Size.Height * Scaling); + + if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) + { + _fbData?.Dispose(); + _fbData = new FramebufferData(width, height, bytesPerPixel); + } + + var pixelFormat = ColorType.ToPixelFormat(); + var data = _fbData.Value; + return new LockedFramebuffer( + data.Address, data.Size, data.RowBytes, + new Vector(dpi, dpi), pixelFormat, _onDisposeAction); + } + + private void Blit() + { + if (_fbData != null) + { + var data = _fbData.Value; + _blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); + } + } + + private readonly struct FramebufferData + { + private readonly byte[]? _data; + private readonly GCHandle _dataHandle; + + public PixelSize Size { get; } + + public int RowBytes { get; } + + public IntPtr Address => _dataHandle.AddrOfPinnedObject(); + + public FramebufferData(int width, int height, int bytesPerPixel) + { + Size = new PixelSize(width, height); + RowBytes = width * bytesPerPixel; + _data = new byte[width * height * bytesPerPixel]; + _dataHandle = GCHandle.Alloc(_data, GCHandleType.Pinned); + } + + public void Dispose() + { + _dataHandle.Free(); + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs index 512309cfe3..fb49df338b 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs @@ -3,7 +3,7 @@ using SkiaSharp; namespace Avalonia.Web.Blazor { - internal class BlazorSkiaSurface + internal class BlazorSkiaSurface : IBlazorSkiaSurface { public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) { diff --git a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs new file mode 100644 index 0000000000..5463893e27 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Web.Blazor +{ + internal interface IBlazorSkiaSurface + { + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index a8a1a970dc..e240f1554e 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -16,7 +16,7 @@ namespace Avalonia.Web.Blazor internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost { private Size _clientSize; - private BlazorSkiaSurface? _currentSurface; + private IBlazorSkiaSurface? _currentSurface; private IInputRoot? _inputRoot; private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly AvaloniaView _avaloniaView; @@ -40,6 +40,11 @@ namespace Avalonia.Web.Blazor new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); } + internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + _currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + } + public void SetClientSize(SKSize size, double dpi) { var newSize = new Size(size.Width, size.Height); From e50b416d5bf1529544830d5a291a1fb2f1b0da64 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Jun 2022 12:50:52 +0200 Subject: [PATCH 166/481] Fix parent selectors with /template/ at end. Previously the combinator state was lost when traversing nested selectors. To fix it, instead of running the parent selector in `NestingSelector.Evaluate`, return the parent selector with `MovePrevious`. This required `MovePrevious` to be aware of parent styles and because I had to change its signature, I also made it internal as it doesn't need to be a public API. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- .../Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 8 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 2 +- .../Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 12 ++- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- .../Styling/SelectorTests_Nesting.cs | 80 +++++++++++++++++++ 11 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 9512dc34df..bc1d257ce6 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -64,7 +64,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 677a924189..3a16574e04 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -69,7 +69,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 77c5b719c6..dd0bac31c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -17,7 +17,7 @@ namespace Avalonia.Styling { if (parent is Style s && s.Selector is not null) { - return s.Selector.Match(control, s.Parent, subscribe); + return SelectorMatch.AlwaysThisType; } else if (parent is ControlTheme theme) { @@ -32,7 +32,11 @@ namespace Avalonia.Styling "Nesting selector was specified but cannot determine parent selector."); } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? parent) + { + return parent is Style parentStyle ? (parentStyle.Selector, parentStyle.Parent) : (null, null); + } + protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index c7727bb6b8..ebde392a3b 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -66,7 +66,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index f473791664..e6d9cf58a9 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -104,7 +104,7 @@ namespace Avalonia.Styling return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index af9249864f..115e0aeb95 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -102,7 +102,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => null; internal override void ValidateNestingSelector(bool inControlTheme) diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 48136ba2de..96f8c8dfeb 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -89,7 +89,7 @@ namespace Avalonia.Styling } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 7ce17518dd..cc8598c5e3 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -84,7 +84,13 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - protected abstract Selector? MovePrevious(); + /// + /// The parent style, if the selector is on a nested style. + /// + /// + /// The previous selector, and its nesting parent. + /// + private protected abstract (Selector?, IStyle?) MovePrevious(IStyle? nestingParent); /// /// Moves to the previous selector or the parent selector. @@ -142,14 +148,14 @@ namespace Avalonia.Styling ref AndActivatorBuilder activators, ref Selector? combinator) { - var previous = selector.MovePrevious(); + var (previous, previousParent) = selector.MovePrevious(parent); // Selectors are stored from right-to-left, so we recurse into the selector in order to // reverse this order, because the type selector will be on the left and is our best // opportunity to exit early. if (previous != null && !previous.IsCombinator) { - var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); + var previousMatch = Match(control, previous, previousParent, subscribe, ref activators, ref combinator); if (previousMatch < SelectorMatchResult.Sometimes) { diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 278e24a203..a68b7003b8 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -48,7 +48,7 @@ namespace Avalonia.Styling return _parent.Match(templatedParent, parent, subscribe); } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 6681a7da36..3a2150b1e9 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -139,7 +139,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index 1520dc329d..bd7f0338e6 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Templates; using Avalonia.Styling; using Avalonia.Styling.Activators; using Xunit; @@ -148,6 +149,85 @@ namespace Avalonia.Base.UnitTests.Styling control.Classes.Remove("foo"); Assert.False(sink.Active); } + + [Fact] + public void Template_Nesting_OfType_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + var button = new Button + { + Template = new FuncControlTemplate((x, _) => control), + }; + + button.ApplyTemplate(); + + Style nested; + var parent = new Style(x => x.OfType /// The control. - private void ApplyTemplatedParent(IControl control) + internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { - control.SetValue(TemplatedParentProperty, this); + control.SetValue(TemplatedParentProperty, templatedParent); var children = control.LogicalChildren; var count = children.Count; for (var i = 0; i < count; i++) { - if (children[i] is IControl child) + if (children[i] is IStyledElement child) { - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, templatedParent); } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 91c93c87c8..bb18bf4c64 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -271,8 +271,9 @@ namespace Avalonia.Controls _popupHost = OverlayPopupHost.CreatePopupHost(control, null); _popupHost.SetChild(this); ((ISetLogicalParent)_popupHost).SetParent(control); - - _popupHost.ConfigurePosition(control, GetPlacement(control), + ApplyTemplatedParent(this, control.TemplatedParent); + + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); WindowManagerAddShadowHintChanged(_popupHost, false); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..6fb7b1448c 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -877,6 +878,110 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ToolTip_Is_Freed() + { + using (Start()) + { + Func run = () => + { + var window = new Window(); + var source = new Button + { + Template = new FuncControlTemplate /// The other geometry. /// A new representing the intersection. - IGeometryImpl Intersect(IGeometryImpl geometry); + IGeometryImpl? Intersect(IGeometryImpl geometry); /// /// Indicates whether the geometry's stroke contains the specified point. From 14a7ee7e319b7a26d156770fc2b6c0ccb5243a14 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Mon, 4 Jul 2022 15:07:23 +0200 Subject: [PATCH 188/481] Extend docs --- src/Avalonia.Base/Platform/IGeometryImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index 8760f9ed8a..5826cfb2ff 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -38,7 +38,7 @@ namespace Avalonia.Platform /// Intersects the geometry with another geometry. /// /// The other geometry. - /// A new representing the intersection. + /// A new representing the intersection or null when the operation failed. IGeometryImpl? Intersect(IGeometryImpl geometry); /// From 423dacda5bf736067aa281ec86d85bd284d321ba Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 14:36:58 +0100 Subject: [PATCH 189/481] [OSX] popups shouldnt have shadow --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 1 - native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 45d78b3926..77f53332cd 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -48,7 +48,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, [Window setContentMaxSize:lastMaxSize]; [Window setOpaque:false]; - [Window setHasShadow:true]; } HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 85a89955f4..95f61422cb 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -24,6 +24,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase _lastTitle = @""; _parent = nullptr; WindowEvents = events; + + [Window setHasShadow:true]; OnInitialiseNSWindow(); } From b95e377a7ea2d0af6f7ae9aa9cd149a8b6fe3933 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Jul 2022 17:30:57 +0200 Subject: [PATCH 190/481] Added failing batch update test. --- .../AvaloniaObjectTests_BatchUpdate.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 01d5752ead..45de860894 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -611,6 +611,36 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foo", notifications[1].NewValue); } + [Fact] + public void Can_Run_Empty_Batch_Update_When_Ending_Batch_Update() + { + var target = new TestClass(); + var raised = 0; + var notifications = new List(); + + target.Foo = "foo"; + target.Bar = "bar"; + + target.BeginBatchUpdate(); + target.ClearValue(TestClass.FooProperty); + target.ClearValue(TestClass.BarProperty); + target.PropertyChanged += (sender, e) => + { + if (e.Property == TestClass.BarProperty) + { + target.BeginBatchUpdate(); + target.EndBatchUpdate(); + } + + ++raised; + }; + target.EndBatchUpdate(); + + Assert.Null(target.Foo); + Assert.Null(target.Bar); + Assert.Equal(2, raised); + } + public class TestClass : AvaloniaObject { public static readonly StyledProperty FooProperty = From 259c2d8020c3891f2f3394290568e558e9544621 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Jul 2022 17:32:38 +0200 Subject: [PATCH 191/481] Remove check for invalid state. This state is not actually invalid: it can happen when running an empty batch update of >1 value which causes new, empty batch update to run in a `PropertyChanged` handler triggered from the first batch update. --- src/Avalonia.Base/ValueStore.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 69c644dff9..bf29e0b0ac 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -462,10 +462,6 @@ namespace Avalonia values.Remove(entry.property); } } - else - { - throw new AvaloniaInternalException("Value could not be found at the end of batch update."); - } // If a new batch update was started while ending this one, abort. if (_batchUpdateCount > 0) From 4d23058c9b940bd993ba7e08536e4725c1deb14c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Jul 2022 17:37:04 +0200 Subject: [PATCH 192/481] Don't run an unnecessary batch update. If there are no styles to detach, there's no reason to run a batch update. --- src/Avalonia.Base/StyledElement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 189d73c502..ecf5d95ffc 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -832,7 +832,7 @@ namespace Avalonia private void DetachStyles() { - if (_appliedStyles is object) + if (_appliedStyles?.Count > 0) { BeginBatchUpdate(); From c7b65512bdc42cf6a6a9c1eb96a53c2134b35689 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Mon, 4 Jul 2022 18:46:39 +0200 Subject: [PATCH 193/481] Some QOL for DevTools navigate feature --- .../ViewModels/ControlDetailsViewModel.cs | 108 ++++++++++++------ .../Diagnostics/Views/ControlDetailsView.xaml | 6 +- .../Views/ControlDetailsView.xaml.cs | 2 +- 3 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index e383c160e3..f8e2e0544f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -113,6 +113,8 @@ namespace Avalonia.Diagnostics.ViewModels } } + public bool CanNavigateToParentProperty => _selectedEntitiesStack.Count >= 1; + private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { if (value is StaticResourceExtension staticResource) @@ -415,7 +417,14 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void ApplySelectedProperty() + private static IEnumerable GetAllPublicProperties(Type type) + { + return type + .GetProperties() + .Concat(type.GetInterfaces().SelectMany(i => i.GetProperties())); + } + + public void NavigateToSelectedProperty() { var selectedProperty = SelectedProperty; var selectedEntity = SelectedEntity; @@ -423,72 +432,103 @@ namespace Avalonia.Diagnostics.ViewModels if (selectedEntity == null || selectedProperty == null || selectedProperty.PropertyType == typeof(string) - || selectedProperty.PropertyType.IsValueType - ) + || selectedProperty.PropertyType.IsValueType) return; - object? property; - if (selectedProperty.Key is AvaloniaProperty avaloniaProperty) + object? property = null; + + switch (selectedProperty) { - property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty); + case AvaloniaPropertyViewModel avaloniaProperty: + + property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty.Property); + + break; + + case ClrPropertyViewModel clrProperty: + { + property = GetAllPublicProperties(selectedEntity.GetType()) + .FirstOrDefault(pi => clrProperty.Property == pi)? + .GetValue(selectedEntity); + + break; + } } - else + + if (property == null) + return; + + _selectedEntitiesStack.Push((Name:selectedEntityName!, Entry:selectedEntity)); + + var propertyName = selectedProperty.Name; + + //Strip out interface names + if (propertyName.LastIndexOf('.') is var p && p != -1) { - property = selectedEntity.GetType().GetProperties() - .FirstOrDefault(pi => pi.Name == selectedProperty.Name - && pi.DeclaringType == selectedProperty.DeclaringType - && pi.PropertyType.Name == selectedProperty.PropertyType.Name) - ?.GetValue(selectedEntity); + propertyName = propertyName.Substring(p + 1); } - if (property == null) return; - _selectedEntitiesStack.Push((Name:selectedEntityName!,Entry:selectedEntity)); - NavigateToProperty(property, selectedProperty.Name); + + NavigateToProperty(property, selectedEntityName + "." + propertyName); + + RaisePropertyChanged(nameof(CanNavigateToParentProperty)); } - public void ApplyParentProperty() + public void NavigateToParentProperty() { - if (_selectedEntitiesStack.Any()) + if (_selectedEntitiesStack.Count > 0) { var property = _selectedEntitiesStack.Pop(); NavigateToProperty(property.Entry, property.Name); + + RaisePropertyChanged(nameof(CanNavigateToParentProperty)); } } - protected void NavigateToProperty(object o, string? entityName) + protected void NavigateToProperty(object o, string? entityName) { var oldSelectedEntity = SelectedEntity; - if (oldSelectedEntity is IAvaloniaObject ao1) - { - ao1.PropertyChanged -= ControlPropertyChanged; - } - else if (oldSelectedEntity is INotifyPropertyChanged inpc1) + + switch (oldSelectedEntity) { - inpc1.PropertyChanged -= ControlPropertyChanged; + case IAvaloniaObject ao1: + ao1.PropertyChanged -= ControlPropertyChanged; + break; + + case INotifyPropertyChanged inpc1: + inpc1.PropertyChanged -= ControlPropertyChanged; + break; } - + SelectedEntity = o; SelectedEntityName = entityName; SelectedEntityType = o.ToString(); + var properties = GetAvaloniaProperties(o) .Concat(GetClrProperties(o, _showImplementedInterfaces)) .OrderBy(x => x, PropertyComparer.Instance) .ThenBy(x => x.Name) .ToArray(); - _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToArray()); + _propertyIndex = properties + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.ToArray()); + + TreePage.PropertiesFilter.FilterString = string.Empty; var view = new DataGridCollectionView(properties); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); view.Filter = FilterProperty; PropertiesView = view; - if (o is IAvaloniaObject ao2) + switch (o) { - ao2.PropertyChanged += ControlPropertyChanged; - } - else if (o is INotifyPropertyChanged inpc2) - { - inpc2.PropertyChanged += ControlPropertyChanged; + case IAvaloniaObject ao2: + ao2.PropertyChanged += ControlPropertyChanged; + break; + + case INotifyPropertyChanged inpc2: + inpc2.PropertyChanged += ControlPropertyChanged; + break; } } @@ -498,7 +538,9 @@ namespace Avalonia.Diagnostics.ViewModels if (SelectedEntity != _avaloniaObject) { - NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); + NavigateToProperty( + _avaloniaObject, + (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); } if (PropertiesView is null) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index cc392853be..a426b387f7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -30,7 +30,11 @@ - public string? DefaultExtension { get; set; } - /// - /// Gets or sets the initial location where the file open picker looks for files to present to the user. - /// - public IStorageFolder? SuggestedStartLocation { get; set; } - /// /// Gets or sets the collection of valid file types that the user can choose to assign to a file. /// diff --git a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs index de90da30b2..df9fa8fd35 100644 --- a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs @@ -3,20 +3,10 @@ /// /// Options class for method. /// -public class FolderPickerOpenOptions +public class FolderPickerOpenOptions : PickerOptions { - /// - /// Gets or sets the text that appears in the title bar of a folder dialog. - /// - public string? Title { get; set; } - /// /// Gets or sets an option indicating whether open picker allows users to select multiple folders. /// public bool AllowMultiple { get; set; } - - /// - /// Gets or sets the initial location where the file open picker looks for files to present to the user. - /// - public IStorageFolder? SuggestedStartLocation { get; set; } } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index 65811b7fbd..d21c950862 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -17,5 +17,4 @@ public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem [NotClientImplementable] public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem { - } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 2f12514e50..965caf8216 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -14,7 +14,7 @@ public interface IStorageFile : IStorageItem /// Returns true, if file is readable. /// bool CanOpenRead { get; } - + /// /// Opens a stream for read access. /// @@ -24,9 +24,9 @@ public interface IStorageFile : IStorageItem /// Returns true, if file is writeable. /// bool CanOpenWrite { get; } - + /// /// Opens stream for writing to the file. /// Task OpenWrite(); -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 83b316bc3b..25b9f01a92 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -8,5 +8,4 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFolder : IStorageItem { - -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 078311a286..8513ebc7d9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -27,7 +27,7 @@ public interface IStorageItem : IDisposable /// Browser and iOS backends might return relative uris. /// bool TryGetUri([NotNullWhen(true)] out Uri? uri); - + /// /// Gets the basic properties of the current item. /// @@ -37,7 +37,7 @@ public interface IStorageItem : IDisposable /// Returns true is item can be bookmarked and reused later. /// bool CanBookmark { get; } - + /// /// Saves items to a bookmark. /// diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 32fb148790..0f5cf931d9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -11,7 +11,7 @@ public interface IStorageProvider /// Returns true if it's possible to open file picker on the current platform. /// bool CanOpen { get; } - + /// /// Opens file picker dialog. /// @@ -22,7 +22,7 @@ public interface IStorageProvider /// Returns true if it's possible to open save file picker on the current platform. /// bool CanSave { get; } - + /// /// Opens save file picker dialog. /// @@ -33,13 +33,13 @@ public interface IStorageProvider /// Returns true if it's possible to open folder picker on the current platform. /// bool CanPickFolder { get; } - + /// /// Opens folder picker dialog. /// /// Array of selected or empty collection if user canceled the dialog. Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); - + /// /// Open from the bookmark ID. /// diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs new file mode 100644 index 0000000000..6f97916a26 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Common options for , and methods. +/// +public class PickerOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a folder dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs index 41b9bfa941..a63973ccf8 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs @@ -16,7 +16,7 @@ public class StorageItemProperties DateCreated = dateCreated; DateModified = dateModified; } - + /// /// Gets the size of the file in bytes. /// @@ -40,4 +40,4 @@ public class StorageItemProperties /// Can be null if property is not available. /// public DateTimeOffset? DateModified { get; } -} \ No newline at end of file +} From 6370ae38de78ab9c4bafa343e8870ee6d531b49c Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 5 Jul 2022 21:58:26 -0400 Subject: [PATCH 208/481] Switch to TemplateBinding in ColorPicker where possible --- .../Themes/Fluent/ColorPicker.xaml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 878d7819eb..46d627b3a1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -49,31 +49,31 @@ + Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}" + ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}" + HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}" + IsAlphaVisible="{TemplateBinding IsAlphaVisible}" + IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}" + IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}" + IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}" + IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}" + IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}" + IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}" + IsHexInputVisible="{TemplateBinding IsHexInputVisible}" + MaxHue="{TemplateBinding MaxHue}" + MaxSaturation="{TemplateBinding MaxSaturation}" + MaxValue="{TemplateBinding MaxValue}" + MinHue="{TemplateBinding MinHue}" + MinSaturation="{TemplateBinding MinSaturation}" + MinValue="{TemplateBinding MinValue}" + PaletteColors="{TemplateBinding PaletteColors}" + PaletteColumnCount="{TemplateBinding PaletteColumnCount}" + Palette="{TemplateBinding Palette}" + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ShowAccentColors="{TemplateBinding ShowAccentColors}" /> From a24720816f593e24bda2dd58ff6b6a56cdb23fd1 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 5 Jul 2022 22:12:18 -0400 Subject: [PATCH 209/481] Standardize ColorControl resource names This follows the Fluent standard where the resource is prefixed with the control itself --- .../Themes/Default/ColorPreviewer.xaml | 18 +++++------ .../Themes/Default/ColorSlider.xaml | 4 +-- .../Themes/Default/Default.xaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorPreviewer.xaml | 18 +++++------ .../Themes/Fluent/ColorSlider.xaml | 4 +-- .../Themes/Fluent/ColorView.xaml | 30 +++++++++---------- .../Themes/Fluent/Fluent.xaml | 2 +- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 24242fc251..6ac0982408 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -9,8 +9,8 @@ - 80 - 40 + 80 + 40 + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 867e0f223f..9e795d81a2 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -13,6 +13,8 @@ + + 48 30 @@ -90,29 +92,26 @@ - - + + - - - + CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}" + Background="Transparent" + BorderBrush="Transparent" + BorderThickness="0,1,0,0" /> Date: Tue, 5 Jul 2022 23:32:35 -0400 Subject: [PATCH 211/481] Add code to hide the tab strip when only one tab is visible --- .../ColorView/ColorView.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 8fb80980d5..bea982b7ea 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -4,9 +4,11 @@ using System.Collections.ObjectModel; using System.Globalization; using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -82,19 +84,19 @@ namespace Avalonia.Controls if (_tabControl != null && _tabControl.Items != null) { - // Determine if any item is visible - bool isAnyItemVisible = false; + // Determine the number of visible tab items + int numVisibleItems = 0; foreach (var item in _tabControl.Items) { if (item is Control control && control.IsVisible) { - isAnyItemVisible = true; - break; + numVisibleItems++; } } - if (isAnyItemVisible) + // Verify the selection + if (numVisibleItems > 0) { object? selectedItem = null; @@ -140,6 +142,23 @@ namespace Avalonia.Controls _tabControl.IsVisible = false; } + // Hide the "tab strip" if there is only one tab + // This allows, for example, to view only the palette + /* + var itemsPresenter = _tabControl.FindDescendantOfType(); + if (itemsPresenter != null) + { + if (numVisibleItems == 1) + { + itemsPresenter.IsVisible = false; + } + else + { + itemsPresenter.IsVisible = true; + } + } + */ + // Note that if externally the SelectedIndex is set to 4 or something // outside the valid range, the TabControl will ignore it and replace it // with a valid SelectedIndex. This however is not propagated back through From 0c81cb24801b7c7761c0b00f0675c8ff08754822 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 9 Jun 2022 10:56:45 +0200 Subject: [PATCH 212/481] fix: DBus Null annotations --- Avalonia.sln | 1 + .../Avalonia.FreeDesktop.csproj | 4 ++ src/Avalonia.FreeDesktop/DBusCallQueue.cs | 20 +++----- src/Avalonia.FreeDesktop/DBusHelper.cs | 4 +- .../DBusIme/DBusTextInputMethodBase.cs | 46 ++++++++++--------- .../DBusIme/Fcitx/FcitxDBus.cs | 28 +++++------ .../DBusIme/Fcitx/FcitxICWrapper.cs | 31 +++++++------ .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 38 ++++++++------- .../DBusIme/IBus/IBusDBus.cs | 38 +++++++-------- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 30 ++++++++---- .../DBusIme/X11DBusImeHelper.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenu.cs | 16 +++---- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 36 +++++++-------- src/Avalonia.FreeDesktop/DBusRequest.cs | 2 +- .../LinuxMountedVolumeInfoProvider.cs | 2 +- .../Avalonia.SourceGenerator.csproj | 1 + .../Output/DrmOutput.cs | 2 +- .../IsExternalInit.cs | 0 18 files changed, 161 insertions(+), 140 deletions(-) rename src/{Avalonia.SourceGenerator => Shared}/IsExternalInit.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index 25c7daf080..8d2479a663 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -38,6 +38,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a5cb207223..3b1c6cc7b1 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,6 +5,10 @@ enable + + + + diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index 5cd748be02..e7c07dcbf9 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -8,10 +8,9 @@ namespace Avalonia.FreeDesktop { private readonly Func _errorHandler; - class Item + record Item(Func Callback) { - public Func Callback; - public Action OnFinish; + public Action? OnFinish; } private Queue _q = new Queue(); private bool _processing; @@ -23,19 +22,15 @@ namespace Avalonia.FreeDesktop public void Enqueue(Func cb) { - _q.Enqueue(new Item - { - Callback = cb - }); + _q.Enqueue(new Item(cb)); Process(); } public Task EnqueueAsync(Func cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _q.Enqueue(new Item + _q.Enqueue(new Item(cb) { - Callback = cb, OnFinish = e => { if (e == null) @@ -51,13 +46,12 @@ namespace Avalonia.FreeDesktop public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _q.Enqueue(new Item - { - Callback = async () => + _q.Enqueue(new Item(async () => { var res = await cb(); tcs.TrySetResult(res); - }, + }) + { OnFinish = e => { if (e != null) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 7204e51dbd..9f9d75b411 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop private readonly object _lock = new(); private SynchronizationContext? _ctx; - public override void Post(SendOrPostCallback d, object state) + public override void Post(SendOrPostCallback d, object? state) { lock (_lock) { @@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop } } - public override void Send(SendOrPostCallback d, object state) + public override void Send(SendOrPostCallback d, object? state) { lock (_lock) { diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 864c579319..eef865d458 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Reflection; using System.Threading.Tasks; -using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; @@ -26,7 +24,7 @@ namespace Avalonia.FreeDesktop.DBusIme return (im, im); } } - + internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl { private List _disposables = new List(); @@ -34,7 +32,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected Connection Connection { get; } private readonly string[] _knownNames; private bool _connecting; - private string _currentName; + private string? _currentName; private DBusCallQueue _queue; private bool _controlActive, _windowActive; private bool? _imeActive; @@ -42,9 +40,9 @@ namespace Avalonia.FreeDesktop.DBusIme private PixelRect? _lastReportedRect; private double _scaling = 1; private PixelPoint _windowPosition; - + protected bool IsConnected => _currentName != null; - + public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { _queue = new DBusCallQueue(QueueOnError); @@ -58,18 +56,18 @@ namespace Avalonia.FreeDesktop.DBusIme foreach (var name in _knownNames) _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); } - + protected abstract Task Connect(string name); protected string GetAppName() => - Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - + Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; + private async void OnNameChange(ServiceOwnerChangedEventArgs args) { if (args.NewOwner != null && _currentName == null) { _onlineNamesQueue.Enqueue(args.ServiceName); - if(!_connecting) + if (!_connecting) { _connecting = true; try @@ -98,25 +96,25 @@ namespace Avalonia.FreeDesktop.DBusIme _connecting = false; } } - + } - + // IME has crashed if (args.NewOwner == null && args.ServiceName == _currentName) { _currentName = null; - foreach(var s in _disposables) + foreach (var s in _disposables) s.Dispose(); _disposables.Clear(); - + OnDisconnected(); Reset(); - + // Watch again Watch(); } } - + protected virtual Task Disconnect() { return Task.CompletedTask; @@ -124,7 +122,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected virtual void OnDisconnected() { - + } protected virtual void Reset() @@ -149,10 +147,14 @@ namespace Avalonia.FreeDesktop.DBusIme OnDisconnected(); _currentName = null; } - + protected void Enqueue(Func cb) => _queue.Enqueue(cb); - protected void AddDisposable(IDisposable d) => _disposables.Add(d); + protected void AddDisposable(IDisposable? d) + { + if(d is { }) + _disposables.Add(d); + } public void Dispose() { @@ -198,7 +200,7 @@ namespace Avalonia.FreeDesktop.DBusIme UpdateActive(); } - void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _controlActive = client is { }; UpdateActive(); @@ -225,7 +227,7 @@ namespace Avalonia.FreeDesktop.DBusIme } } - private Action _onCommit; + private Action? _onCommit; event Action IX11InputMethodControl.Commit { add => _onCommit += value; @@ -234,7 +236,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected void FireCommit(string s) => _onCommit?.Invoke(s); - private Action _onForward; + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { add => _onForward += value; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs index 7ce2339763..06afacaa29 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs @@ -31,15 +31,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task DestroyICAsync(); Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action onError = null); - Task WatchCloseIMAsync(Action handler, Action onError = null); - Task WatchCommitStringAsync(Action handler, Action onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + Task WatchEnableIMAsync(Action handler, Action? onError = null); + Task WatchCloseIMAsync(Action handler, Action? onError = null); + Task WatchCommitStringAsync(Action handler, Action? onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); + Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); + Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); } [DBusInterface("org.fcitx.Fcitx.InputContext1")] @@ -54,11 +54,11 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task DestroyICAsync(); Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + Task WatchCommitStringAsync(Action handler, Action? onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); } [DBusInterface("org.fcitx.Fcitx.InputMethod1")] diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index a03ea213aa..6c503edb41 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,8 +5,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1 _modern; - private readonly IFcitxInputContext _old; + private readonly IFcitxInputContext1? _modern; + private readonly IFcitxInputContext? _old; public FcitxICWrapper(IFcitxInputContext old) { @@ -18,34 +18,37 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx _modern = modern; } - public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); + public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; - public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); + public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => - _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); - public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); + _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { if(_old!=null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; - return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); + return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); + public Task WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? Task.FromResult(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) + public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) { return _old?.WatchForwardKeyAsync(handler) - ?? _modern.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))); + ?? _modern?.WatchForwardKeyAsync(ev => + handler((ev.keyval, ev.state, ev.type ? 1 : 0))) + ?? Task.FromResult(default(IDisposable?)); } public Task SetCapacityAsync(uint flags) => - _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); + _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 0b85965de7..791431dfa7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxX11TextInputMethod : DBusTextInputMethodBase { - private FcitxICWrapper _context; + private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; public FcitxX11TextInputMethod(Connection connection) : base(connection, @@ -49,7 +49,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context.DestroyICAsync(); + protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -60,18 +60,18 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } protected override Task SetCursorRectCore(PixelRect cursorRect) => - _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), - Math.Max(1, cursorRect.Height)); - - protected override Task SetActiveCore(bool active) - { - if (active) - return _context.FocusInAsync(); - else - return _context.FocusOutAsync(); - } + _context?.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), + Math.Max(1, cursorRect.Height)) + ?? Task.CompletedTask; + + protected override Task SetActiveCore(bool active)=> (active + ? _context?.FocusInAsync() + : _context?.FocusOutAsync()) + ?? Task.CompletedTask; + - protected override Task ResetContextCore() => _context.ResetAsync(); + protected override Task ResetContextCore() => _context?.ResetAsync() + ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -88,9 +88,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - - return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, - (uint)args.Timestamp).ConfigureAwait(false); + if (_context is { }) + { + return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, + (uint)args.Timestamp).ConfigureAwait(false); + } + else + { + return false; + } } public override void SetOptions(TextInputOptions options) => diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs index 26c0d249f3..4ef034adb9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs @@ -22,25 +22,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Task GetEngineAsync(); Task DestroyAsync(); Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null); - Task WatchShowLookupTableAsync(Action handler, Action onError = null); - Task WatchHideLookupTableAsync(Action handler, Action onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action onError = null); + Task WatchCommitTextAsync(Action cb, Action? onError = null); + Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); + Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); + Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); + Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); + Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); + Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); + Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); + Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); + Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); + Task WatchShowLookupTableAsync(Action handler, Action? onError = null); + Task WatchHideLookupTableAsync(Action handler, Action? onError = null); + Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); + Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); + Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); + Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); + Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); + Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 1397eaa57b..2324ca44a7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -9,7 +9,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext _context; + private IIBusInputContext? _context; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") @@ -53,16 +53,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnCommitText(object wtf) { // Hello darkness, my old friend - var prop = wtf.GetType().GetField("Item3"); - if (prop != null) + if (wtf.GetType().GetField("Item3") is { } prop) { - var text = (string)prop.GetValue(wtf); + var text = prop.GetValue(wtf) as string; if (!string.IsNullOrEmpty(text)) - FireCommit(text); + FireCommit(text!); } } - protected override Task Disconnect() => _context.DestroyAsync(); + protected override Task Disconnect() => _context?.DestroyAsync() + ?? Task.CompletedTask; protected override void OnDisconnected() { @@ -71,13 +71,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus } protected override Task SetCursorRectCore(PixelRect rect) - => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); + => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) + ?? Task.CompletedTask; protected override Task SetActiveCore(bool active) - => active ? _context.FocusInAsync() : _context.FocusOutAsync(); + => (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) + ?? Task.CompletedTask; protected override Task ResetContextCore() - => _context.ResetAsync(); + => _context?.ResetAsync() ?? Task.CompletedTask; protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -94,7 +96,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + if(_context is { }) + { + return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + } + else + { + return Task.FromResult(false); + } + } public override void SetOptions(TextInputOptions options) diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index 7f71ecf0ff..86978c8b60 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop.DBusIme new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - static Func DetectInputMethod() + static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs index 7180345386..3a1c65e7c9 100644 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ b/src/Avalonia.FreeDesktop/DBusMenu.cs @@ -28,18 +28,18 @@ namespace Avalonia.FreeDesktop.DBusMenu Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); Task AboutToShowAsync(int Id); Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError = null); + Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); + Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); + Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); } [Dictionary] class DBusMenuProperties { public uint Version { get; set; } = default (uint); - public string TextDirection { get; set; } = default (string); - public string Status { get; set; } = default (string); - public string[] IconThemePath { get; set; } = default (string[]); + public string? TextDirection { get; set; } = default (string); + public string? Status { get; set; } = default (string); + public string[]? IconThemePath { get; set; } = default (string[]); } @@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop.DBusMenu Task UnregisterWindowAsync(uint WindowId); Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action onError = null); + Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); + Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 206c24ad5e..c0511420a6 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) { if (DBusHelper.Connection == null) return null; @@ -37,10 +37,10 @@ namespace Avalonia.FreeDesktop { private readonly Connection _dbus; private readonly uint _xid; - private IRegistrar _registrar; + private IRegistrar? _registrar; private bool _disposed; private uint _revision = 1; - private NativeMenu _menu; + private NativeMenu? _menu; private readonly Dictionary _idsToItems = new Dictionary(); private readonly Dictionary _itemsToIds = new Dictionary(); private readonly HashSet _menus = new HashSet(); @@ -73,10 +73,10 @@ namespace Avalonia.FreeDesktop if (_appMenu) { await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection.CreateProxy( + _registrar = DBusHelper.Connection?.CreateProxy( "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); - if (!_disposed) + if (!_disposed && _registrar is { }) await _registrar.RegisterWindowAsync(_xid, ObjectPath); } else @@ -109,9 +109,9 @@ namespace Avalonia.FreeDesktop public bool IsNativeMenuExported { get; private set; } - public event EventHandler OnIsNativeMenuExportedChanged; + public event EventHandler? OnIsNativeMenuExportedChanged; - public void SetNativeMenu(NativeMenu menu) + public void SetNativeMenu(NativeMenu? menu) { if (menu == null) menu = new NativeMenu(); @@ -153,7 +153,7 @@ namespace Avalonia.FreeDesktop Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) + private (NativeMenuItemBase? item, NativeMenu? menu) GetMenu(int id) { if (id == 0) return (null, _menu); @@ -161,7 +161,7 @@ namespace Avalonia.FreeDesktop return (item, (item as NativeMenuItem)?.Menu); } - private void EnsureSubscribed(NativeMenu menu) + private void EnsureSubscribed(NativeMenu? menu) { if(menu!=null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; @@ -180,12 +180,12 @@ namespace Avalonia.FreeDesktop return id; } - private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { QueueReset(); } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { QueueReset(); } @@ -216,7 +216,7 @@ namespace Avalonia.FreeDesktop "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) + object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; @@ -302,7 +302,7 @@ namespace Avalonia.FreeDesktop } private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) + KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names?.Length > 0 != true) names = AllProperties; @@ -336,7 +336,7 @@ namespace Avalonia.FreeDesktop return Task.FromResult(rv); } - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) + (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { var id = item == null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); @@ -414,22 +414,22 @@ namespace Avalonia.FreeDesktop private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)> LayoutUpdated; + private event Action<(uint revision, int parent)>? LayoutUpdated; private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } private event Action PropertiesChanged { add { } remove { } } - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) + async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) { ItemsPropertiesUpdated += handler; return Disposable.Create(() => ItemsPropertiesUpdated -= handler); } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError) + async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) { LayoutUpdated += handler; return Disposable.Create(() => LayoutUpdated -= handler); } - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError) + async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) { ItemActivationRequested+= handler; return Disposable.Create(() => ItemActivationRequested -= handler); diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs index 940a476916..d84905324f 100644 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ b/src/Avalonia.FreeDesktop/DBusRequest.cs @@ -11,6 +11,6 @@ namespace Avalonia.FreeDesktop internal interface IRequest : IDBusObject { Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action onError = null); + Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); } } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs index d68c02bfd6..b69ea68a76 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -10,7 +10,7 @@ namespace Avalonia.FreeDesktop public IDisposable Listen(ObservableCollection mountedDrives) { Contract.Requires(mountedDrives != null); - return new LinuxMountedVolumeInfoListener(ref mountedDrives); + return new LinuxMountedVolumeInfoListener(ref mountedDrives!); } } } diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..b5c955a8a6 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -11,6 +11,7 @@ all + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index ee4125101c..46a985c0e8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -133,7 +133,7 @@ namespace Avalonia.LinuxFramebuffer.Output var device = gbm_create_device(card.Fd); _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); - if(_gbmTargetSurface == null) + if(_gbmTargetSurface == IntPtr.Zero) throw new InvalidOperationException("Unable to create GBM surface"); _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Shared/IsExternalInit.cs similarity index 100% rename from src/Avalonia.SourceGenerator/IsExternalInit.cs rename to src/Shared/IsExternalInit.cs From 030c956e31d77e2572088b94daf69a8f0ec317a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 5 Jul 2022 18:31:49 +0200 Subject: [PATCH 213/481] Revert "Make MovePreviousOrParent internal as well." This reverts commit 5e1f28f2a9e3a4f8606bf8efb6aa393d12f62d45. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- src/Avalonia.Base/Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 2 +- src/Avalonia.Base/Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 2 +- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index b3f6794154..bc1d257ce6 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -65,6 +65,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => _parent; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 7f36c27a37..3a16574e04 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -70,6 +70,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => _parent; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 60ad8106d9..dd0bac31c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -37,6 +37,6 @@ namespace Avalonia.Styling return parent is Style parentStyle ? (parentStyle.Selector, parentStyle.Parent) : (null, null); } - private protected override Selector? MovePreviousOrParent() => null; + protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index be5cfaca49..ebde392a3b 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -67,6 +67,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index 40fb5a4434..e6d9cf58a9 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -105,7 +105,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() { diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 2ab00d65f8..115e0aeb95 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -103,7 +103,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => null; + protected override Selector? MovePreviousOrParent() => null; internal override void ValidateNestingSelector(bool inControlTheme) { diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 62caa9bab3..96f8c8dfeb 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) { diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 67109961ba..cc8598c5e3 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -95,7 +95,7 @@ namespace Avalonia.Styling /// /// Moves to the previous selector or the parent selector. /// - private protected abstract Selector? MovePreviousOrParent(); + protected abstract Selector? MovePreviousOrParent(); internal virtual void ValidateNestingSelector(bool inControlTheme) { diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 0af263b4e5..a68b7003b8 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -49,6 +49,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => _parent; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 7783d3f653..3a2150b1e9 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -140,7 +140,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() { From a341c33b5526c1b2fc886db3d07025e6fbb8110d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 5 Jul 2022 18:58:38 +0200 Subject: [PATCH 214/481] Revert "Fix parent selectors with /template/ at end." This reverts commit e50b416d5bf1529544830d5a291a1fb2f1b0da64 as it was not functioning correctly. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- src/Avalonia.Base/Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 8 ++------ src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 2 +- src/Avalonia.Base/Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 12 +++--------- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- 10 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index bc1d257ce6..9512dc34df 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -64,7 +64,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 3a16574e04..677a924189 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -69,7 +69,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index dd0bac31c6..77c5b719c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -17,7 +17,7 @@ namespace Avalonia.Styling { if (parent is Style s && s.Selector is not null) { - return SelectorMatch.AlwaysThisType; + return s.Selector.Match(control, s.Parent, subscribe); } else if (parent is ControlTheme theme) { @@ -32,11 +32,7 @@ namespace Avalonia.Styling "Nesting selector was specified but cannot determine parent selector."); } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? parent) - { - return parent is Style parentStyle ? (parentStyle.Selector, parentStyle.Parent) : (null, null); - } - + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index ebde392a3b..c7727bb6b8 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -66,7 +66,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index e6d9cf58a9..f473791664 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -104,7 +104,7 @@ namespace Avalonia.Styling return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 115e0aeb95..af9249864f 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -102,7 +102,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => null; internal override void ValidateNestingSelector(bool inControlTheme) diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 96f8c8dfeb..48136ba2de 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -89,7 +89,7 @@ namespace Avalonia.Styling } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index cc8598c5e3..7ce17518dd 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -84,13 +84,7 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - /// - /// The parent style, if the selector is on a nested style. - /// - /// - /// The previous selector, and its nesting parent. - /// - private protected abstract (Selector?, IStyle?) MovePrevious(IStyle? nestingParent); + protected abstract Selector? MovePrevious(); /// /// Moves to the previous selector or the parent selector. @@ -148,14 +142,14 @@ namespace Avalonia.Styling ref AndActivatorBuilder activators, ref Selector? combinator) { - var (previous, previousParent) = selector.MovePrevious(parent); + var previous = selector.MovePrevious(); // Selectors are stored from right-to-left, so we recurse into the selector in order to // reverse this order, because the type selector will be on the left and is our best // opportunity to exit early. if (previous != null && !previous.IsCombinator) { - var previousMatch = Match(control, previous, previousParent, subscribe, ref activators, ref combinator); + var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); if (previousMatch < SelectorMatchResult.Sometimes) { diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index a68b7003b8..278e24a203 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -48,7 +48,7 @@ namespace Avalonia.Styling return _parent.Match(templatedParent, parent, subscribe); } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 3a2150b1e9..6681a7da36 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -139,7 +139,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() From 3e17bd066240ef2661ea305b7c16be78ac090877 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 Jul 2022 10:06:39 +0200 Subject: [PATCH 215/481] Skip these failing tests for now. I will work on a fix. --- .../Styling/SelectorTests_Nesting.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index a60d21a018..747ad585e7 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -149,8 +149,8 @@ namespace Avalonia.Base.UnitTests.Styling control.Classes.Remove("foo"); Assert.False(sink.Active); } - - [Fact] + + [Fact(Skip = "Template selectors a the end of nesting parent selectors currently broken")] public void Template_Nesting_OfType_Matches() { var control = new Control1 { Classes = { "foo" } }; @@ -198,7 +198,7 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(SelectorMatchResult.Sometimes, match.Result); } - [Fact] + [Fact(Skip = "Template selectors a the end of nesting parent selectors currently broken")] public void Class_Template_Nesting_OfType_Matches() { var control = new Control1 { Classes = { "foo" } }; From b63b84860ca69e453b0c719873be381a1dba7099 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 6 Jul 2022 09:58:36 +0200 Subject: [PATCH 216/481] fix(DataGrid): CS8073 The result of the expression is always 'true' --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index aaac3f8f9c..d42468f47e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5990,15 +5990,14 @@ namespace Avalonia.Controls /// The formatted string. private string FormatClipboardContent(DataGridRowClipboardEventArgs e) { - StringBuilder text = new StringBuilder(); - for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++) - { - DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex]; - if (cellContent != null) - { - text.Append(cellContent.Content); - } - if (cellIndex < e.ClipboardRowContent.Count - 1) + var text = new StringBuilder(); + var clipboardRowContent = e.ClipboardRowContent; + var numberOfItem = clipboardRowContent.Count; + for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) + { + var cellContent = clipboardRowContent[cellIndex]; + text.Append(cellContent.Content); + if (cellIndex < numberOfItem - 1) { text.Append('\t'); } From 407f761201a287e22bc34f086f7491d7632b42c4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 12:34:38 +0100 Subject: [PATCH 217/481] add windows task --- azure-pipelines-integrationtests.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 94a80eef44..4f3ac57e73 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -6,10 +6,16 @@ trigger: - master -pool: - name: 'AvaloniaMacPool' -steps: -- script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh - displayName: 'run integration tests' +- job: Mac + pool: + name: 'AvaloniaMacPool' + steps: + - script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh + displayName: 'run integration tests' + + +- job: Windows + pool: + vmImage: 'windows-2022' From 03b0fcb4549af35d142dcf9cb408fc49b6032e7d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 12:40:42 +0100 Subject: [PATCH 218/481] add windows integration tests --- azure-pipelines-integrationtests.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 4f3ac57e73..05872803b4 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -6,7 +6,7 @@ trigger: - master - +jobs: - job: Mac pool: name: 'AvaloniaMacPool' @@ -19,3 +19,20 @@ trigger: - job: Windows pool: vmImage: 'windows-2022' + + steps: + - task: Windows Application Driver@0 + inputs: + OperationType: 'Start' + AgentResolution: '1080p' + displayName: 'Start WinAppDriver' + + - task: DotNetCoreCLI@2 + inputs: + command: 'test' + projects: 'tests/Avalonia.IntegrationTests.Appium/' + + - task: Windows Application Driver@0 + inputs: + OperationType: 'Stop' + displayName: 'Stop WinAppDriver' From 7b12c76e1d79a8948bd1868018c1a829be1a5175 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 13:21:48 +0100 Subject: [PATCH 219/481] build test app first. --- azure-pipelines-integrationtests.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 05872803b4..941dd01249 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -21,16 +21,26 @@ jobs: vmImage: 'windows-2022' steps: + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 6.0.202' + inputs: + version: 6.0.202 + - task: Windows Application Driver@0 inputs: OperationType: 'Start' AgentResolution: '1080p' displayName: 'Start WinAppDriver' + + - task: DotNetCoreCLI@2 + inputs: + command: 'build' + projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 inputs: command: 'test' - projects: 'tests/Avalonia.IntegrationTests.Appium/' + projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' - task: Windows Application Driver@0 inputs: From 0bb62fa8092421d6bf2f72479041f6bc93fffe34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 13:39:03 +0100 Subject: [PATCH 220/481] use 4k res on windows tests --- azure-pipelines-integrationtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 941dd01249..c3f6292703 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -29,7 +29,7 @@ jobs: - task: Windows Application Driver@0 inputs: OperationType: 'Start' - AgentResolution: '1080p' + AgentResolution: '4K' displayName: 'Start WinAppDriver' - task: DotNetCoreCLI@2 From 230166d0bc894dc802dc85fc25ef19452caa414c Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 6 Jul 2022 14:54:33 +0200 Subject: [PATCH 221/481] EffectiveViewportChangedListeners thread safety improved --- src/Avalonia.Base/Layout/LayoutManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index fc988a8d6c..dd2db9304d 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -340,7 +340,7 @@ namespace Avalonia.Layout { for (var i = 0; i < count; ++i) { - var l = _effectiveViewportChangedListeners[i]; + var l = listeners[i]; if (!l.Listener.IsAttachedToVisualTree) { @@ -352,7 +352,7 @@ namespace Avalonia.Layout if (viewport != l.Viewport) { l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport)); - _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport); + l.Viewport = viewport; } } } @@ -404,7 +404,7 @@ namespace Avalonia.Layout } } - private readonly struct EffectiveViewportChangedListener + private struct EffectiveViewportChangedListener { public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) { @@ -413,7 +413,7 @@ namespace Avalonia.Layout } public ILayoutable Listener { get; } - public Rect Viewport { get; } + public Rect Viewport { get; set; } } } } From c034525d29dd2d6e4c95aebf17751ccfc6332c0e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 14:40:46 +0100 Subject: [PATCH 222/481] win32, keep window size at all scale settings when entering and exiting fs mode. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 82f1fa7a3e..259f5c996b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -870,7 +870,14 @@ namespace Avalonia.Win32 if (fullscreen) { GetWindowRect(_hwnd, out var windowRect); - _savedWindowInfo.WindowRect = windowRect; + GetClientRect(_hwnd, out var clientRect); + + clientRect.left += windowRect.left; + clientRect.right += windowRect.left; + clientRect.top += windowRect.top; + clientRect.bottom += windowRect.top; + + _savedWindowInfo.WindowRect = clientRect; var current = GetStyle(); var currentEx = GetExtendedStyle(); @@ -907,10 +914,10 @@ namespace Avalonia.Win32 SetExtendedStyle(_savedWindowInfo.ExStyle, false); // On restore, resize to the previous saved rect size. - var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + var newClientRect = _savedWindowInfo.WindowRect.ToPixelRect(); - SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, - new_rect.Height, + SetWindowPos(_hwnd, IntPtr.Zero, newClientRect.X, newClientRect.Y, newClientRect.Width, + newClientRect.Height, SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); UpdateWindowProperties(_windowProperties, true); From 84a4de5c070f9dde1e0e752194f51c76daa978d3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 16:43:50 +0100 Subject: [PATCH 223/481] make splitview not culture sensitive. --- src/Avalonia.Controls/SplitView.cs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index 532cb1d329..caefad4af7 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -431,18 +431,40 @@ namespace Avalonia.Controls } } + private string GetPsuedoClass(SplitViewDisplayMode mode) + { + return mode switch + { + SplitViewDisplayMode.Inline => "inline", + SplitViewDisplayMode.CompactInline => "compactinline", + SplitViewDisplayMode.Overlay => "overlay", + SplitViewDisplayMode.CompactOverlay => "compactoverlay", + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) + }; + } + + private string GetPsuedoClass(SplitViewPanePlacement placement) + { + return placement switch + { + SplitViewPanePlacement.Left => "left", + SplitViewPanePlacement.Right => "right", + _ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null) + }; + } + private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = e.OldValue!.ToString()!.ToLower(); - var newState = e.NewValue!.ToString()!.ToLower(); + var oldState = GetPsuedoClass(e.GetOldValue()); + var newState = GetPsuedoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); } private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = e.OldValue!.ToString()!.ToLower(); - var newState = e.NewValue!.ToString()!.ToLower(); + var oldState = GetPsuedoClass(e.GetOldValue()); + var newState = GetPsuedoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); From b7a3bce6f99202fce9128c9824d3157e91c6d615 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 17:21:41 +0100 Subject: [PATCH 224/481] fix compiler warning. --- src/Avalonia.Controls/SplitView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index caefad4af7..f560251e53 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -469,7 +469,7 @@ namespace Avalonia.Controls PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); - var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch + var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue() switch { SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), From cc2d791f3683ae77c21ba8d7b9ece37ac8852a7e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 6 Jul 2022 19:45:01 +0200 Subject: [PATCH 225/481] Update samples/Sandbox/MainWindow.axaml Co-authored-by: Max Katz --- samples/Sandbox/MainWindow.axaml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index a834e3fef3..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,19 +1,4 @@ - - - - This is a - TextBlock - with several - Span elements, - - using a variety of styles - . - - - - - From 97a5a9e1f64110d01b8d8bf3937a3518f6fd567b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 6 Jul 2022 14:24:55 -0400 Subject: [PATCH 226/481] Add folder.GetItemsAsync API --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 16 ++++++- .../Platform/Storage/AndroidStorageItem.cs | 25 +++++++++++ .../Storage/FileIO/BclStorageFolder.cs | 12 +++++ .../Platform/Storage/IStorageFolder.cs | 11 ++++- .../Interop/Storage/StorageProviderInterop.cs | 25 +++++++++++ .../Interop/Typescript/StorageProvider.ts | 45 +++++++++++++------ .../Avalonia.iOS/Storage/IOSStorageItem.cs | 17 +++++++ 7 files changed, 134 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b6db1255..e13c2052eb 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -243,8 +243,8 @@ namespace ControlCatalog.Pages async Task SetPickerResult(IReadOnlyCollection? items) { items ??= Array.Empty(); - var mappedResults = items.Select(FullPathOrName).ToList(); bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark"; + var mappedResults = new List(); if (items.FirstOrDefault() is IStorageItem item) { @@ -293,7 +293,19 @@ Content: lastSelectedDirectory = await item.GetParentAsync(); if (lastSelectedDirectory is not null) { - mappedResults.Insert(0, "Parent: " + FullPathOrName(lastSelectedDirectory)); + mappedResults.Add(FullPathOrName(lastSelectedDirectory)); + } + + foreach (var selectedItem in items) + { + mappedResults.Add("+> " + FullPathOrName(selectedItem)); + if (selectedItem is IStorageFolder folder) + { + foreach (var innerItems in await folder.GetItemsAsync()) + { + mappedResults.Add("++> " + FullPathOrName(innerItems)); + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 50581d47b1..1e81642e15 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -106,6 +107,30 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar { return Task.FromResult(new StorageItemProperties()); } + + public async Task> GetItemsAsync() + { + using var javaFile = new JavaFile(Uri.Path!); + + // Java file represents files AND directories. Don't be confused. + var files = await javaFile.ListFilesAsync().ConfigureAwait(false); + if (files is null) + { + return Array.Empty(); + } + + return files + .Select(f => (file: f, uri: AndroidUri.FromFile(f))) + .Where(t => t.uri is not null) + .Select(t => t.file switch + { + { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), + { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } } internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 7267017eaf..0a22f4bd03 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Security; using System.Threading.Tasks; using Avalonia.Metadata; @@ -43,6 +45,16 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } + public Task> GetItemsAsync() + { + var items = _directoryInfo.GetDirectories() + .Select(d => (IStorageItem)new BclStorageFolder(d)) + .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) + .ToArray(); + + return Task.FromResult>(items); + } + public virtual Task SaveBookmark() { return Task.FromResult(_directoryInfo.FullName); diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 25b9f01a92..0ffb9f41c6 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -1,4 +1,6 @@ -using Avalonia.Metadata; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; namespace Avalonia.Platform.Storage; @@ -8,4 +10,11 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFolder : IStorageItem { + /// + /// Gets the files and subfolders in the current folder. + /// + /// + /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an implementation object. + /// + Task> GetItemsAsync(); } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs index 14dc53d7b5..129463774c 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs @@ -196,5 +196,30 @@ namespace Avalonia.Web.Blazor.Interop.Storage public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) { } + + public async Task> GetItemsAsync() + { + var items = await FileHandle.InvokeAsync("getItems"); + if (items is null) + { + return Array.Empty(); + } + + var count = items.Invoke("count"); + + return Enumerable.Range(0, count) + .Select(index => + { + var reference = items.Invoke("at", index); + return reference.Invoke("getKind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }; + }) + .Where(i => i is not null) + .ToArray()!; + } } } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts index c32eef3226..aee74b9067 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts @@ -14,6 +14,8 @@ declare global { queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; + + entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>; } type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; @@ -53,7 +55,7 @@ class IndexedDbWrapper { } public connect(): Promise { - var conn = window.indexedDB.open(this.databaseName, 1); + const conn = window.indexedDB.open(this.databaseName, 1); conn.onupgradeneeded = event => { const db = (>event.target).result; @@ -85,7 +87,7 @@ class InnerDbConnection { const os = this.openStore(store, "readwrite"); return new Promise((resolve, reject) => { - var response = os.put(obj, key); + const response = os.put(obj, key); response.onsuccess = () => { resolve(response.result); }; @@ -99,7 +101,7 @@ class InnerDbConnection { const os = this.openStore(store, "readonly"); return new Promise((resolve, reject) => { - var response = os.get(key); + const response = os.get(key); response.onsuccess = () => { resolve(response.result); }; @@ -113,7 +115,7 @@ class InnerDbConnection { const os = this.openStore(store, "readwrite"); return new Promise((resolve, reject) => { - var response = os.delete(key); + const response = os.delete(key); response.onsuccess = () => { resolve(); }; @@ -134,17 +136,20 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ ]) class StorageItem { - constructor(private handle: FileSystemFileHandle, private bookmarkId?: string) { } + constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { } public getName(): string { return this.handle.name } + public getKind(): string { + return this.handle.kind; + } + public async openRead(): Promise { await this.verityPermissions('read'); - var file = await this.handle.getFile(); - return file; + return await this.handle.getFile(); } public async openWrite(): Promise { @@ -154,7 +159,7 @@ class StorageItem { } public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> { - var file = this.handle.getFile && await this.handle.getFile(); + const file = this.handle.getFile && await this.handle.getFile(); return file && { Size: file.size, @@ -163,6 +168,18 @@ class StorageItem { } } + public async getItems(): Promise { + if (this.handle.kind !== "directory"){ + return new StorageItems([]); + } + + const items: StorageItem[] = []; + for await (const [key, value] of this.handle.entries()) { + items.push(new StorageItem(value)); + } + return new StorageItems(items); + } + private async verityPermissions(mode: PermissionsMode): Promise { if (await this.handle.queryPermission({ mode }) === 'granted') { return; @@ -235,12 +252,12 @@ export class StorageProvider { } public static async selectFolderDialog( - startIn: StartInDirectory | null) + startIn: StorageItem | null) : Promise { // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. const options: DirectoryPickerOptions = { - startIn: (startIn || undefined) + startIn: (startIn?.handle || undefined) }; const handle = await window.showDirectoryPicker(options); @@ -248,12 +265,12 @@ export class StorageProvider { } public static async openFileDialog( - startIn: StartInDirectory | null, multiple: boolean, + startIn: StorageItem | null, multiple: boolean, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) : Promise { const options: OpenFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), multiple, excludeAcceptAllOption, types: (types || undefined) @@ -264,12 +281,12 @@ export class StorageProvider { } public static async saveFileDialog( - startIn: StartInDirectory | null, suggestedName: string | null, + startIn: StorageItem | null, suggestedName: string | null, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) : Promise { const options: SaveFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), suggestedName: (suggestedName || undefined), excludeAcceptAllOption, types: (types || undefined) diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 6fb296d0e0..cfb2a497be 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform.Storage; @@ -118,4 +120,19 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder public IOSStorageFolder(NSUrl url) : base(url) { } + + public Task> GetItemsAsync() + { + var content = NSFileManager.DefaultManager.GetDirectoryContent(Url, null, NSDirectoryEnumerationOptions.None, out var error); + if (error is not null) + { + return Task.FromException>(new NSErrorException(error)); + } + + var items = content + .Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u)) + .ToArray(); + + return Task.FromResult>(items); + } } From 5a8c9f9c0904c83095291d9ae0c02abd8a9d259f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 6 Jul 2022 14:31:00 -0400 Subject: [PATCH 227/481] Use Async in naming --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 10 +++++----- .../Platform/Storage/AndroidStorageItem.cs | 8 ++++---- .../Platform/Storage/FileIO/BclStorageFile.cs | 8 ++++---- .../Platform/Storage/FileIO/BclStorageFolder.cs | 4 ++-- .../Platform/Storage/IStorageBookmarkItem.cs | 2 +- src/Avalonia.Base/Platform/Storage/IStorageFile.cs | 4 ++-- src/Avalonia.Base/Platform/Storage/IStorageItem.cs | 2 +- .../Interop/Storage/StorageProviderInterop.cs | 8 ++++---- src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs | 8 ++++---- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e13c2052eb..036dccde0e 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -195,10 +195,10 @@ namespace ControlCatalog.Pages { // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER - await using var stream = await file.OpenWrite(); + await using var stream = await file.OpenWriteAsync(); await using var reader = new System.IO.StreamWriter(stream); #else - using var stream = await file.OpenWrite(); + using var stream = await file.OpenWriteAsync(); using var reader = new System.IO.StreamWriter(stream); #endif await reader.WriteLineAsync(openedFileContent.Text); @@ -243,7 +243,7 @@ namespace ControlCatalog.Pages async Task SetPickerResult(IReadOnlyCollection? items) { items ??= Array.Empty(); - bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark"; + bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark"; var mappedResults = new List(); if (items.FirstOrDefault() is IStorageItem item) @@ -267,9 +267,9 @@ Content: if (file.CanOpenRead) { #if NET6_0_OR_GREATER - await using var stream = await file.OpenRead(); + await using var stream = await file.OpenReadAsync(); #else - using var stream = await file.OpenRead(); + using var stream = await file.OpenReadAsync(); #endif using var reader = new System.IO.StreamReader(stream); diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 1e81642e15..a9b2e16d43 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -36,13 +36,13 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.FromResult(Uri.ToString()); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.CompletedTask; @@ -143,10 +143,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public bool CanOpenWrite => true; - public Task OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); - public Task OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) + public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) ?? throw new InvalidOperationException("Failed to open content stream")); private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5af02219ce..cf21e9b8b5 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -47,22 +47,22 @@ public class BclStorageFile : IStorageBookmarkFile return Task.FromResult(null); } - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(_fileInfo.OpenRead()); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(_fileInfo.OpenWrite()); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_fileInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 0a22f4bd03..cd6c8be1ae 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -55,12 +55,12 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult>(items); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_directoryInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index d21c950862..40f2720ee8 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageBookmarkItem : IStorageItem { - Task ReleaseBookmark(); + Task ReleaseBookmarkAsync(); } [NotClientImplementable] diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 965caf8216..46aa6efa72 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// - Task OpenRead(); + Task OpenReadAsync(); /// /// Returns true, if file is writeable. @@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// - Task OpenWrite(); + Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 8513ebc7d9..f5469d31c9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable /// /// Returns identifier of a bookmark. Can be null if OS denied request. /// - Task SaveBookmark(); + Task SaveBookmarkAsync(); /// /// Gets the parent folder of the current storage item. diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs index 129463774c..2bc46e97b5 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs @@ -145,7 +145,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { return FileHandle.InvokeAsync("saveBookmark").AsTask(); } @@ -155,7 +155,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage return Task.FromResult(null); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { return FileHandle.InvokeAsync("deleteBookmark").AsTask(); } @@ -174,7 +174,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenRead => true; - public async Task OpenRead() + public async Task OpenReadAsync() { var stream = await FileHandle.InvokeAsync("openRead"); // Remove maxAllowedSize limit, as developer can decide if they read only small part or everything. @@ -182,7 +182,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenWrite => true; - public async Task OpenWrite() + public async Task OpenWriteAsync() { var properties = await FileHandle.InvokeAsync("getProperties"); var streamWriter = await FileHandle.InvokeAsync("openWrite"); diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index cfb2a497be..a801e83562 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -51,13 +51,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem return Task.FromResult(new IOSStorageFolder(Url.RemoveLastPathComponent())); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // no-op return Task.CompletedTask; } - public Task SaveBookmark() + public Task SaveBookmarkAsync() { try { @@ -104,12 +104,12 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public bool CanOpenWrite => true; - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Write)); } From 45ae4221dc8cf2f40a92c4d57dbdca7a3aacdd64 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 20:58:36 +0100 Subject: [PATCH 228/481] correct spelling. --- src/Avalonia.Controls/SplitView.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index f560251e53..c344dd795d 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -431,7 +431,7 @@ namespace Avalonia.Controls } } - private string GetPsuedoClass(SplitViewDisplayMode mode) + private string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch { @@ -443,7 +443,7 @@ namespace Avalonia.Controls }; } - private string GetPsuedoClass(SplitViewPanePlacement placement) + private string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch { @@ -455,16 +455,16 @@ namespace Avalonia.Controls private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = GetPsuedoClass(e.GetOldValue()); - var newState = GetPsuedoClass(e.GetNewValue()); + var oldState = GetPseudoClass(e.GetOldValue()); + var newState = GetPseudoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); } private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = GetPsuedoClass(e.GetOldValue()); - var newState = GetPsuedoClass(e.GetNewValue()); + var oldState = GetPseudoClass(e.GetOldValue()); + var newState = GetPseudoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); From 002377044aea087f4bf3ca4b2af088be469c7b2b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 09:50:06 +0200 Subject: [PATCH 230/481] Disallow selectors with trailing /template/. It's an invalid selector: what does `Button /template/` select? --- src/Avalonia.Base/Styling/Style.cs | 16 ++++- .../Styling/SelectorTests_Nesting.cs | 72 ------------------- .../Styling/StyleTests.cs | 7 ++ 3 files changed, 22 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 77c4e62d29..c61b08b2a1 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -7,6 +7,8 @@ namespace Avalonia.Styling /// public class Style : StyleBase { + private Selector? _selector; + /// /// Initializes a new instance of the class. /// @@ -26,7 +28,11 @@ namespace Avalonia.Styling /// /// Gets or sets the style's selector. /// - public Selector? Selector { get; set; } + public Selector? Selector + { + get => _selector; + set => _selector = ValidateSelector(value); + } public override SelectorMatchResult TryAttach(IStyleable target, object? host) { @@ -88,5 +94,13 @@ namespace Avalonia.Styling base.SetParent(parent); } + + private static Selector? ValidateSelector(Selector? selector) + { + if (selector is TemplateSelector) + throw new InvalidOperationException( + "Invalid selector: Template selector must be followed by control selector."); + return selector; + } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index 747ad585e7..9048b488b6 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -150,78 +150,6 @@ namespace Avalonia.Base.UnitTests.Styling Assert.False(sink.Active); } - [Fact(Skip = "Template selectors a the end of nesting parent selectors currently broken")] - public void Template_Nesting_OfType_Matches() - { - var control = new Control1 { Classes = { "foo" } }; - var button = new Button - { - Template = new FuncControlTemplate((x, _) => control), - }; - - button.ApplyTemplate(); - - Style nested; - var parent = new Style(x => x.OfType /// The pseudoclass name. bool Remove(string name); + + /// + /// Returns whether a pseudoclass is present in the collection. + /// + /// The pseudoclass name. + /// Whether the pseudoclass is present. + bool Has(string name); } } From 05c1f583087eb864544a0c1c71cd3421584c5a9a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 17:06:44 +0200 Subject: [PATCH 234/481] Added ItemContainerTheme property. Otherwise there's no easy way to apply themes to item containers. --- .../Generators/IItemContainerGenerator.cs | 6 +++ .../Generators/ItemContainerGenerator.cs | 16 ++++++- .../Generators/ItemContainerGenerator`1.cs | 21 ++++----- .../Generators/TreeItemContainerGenerator.cs | 5 +++ src/Avalonia.Controls/ItemsControl.cs | 21 +++++++++ src/Avalonia.Controls/TreeViewItem.cs | 5 +++ .../ItemsControlTests.cs | 28 +++++++++--- .../ListBoxTests.cs | 44 +++++++++++++++++++ .../TreeViewTests.cs | 29 ++++++++++++ 9 files changed, 159 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 70d9dbec08..f9772cb399 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls.Templates; +using Avalonia.Styling; namespace Avalonia.Controls.Generators { @@ -14,6 +15,11 @@ namespace Avalonia.Controls.Generators /// IEnumerable Containers { get; } + /// + /// Gets or sets the theme to be applied to the items in the control. + /// + ControlTheme? ItemContainerTheme { get; set; } + /// /// Gets or sets the data template used to display the items in the control. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index a76dcbe9c8..8b36b07cec 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Styling; namespace Avalonia.Controls.Generators { @@ -35,6 +36,11 @@ namespace Avalonia.Controls.Generators /// public event EventHandler? Recycled; + /// + /// Gets or sets the theme to be applied to the items in the control. + /// + public ControlTheme? ItemContainerTheme { get; set; } + /// /// Gets or sets the data template used to display the items in the control. /// @@ -190,10 +196,18 @@ namespace Avalonia.Controls.Generators result.SetValue( ContentPresenter.ContentTemplateProperty, ItemTemplate, - BindingPriority.TemplatedParent); + BindingPriority.Style); } } + if (ItemContainerTheme != null) + { + result.SetValue( + StyledElement.ThemeProperty, + ItemContainerTheme, + BindingPriority.TemplatedParent); + } + return result; } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 635f3a7d37..3ff1b0702d 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -44,28 +44,29 @@ namespace Avalonia.Controls.Generators { var container = item as T; - if (container != null) + if (container is null) { - return container; - } - else - { - var result = new T(); + container = new T(); if (ContentTemplateProperty != null) { - result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); + container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); } - result.SetValue(ContentProperty, item, BindingPriority.Style); + container.SetValue(ContentProperty, item, BindingPriority.Style); if (!(item is IControl)) { - result.DataContext = item; + container.DataContext = item; } + } - return result; + if (ItemContainerTheme != null) + { + container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style); } + + return container; } /// diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 536a5fdd06..4e3deb5552 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -71,6 +71,11 @@ namespace Avalonia.Controls.Generators var template = GetTreeDataTemplate(item, ItemTemplate); var result = new T(); + if (ItemContainerTheme != null) + { + result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style); + } + result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); var itemsSelector = template.ItemsSelector(item); diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 56b0014c05..427f5c8a9b 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -15,6 +15,7 @@ using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -36,6 +37,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemContainerThemeProperty = + AvaloniaProperty.Register(nameof(ItemContainerTheme)); + /// /// Defines the property. /// @@ -88,6 +95,7 @@ namespace Avalonia.Controls { _itemContainerGenerator = CreateItemContainerGenerator(); + _itemContainerGenerator.ItemContainerTheme = ItemContainerTheme; _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); @@ -108,6 +116,15 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + /// + /// Gets or sets the that is applied to the container element generated for each item. + /// + public ControlTheme? ItemContainerTheme + { + get { return GetValue(ItemContainerThemeProperty); } + set { SetValue(ItemContainerThemeProperty, value); } + } + /// /// Gets the number of items in . /// @@ -349,6 +366,10 @@ namespace Avalonia.Controls { UpdatePseudoClasses(change.GetNewValue()); } + else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) + { + _itemContainerGenerator.ItemContainerTheme = change.GetNewValue(); + } } /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 490b0b3ce3..2e3aa037c2 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -121,6 +121,11 @@ namespace Avalonia.Controls { ItemTemplate = _treeView.ItemTemplate; } + + if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null) + { + ItemContainerTheme = _treeView.ItemContainerTheme; + } } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index bfece7871c..f08653a4f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -1,15 +1,14 @@ +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.VisualTree; -using Xunit; -using System.Collections.ObjectModel; +using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.Input; -using System.Collections.Generic; +using Xunit; namespace Avalonia.Controls.UnitTests { @@ -62,6 +61,25 @@ namespace Avalonia.Controls.UnitTests Assert.Null(container.TemplatedParent); } + [Fact] + public void Container_Should_Have_Theme_Set_To_ItemContainerTheme() + { + var theme = new ControlTheme(); + var target = new ItemsControl + { + ItemContainerTheme = theme, + }; + + target.Template = GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var container = (ContentPresenter)target.Presenter.Panel.Children[0]; + + Assert.Same(container.Theme, theme); + } + [Fact] public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index e87990ebb1..afa153a593 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -94,6 +94,50 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Container_Should_Have_Theme_Set_To_ItemContainerTheme() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] { "Foo", "Bar", "Baz " }; + var theme = new ControlTheme(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemContainerTheme = theme, + }; + + Prepare(target); + + var container = (ListBoxItem)target.Presenter.Panel.Children[0]; + + Assert.Same(container.Theme, theme); + } + } + + [Fact] + public void Inline_Item_Should_Have_Theme_Set_To_ItemContainerTheme() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] { "Foo", "Bar", "Baz " }; + var theme = new ControlTheme(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = new[] { new ListBoxItem() }, + ItemContainerTheme = theme, + }; + + Prepare(target); + + var container = (ListBoxItem)target.Presenter.Panel.Children[0]; + + Assert.Same(container.Theme, theme); + } + } + [Fact] public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2169b15cad..d784caf2db 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -72,6 +72,35 @@ namespace Avalonia.Controls.UnitTests Assert.All(items, x => Assert.IsType(x.HeaderPresenter.Child)); } + [Fact] + public void Items_Should_Be_Created_Using_ItemConatinerTheme_If_Present() + { + TreeView target; + var theme = new ControlTheme(); + + var root = new TestRoot + { + Child = target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + ItemContainerTheme = theme, + ItemTemplate = new FuncTreeDataTemplate( + (_, __) => new Canvas(), + x => x.Children), + } + }; + + ApplyTemplates(target); + + var items = target.ItemContainerGenerator.Index.Containers + .OfType() + .ToList(); + + Assert.Equal(5, items.Count); + Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme)); + } + [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { From f80cf2d355f813d9294523984c6323398b4ef65d Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 7 Jul 2022 23:01:38 +0300 Subject: [PATCH 235/481] add base.OnAttachedToVisualTree(e) call to repeater --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 3f42d95deb..ae663defd3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -421,6 +421,7 @@ namespace Avalonia.Controls protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + base.OnAttachedToVisualTree(e); InvalidateMeasure(); _viewportManager.ResetScrollers(); } From 012d3763693d332e05c12e6ba6ae0e06cf3b72e4 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 7 Jul 2022 20:48:37 -0400 Subject: [PATCH 236/481] Standardize converters --- .../Themes/Default/ColorPreviewer.xaml | 16 +++----- .../Themes/Default/ColorSlider.xaml | 22 ++++------- .../Themes/Default/ColorSpectrum.xaml | 31 ++++++--------- .../Themes/Default/Default.xaml | 14 ++++++- .../Themes/Fluent/ColorPicker.xaml | 13 +++---- .../Themes/Fluent/ColorPreviewer.xaml | 16 +++----- .../Themes/Fluent/ColorSlider.xaml | 22 ++++------- .../Themes/Fluent/ColorSpectrum.xaml | 31 ++++++--------- .../Themes/Fluent/ColorView.xaml | 39 +++++++++---------- .../Themes/Fluent/Fluent.xaml | 14 ++++++- 10 files changed, 101 insertions(+), 117 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 6ac0982408..ad07da2b16 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,14 +1,10 @@  - - - - + 80 40 @@ -36,11 +32,11 @@ Grid.Column="0" CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}" Tag="-2" - Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='2'}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml index bf2f632fd8..729c5be313 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -1,13 +1,7 @@  - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index 8d77f8d3c1..84a94486c8 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -1,14 +1,10 @@  - - - - + 80 40 @@ -36,11 +32,11 @@ Grid.Column="0" CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}" Tag="-2" - Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-2'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='1'}" /> + Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='2'}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index bb6e026f9e..594a2c694f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -1,13 +1,7 @@  - - - - - @@ -255,8 +252,8 @@ - @@ -363,12 +360,12 @@ Grid.Column="0" Content="RGB" CornerRadius="4,0,0,4" - IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}"/> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" /> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converters="using:Avalonia.Controls.Converters"> + + + + + + + + + + + From 38bdb8a7b34051e4898aa9c0a1dc3a3fff25ae52 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 7 Jul 2022 20:53:21 -0400 Subject: [PATCH 237/481] Remove unused code --- .../ColorPicker/ColorPicker.cs | 8 +------- src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index d34a91d1bb..39369bcbdb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Controls +namespace Avalonia.Controls { /// /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index bea982b7ea..89f1afb1ac 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -28,7 +28,6 @@ namespace Avalonia.Controls private TextBox? _hexTextBox; private TabControl? _tabControl; - private ObservableCollection _paletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); protected bool ignorePropertyChanged = false; From 443977b85d1985dadf9f5248b4ca7f194503ab76 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 7 Jul 2022 21:10:02 -0400 Subject: [PATCH 238/481] Specify TargetType and DataType in most templates --- .../Themes/Default/ColorPreviewer.xaml | 2 +- .../Themes/Default/ColorSlider.xaml | 4 ++-- .../Themes/Default/ColorSpectrum.xaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorPreviewer.xaml | 2 +- .../Themes/Fluent/ColorSlider.xaml | 4 ++-- .../Themes/Fluent/ColorSpectrum.xaml | 2 +- .../Themes/Fluent/ColorView.xaml | 10 ++++++---- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index ad07da2b16..3a4efaa42a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -13,7 +13,7 @@ - + - + @@ -93,7 +93,7 @@ - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 95af16d12e..0e57f6b483 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -6,7 +6,7 @@ - + + + +"; + + var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + contentControl.Measure(new Size(10, 10)); + + var result = contentControl.GetTemplateChildren().OfType().First(); + + Assert.Equal("Hello", result.Content); + } + } + [Fact] public void Binds_To_Source() { From 142b878984db2f3be0977b4073a83ff3247ab2f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:05:25 -0400 Subject: [PATCH 242/481] Fix special case of compiled binding with Path child --- .../XamlIlBindingPathHelper.cs | 8 +++-- .../CompiledBindingExtensionTests.cs | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index dd809956cd..8a115bf8fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -47,7 +47,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return startTypeResolver(); } - if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) + if (bindingPathAssignment.Values[0] is XamlIlBindingPathNode pathNode) + { + bindingResultType = pathNode.BindingResultType; + } + else if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) { var transformed = TransformBindingPath( context, @@ -63,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } else { - throw new InvalidOperationException(); + throw new InvalidOperationException("Invalid state of Path property"); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index e6df80f17e..754a819182 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -102,6 +102,41 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.StringProperty, textBlock.Text); } } + + [Fact] + public void ResolvesPathPassedByPropertyWithInnerItemTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("itemsControl"); + + var dataContext = new TestDataContext + { + ListProperty = + { + "Hello" + } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ListProperty, textBlock.Items); + } + } [Fact] public void ResolvesStreamTaskBindingCorrectly() From 2619ab17595b6317fe4afdf1bc0917cb325c5a1d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:13 -0400 Subject: [PATCH 243/481] Fix compiled binding with TemplatedParent relative source --- .../AvaloniaXamlIlCompiler.cs | 6 ++-- .../AvaloniaXamlIlBindingPathParser.cs | 25 ++++++++++------ .../XamlIlBindingPathHelper.cs | 1 + .../CompiledBindingExtensionTests.cs | 30 +++++++++++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 04a61e5f10..e3a55feac9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -45,10 +45,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlReorderClassesPropertiesTransformer() ); - InsertBefore( - new AvaloniaXamlIlBindingPathParser(), + InsertBefore( new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 890cbb69bf..60c880c6aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,11 +121,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } - - var mode = relativeSourceObject.Children + + var modeProperty = relativeSourceObject.Children .OfType() - .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") - ?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? + .Values.FirstOrDefault() as XamlAstTextNode + ?? relativeSourceObject.Arguments.OfType().FirstOrDefault(); + + var mode = modeProperty?.Text; if (relativeSourceObject.Arguments.Count == 0 && mode == null) { mode = "FindAncestor"; @@ -212,16 +215,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } else if (mode == "TemplatedParent") { - var parentType = context.ParentNodes().OfType() + var contentTemplateNode = context.ParentNodes().OfType() .FirstOrDefault(x => - x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) - ?.TargetType.GetClrType(); - - if (parentType is null) + x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate); + if (contentTemplateNode is null) { throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); } + var parentType = contentTemplateNode.TargetType.GetClrType(); + if (parentType is null) + { + throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + } + convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; } else diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 8a115bf8fc..ae29dcf9cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -244,6 +244,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case TemplatedParentBindingExpressionNode templatedParent: var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); + nodes.Add(new SelfPathElementNode(selfType)); nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( templatedParentField, templatedParent.Type)); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 754a819182..5ca25a176c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Reactive.Subjects; -using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -17,8 +17,6 @@ using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.UnitTests; -using JetBrains.Annotations; -using XamlX; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions @@ -676,6 +674,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void ResolvesRelativeSourceBindingFromTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + + var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + contentControl.Measure(new Size(10, 10)); + + var result = contentControl.GetTemplateChildren().OfType().First(); + Assert.Equal("Hello", result.Content); + } + } [Fact] public void ResolvesElementNameInTemplate() From 5d234cd49c7e7da9c446d7450f06267053a71999 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:30 -0400 Subject: [PATCH 244/481] Remove reflection bindings from standard controls --- .../Diagnostics/Controls/FilterTextBox.axaml | 2 +- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index b7995c38e3..a1a2ab34bf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -19,7 +19,7 @@ Classes="textBoxClearButton" ToolTip.Tip="Clear" Cursor="Hand" - Command="{ReflectionBinding $parent[TextBox].Clear}" + Command="{Binding $parent[TextBox].Clear}" Opacity="0.5" /> diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a463334a76..482b219589 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,7 +21,7 @@ - + @@ -32,7 +32,7 @@ - + From ff3b23b2d6eb3e874ad2c71393181b7756ffb5ae Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:17:05 -0400 Subject: [PATCH 245/481] Formatting --- .../Transformers/AvaloniaXamlIlBindingPathParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 60c880c6aa..6150265cf8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,7 +121,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } - + var modeProperty = relativeSourceObject.Children .OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? @@ -226,7 +226,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var parentType = contentTemplateNode.TargetType.GetClrType(); if (parentType is null) { - throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + throw new XamlParseException("TargetType has to be set on ControlTemplate or it should be defined inside of a Style.", binding); } convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; From 34f8f3d5564d3d7b1584746c8dcaa887a5e235f5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:57:06 -0400 Subject: [PATCH 246/481] Fix DataType set on Binding extension + add tests --- .../AvaloniaXamlIlBindingPathTransformer.cs | 5 + .../CompiledBindings/CompiledBindingPath.cs | 2 + .../CompiledBindingExtensionTests.cs | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 3cc3504e16..e509d47932 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -105,6 +105,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; } + + if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlTypeExtensionNode typeNode) + { + startType = typeNode.Value.GetClrType(); + } Func startTypeResolver = startType is not null ? () => startType : () => { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 73a14fd437..f78d4867ff 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -74,6 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return pathRoot ?? new EmptyExpressionNode(); } + internal IEnumerable Elements => _elements; + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; internal object RawSource { get; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 5ca25a176c..215ae4d54f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -9,10 +9,13 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; @@ -135,6 +138,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.ListProperty, textBlock.Items); } } + + [Fact] + public void ResolvesDataTypeFromBindingProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesDataTypeFromBindingProperty_TypeExtension() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } [Fact] public void ResolvesStreamTaskBindingCorrectly() @@ -1479,6 +1532,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void ResolvesDataTypeForAssignBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" +"; + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var compiledPath = ((CompiledBindingExtension)control.X).Path; + + var node = Assert.IsType(Assert.Single(compiledPath.Elements)); + Assert.Equal(typeof(string), node.Property.PropertyType); + } + } + + [Fact] + public void ResolvesDataTypeForAssignBinding_FromBindingProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" +"; + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var compiledPath = ((CompiledBindingExtension)control.X).Path; + + var node = Assert.IsType(Assert.Single(compiledPath.Elements)); + Assert.Equal(typeof(string), node.Property.PropertyType); + } + } + void Throws(string type, Action cb) { try @@ -1656,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } public class CustomDataTemplateInherit : CustomDataTemplate { } + + public class AssignBindingControl : Control + { + [AssignBinding] public IBinding X { get; set; } + } } From 8e57d0b7c6b52e6598570795e206a4a27e7e13b9 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 8 Jul 2022 11:52:55 +0300 Subject: [PATCH 247/481] Fix bindings to Types. --- .../Data/Core/Plugins/InpcPropertyAccessorPlugin.cs | 2 +- tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index b93bf87fdf..91d69b5d3d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) { - if (instance is IReflectableType reflectableType) + if (instance is IReflectableType reflectableType && instance is not Type) return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags); var type = instance.GetType(); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 11a22f0dec..f4a4a2934f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -637,6 +637,17 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source["Foo"]); } + [Fact] + public void Binding_To_Types_Should_Work() + { + var type = typeof(string); + var textBlock = new TextBlock() { DataContext = type }; + using (textBlock.Bind(TextBlock.TextProperty, new Binding("Name"))) + { + Assert.Equal("String", textBlock.Text); + }; + } + private class StyledPropertyClass : AvaloniaObject { public static readonly StyledProperty DoubleValueProperty = From 0553c7ceabf01199a0c45cf21c3e6126fde1e660 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 8 Jul 2022 15:32:09 +0100 Subject: [PATCH 248/481] add a failing unit test --- .../WindowTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a8c9b68d12..ab4fd566c8 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -540,6 +540,51 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(window.Position, expectedPosition); } } + + [Fact] + public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen_1080_175() + { + var screen1 = new Mock(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true); + + var screens = new Mock(); + screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object }); + screens.Setup(x => x.ScreenFromPoint(It.IsAny())).Returns(screen1.Object); + + + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.DesktopScaling).Returns(1.75); + windowImpl.Setup(x => x.RenderScaling).Returns(1.75); + windowImpl.Setup(x => x.Screen).Returns(screens.Object); + + var clientSize = new Size(801.142, 366); + + windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => + { + clientSize = x;//.Constrain(screen1.Object.Bounds.Size.ToSize(1.75)); + windowImpl.Object.Resized?.Invoke(clientSize, y); + }); + + windowImpl.Setup(x => x.FrameSize).Returns(() => clientSize.Inflate(new Thickness(5, 25, 5, 5))); + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(windowImpl.Object); + window.WindowStartupLocation = WindowStartupLocation.CenterScreen; + window.MinWidth = 720; + window.MinHeight = 480; + //window.Width = 720; + //window.Height = 480; + + window.Show(); + + var expectedPosition = new PixelPoint(321, 36); + + Assert.Equal(window.Position, expectedPosition); + } + } [Fact] public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() From 7cbeb7ad3f4aac3412efd662d5dac4f6f984cdbb Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 8 Jul 2022 18:56:29 +0200 Subject: [PATCH 249/481] Rework TextLineImpl hit testing --- .../ControlCatalog/Pages/TextBlockPage.xaml | 2 +- samples/Sandbox/MainWindow.axaml | 1 + samples/Sandbox/Sandbox.csproj | 1 + src/Avalonia.Base/Media/GlyphRun.cs | 109 ++++- .../Media/TextFormatting/TextLineImpl.cs | 371 +++++++++--------- .../Media/TextFormatting/TextLayoutTests.cs | 84 +++- 6 files changed, 366 insertions(+), 202 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index cb49ba96c6..32914428ed 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -118,7 +118,7 @@ - + This is a TextBlock with several diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..0c5a7a11e3 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,5 @@ + diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index eab654acb6..20c7f29201 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index ac87d521a5..703b56b0e8 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -614,17 +614,29 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { + var firstCluster = 0; + var lastCluster = Characters.Length - 1; + + if (!IsLeftToRight) + { + var cluster = firstCluster; + firstCluster = lastCluster; + lastCluster = cluster; + } + if (GlyphClusters != null && GlyphClusters.Count > 0) { - var firstCluster = GlyphClusters[0]; + firstCluster = GlyphClusters[0]; + lastCluster = GlyphClusters[GlyphClusters.Count - 1]; _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); } + var isReversed = firstCluster > lastCluster; var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var widthIncludingTrailingWhitespace = 0d; - var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount); + var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); for (var index = 0; index < GlyphIndices.Count; index++) { @@ -635,16 +647,16 @@ namespace Avalonia.Media var width = widthIncludingTrailingWhitespace; - if (IsLeftToRight) + if (isReversed) { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = 0; index < glyphCount; index++) { width -= GetGlyphAdvance(index, out _); - } + } } else { - for (var index = 0; index < glyphCount; index++) + for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) { width -= GetGlyphAdvance(index, out _); } @@ -654,16 +666,15 @@ namespace Avalonia.Media height); } - private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount) - { - glyphCount = 0; - newLineLength = 0; - - if (Characters.IsEmpty) + private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount) + { + if (isReversed) { - return 0; + return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } + glyphCount = 0; + newLineLength = 0; var trailingWhitespaceLength = 0; if (GlyphClusters == null) @@ -732,6 +743,78 @@ namespace Avalonia.Media return trailingWhitespaceLength; } + private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + { + glyphCount = 0; + newLineLength = 0; + var trailingWhitespaceLength = 0; + + if (GlyphClusters == null) + { + for (var i = 0; i < Characters.Length;) + { + var codepoint = Codepoint.ReadAt(_characters, i, out var count); + + if (!codepoint.IsWhiteSpace) + { + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength++; + } + + trailingWhitespaceLength++; + + i += count; + glyphCount++; + } + } + else + { + for (var i = 0; i < GlyphClusters.Count; i++) + { + var currentCluster = GlyphClusters[i]; + var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); + + if (!codepoint.IsWhiteSpace) + { + break; + } + + var clusterLength = 1; + + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; + + if (currentCluster == nextCluster) + { + clusterLength++; + i--; + + continue; + } + + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; + } + } + + return trailingWhitespaceLength; + } + private void Set(ref T field, T value) { _glyphRunImpl?.Dispose(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7c686358e2..f4a0324d90 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -166,58 +166,74 @@ namespace Avalonia.Media.TextFormatting if (distance <= 0) { - // hit happens before the line, return the first position var firstRun = _textRuns[0]; - if (firstRun is ShapedTextCharacters shapedTextCharacters) - { - return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _); - } + return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + } - return _resolvedFlowDirection == FlowDirection.LeftToRight ? - new CharacterHit(FirstTextSourceIndex) : - new CharacterHit(FirstTextSourceIndex + Length); + if (distance > WidthIncludingTrailingWhitespace) + { + var lastRun = _textRuns[_textRuns.Count - 1]; + + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width); } // process hit that happens within the line var characterHit = new CharacterHit(); var currentPosition = FirstTextSourceIndex; + var currentDistance = 0.0; foreach (var currentRun in _textRuns) { - switch (currentRun) + if (currentDistance + currentRun.Size.Width < distance) { - case ShapedTextCharacters shapedRun: - { - characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + currentDistance += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; - var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + continue; + } - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - break; - } - default: + break; + } + + return characterHit; + } + + private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance) + { + CharacterHit characterHit; + + switch (run) + { + case ShapedTextCharacters shapedRun: + { + characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + + var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + + if (!shapedRun.GlyphRun.IsLeftToRight) { - if (distance < currentRun.Size.Width / 2) - { - characterHit = new CharacterHit(currentPosition); - } - else - { - characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength); - } - break; + offset = Math.Max(0, offset - shapedRun.Text.End); } - } - if (distance <= currentRun.Size.Width) - { - break; - } + characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); - distance -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + break; + } + default: + { + if (distance < run.Size.Width / 2) + { + characterHit = new CharacterHit(currentPosition); + } + else + { + characterHit = new CharacterHit(currentPosition, run.TextSourceLength); + } + break; + } } return characterHit; @@ -226,136 +242,122 @@ namespace Avalonia.Media.TextFormatting /// public override double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var isTrailingHit = characterHit.TrailingLength > 0; + var flowDirection = _paragraphProperties.FlowDirection; var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var currentDistance = Start; var currentPosition = FirstTextSourceIndex; var remainingLength = characterIndex - FirstTextSourceIndex; - GlyphRun? lastRun = null; + var currentDistance = Start; - for (var index = 0; index < _textRuns.Count; index++) + if (flowDirection == FlowDirection.LeftToRight) + { + for (var index = 0; index < _textRuns.Count; index++) + { + var currentRun = _textRuns[index]; + + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, + flowDirection, out var distance, out _)) + { + return currentDistance + distance; + } + + //No hit hit found so we add the full width + currentDistance += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; + remainingLength -= currentRun.TextSourceLength; + } + } + else { - var textRun = _textRuns[index]; + currentDistance += WidthIncludingTrailingWhitespace; - switch (textRun) + for (var index = _textRuns.Count - 1; index >= 0; index--) { - case ShapedTextCharacters shapedTextCharacters: + var currentRun = _textRuns[index]; + + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, + flowDirection, out var distance, out var currentGlyphRun)) + { + if (currentGlyphRun != null) { - var currentRun = shapedTextCharacters.GlyphRun; + distance = currentGlyphRun.Size.Width - distance; + } - if (lastRun != null) - { - if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight && - currentRun.Characters.Start == characterHit.FirstCharacterIndex && - characterHit.TrailingLength == 0) - { - return currentDistance; - } - } + return currentDistance - distance; + } - //Look for a hit in within the current run - if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length) - { - characterHit = new CharacterHit(textRun.Text.Start + remainingLength); + //No hit hit found so we add the full width + currentDistance -= currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; + remainingLength -= currentRun.TextSourceLength; + } + } - var distance = currentRun.GetDistanceFromCharacterHit(characterHit); + return currentDistance; + } - return currentDistance + distance; - } + private static bool TryGetDistanceFromCharacterHit( + DrawableTextRun currentRun, + CharacterHit characterHit, + int currentPosition, + int remainingLength, + FlowDirection flowDirection, + out double distance, + out GlyphRun? currentGlyphRun) + { + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + var isTrailingHit = characterHit.TrailingLength > 0; - //Look at the left and right edge of the current run - if (currentRun.IsLeftToRight) - { - if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) - { - if (characterIndex <= currentPosition) - { - return currentDistance; - } - } - else - { - if (characterIndex == currentPosition) - { - return currentDistance; - } - } + distance = 0; + currentGlyphRun = null; - if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit) - { - return currentDistance + currentRun.Size.Width; - } - } - else - { - if (characterIndex == currentPosition) - { - return currentDistance + currentRun.Size.Width; - } - - var nextRun = index + 1 < _textRuns.Count ? - _textRuns[index + 1] as ShapedTextCharacters : - null; + switch (currentRun) + { + case ShapedTextCharacters shapedTextCharacters: + { + currentGlyphRun = shapedTextCharacters.GlyphRun; - if (nextRun != null) - { - if (nextRun.ShapedBuffer.IsLeftToRight) - { - if (characterIndex == currentPosition + textRun.Text.Length) - { - return currentDistance; - } - } - else - { - if (currentPosition + nextRun.Text.Length == characterIndex) - { - return currentDistance; - } - } - } - else - { - if (characterIndex > currentPosition + textRun.Text.Length) - { - return currentDistance; - } - } - } + if (currentPosition + remainingLength < currentPosition + currentRun.Text.Length) + { + characterHit = new CharacterHit(currentRun.Text.Start + remainingLength); - lastRun = currentRun; + distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); - break; + return true; } - default: + + if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit) { - if (characterIndex == currentPosition) + if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - return currentDistance; + distance = currentGlyphRun.Size.Width; } - if (characterIndex == currentPosition + textRun.TextSourceLength) - { - return currentDistance + textRun.Size.Width; - } + return true; + } - break; + break; + } + default: + { + if (characterIndex == currentPosition) + { + return true; } - } - //No hit hit found so we add the full width - currentDistance += textRun.Size.Width; - currentPosition += textRun.TextSourceLength; - remainingLength -= textRun.TextSourceLength; + if (characterIndex == currentPosition + currentRun.TextSourceLength) + { + distance = currentRun.Size.Width; - if (remainingLength <= 0) - { - break; - } + return true; + + } + + break; + } } - return currentDistance; + return false; } /// @@ -460,20 +462,33 @@ namespace Avalonia.Media.TextFormatting var startIndex = currentRun.Text.Start + offset; - var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex + remainingLength) : - new CharacterHit(startIndex)); + double startOffset; + double endOffset; - endX += endOffset; + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex) : - new CharacterHit(startIndex + remainingLength)); + if (currentPosition < startIndex) + { + startOffset = endOffset; + } + else + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + } startX += startOffset; + endX += endOffset; + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); @@ -504,7 +519,7 @@ namespace Avalonia.Media.TextFormatting } //Lines that only contain a linebreak need to be covered here - if(characterLength == 0) + if (characterLength == 0) { characterLength = NewLineLength; } @@ -532,19 +547,9 @@ namespace Avalonia.Media.TextFormatting currentWidth += runwidth; currentPosition += characterLength; - if (currentDirection == FlowDirection.LeftToRight) + if (currentPosition > characterIndex) { - if (currentPosition > characterIndex) - { - break; - } - } - else - { - if (currentPosition <= firstTextSourceIndex) - { - break; - } + break; } startX = endX; @@ -571,7 +576,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start + WidthIncludingTrailingWhitespace; + var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; var currentRect = Rect.Empty; @@ -582,7 +587,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex) { startX -= currentRun.Size.Width; @@ -601,20 +606,31 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; var startIndex = currentRun.Text.Start + offset; + double startOffset; + double endOffset; - var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex + remainingLength) : - new CharacterHit(startIndex)); + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + if (currentPosition < startIndex) + { + startOffset = endOffset = 0; + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - endX += endOffset - currentShapedRun.Size.Width; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + } + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex) : - new CharacterHit(startIndex + remainingLength)); + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } - startX += startOffset - currentShapedRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); @@ -652,9 +668,10 @@ namespace Avalonia.Media.TextFormatting } var runWidth = endX - startX; - var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + + if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) { currentRect = currentRect.WithWidth(currentWidth + runWidth); @@ -674,19 +691,9 @@ namespace Avalonia.Media.TextFormatting currentWidth += runWidth; currentPosition += characterLength; - if (currentDirection == FlowDirection.LeftToRight) + if (currentPosition > characterIndex) { - if (currentPosition > characterIndex) - { - break; - } - } - else - { - if (currentPosition <= firstTextSourceIndex) - { - break; - } + break; } lastDirection = currentDirection; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7e1103d624..631d0881b0 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -154,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { j += inner.Current.Text.Length; - if(j + i > text.Length) + if (j + i > text.Length) { break; } @@ -738,7 +738,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - + var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var rects = layout.HitTestTextRange(0, 7).ToArray(); @@ -746,7 +746,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, rects.Length); var expected = rects[0]; - + Assert.Equal(expected.Left, start); Assert.Equal(expected.Right, end); } @@ -818,11 +818,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); Assert.Equal(expected, actual); - } + } } } } - + [Fact] public void Should_Layout_Empty_String() { @@ -833,11 +833,83 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Typeface.Default, 12, Brushes.Black); - + Assert.True(layout.Bounds.Height > 0); } } + [Fact] + public void Should_HitTestPoint_RightToLeft() + { + using (Start()) + { + var text = "אאא AAA"; + + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black, + flowDirection: FlowDirection.RightToLeft); + + var firstRun = layout.TextLines[0].TextRuns[0] as ShapedTextCharacters; + + var hit = layout.HitTestPoint(new Point()); + + Assert.Equal(4, hit.TextPosition); + + var currentX = 0.0; + + for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + { + var cluster = firstRun.GlyphRun.GlyphClusters[i]; + var advance = firstRun.GlyphRun.GlyphAdvances[i]; + + hit = layout.HitTestPoint(new Point(currentX, 0)); + + Assert.Equal(cluster, hit.TextPosition); + + var hitRange = layout.HitTestTextRange(hit.TextPosition, 1); + + var distance = hitRange.First().Left; + + Assert.Equal(currentX, distance); + + currentX += advance; + } + + var secondRun = layout.TextLines[0].TextRuns[1] as ShapedTextCharacters; + + hit = layout.HitTestPoint(new Point(firstRun.Size.Width, 0)); + + Assert.Equal(7, hit.TextPosition); + + hit = layout.HitTestPoint(new Point(layout.TextLines[0].WidthIncludingTrailingWhitespace, 0)); + + Assert.Equal(0, hit.TextPosition); + + currentX = firstRun.Size.Width + 0.5; + + for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + { + var cluster = secondRun.GlyphRun.GlyphClusters[i]; + var advance = secondRun.GlyphRun.GlyphAdvances[i]; + + hit = layout.HitTestPoint(new Point(currentX, 0)); + + Assert.Equal(cluster, hit.CharacterHit.FirstCharacterIndex); + + var hitRange = layout.HitTestTextRange(hit.CharacterHit.FirstCharacterIndex, hit.CharacterHit.TrailingLength); + + var distance = hitRange.First().Left + 0.5; + + Assert.Equal(currentX, distance); + + currentX += advance; + } + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface From b9aa49bc05d039b01a52e0a10d0494be6bdb92d9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 8 Jul 2022 18:31:49 +0100 Subject: [PATCH 250/481] Add failing unit test. --- .../Avalonia.Controls.UnitTests/WindowTests.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index ab4fd566c8..3677d62050 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -542,28 +542,25 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen_1080_175() + public void Window_Should_Be_Sized_To_MinSize_If_InitialSize_Less_Than_MinSize() { var screen1 = new Mock(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true); - var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object }); screens.Setup(x => x.ScreenFromPoint(It.IsAny())).Returns(screen1.Object); - var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.DesktopScaling).Returns(1.75); windowImpl.Setup(x => x.RenderScaling).Returns(1.75); windowImpl.Setup(x => x.Screen).Returns(screens.Object); - var clientSize = new Size(801.142, 366); + var clientSize = new Size(400.142, 366); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) .Callback((x, y) => { - clientSize = x;//.Constrain(screen1.Object.Bounds.Size.ToSize(1.75)); + clientSize = x; windowImpl.Object.Resized?.Invoke(clientSize, y); }); @@ -575,14 +572,10 @@ namespace Avalonia.Controls.UnitTests window.WindowStartupLocation = WindowStartupLocation.CenterScreen; window.MinWidth = 720; window.MinHeight = 480; - //window.Width = 720; - //window.Height = 480; window.Show(); - - var expectedPosition = new PixelPoint(321, 36); - - Assert.Equal(window.Position, expectedPosition); + + Assert.Equal(new Size(720, 480), window.Bounds.Size); } } From 05f4bcad80fdbf621f57c34303b2207eb35ea519 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 8 Jul 2022 23:40:08 -0400 Subject: [PATCH 251/481] Enable compiled bindings for ColorPicker and ColorView --- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorView.xaml | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b9133d6bfb..b34ac68be5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -1,7 +1,7 @@  + x:CompileBindings="True"> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index a0687ed972..bcdb72d96c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -5,7 +5,7 @@ xmlns:primitives="using:Avalonia.Controls.Primitives" xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib" - x:CompileBindings="False"> + x:CompileBindings="True"> @@ -244,12 +244,20 @@ - - + From 8f0b3911a5216dee5d44b5d5719f34b3a0ef2973 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 12:22:57 +0200 Subject: [PATCH 252/481] Added failing tests for MenuItem.TemplatedParent. Seems #8412 caused `Menu.TemplatedParent` to get set to the parent `MenuItem` when re-opening a popup. --- .../MenuItemTests.cs | 44 ++++++ .../Primitives/PopupTests.cs | 147 +++++++++++++++++- 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index db31d22b4f..d25a790fde 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -8,6 +8,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -301,6 +302,49 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(3, canExecuteCallCount); } } + + + [Fact] + public void TemplatedParent_Should_Not_Be_Applied_To_Submenus() + { + using (Application()) + { + MenuItem topLevelMenu; + MenuItem childMenu1; + MenuItem childMenu2; + var menu = new Menu + { + Items = new[] + { + (topLevelMenu = new MenuItem + { + Header = "Foo", + Items = new[] + { + (childMenu1 = new MenuItem { Header = "Bar" }), + (childMenu2 = new MenuItem { Header = "Baz" }), + } + }), + } + }; + + var window = new Window { Content = menu }; + window.LayoutManager.ExecuteInitialLayoutPass(); + + topLevelMenu.IsSubMenuOpen = true; + + Assert.True(((IVisual)childMenu1).IsAttachedToVisualTree); + Assert.Null(childMenu1.TemplatedParent); + Assert.Null(childMenu2.TemplatedParent); + + topLevelMenu.IsSubMenuOpen = false; + topLevelMenu.IsSubMenuOpen = true; + + Assert.Null(childMenu1.TemplatedParent); + Assert.Null(childMenu2.TemplatedParent); + } + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index d9cb40d1cc..5f91f2e2a1 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -295,7 +295,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() + public void ContentControl_With_Popup_In_Template_Should_Set_TemplatedParent() { // Test uses OverlayPopupHost default template using (CreateServices()) @@ -384,6 +384,134 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void ItemsControl_With_Popup_In_Template_Should_Set_TemplatedParent() + { + // Test uses OverlayPopupHost default template + using (CreateServices()) + { + PopupItemsControl target; + var item = new Border(); + var root = PreparedWindow(target = new PopupItemsControl + { + Items = new[] { item }, + Template = new FuncControlTemplate(PopupItemsControlTemplate), + }); ; + root.Show(); + + target.ApplyTemplate(); + + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); + popup.Open(); + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); + + var children = popupRoot.GetVisualDescendants().ToList(); + var types = children.Select(x => x.GetType().Name).ToList(); + + if (UsePopupHost) + { + Assert.Equal( + new[] + { + "LayoutTransformControl", + "VisualLayerManager", + "ContentPresenter", + "ItemsPresenter", + "StackPanel", + "Border", + }, + types); + } + else + { + Assert.Equal( + new[] + { + "LayoutTransformControl", + "Panel", + "Border", + "VisualLayerManager", + "ContentPresenter", + "ItemsPresenter", + "StackPanel", + "Border", + }, + types); + } + + var templatedParents = children + .OfType() + .Select(x => x.TemplatedParent).ToList(); + + if (UsePopupHost) + { + Assert.Equal( + new object[] + { + popupRoot, + popupRoot, + popupRoot, + target, + target, + null, + }, + templatedParents); + } + else + { + Assert.Equal( + new object[] + { + popupRoot, + popupRoot, + popupRoot, + popupRoot, + popupRoot, + target, + target, + null, + }, + templatedParents); + } + } + } + + [Fact] + public void Should_Not_Overwrite_TemplatedParent_Of_Item_In_ItemsControl_With_Popup_On_Second_Open() + { + // Test uses OverlayPopupHost default template + using (CreateServices()) + { + PopupItemsControl target; + var item = new Border(); + var root = PreparedWindow(target = new PopupItemsControl + { + Items = new[] { item }, + Template = new FuncControlTemplate(PopupItemsControlTemplate), + }); + root.Show(); + + target.ApplyTemplate(); + + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); + popup.Open(); + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); + + Assert.Null(item.TemplatedParent); + + popup.Close(); + popup.Open(); + + Assert.Null(item.TemplatedParent); + } + } + [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -979,10 +1107,27 @@ namespace Avalonia.Controls.UnitTests.Primitives }.RegisterInNameScope(scope); } + private static IControl PopupItemsControlTemplate(PopupItemsControl control, INameScope scope) + { + return new Popup + { + Name = "popup", + PlacementTarget = control, + Child = new ItemsPresenter + { + [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], + } + }.RegisterInNameScope(scope); + } + private class PopupContentControl : ContentControl { } + private class PopupItemsControl : ItemsControl + { + } + private class TestControl : Decorator { public event EventHandler DataContextBeginUpdate; From 78a7257a5a3a0cd33ad77382d4d0c29759263dfa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 12:32:22 +0200 Subject: [PATCH 253/481] Don't apply templated parent if already set. When applying the `TemplatedParent`, don't move into trees that already have the `TemplatedParent` property set - in this case we can assume that the templated parent has already been set for this tree, and it fixes the problem with menus added in the previous commit. --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 4403bfce51..c3e0f3e523 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -396,7 +396,7 @@ namespace Avalonia.Controls.Primitives for (var i = 0; i < count; i++) { - if (children[i] is IStyledElement child) + if (children[i] is IStyledElement child && child.TemplatedParent is null) { ApplyTemplatedParent(child, templatedParent); } From 5da9d528c9a9901afa6cd6e856b9055022cdb706 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 14:50:52 +0200 Subject: [PATCH 254/481] Added missing param docs. --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index c3e0f3e523..e50f991cdb 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -387,6 +387,7 @@ namespace Avalonia.Controls.Primitives /// Sets the TemplatedParent property for the created template children. /// /// The control. + /// The templated parent to apply. internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { control.SetValue(TemplatedParentProperty, templatedParent); From 435419588a3702efc1f67eec7a6865c64a03cbb5 Mon Sep 17 00:00:00 2001 From: Eric M Date: Wed, 6 Jul 2022 21:18:40 +1000 Subject: [PATCH 255/481] Allow customisation of ProgressBar text format. --- .../ControlCatalog/Pages/ProgressBarPage.xaml | 27 ++++++++++++++----- .../Converters/StringFormatConverter.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/ProgressBar.cs | 23 ++++++++++++++++ .../Controls/ProgressBar.xaml | 21 ++++++++++++--- .../Controls/ProgressBar.xaml | 16 ++++++++++- 5 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 src/Avalonia.Controls/Converters/StringFormatConverter.cs diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index db7d7d3280..8e73f1d0f5 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -1,22 +1,37 @@ A progress bar control - + + + Maximum + + + + Minimum + + + + Progress Text Format + + - + - + - - + + - + diff --git a/src/Avalonia.Controls/Converters/StringFormatConverter.cs b/src/Avalonia.Controls/Converters/StringFormatConverter.cs new file mode 100644 index 0000000000..ae920dac7e --- /dev/null +++ b/src/Avalonia.Controls/Converters/StringFormatConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters; + +/// +/// Calls on the passed in values, where the first element in the list +/// is the string, and everything after it is passed into the object array in order. +/// +public class StringFormatConverter : IMultiValueConverter +{ + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + try + { + return string.Format((string)values[0]!, values.Skip(1).ToArray()); + } + catch (Exception e) + { + return new BindingNotification(e, BindingErrorType.Error); + } + } +} diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 1075328c67..70edeadfd9 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -96,6 +96,7 @@ namespace Avalonia.Controls } } + private double _percentage; private double _indeterminateStartingOffset; private double _indeterminateEndingOffset; private Border? _indicator; @@ -106,9 +107,17 @@ namespace Avalonia.Controls public static readonly StyledProperty ShowProgressTextProperty = AvaloniaProperty.Register(nameof(ShowProgressText)); + public static readonly StyledProperty ProgressTextFormatProperty = + AvaloniaProperty.Register(nameof(ProgressTextFormat), "{1:0}%"); + public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + public static readonly DirectProperty PercentageProperty = + AvaloniaProperty.RegisterDirect( + nameof(Percentage), + o => o.Percentage); + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( @@ -123,6 +132,12 @@ namespace Avalonia.Controls p => p.IndeterminateEndingOffset, (p, o) => p.IndeterminateEndingOffset = o); + public double Percentage + { + get { return _percentage; } + private set { SetAndRaise(PercentageProperty, ref _percentage, value); } + } + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateStartingOffset { @@ -165,6 +180,12 @@ namespace Avalonia.Controls set => SetValue(ShowProgressTextProperty, value); } + public string ProgressTextFormat + { + get => GetValue(ProgressTextFormatProperty); + set => SetValue(ProgressTextFormatProperty, value); + } + public Orientation Orientation { get => GetValue(OrientationProperty); @@ -245,6 +266,8 @@ namespace Avalonia.Controls _indicator.Width = bounds.Width * percent; else _indicator.Height = bounds.Height * percent; + + Percentage = percent * 100; } } } diff --git a/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml index fd847b5d65..3f684f3936 100644 --- a/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/Controls/ProgressBar.xaml @@ -1,4 +1,6 @@ - + @@ -11,10 +13,13 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 3fb0d43342..b8fea84ae0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -1,4 +1,5 @@ - + @@ -7,14 +8,11 @@ - - - 8,5,8,6 - - - - - - - - + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index 836cc27db3..a5234f5771 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -1,28 +1,26 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + - @@ -32,118 +30,79 @@ M0,9 L10,0 20,9 19,10 10,2 1,10 z M0,1 L10,10 20,1 19,0 10,8 1,0 z - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml index 43e4a1fcea..3434f9b6e5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml @@ -1,38 +1,40 @@ - - - + + - - + CornerRadius="{TemplateBinding CornerRadius}" + HeaderBackground="{TemplateBinding HeaderBackground}" /> + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml index 3a6af60983..0381022910 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml @@ -1,121 +1,109 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + + + + - + - - + + + - - - - - - + + + + + - - + + + + - - - - - - - + - - - - - - - - - - - - + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index c6f70d05e1..83a254be56 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -1,182 +1,180 @@ - - - - - - - - + + + + + + + 12 32 - - - - - - - - + Width="3" + Height="3" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index 62874f4884..3c45de18c6 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -1,72 +1,57 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml b/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml index baba0649aa..6c05a62250 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml @@ -1,17 +1,20 @@ - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index e9830fb228..2eee425cb0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -1,297 +1,293 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + + + + + + - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 93ecc438eb..2dc7439001 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -1,232 +1,228 @@ - - - - - - Item 1 - Item 2 - - - Item 1 - Item 2 - - - - - + + + + + + Item 1 + Item 2 + + + Item 1 + Item 2 + + + + + 0,0,0,4 15 7 - + 12,5,0,7 11,5,32,6 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Background="Transparent" + Margin="0,1,1,1" + Width="30" + IsVisible="False" + HorizontalAlignment="Right" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml index 0debe87445..88f9c0ea15 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml @@ -1,90 +1,85 @@ - - - - - - Item 1 - Item 2 long - Item 3 - Item 4 - + + + + + + Item 1 + Item 2 long + Item 3 + Item 4 + + - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Common.xaml b/src/Avalonia.Themes.Fluent/Controls/Common.xaml deleted file mode 100644 index e09e39d7cb..0000000000 --- a/src/Avalonia.Themes.Fluent/Controls/Common.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml index d32bc399b6..5777603074 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml @@ -1,16 +1,19 @@ - + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml index df800b4a06..fe0f0f3f6c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml @@ -1,72 +1,72 @@ - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index 649a186c7e..6ed73148a7 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -1,104 +1,103 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 11d6b9fdfc..a7093c1341 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -5,17 +5,16 @@ // All other rights reserved. --> - - - - - - - - + + + + + + 0,0,0,4 40 40 @@ -27,328 +26,327 @@ 0,3,0,6 9,3,0,6 1 - - - - - - - - + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + - - + + + + + + + + + + - - - + + + + - + - + + + - + - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + - - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml index b96c689ab6..3206cb8cb1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml @@ -1,103 +1,101 @@ - - - - - - - - - - - - - 32 - + + + + + + + + + - - - - - - - - - - + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index 79560be933..89eaec70ad 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -1,19 +1,23 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index 33d502772e..36f6008782 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -1,6 +1,6 @@ - + @@ -34,72 +34,34 @@ - - 16 - 16 + 16 + 16 - 1 + 1 - 1,1,0,1 - 1,1,1,0 - 0,1,1,1 - 1,0,1,1 + 1,1,0,1 + 1,1,1,0 + 0,1,1,1 + 1,0,1,1 - - + + - - + + - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5b217e4764..286b3e3fcd 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -1,70 +1,78 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml index 92f8177ead..2dca5b0770 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml @@ -1,9 +1,7 @@ - - - 1 - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml index 91bf71ed4d..c3f489da80 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml @@ -1,4 +1,4 @@ - 0 diff --git a/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml b/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml index e3a7b04f33..ca57eccd13 100644 --- a/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml @@ -1,24 +1,22 @@ - - - - - + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml index 19d13b6399..b8d7c2c4ef 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml @@ -1,16 +1,19 @@ - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Label.xaml b/src/Avalonia.Themes.Fluent/Controls/Label.xaml index d41e4e2166..ad57239648 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Label.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Label.xaml @@ -1,18 +1,22 @@ - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index 8011ed9daf..b1d4b77a3f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -1,45 +1,47 @@ - - - - - Test - Test - Test - Test - - - - - + + + + + Test + Test + Test + Test + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml index 11f3c12772..c00e920f75 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml @@ -1,94 +1,79 @@ - - - - - Disabled - - Test - - Test - - - - + + + + + Disabled + + Test + + Test + + + 12,9,12,12 - - + + + + + + + + - + - - + + - - - + + - - - + + - + - - - + + - - - + + - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index 60e16fcff2..dc454e4fdb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -1,324 +1,324 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - + + - - + + + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index 4eb724a926..d42e413f00 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -1,36 +1,35 @@ - + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml index ff50acab5e..7769f0a5c9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml @@ -1,35 +1,34 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 33cf6bfdde..b7a9435749 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -1,4 +1,4 @@ - - -4 + -4 0,0,12,0 24,0,0,0 M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index d40ba0cc1d..753c03992a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -1,30 +1,29 @@ - - - - - - + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml index 924d977eb5..e55fcb90e8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml @@ -1,11 +1,11 @@ - + - + - - - - - - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 36ab07e3e3..74ffff0f5c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -1,68 +1,67 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml index 31b43c39cd..0b587e6037 100644 --- a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml @@ -1,20 +1,23 @@ - + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml index d4952b3571..966c6c2632 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml @@ -1,29 +1,26 @@ - - - - - M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z - - - - - - + + + + + M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml index f608cf55f5..f57ecd2013 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml @@ -1,28 +1,28 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a463334a76..3c7d22e13f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -1,164 +1,179 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml index e967dc8807..a1c4ba4f2d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml @@ -1,4 +1,4 @@ - + @@ -9,7 +9,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index 7fa515d3f7..670212c4b1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -1,63 +1,65 @@ - - - - - - - - - + + + + + + + + + 8,5,8,6 - - - + + + + + + + + + + + + + + + + - + - + - - + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml index 4727ff72b9..7d87a3e35a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml @@ -1,4 +1,4 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Separator.xaml b/src/Avalonia.Themes.Fluent/Controls/Separator.xaml index 5d95ccc404..afbc3770c1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Separator.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Separator.xaml @@ -1,7 +1,7 @@ - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index cd2c02c567..b468f7f4ad 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -1,19 +1,20 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + 0,0,0,4 15 15 @@ -24,252 +25,364 @@ 20 20 20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + - + - + + + + - + - - - - - - - - - + - - - - - - - - - - - + - - - - - - - - + - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index 91c901f567..fb0460d9a1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True" xmlns:converters="using:Avalonia.Controls.Converters"> - diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml index 55d46e32a1..6b9b94852f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml @@ -1,4 +1,4 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml index 322f6ce89e..1cc31f8e15 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml @@ -1,65 +1,63 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - 0 0 0 2 - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml index ebe6f82917..110ffc2f48 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml @@ -1,132 +1,133 @@ - - - - - - - - - - + + + + + + + + + + 48 24 2 - - + + + + + + + + + + + + + + + + + + + - - - + + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - + - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml index 681ac48850..15c3ba2f37 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml @@ -1,32 +1,34 @@ - - - - - Item 1 - Item 2 - Disabled - - - - - + + + + + Item 1 + Item 2 + Disabled + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml index 59f68a1547..5ccd430d33 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml @@ -1,107 +1,108 @@ - - - - - Leaf - Arch - - - - + + + + + Leaf + Arch + + + + 48 2 - - + + + + + + + + + + + + + + + + + + + - + - - - - + + + + - - - - + + + + - - + + - - + + - - + + - - + + - - - + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml index 40d9b11f7c..0fd3758d77 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml @@ -1,4 +1,4 @@ - @@ -10,7 +10,7 @@ TextAlignment="Center"/> - + 0,0,0,4 M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z @@ -36,7 +36,7 @@ - + @@ -73,32 +73,32 @@ VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> - - - - - + + + + + @@ -159,7 +159,7 @@ - + @@ -237,7 +237,7 @@ - + - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml index 4dba5b4ba4..2b97df020f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml @@ -1,53 +1,55 @@ - - - - - - - + + - + - + - + - + - + - - + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index e0d54d7232..b53980b792 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -1,5 +1,5 @@ - + @@ -10,104 +10,111 @@ - 8,5,8,6 - - - - - + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + + - + - - + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml index a2b50a859d..533903bec7 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml @@ -1,288 +1,307 @@ - - + + + + + + + + + + + + + + + + + 0,0,0,6 6 6 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index 2d18be91cb..1070f3b68c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -1,87 +1,71 @@ - - - - - - Hover Here - - + + + + Hover Here + + + - - - - ToolTip - A control which pops up a hint when a control is hovered - - - ToolTip bottom placement - - - + Padding="50" + ToolTip.Placement="Bottom"> + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + + - 320 - - - - + + + + + + + + + + + + + + + + + + + + + + - + - - + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml index 6a4d56ccb7..f28dc66a5f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml @@ -1,20 +1,19 @@ - - - + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml index 656b72e07b..4fcec79f25 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml @@ -1,28 +1,31 @@ - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index f86b67bb6c..6510832eb3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -1,4 +1,4 @@ - @@ -53,7 +53,6 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index 90963c606c..d78fd76a37 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -1,27 +1,30 @@ - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml index 8d14c2d972..d5d5114c1b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml @@ -1,47 +1,58 @@ - - + + + + + + + + + + + + + + + + + + + + - + - + - + - - + + + From afe3486e9f3ea7a493c7e66ca3e5b59682357dcb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 11:15:41 +0200 Subject: [PATCH 258/481] Remove trailing /template/ selectors. --- .../Controls/ButtonSpinner.xaml | 12 +-- .../Controls/CalendarDatePicker.xaml | 12 +-- .../Controls/CalendarDayButton.xaml | 6 +- .../Controls/CheckBox.xaml | 88 +++++++++---------- .../Controls/ComboBox.xaml | 50 +++++------ .../Controls/Slider.xaml | 56 ++++++------ .../Controls/TimePicker.xaml | 6 +- 7 files changed, 113 insertions(+), 117 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index a5234f5771..f99f1040e5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -87,20 +87,20 @@ - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 83a254be56..eefd9e9b51 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -106,13 +106,13 @@ - - @@ -152,13 +152,13 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml index b119dd1355..9f476d51bc 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml @@ -84,11 +84,11 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index 2eee425cb0..44e1afefa4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -63,64 +63,64 @@ - - - - - - - - - - - - @@ -145,64 +145,64 @@ - - - - - - - - - - - - @@ -227,64 +227,64 @@ - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 2dc7439001..8ef8388cf1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -144,17 +144,15 @@ - - - + + + @@ -175,51 +173,51 @@ - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index b468f7f4ad..c74878e8be 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -283,17 +283,15 @@ - - - + + + @@ -316,70 +314,70 @@ - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 69b2d32dda..dc09032a36 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -82,11 +82,11 @@ --> - - From 671b7ea83e75a2b442a4ee0d5877c0e269dfbc48 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 11:50:33 +0200 Subject: [PATCH 259/481] WIP: HamburgerMenu. --- samples/ControlCatalog/App.xaml | 8 +- .../HamburgerMenu/HamburgerMenu.xaml | 260 +++++++++--------- 2 files changed, 139 insertions(+), 129 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index d0e1bd885e..c7ad72d2c5 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -5,6 +5,13 @@ x:CompileBindings="True" Name="Avalonia ControlCatalog" x:Class="ControlCatalog.App"> + + + + + + + - diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 1d58c465a0..5f8bfe4fb5 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -1,6 +1,6 @@ - + @@ -20,21 +20,84 @@ - - 40 - 220 - 36 - 36 - 32 - 12,0,0,0 - 52,0,0,0 - 212,0,0,0 - 1 1 1 1 #2000, 0 0 1 1 #2fff - 0 0 1 1 #2000 - + 40 + 220 + 36 + 36 + 32 + 12,0,0,0 + 52,0,0,0 + 212,0,0,0 + 1 1 1 1 #2000, 0 0 1 1 #2fff + 0 0 1 1 #2000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + IEnumerable Containers { get; } + /// + /// Gets or sets the theme to be applied to the items in the control. + /// + ControlTheme? ItemContainerTheme { get; set; } + /// /// Gets or sets the data template used to display the items in the control. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index a76dcbe9c8..8b36b07cec 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Styling; namespace Avalonia.Controls.Generators { @@ -35,6 +36,11 @@ namespace Avalonia.Controls.Generators /// public event EventHandler? Recycled; + /// + /// Gets or sets the theme to be applied to the items in the control. + /// + public ControlTheme? ItemContainerTheme { get; set; } + /// /// Gets or sets the data template used to display the items in the control. /// @@ -190,10 +196,18 @@ namespace Avalonia.Controls.Generators result.SetValue( ContentPresenter.ContentTemplateProperty, ItemTemplate, - BindingPriority.TemplatedParent); + BindingPriority.Style); } } + if (ItemContainerTheme != null) + { + result.SetValue( + StyledElement.ThemeProperty, + ItemContainerTheme, + BindingPriority.TemplatedParent); + } + return result; } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 635f3a7d37..3ff1b0702d 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -44,28 +44,29 @@ namespace Avalonia.Controls.Generators { var container = item as T; - if (container != null) + if (container is null) { - return container; - } - else - { - var result = new T(); + container = new T(); if (ContentTemplateProperty != null) { - result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); + container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); } - result.SetValue(ContentProperty, item, BindingPriority.Style); + container.SetValue(ContentProperty, item, BindingPriority.Style); if (!(item is IControl)) { - result.DataContext = item; + container.DataContext = item; } + } - return result; + if (ItemContainerTheme != null) + { + container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style); } + + return container; } /// diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 536a5fdd06..4e3deb5552 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -71,6 +71,11 @@ namespace Avalonia.Controls.Generators var template = GetTreeDataTemplate(item, ItemTemplate); var result = new T(); + if (ItemContainerTheme != null) + { + result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style); + } + result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); var itemsSelector = template.ItemsSelector(item); diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 56b0014c05..427f5c8a9b 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -15,6 +15,7 @@ using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -36,6 +37,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemContainerThemeProperty = + AvaloniaProperty.Register(nameof(ItemContainerTheme)); + /// /// Defines the property. /// @@ -88,6 +95,7 @@ namespace Avalonia.Controls { _itemContainerGenerator = CreateItemContainerGenerator(); + _itemContainerGenerator.ItemContainerTheme = ItemContainerTheme; _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); @@ -108,6 +116,15 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + /// + /// Gets or sets the that is applied to the container element generated for each item. + /// + public ControlTheme? ItemContainerTheme + { + get { return GetValue(ItemContainerThemeProperty); } + set { SetValue(ItemContainerThemeProperty, value); } + } + /// /// Gets the number of items in . /// @@ -349,6 +366,10 @@ namespace Avalonia.Controls { UpdatePseudoClasses(change.GetNewValue()); } + else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) + { + _itemContainerGenerator.ItemContainerTheme = change.GetNewValue(); + } } /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 490b0b3ce3..2e3aa037c2 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -121,6 +121,11 @@ namespace Avalonia.Controls { ItemTemplate = _treeView.ItemTemplate; } + + if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null) + { + ItemContainerTheme = _treeView.ItemContainerTheme; + } } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index bfece7871c..f08653a4f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -1,15 +1,14 @@ +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.VisualTree; -using Xunit; -using System.Collections.ObjectModel; +using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.Input; -using System.Collections.Generic; +using Xunit; namespace Avalonia.Controls.UnitTests { @@ -62,6 +61,25 @@ namespace Avalonia.Controls.UnitTests Assert.Null(container.TemplatedParent); } + [Fact] + public void Container_Should_Have_Theme_Set_To_ItemContainerTheme() + { + var theme = new ControlTheme(); + var target = new ItemsControl + { + ItemContainerTheme = theme, + }; + + target.Template = GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var container = (ContentPresenter)target.Presenter.Panel.Children[0]; + + Assert.Same(container.Theme, theme); + } + [Fact] public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index e87990ebb1..afa153a593 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -94,6 +94,50 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Container_Should_Have_Theme_Set_To_ItemContainerTheme() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] { "Foo", "Bar", "Baz " }; + var theme = new ControlTheme(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemContainerTheme = theme, + }; + + Prepare(target); + + var container = (ListBoxItem)target.Presenter.Panel.Children[0]; + + Assert.Same(container.Theme, theme); + } + } + + [Fact] + public void Inline_Item_Should_Have_Theme_Set_To_ItemContainerTheme() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] { "Foo", "Bar", "Baz " }; + var theme = new ControlTheme(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = new[] { new ListBoxItem() }, + ItemContainerTheme = theme, + }; + + Prepare(target); + + var container = (ListBoxItem)target.Presenter.Panel.Children[0]; + + Assert.Same(container.Theme, theme); + } + } + [Fact] public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2169b15cad..d784caf2db 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -72,6 +72,35 @@ namespace Avalonia.Controls.UnitTests Assert.All(items, x => Assert.IsType(x.HeaderPresenter.Child)); } + [Fact] + public void Items_Should_Be_Created_Using_ItemConatinerTheme_If_Present() + { + TreeView target; + var theme = new ControlTheme(); + + var root = new TestRoot + { + Child = target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + ItemContainerTheme = theme, + ItemTemplate = new FuncTreeDataTemplate( + (_, __) => new Canvas(), + x => x.Children), + } + }; + + ApplyTemplates(target); + + var items = target.ItemContainerGenerator.Index.Containers + .OfType() + .ToList(); + + Assert.Equal(5, items.Count); + Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme)); + } + [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { From 209797e5e1375c2c20a335d3bc2cf66810d9b129 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 23:32:23 +0200 Subject: [PATCH 261/481] Ported HamburgerMenu to ControlTheme. --- .../HamburgerMenu/HamburgerMenu.xaml | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 5f8bfe4fb5..73f8e480f5 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -62,9 +62,28 @@ TextElement.FontWeight="{TemplateBinding FontWeight}" /> + + + + + + + + + + + + + + + + Background="{DynamicResource TabItemHeaderSelectedPipeFill}" + IsVisible="False" + CornerRadius="{DynamicResource ControlCornerRadius}"/> + + + + + + + + - + + + @@ -109,7 +157,8 @@ CompactPaneLength="{StaticResource PaneCompactWidth}" DisplayMode="Inline" IsPaneOpen="True" - OpenPaneLength="{StaticResource PaneExpandWidth}"> + OpenPaneLength="{StaticResource PaneExpandWidth}" + PaneBackground="Transparent"> @@ -123,11 +172,6 @@ ItemTemplate="{TemplateBinding ItemTemplate}" Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}"> - - - @@ -187,7 +232,7 @@ HorizontalAlignment="Left" VerticalAlignment="Top" HorizontalContentAlignment="Center" - Classes="NavigationButton" + Theme="{StaticResource NavigationButton}" CornerRadius="4" IsChecked="{Binding #PART_NavigationPane.IsPaneOpen, Mode=TwoWay}"> @@ -196,18 +241,12 @@ - - @@ -221,26 +260,6 @@ - - From 59c58e567242f9641759ef1f1d5dc8380792e03b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 11:47:26 +0200 Subject: [PATCH 262/481] Don't set Foreground in PathIcon theme. It is more useful if this property inherits by default. --- src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml index 966c6c2632..b9967a1b44 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml @@ -9,7 +9,6 @@ - From 052cf699d00e30a51347845789bf20f1cd18965d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 11:47:44 +0200 Subject: [PATCH 263/481] Ported ScrollBar to ControlTheme. --- .../Controls/FluentControls.xaml | 2 +- .../Controls/ScrollBar.xaml | 521 +++++++++--------- 2 files changed, 252 insertions(+), 271 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 286b3e3fcd..d344fedf0a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -34,6 +34,7 @@ + @@ -64,7 +65,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml index 7d87a3e35a..4544fbe339 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml @@ -1,302 +1,283 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + - - - - - - - - + + + - + + + - + + + + - - - - - - - - - - - - - - - - - + + + + + + - + - + + + + + + + + + + From 9d0132dc24535b89aed7e04fc8da2cfecad061f6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 11:56:21 +0200 Subject: [PATCH 264/481] Remove unneeded normal state. --- .../Controls/ComboBox.xaml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 8ef8388cf1..b3da7735eb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -72,7 +72,8 @@ Background="{DynamicResource ComboBoxBackgroundUnfocused}" BorderBrush="{DynamicResource ComboBoxBackgroundBorderBrushUnfocused}" BorderThickness="{TemplateBinding BorderThickness}" - CornerRadius="{TemplateBinding CornerRadius}" /> + CornerRadius="{TemplateBinding CornerRadius}" + IsVisible="False"/> + VerticalAlignment="Center" + Foreground="{DynamicResource ComboBoxDropDownGlyphForeground}" + Data="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z"/> - - - - - + + - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index b53980b792..617f3e9c45 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -10,111 +10,117 @@ - 8,5,8,6 + 8,5,8,6 - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + - - - - - - - - - + + + - + + From c15837b743264282fa070eea4fbc63ebacd3d5c1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 13:55:18 +0200 Subject: [PATCH 266/481] Ported ButtonSpinner to ControlTheme. --- .../Controls/ButtonSpinner.xaml | 228 ++++++++++-------- 1 file changed, 133 insertions(+), 95 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index f99f1040e5..980ea02a73 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -3,106 +3,144 @@ xmlns:sys="clr-namespace:System;assembly=netstandard" xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls" x:CompileBindings="True"> - - - - - - - - - - - - - + + + + + + + + + + + + + + + Error + + + + + + + - - + + + M0,9 L10,0 20,9 19,10 10,2 1,10 z + M0,1 L10,10 20,1 19,0 10,8 1,0 z - M0,9 L10,0 20,9 19,10 10,2 1,10 z - M0,1 L10,10 20,1 19,0 10,8 1,0 z + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + + - - + + + + + + + + - + + + + From ddbc9fbfcaf7524fcd7ed03a3f481dab7ecff1bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:01:56 +0200 Subject: [PATCH 267/481] Tweaked CheckBox ControlTheme. --- .../Controls/CheckBox.xaml | 451 +++++++++--------- 1 file changed, 228 insertions(+), 223 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index 44e1afefa4..9abcf5d32b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -1,293 +1,298 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Unchecked + Checked + Indeterminate + Checkbox should wrap its text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + - + - + + + - - - - - - - - + - - - - - - - - + - - - - - - - - + - + + + - - - - - - - - + + + + - - - - - - - - + - - - - - - - - + - + + From 47215816a22a68fd5c7ba863e78843127ba6e603 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:08:03 +0200 Subject: [PATCH 268/481] Validated ComboBox/Item ControlTheme. --- .../Controls/ComboBox.xaml | 416 +++++++++--------- .../Controls/ComboBoxItem.xaml | 153 +++---- 2 files changed, 290 insertions(+), 279 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index b3da7735eb..ec78bf6ab4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -1,217 +1,227 @@  - - - - - Item 1 - Item 2 - - - Item 1 - Item 2 - - - - - - 0,0,0,4 - 15 - 7 - - 12,5,0,7 - 11,5,32,6 - 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + Item 1 + Item 2 + + + Item 1 + Item 2 + + + + + + Error + + + + + + + + + 0,0,0,4 + 15 + 7 + + 12,5,0,7 + 11,5,32,6 + 32 + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + - - - - - - - - - - - - - - + UseLayoutRounding="False" + IsHitTestVisible="False" + Height="12" + Width="12" + Margin="0,0,10,0" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Foreground="{DynamicResource ComboBoxDropDownGlyphForeground}" + Data="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z"/> + + + + + + + + + + + + + + + + + + - - + + + - - - - - - + - - - - + + - - - - - - + + + + - - - - - - + + + - - + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml index 88f9c0ea15..581cbaf80a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml @@ -1,85 +1,86 @@  - - - - - Item 1 - Item 2 long - Item 3 - Item 4 - - + + + + + Item 1 + Item 2 long + Item 3 + Item 4 + Item 5 + - - - - - - - - - - - - - - - + + + + + + + + + - - + - - - - - - - - - - - - + + - + + From 100978a5bf491b75d89fa0669fcce52a212d021d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:09:41 +0200 Subject: [PATCH 269/481] Formatted ContentControl ControlTheme. --- .../Controls/ContentControl.xaml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml index 5777603074..4caf156366 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml @@ -1,19 +1,19 @@ - - - - - - - + + + + + + + From e89060fbc3c655340fc254ca26744ab9ddcd3ab9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:18:48 +0200 Subject: [PATCH 270/481] Formatted DataValidationErrors ControlTheme. --- .../Controls/DataValidationErrors.xaml | 198 +++++++++--------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index 6ed73148a7..cc55f84d80 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -1,103 +1,103 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ebe9d6b086c3da15678aea2408412df18ce520e7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:29:30 +0200 Subject: [PATCH 271/481] Tidied up DropDownButton ControlTheme. --- .../Controls/DropDownButton.xaml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml index 3206cb8cb1..96fd575f4f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml @@ -57,26 +57,26 @@ Margin="0,0,10,0" Data="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z" HorizontalAlignment="Right" - VerticalAlignment="Center" /> + VerticalAlignment="Center" + Foreground="{DynamicResource ComboBoxDropDownGlyphForeground}"/> - - - - + + + - - From 6f797a9c56f643cc724521e75d5ba9ba9a3882bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:30:04 +0200 Subject: [PATCH 272/481] Tidied up EmbeddableControlRoot ControlTheme. --- .../Controls/EmbeddableControlRoot.xaml | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index 89eaec70ad..f8b4854553 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -1,23 +1,22 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + From 4f3fafc9dfa6835ed1707715eabd0665dda9bb52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:56:36 +0200 Subject: [PATCH 273/481] Tweaked Expander ControlTheme. And added "Rounded" option to ControlCatalog for testing. --- .../ControlCatalog/Pages/ExpanderPage.xaml | 15 ++-- .../ControlCatalog/Pages/ExpanderPage.xaml.cs | 2 + .../ViewModels/ExpanderPageViewModel.cs | 27 +++++++ .../Controls/Expander.xaml | 71 +++++++++---------- 4 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 samples/ControlCatalog/ViewModels/ExpanderPageViewModel.cs diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml index cef473af04..f9ae1c7a3c 100644 --- a/samples/ControlCatalog/Pages/ExpanderPage.xaml +++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml @@ -8,26 +8,31 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + Expanded content - + Expanded content - + Expanded content - + Expanded content - + Rounded + diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs index 52166d1a5f..e8a080899a 100644 --- a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -8,6 +9,7 @@ namespace ControlCatalog.Pages public ExpanderPage() { this.InitializeComponent(); + DataContext = new ExpanderPageViewModel(); } private void InitializeComponent() diff --git a/samples/ControlCatalog/ViewModels/ExpanderPageViewModel.cs b/samples/ControlCatalog/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..18511a6071 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,27 @@ +using Avalonia; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ExpanderPageViewModel : ViewModelBase + { + private CornerRadius _cornerRadius; + private bool _rounded; + + public CornerRadius CornerRadius + { + get => _cornerRadius; + private set => RaiseAndSetIfChanged(ref _cornerRadius, value); + } + + public bool Rounded + { + get => _rounded; + set + { + if (RaiseAndSetIfChanged(ref _rounded, value)) + CornerRadius = _rounded ? new CornerRadius(25) : default; + } + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index 36f6008782..7d5bb48333 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -3,29 +3,39 @@ x:CompileBindings="True"> - - - - - - - - + + Expanded content - + Expanded content - + Expanded content - + + + Expanded content + + + + + + + + + + + Expanded content + + + Expanded content @@ -36,25 +46,19 @@ 16 16 - 1 - 1,1,0,1 1,1,1,0 0,1,1,1 1,0,1,1 - - - - - + - - @@ -186,28 +190,28 @@ - - - - @@ -244,7 +240,6 @@ - From c246d76a206df3b8325006d88ef8aa7891772e1a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:58:15 +0200 Subject: [PATCH 274/481] Formated ItemsControl ControlTheme. --- .../Controls/ItemsControl.xaml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml index b8d7c2c4ef..fc85b7b9ff 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml @@ -1,19 +1,19 @@ - - - - - - - - - + + + + + + + + + From 4d73e2b3a946fa65db1df2c7b7c222e0c1b6410a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 14:58:40 +0200 Subject: [PATCH 275/481] Formatted Label ControlTheme. --- .../Controls/Label.xaml | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Label.xaml b/src/Avalonia.Themes.Fluent/Controls/Label.xaml index ad57239648..ff6909ee87 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Label.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Label.xaml @@ -1,22 +1,22 @@ - - - - - - - - + + + + + + + + From 901026a187d03fa020c7c503efb443f855cc32f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:01:03 +0200 Subject: [PATCH 276/481] Formatted ListBox/Item ControlTheme. --- .../Controls/ListBox.xaml | 88 ++++++------ .../Controls/ListBoxItem.xaml | 132 +++++++++--------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index b1d4b77a3f..c17dc65cca 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -1,47 +1,47 @@  - - - - Test - Test - Test - Test - - - - - - - - - - - - - - - - - - - - - - - + + + + Test + Test + Test + Test + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml index c00e920f75..fc1d0c5ff1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml @@ -1,79 +1,79 @@  - - - - Disabled - - Test - - Test - - - - 12,9,12,12 - - - - - - - - + + + + Disabled + + Test + + Test + + + + 12,9,12,12 + + + + + + + + - + - - + + + + + + - - - - - - - - - - + + - - + + - + + From 800967c62742ca9934f47d6e68be94095b1241c1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:02:52 +0200 Subject: [PATCH 277/481] Formatted Menu/FlyoutPresenter ControlTheme. --- src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 62 ++++++++--------- .../Controls/MenuFlyoutPresenter.xaml | 67 ++++++++++--------- 2 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index d42e413f00..11d21b6b23 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -1,35 +1,35 @@ - - - - - - - - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + + + - 32 + 32 - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml index 7769f0a5c9..264438390d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml @@ -1,34 +1,35 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + From f82541a90d4916f192e432c76fa329d4d3dd3dbf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:09:47 +0200 Subject: [PATCH 278/481] Tweaked NotificationCard ControlTheme. --- .../Accents/FluentControlResourcesDark.xaml | 1 + .../Accents/FluentControlResourcesLight.xaml | 1 + .../Controls/NotificationCard.xaml | 82 +++++++++---------- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index c5bb70bed3..9d04009aed 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -308,6 +308,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 8d38d39bd5..7b673767b8 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -304,6 +304,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml index e55fcb90e8..51dcdd42e3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml @@ -9,7 +9,7 @@ - + @@ -53,47 +53,47 @@ - + - + - - - - + + + + From db954bfd0ebf0401a9bc1cfc6dc517cd1e2d5a08 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:11:10 +0200 Subject: [PATCH 279/481] Formatted NumericUpDown ControlTheme. --- .../Controls/NumericUpDown.xaml | 126 +++++++++--------- 1 file changed, 62 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 74ffff0f5c..872367c6da 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -1,67 +1,65 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + From 474df23c0715253fe3be730fc1cdd59d093a2749 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:11:35 +0200 Subject: [PATCH 280/481] Formatted OverlayPopupHost ControlTheme. --- .../Controls/OverlayPopupHost.xaml | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml index 0b587e6037..28d8fb508c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml @@ -1,23 +1,23 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + From 0d7bb472b38e042e44b1200f0f08e0c405f6c353 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:12:20 +0200 Subject: [PATCH 281/481] Formatted PathIcon ControlTheme. --- .../Controls/PathIcon.xaml | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml index b9967a1b44..5584eb3ab9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml @@ -1,25 +1,25 @@  - - - - M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z - - - - - - - - - - - - - - - - - - + + + + M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z + + + + + + + + + + + + + + + + + + From 8b69e083e8968df9361378454ddad87b834159f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:12:54 +0200 Subject: [PATCH 282/481] Formatted PopupRoot ControlTheme. --- .../Controls/PopupRoot.xaml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml index f57ecd2013..5a924830b1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml @@ -1,28 +1,28 @@  - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + From 3dc70c0725526db91f6f9c765ba212d363ef1514 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:17:15 +0200 Subject: [PATCH 283/481] Checked ProgressBar ControlTheme. --- .../Controls/ProgressBar.xaml | 348 +++++++++--------- 1 file changed, 176 insertions(+), 172 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index 3c7d22e13f..6bf813385d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -2,178 +2,182 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True"> - - + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d284822731e941224c76f0bf9eda3fa1349f3470 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 15:20:16 +0200 Subject: [PATCH 284/481] Tweaked RepeatButton ControlTheme. --- .../Controls/RepeatButton.xaml | 110 +++++++++--------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index 670212c4b1..5e789e7e10 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -1,65 +1,63 @@  - - - - - - - - - 8,5,8,6 + + + + + + + + + - - - - - - - - - - - - - - - - + 8,5,8,6 - + + + + + + + + + + + + + + + + - + - + - + - - + + From 250b5b91ef8c5d4d8fefd66c14029ee0720fe0ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:02:03 +0200 Subject: [PATCH 285/481] Tweaked Slider ControlTheme. --- .../Controls/Slider.xaml | 534 +++++++++--------- 1 file changed, 269 insertions(+), 265 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index c74878e8be..9378640d64 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -1,57 +1,71 @@ - - - - - - - - - - - - - - - - 0,0,0,4 - 15 - 15 - 32 - 32 - 10 - 20 - 20 - 20 - 20 - + + + + + + + + + + + + + Error + + + + + + + + + + + + + + + 0,0,0,4 + 15 + 15 + 32 + 32 + 10 + 20 + 20 + 20 + 20 - + - - + + - - - - - - - - - + + + + + + + + + + @@ -65,225 +79,215 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -326,7 +330,7 @@ - @@ -344,18 +348,18 @@ - - + @@ -370,14 +374,14 @@ - From e5f70deab03fea424de8b06a98c4e50f23eecde6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:04:37 +0200 Subject: [PATCH 286/481] Formatted TabControl ControlTheme. --- .../Controls/TabControl.xaml | 111 +++++++++--------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml index 1cc31f8e15..17d179e0cd 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml @@ -1,63 +1,60 @@  - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - 0 0 0 2 + 0 0 0 2 - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - + + + + From 27abe8200987ac1ab627549f3642d1f9353e9e18 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:15:06 +0200 Subject: [PATCH 287/481] Twaked TabItem ControlTheme. --- .../Controls/TabItem.xaml | 230 +++++++++--------- 1 file changed, 113 insertions(+), 117 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml index 110ffc2f48..e323be72e2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml @@ -2,132 +2,128 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True"> - - - - - - - - - - 48 - 24 - 2 + + + + + + + + + - - - - - - - - - - - - - - - - - - - + 48 + 24 + 2 - - - + + + + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - + - - - - + + + + From 1af445893abf5d66af80bedd05d932e8e0febe44 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:20:33 +0200 Subject: [PATCH 288/481] Tweaked TabStrip/Item ControlTemplates. --- .../Controls/TabStrip.xaml | 62 +++--- .../Controls/TabStripItem.xaml | 180 +++++++++--------- 2 files changed, 116 insertions(+), 126 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml index 15c3ba2f37..59cb7f6752 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml @@ -1,34 +1,34 @@  - - - - Item 1 - Item 2 - Disabled - - - - - - - - - - - - - - - - - - + + + + Item 1 + Item 2 + Disabled + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml index 5ccd430d33..fab8ba87aa 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml @@ -2,107 +2,97 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True"> - - - - Leaf - Arch - - - - - 48 - 2 + + + + Leaf + Arch + Background + + + - - - - - - - - - - - - - - - - - - - + 48 + 2 - + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - + + - - + + - - + + - - + + - - - - - - + + + From c27f5e798e0bb1a15491c931abc3330fa3e17d4e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:24:51 +0200 Subject: [PATCH 289/481] Tweaked ToggleButton ControlTheme. --- src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index 617f3e9c45..36c0c50064 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -45,10 +45,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + 0,0,0,6 + 6 + 6 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e8091cfe80759251c7fe35f834c91fe606b8576c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:48:04 +0200 Subject: [PATCH 291/481] Tweaked ToolTip ControlTheme. --- .../Controls/ToolTip.xaml | 113 ++++++++---------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index 1070f3b68c..bc9dceb545 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -2,70 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - - - Hover Here - - - - - - ToolTip - A control which pops up a hint when a control is hovered - - - ToolTip bottom placement - - - + + + + Text Content + Very long text content which should exceed the maximum with of the tooltip and wrap. + + + Multi-line + Control Content + + + + + - 320 + 320 - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + + From 433325e7463376756de70912a9fac103c958d54c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:48:44 +0200 Subject: [PATCH 292/481] Formatted TransitioningContentControl ControlTheme. --- .../Controls/TransitioningContentControl.xaml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml index f28dc66a5f..f1971fe236 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml @@ -1,19 +1,19 @@  - - - - - - - + + + + + + + From 1bbff64d1fffd557c031e5fc4e78b003fdfe2c35 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:50:39 +0200 Subject: [PATCH 293/481] Added preview to TreeView ControlTheme. --- .../Controls/TreeView.xaml | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml index 4fcec79f25..c340599000 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml @@ -1,31 +1,43 @@ + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + From 42da6cde7c1d237161768939c032bbf8a54b3f53 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:56:49 +0200 Subject: [PATCH 294/481] Formatted Window ControlTheme. --- .../Controls/Window.xaml | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index d78fd76a37..35cc81663f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -1,30 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + From fa65cae7d82d6d3769d9a74f46a0033ea9852967 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 16:58:05 +0200 Subject: [PATCH 295/481] Formatted WindowNotificationManager ControlTheme. --- .../Controls/WindowNotificationManager.xaml | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml index d5d5114c1b..77f49941f0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml @@ -1,58 +1,57 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + - + - + - - + + From 5229cf57336c771ecda45dfea704340a7d1141e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 17:00:12 +0200 Subject: [PATCH 296/481] Formatted Calendar ControlTheme. --- .../Controls/Calendar.xaml | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml index 3434f9b6e5..8d54cb93ca 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml @@ -5,36 +5,36 @@ // All other rights reserved. --> - - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + From 7af2eb9a29e0ea5a2b8b08b3a00b72518b34b6ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 17:08:40 +0200 Subject: [PATCH 297/481] Formatted CalendarButton ControlTheme. --- .../Controls/CalendarButton.xaml | 178 +++++++++--------- 1 file changed, 93 insertions(+), 85 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml index 0381022910..d1aee7ee9a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml @@ -4,106 +4,114 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - + + + + + - - - - - - + - - - - + + - - - + - - - - + - - - - + + + + + + From f66d9aca7a7c034ecdc6ec1565176790677c1c39 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 17:10:48 +0200 Subject: [PATCH 298/481] Formatted CalendarDayButton ControlTheme. --- .../Controls/CalendarDayButton.xaml | 172 +++++++++--------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml index 9f476d51bc..dd2692cc5a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml @@ -6,99 +6,107 @@ --> - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + - - - - - - + + - - - - + - - + - - + + + + + + + From 1e642fb3218d990cc8bbbe84e22c8f8d336ca52b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 17:23:24 +0200 Subject: [PATCH 299/481] Tweaked CalendarItem ControlTheme. --- .../Controls/CalendarItem.xaml | 276 +++++++++--------- 1 file changed, 143 insertions(+), 133 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml index f8fec58ef7..6914a0db08 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml @@ -6,145 +6,155 @@ --> - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + 2000-01-01 + 2000-01-05 + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + From f6e8dda027e159820f744a03ebff8cca80d1c998 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 17:26:17 +0200 Subject: [PATCH 300/481] Formatted CalendarDatePicker ControlTheme. --- .../Controls/CalendarDatePicker.xaml | 328 +++++++++--------- 1 file changed, 163 insertions(+), 165 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index eefd9e9b51..54f6a200b8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -2,179 +2,177 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - - - - - - 12 - 32 + + + + + - - - - - - - - - - - 12 + 32 + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - + + + + + + + + + + + From 83140fe427b8ddcb691b7dc063e5af965de40180 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 00:18:28 +0200 Subject: [PATCH 305/481] Reset button animations correctly. Otherwise they keep on animating way after the interaction. --- src/Avalonia.Themes.Fluent/Controls/Button.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7ac5c4351a..7828fd52ed 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -20,6 +20,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index 5e789e7e10..a54187104b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -23,6 +23,7 @@ + + From 0d5b10ec3aa041b21d5cd4a785b37201dec03f7a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 00:25:04 +0200 Subject: [PATCH 306/481] Tweaked DatePicker ControlTheme. --- .../Controls/DatePicker.xaml | 629 +++++++++--------- 1 file changed, 317 insertions(+), 312 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index a7093c1341..0537feb60b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -9,344 +9,349 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - - - - + + + + + - 0,0,0,4 - 40 - 40 - 41 - 296 - 456 - 0,3,0,6 - 9,3,0,6 - 0,3,0,6 - 9,3,0,6 - 1 + 0,0,0,4 + 40 + 40 + 41 + 296 + 456 + 0,3,0,6 + 9,3,0,6 + 0,3,0,6 + 9,3,0,6 + 1 - - - - - - - - + + + + + + + - - + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - + - - + - - - - - - - - - - + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + - + + + + + + + + + + + + + + - - - + - - - - + + + - + + + + - - - + - + + + - + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + - + - - - + + + From c4541e27db84734a963dc785060ca023a2606283 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 00:48:00 +0200 Subject: [PATCH 307/481] Ported ManagedFileChooser to ControlTheme. --- .../Controls/FluentControls.xaml | 4 +- .../Controls/ManagedFileChooser.xaml | 156 +++++++++--------- 2 files changed, 82 insertions(+), 78 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 0b5f6a4a72..1c936ba097 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -59,6 +59,8 @@ + + @@ -72,7 +74,5 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index dc454e4fdb..55f4893057 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -1,8 +1,14 @@ - - + - + + + + - - - - - - - - - - + + + - - - + + + From da264ac4d0587b7cd6bd36cf0181caafacd83642 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 02:20:14 +0200 Subject: [PATCH 308/481] WIP: Menu/Item. Something strange is happening when you open the menu a 3rd time. --- .../Controls/FluentControls.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 82 ++++- .../Controls/MenuItem.xaml | 303 +++++++----------- 3 files changed, 191 insertions(+), 196 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 1c936ba097..6d51dac17f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -26,6 +26,7 @@ + @@ -67,7 +68,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index 11d21b6b23..3e0e81f870 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -3,17 +3,95 @@ - - + + + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + 😊 + - - - - + + + + - - + + + + + - - + + - + - - - -4 - 0,0,12,0 - 24,0,0,0 - M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z - + + -4 + 0,0,12,0 + 24,0,0,0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z - - - - - - - - - - - - - - - - - + - - - - - + + + + + - - - - + + + + + - + + + + + - + - + - - + + + From d2e4d58f178113d500ca5dffc06734be751ce403 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 15:17:15 +0200 Subject: [PATCH 309/481] Formatted NativeMenuBar ControlTheme. --- src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 753c03992a..4f51ebfa7f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -25,5 +25,5 @@ - + From d902c3eb0ca5a7559d1e7843abd93f567fe065bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 15:53:41 +0200 Subject: [PATCH 310/481] Port ScrollViewer to ControlTheme. Including splitting out the menu scroller into a separate xaml file for separate preview. --- .../Controls/FluentControls.xaml | 1 + .../Controls/MenuItem.xaml | 28 ++- .../Controls/MenuScrollViewer.xaml | 98 +++++++++++ .../Controls/ScrollViewer.xaml | 164 +++++++++--------- 4 files changed, 204 insertions(+), 87 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 6d51dac17f..3e4361465c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -25,6 +25,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 3ec65c4936..8f77d1bc1b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -5,8 +5,8 @@ x:DataType="MenuItem" x:CompileBindings="True"> - - + + @@ -22,8 +22,26 @@ - - + + + + + + + + + + + + + + + + + + + + @@ -113,7 +131,7 @@ MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}" HorizontalAlignment="Stretch" CornerRadius="{DynamicResource OverlayCornerRadius}"> - + + + + + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + Item 6 + Item 7 + Item 8 + Item 9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index a6e02d2769..55adb54d9b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -1,87 +1,87 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + Item 6 + Item 7 + Item 8 + Item 9 + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + From 1d15d1b51343137a24171002b6f07ac98517b1af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 00:04:29 +0200 Subject: [PATCH 311/481] Ported SplitView to ControlTheme. --- .../Controls/FluentControls.xaml | 2 +- .../Controls/SplitView.xaml | 444 +++++++++--------- 2 files changed, 231 insertions(+), 215 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 3e4361465c..fc89d7c21e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -39,6 +39,7 @@ + @@ -72,7 +73,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml index 6b9b94852f..9feb6c7fc6 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml @@ -1,4 +1,4 @@ - @@ -15,219 +15,235 @@ - - 320 - 48 - 00:00:00.2 - 00:00:00.1 - 0.1,0.9,0.2,1.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b1038f115b8abdc48ae884ec531a3ae385731010 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 01:31:23 +0200 Subject: [PATCH 312/481] Ported TreeViewItem to ControlTheme. --- .../Controls/FluentControls.xaml | 4 +- .../Controls/TreeViewItem.xaml | 178 +++++++++--------- 2 files changed, 87 insertions(+), 95 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index fc89d7c21e..58c33bf26f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -51,6 +51,7 @@ + @@ -68,10 +69,7 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index 6510832eb3..83abe7848c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -1,4 +1,4 @@ - @@ -6,32 +6,29 @@ - - + + - + - + - - 16 - 12 - 12, 0, 12, 0 - M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z - M0,1 L10,10 20,1 19,0 10,8 1,0 Z - - + 16 + 12 + 12, 0, 12, 0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z + M0,1 L10,10 20,1 19,0 10,8 1,0 Z + - - + + + @@ -78,7 +80,7 @@ @@ -98,83 +100,75 @@ - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - + + + - + + + + From 7fcd73836940e0bf3654fe4e0a86493eb75e8386 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 02:12:56 +0200 Subject: [PATCH 313/481] Tweaked TitleBar ControlTheme. --- src/Avalonia.Controls/Chrome/TitleBar.cs | 15 ++++++------ .../Controls/TitleBar.xaml | 24 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index b152a31587..428bf8f9b0 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -35,7 +35,8 @@ namespace Avalonia.Controls.Chrome } } - IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; + if (!Design.IsDesignMode) + IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } } @@ -44,13 +45,13 @@ namespace Avalonia.Controls.Chrome base.OnApplyTemplate(e); _captionButtons?.Detach(); - + _captionButtons = e.NameScope.Get("PART_CaptionButtons"); - if (VisualRoot is Window window) + if (VisualRoot is Window window && !Design.IsDesignMode) { - _captionButtons?.Attach(window); - + _captionButtons?.Attach(window); + UpdateSize(window); } } @@ -59,7 +60,7 @@ namespace Avalonia.Controls.Chrome { base.OnAttachedToVisualTree(e); - if (VisualRoot is Window window) + if (VisualRoot is Window window && !Design.IsDesignMode) { _disposables = new CompositeDisposable { @@ -90,7 +91,7 @@ namespace Avalonia.Controls.Chrome base.OnDetachedFromVisualTree(e); _disposables?.Dispose(); - + _captionButtons?.Detach(); _captionButtons = null; } diff --git a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml index 2b97df020f..5730b19e78 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml @@ -1,8 +1,8 @@  - - + + @@ -13,11 +13,19 @@ - - + + - - + + @@ -27,10 +35,6 @@ - - From 5864452d75a596bdd7bd0d0cdd6de8f3805d8159 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 15:19:40 +0200 Subject: [PATCH 314/481] Undo changed to TitleBar. It was causing the title bar to be shown in all designer previews. --- src/Avalonia.Controls/Chrome/TitleBar.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 428bf8f9b0..1bf13111a9 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -35,8 +35,7 @@ namespace Avalonia.Controls.Chrome } } - if (!Design.IsDesignMode) - IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; + IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } } @@ -48,7 +47,7 @@ namespace Avalonia.Controls.Chrome _captionButtons = e.NameScope.Get("PART_CaptionButtons"); - if (VisualRoot is Window window && !Design.IsDesignMode) + if (VisualRoot is Window window) { _captionButtons?.Attach(window); @@ -60,7 +59,7 @@ namespace Avalonia.Controls.Chrome { base.OnAttachedToVisualTree(e); - if (VisualRoot is Window window && !Design.IsDesignMode) + if (VisualRoot is Window window) { _disposables = new CompositeDisposable { From 07cce63b8bd4b009bd384e727caaab16d339bc4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 16:57:55 +0200 Subject: [PATCH 315/481] Ported TimePicker to ControlTheme. And refactored shared date/time picker components into a separate file. --- .../Controls/DatePicker.xaml | 207 +++----- .../Controls/DateTimePickerShared.xaml | 116 +++++ .../Controls/FluentControls.xaml | 3 +- .../Controls/TimePicker.xaml | 465 ++++++++++++------ 4 files changed, 501 insertions(+), 290 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 0537feb60b..0441415128 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -10,8 +10,21 @@ xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - + + + + + + + + + + Error + + + + + @@ -27,105 +40,45 @@ 9,3,0,6 1 - - - - - - - - - - - - - - - - - - + - - - - + - - - + - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -148,7 +101,9 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top"/> - - diff --git a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml new file mode 100644 index 0000000000..77271a149f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml @@ -0,0 +1,116 @@ + + + + + + + Standard Item + Month Item + Button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 58c33bf26f..262007082f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -59,7 +59,9 @@ + + @@ -72,6 +74,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index dc09032a36..b5f177b985 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -1,163 +1,320 @@ - - - - - 40 - 1 - 1 - 0,0,0,4 - 40 - 41 - 242 - 456 - 0,3,0,6 - 0,3,0,6 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + Error + + + + + + + + + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + - - - - - + + From 2dda677fc8099a484806be21574e1a5bbbf721b8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 22:26:28 +0200 Subject: [PATCH 316/481] Ported SplitButton to ControlTheme. Not happy with the Tag hack but no better way to add additional states to a control currently. --- .../Controls/FluentControls.xaml | 2 +- .../Controls/SplitButton.xaml | 388 ++++++++---------- 2 files changed, 173 insertions(+), 217 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 262007082f..bc2352d5d0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -39,6 +39,7 @@ + @@ -72,7 +73,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index fb0460d9a1..9a93b2625b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -1,36 +1,120 @@ - - - + + Hello - + + + Hello + + + Disabled + + + Hello + + + Hello + - - 32 - 32 - 1 - 32 + 32 + 32 + 1 + 32 - - - - + + + - + + + + + + + + + + + + + + + + + + + + @@ -40,211 +124,83 @@ - - - - - - - - - - - @@ -147,9 +155,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 0441415128..bc47f3892e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -280,10 +280,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml index 77271a149f..be664b375d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml @@ -15,8 +15,8 @@ Standard Item Month Item Button - - + + @@ -90,6 +90,7 @@ + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml index 0a5147e335..b0ffc97e68 100644 --- a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml @@ -1,175 +1,204 @@ - - - 40 - 1 - 1 - 0,0,0,4 - 40 - 41 - 242 - 456 - 0,3,0,6 - 0,3,0,6 - + + + + + + + + + + + + Error + + + + + + + - - - + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 - + + + + + + + - - - + + + - - + + + - + + + + + - - - + + + - + + - - - - - + - + - + + - - - - - - - - + + + + diff --git a/src/Avalonia.Themes.Default/Controls/TitleBar.xaml b/src/Avalonia.Themes.Default/Controls/TitleBar.xaml index 7f8ed24076..a06dfbb834 100644 --- a/src/Avalonia.Themes.Default/Controls/TitleBar.xaml +++ b/src/Avalonia.Themes.Default/Controls/TitleBar.xaml @@ -1,53 +1,65 @@ - + - + - + - + - + - + - - - + - - + + + diff --git a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml index 17fb2af16c..2cfbcf6b7a 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml @@ -1,40 +1,42 @@ - - - - - - - + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml index 2c831cf360..d00358951a 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml @@ -1,69 +1,28 @@ - - - 0,0,0,6 - 6 - 6 - 0 - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + Classes="h1" /> + TextWrapping="Wrap" /> + Content="Enable automatic Updates?" + OffContent="Uit" + OnContent="Aan" + VerticalAlignment="Bottom" /> + Classes="h1" /> + TextWrapping="Wrap" /> @@ -71,29 +30,71 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/ToolTip.xaml b/src/Avalonia.Themes.Default/Controls/ToolTip.xaml index 35c1dceb8d..c4fddd8c4c 100644 --- a/src/Avalonia.Themes.Default/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToolTip.xaml @@ -1,18 +1,22 @@ - + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Default/Controls/TransitioningContentControl.xaml index 6a4d56ccb7..63185e11e6 100644 --- a/src/Avalonia.Themes.Default/Controls/TransitioningContentControl.xaml +++ b/src/Avalonia.Themes.Default/Controls/TransitioningContentControl.xaml @@ -1,20 +1,21 @@ - - - + + diff --git a/src/Avalonia.Themes.Default/Controls/TreeView.xaml b/src/Avalonia.Themes.Default/Controls/TreeView.xaml index 67a683dad9..2bc38a0673 100644 --- a/src/Avalonia.Themes.Default/Controls/TreeView.xaml +++ b/src/Avalonia.Themes.Default/Controls/TreeView.xaml @@ -1,27 +1,31 @@ - + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Default/Controls/TreeViewItem.xaml index 1996756001..7baf0ef2a6 100644 --- a/src/Avalonia.Themes.Default/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/Controls/TreeViewItem.xaml @@ -1,93 +1,100 @@ - - + + - + + + + + + + + - + + + + + + + + + + + + + + + + + + - - - - - - - - + + diff --git a/src/Avalonia.Themes.Default/Controls/UserControl.xaml b/src/Avalonia.Themes.Default/Controls/UserControl.xaml index d6028daff8..56248f8c5f 100644 --- a/src/Avalonia.Themes.Default/Controls/UserControl.xaml +++ b/src/Avalonia.Themes.Default/Controls/UserControl.xaml @@ -1,16 +1,25 @@ - diff --git a/src/Avalonia.Themes.Default/Controls/Window.xaml b/src/Avalonia.Themes.Default/Controls/Window.xaml index 9c515ebe30..00b108343e 100644 --- a/src/Avalonia.Themes.Default/Controls/Window.xaml +++ b/src/Avalonia.Themes.Default/Controls/Window.xaml @@ -1,26 +1,33 @@ - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Default/Controls/WindowNotificationManager.xaml index 7c1efa2e82..6dacf63be5 100644 --- a/src/Avalonia.Themes.Default/Controls/WindowNotificationManager.xaml +++ b/src/Avalonia.Themes.Default/Controls/WindowNotificationManager.xaml @@ -1,45 +1,52 @@ - - + + + + + + + + + + + + + + + + + + + + - - - - - + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index f266402aef..8f5bea557c 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -1,70 +1,75 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5b8cb5284bf81e1a846ea84a64828c6e7284732b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 29 Jul 2022 02:59:43 -0400 Subject: [PATCH 444/481] Port default theme DataGrid to control themes --- .../Themes/Default.xaml | 652 ++++++++++-------- .../Themes/Fluent.xaml | 3 +- .../Diagnostics/Views/MainWindow.xaml | 5 +- 3 files changed, 364 insertions(+), 296 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 0d1fe43eb6..83d9332613 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -1,305 +1,377 @@ - - 4 - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index a80cc2173c..5ae83427b5 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -57,8 +57,7 @@ diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index c32638f6ca..680424122b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -11,10 +11,7 @@ - - + From ceb59fc475cd8774f03ef472c5becfa81ecfab90 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 29 Jul 2022 03:09:22 -0400 Subject: [PATCH 445/481] Workaround dev tools issue --- samples/ControlCatalog/App.xaml.cs | 9 ++++++--- .../Diagnostics/Views/MainWindow.xaml | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 7ebb87094a..a5535ea52d 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -78,9 +78,12 @@ namespace ControlCatalog public override void Initialize() { - Styles.Insert(0, Fluent); - Styles.Insert(1, ColorPickerFluent); - Styles.Insert(2, DataGridFluent); + Styles.Insert(0, DefaultLight); + Styles.Insert(1, ColorPickerDefault); + Styles.Insert(2, DataGridDefault); + //Styles.Insert(0, Fluent); + //Styles.Insert(1, ColorPickerFluent); + //Styles.Insert(2, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 680424122b..004518598c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -4,7 +4,8 @@ xmlns:diag="clr-namespace:Avalonia.Diagnostics" xmlns:default="using:Avalonia.Themes.Default" Title="Avalonia DevTools" - x:Class="Avalonia.Diagnostics.Views.MainWindow"> + x:Class="Avalonia.Diagnostics.Views.MainWindow" + Theme="{StaticResource {x:Type Window}}"> From 44b8df516c461dbbc38f825c5dab9aa01ee94dd2 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 29 Jul 2022 11:26:55 +0200 Subject: [PATCH 446/481] Set OverlayDismissEventPassThrough to false, add OverlayInputPassThroughElement property --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 50 ++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index a0f3407b7a..bf938abc79 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -16,8 +16,8 @@ namespace Avalonia.Controls.Primitives /// Defines the property /// public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), - x => x.IsOpen); + AvaloniaProperty.RegisterDirect(nameof(IsOpen), + x => x.IsOpen); /// /// Defines the property @@ -39,16 +39,18 @@ namespace Avalonia.Controls.Primitives x => x.ShowMode, (x, v) => x.ShowMode = v); /// - /// Defines the AttachedFlyout property + /// Defines the property /// - public static readonly AttachedProperty AttachedFlyoutProperty = - AvaloniaProperty.RegisterAttached("AttachedFlyout", null); + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); /// - /// Defines the OverlayDismissEventPassThrough property + /// Defines the AttachedFlyout property /// - public static readonly StyledProperty OverlayDismissEventPassThroughProperty = - Popup.OverlayDismissEventPassThroughProperty.AddOwner(); + public static readonly AttachedProperty AttachedFlyoutProperty = + AvaloniaProperty.RegisterAttached("AttachedFlyout", null); private readonly Lazy _popupLazy; private bool _isOpen; @@ -58,10 +60,10 @@ namespace Avalonia.Controls.Primitives private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; + private IInputElement? _overlayInputPassThroughElement; static FlyoutBase() { - OverlayDismissEventPassThroughProperty.OverrideDefaultValue(true); Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); } @@ -109,25 +111,20 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets or sets a value indicating whether the event that closes the flyout is passed - /// through to the parent window. + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. /// - /// - /// Clicks outside the the flyout cause the flyout to close. When is set to - /// false, these clicks will be handled by the flyout and not be registered by the parent - /// window. When set to true, the events will be passed through to the parent window. - /// - public bool OverlayDismissEventPassThrough + public IInputElement? OverlayInputPassThroughElement { - get => GetValue(OverlayDismissEventPassThroughProperty); - set => SetValue(OverlayDismissEventPassThroughProperty, value); + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); } IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; remove => _popupHostChangedHandler -= value; } @@ -200,7 +197,7 @@ namespace Avalonia.Controls.Primitives Popup.OverlayInputPassThroughElement = null; ((ISetLogicalParent)Popup).SetParent(null); - + // Ensure this isn't active _transientDisposable?.Dispose(); _transientDisposable = null; @@ -255,8 +252,7 @@ namespace Avalonia.Controls.Primitives Popup.Child = CreatePresenter(); } - Popup.OverlayInputPassThroughElement = placementTarget; - Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough; + Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; if (CancelOpening()) { @@ -386,7 +382,9 @@ namespace Avalonia.Controls.Primitives var popup = new Popup { WindowManagerAddShadowHint = false, - IsLightDismissEnabled = true + IsLightDismissEnabled = true, + //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss. + OverlayDismissEventPassThrough = false }; popup.Opened += OnPopupOpened; From 29d957282ece12d6f497027ed4fa9d28cfce33fd Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 29 Jul 2022 11:28:19 +0200 Subject: [PATCH 447/481] Remove unsetting --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index bf938abc79..00ebcab70e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -194,7 +194,6 @@ namespace Avalonia.Controls.Primitives IsOpen = false; Popup.IsOpen = false; - Popup.OverlayInputPassThroughElement = null; ((ISetLogicalParent)Popup).SetParent(null); From 81b0dce302f3e588ce2e6e5e16a9d1eb0b5d69ea Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 11:55:52 +0200 Subject: [PATCH 448/481] Fix RichTextBlock Inlines update handling --- samples/Sandbox/MainWindow.axaml | 15 ++++++ samples/Sandbox/MainWindow.axaml.cs | 46 ++++++++++++++++++- .../Media/TextFormatting/TextLayout.cs | 7 ++- .../Media/TextFormatting/TextLine.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 7 ++- src/Avalonia.Controls/RichTextBlock.cs | 22 ++++----- .../RichTextBlockTests.cs | 44 ++++++++++++++++++ .../Media/TextFormatting/TextLayoutTests.cs | 12 +++-- 8 files changed, 135 insertions(+), 20 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 0fc78795a6..cf2ce63c1f 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -9,6 +9,21 @@ + + + + + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 7a8b56bbb6..3040385708 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -3,7 +3,9 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Presenters; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; @@ -11,6 +13,8 @@ namespace Sandbox { public class MainWindow : Window { + private TestViewModel _dc; + public MainWindow() { this.InitializeComponent(); @@ -27,13 +31,30 @@ namespace Sandbox var textPresenter = e.NameScope.Find("PART_TextPresenter") as TextPresenter; - DataContext = new TestViewModel(textPresenter); + _dc = new TestViewModel(textPresenter); + + DataContext = _dc; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + _dc.InlineCollection = new InlineCollection + { + new Run(""), + new Run("test3") {FontWeight = Avalonia.Media.FontWeight.Bold}, + }; + // _dc.Text = "nununu"; + } + + private void TextButton_OnClick(object? sender, RoutedEventArgs e) + { + _dc.Text = "nununu"; + } } public class TestViewModel : ViewModelBase @@ -46,6 +67,29 @@ namespace Sandbox _textPresenter = textPresenter; } + private InlineCollection _inlineCollection; + private string _text; + + public string Text + { + get => _text; + set + { + _text = value; + RaisePropertyChanged(); + } + } + + public InlineCollection InlineCollection + { + get => _inlineCollection; + set + { + _inlineCollection = value; + RaisePropertyChanged(); + } + } + public double Distance { get => _distance; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f3e8b5969c..0828b6518a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -537,8 +537,13 @@ namespace Avalonia.Media.TextFormatting /// /// The collapsing width. /// The . - private TextCollapsingProperties GetCollapsingProperties(double width) + private TextCollapsingProperties? GetCollapsingProperties(double width) { + if(_textTrimming == TextTrimming.None) + { + return null; + } + return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index c8a23097db..61b24dc8c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting /// /// A value that represents a collapsed line that can be displayed. /// - public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + public abstract TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList); /// /// Create a justified line based on justification text properties. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 0ee791d935..a0aae76fad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting } /// - public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) + public override TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList) { if (collapsingPropertiesList.Length == 0) { @@ -128,6 +128,11 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; + if(collapsingProperties is null) + { + return this; + } + var collapsedRuns = collapsingProperties.Collapse(this); if (collapsedRuns is null) diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 0c8b1d125d..2f8bed3be7 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -44,8 +44,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty InlinesProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( nameof(Inlines)); public static readonly DirectProperty CanCopyProperty = @@ -138,7 +138,7 @@ namespace Avalonia.Controls /// Gets or sets the inlines. /// [Content] - public InlineCollection Inlines + public InlineCollection? Inlines { get => GetValue(InlinesProperty); set => SetValue(InlinesProperty, value); @@ -159,7 +159,7 @@ namespace Avalonia.Controls remove => RemoveHandler(CopyingToClipboardEvent, value); } - internal bool HasComplexContent => Inlines.Count > 0; + internal bool HasComplexContent => Inlines != null && Inlines.Count > 0; /// /// Copies the current selection to the Clipboard. @@ -260,18 +260,18 @@ namespace Avalonia.Controls { if (!string.IsNullOrEmpty(_text)) { - Inlines.Add(_text); + Inlines?.Add(_text); _text = null; } - Inlines.Add(text); + Inlines?.Add(text); } } protected override string? GetText() { - return _text ?? Inlines.Text; + return _text ?? Inlines?.Text; } protected override void SetText(string? text) @@ -301,10 +301,10 @@ namespace Avalonia.Controls ITextSource textSource; - var inlines = Inlines; - if (HasComplexContent) { + var inlines = Inlines!; + var textRuns = new List(); foreach (var inline in inlines) @@ -537,7 +537,7 @@ namespace Avalonia.Controls switch (change.Property.Name) { - case nameof(InlinesProperty): + case nameof(Inlines): { OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection); InvalidateTextLayout(); @@ -553,7 +553,7 @@ namespace Avalonia.Controls return ""; } - var text = Inlines.Text ?? Text; + var text = GetText(); if (string.IsNullOrEmpty(text)) { diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs index eb4b88956d..c74f13b808 100644 --- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs @@ -48,5 +48,49 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } } + + [Fact] + public void Changing_Inlines_Should_Invalidate_Measure() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new RichTextBlock(); + + var inlines = new InlineCollection { new Run("Hello") }; + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + target.Inlines = inlines; + + Assert.False(target.IsMeasureValid); + } + } + + [Fact] + public void Changing_Inlines_Should_Reset_Inlines_Parent() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new RichTextBlock(); + + var run = new Run("Hello"); + + target.Inlines.Add(run); + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + target.Inlines = null; + + Assert.Null(run.Parent); + + target.Inlines = new InlineCollection { run }; + + Assert.Equal(target, run.Parent); + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 6d057d900e..c457a96299 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -993,9 +993,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentX = 0.0; - for (int j = 0; j < clusters.Count; j++) - { - var cluster = clusters[j]; + var cluster = text.Length; + + for (int j = 0; j < clusters.Count - 1; j++) + { + var glyphAdvance = glyphAdvances[j]; var characterHit = textLine.GetCharacterHitFromDistance(currentX); @@ -1005,9 +1007,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(currentX, distance); - var glyphAdvance = glyphAdvances[j]; - currentX += glyphAdvance; + + cluster = clusters[j]; } } } From e7be7cb88de6cbea64f8492b6899d53b4878030d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 16:29:36 +0200 Subject: [PATCH 449/481] Fix that thing --- .../Media/TextFormatting/TextLineImpl.cs | 89 +++++++++++++------ 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a0aae76fad..94095cf82b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); } - if (distance > WidthIncludingTrailingWhitespace) + if (distance >= WidthIncludingTrailingWhitespace) { var lastRun = _textRuns[_textRuns.Count - 1]; @@ -195,7 +195,7 @@ namespace Avalonia.Media.TextFormatting if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; - var rightToLeftDistance = shapedRun.Size.Width; + currentPosition += currentRun.TextSourceLength; while (rightToLeftIndex + 1 <= _textRuns.Count - 1) { @@ -206,19 +206,24 @@ namespace Avalonia.Media.TextFormatting break; } - rightToLeftIndex++; + currentPosition += nextShaped.TextSourceLength; - rightToLeftDistance += nextShaped.Size.Width; + rightToLeftIndex++; } - for (var j = rightToLeftIndex; rightToLeftIndex >= 0; j--) + for (var j = i; i <= rightToLeftIndex; j++) { + if(j > _textRuns.Count - 1) + { + break; + } + currentRun = _textRuns[j]; - if(distance <= currentDistance + rightToLeftDistance - currentRun.Size.Width) + if(currentDistance + currentRun.Size.Width <= distance) { - currentPosition += currentRun.TextSourceLength; - rightToLeftDistance -= currentRun.Size.Width; + currentDistance += currentRun.Size.Width; + currentPosition -= currentRun.TextSourceLength; continue; } @@ -255,14 +260,18 @@ namespace Avalonia.Media.TextFormatting { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); - var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + //var offset = 0; - if (!shapedRun.GlyphRun.IsLeftToRight) - { - offset = Math.Max(0, offset - shapedRun.Text.End); - } + //if (shapedRun.GlyphRun.IsLeftToRight) + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + //} + //else + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + //} - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + //characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); break; } @@ -303,35 +312,50 @@ namespace Avalonia.Media.TextFormatting { var i = index; + var rightToLeftWidth = currentRun.Size.Width; + while (i + 1 <= _textRuns.Count - 1) { var nextRun = _textRuns[i + 1]; - if (nextRun is ShapedTextCharacters nextShapedRun) + if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) { - if (nextShapedRun.ShapedBuffer.IsLeftToRight) - { - break; - } - } + i++; + + rightToLeftWidth += nextRun.Size.Width; - i++; + continue; + } + + break; } - while (i > index) + if(i > index) { - var rightToLeftRun = _textRuns[i]; + while (i >= index) + { + currentRun = _textRuns[i]; + + rightToLeftWidth -= currentRun.Size.Width; - currentPosition += rightToLeftRun.TextSourceLength; + if (currentPosition + currentRun.TextSourceLength >= characterIndex) + { + break; + } - remainingLength -= rightToLeftRun.TextSourceLength; + currentPosition += currentRun.TextSourceLength; - i--; + remainingLength -= currentRun.TextSourceLength; + + i--; + } + + currentDistance += rightToLeftWidth; } } - if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, - flowDirection, out var distance, out _)) + if (currentPosition + currentRun.TextSourceLength >= characterIndex && + TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return currentDistance + distance; } @@ -608,6 +632,15 @@ namespace Avalonia.Media.TextFormatting } else { + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + if (currentPosition < firstTextSourceIndex) { startX += currentRun.Size.Width; From d524dc9cdbc498ff2c8a468fe7cdcdc125ce37c9 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 16:56:43 +0200 Subject: [PATCH 450/481] Only apply the font fallback to regions of missing glyphs --- samples/Sandbox/MainWindow.axaml | 26 +--- samples/Sandbox/MainWindow.axaml.cs | 115 +----------------- .../Media/TextFormatting/TextCharacters.cs | 26 ++-- .../Presenters/TextPresenter.cs | 4 +- src/Avalonia.Controls/TextBox.cs | 2 - 5 files changed, 18 insertions(+), 155 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index cf2ce63c1f..d429777eeb 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,29 +1,5 @@ - - - - - - - - - - - - - - - - + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 3040385708..2d36ed6d28 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -13,126 +13,15 @@ namespace Sandbox { public class MainWindow : Window { - private TestViewModel _dc; - public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); - - var textBox = this.FindControl("txtBox"); - - textBox.TemplateApplied += TextBox_TemplateApplied; - } - - private void TextBox_TemplateApplied(object sender, Avalonia.Controls.Primitives.TemplateAppliedEventArgs e) - { - var textBox = sender as TextBox; - - var textPresenter = e.NameScope.Find("PART_TextPresenter") as TextPresenter; - - _dc = new TestViewModel(textPresenter); - - DataContext = _dc; + this.AttachDevTools(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private void Button_OnClick(object? sender, RoutedEventArgs e) - { - _dc.InlineCollection = new InlineCollection - { - new Run(""), - new Run("test3") {FontWeight = Avalonia.Media.FontWeight.Bold}, - }; - // _dc.Text = "nununu"; - } - - private void TextButton_OnClick(object? sender, RoutedEventArgs e) - { - _dc.Text = "nununu"; - } - } - - public class TestViewModel : ViewModelBase - { - private readonly TextPresenter _textPresenter; - private double _distance = 45; - - public TestViewModel(TextPresenter textPresenter) - { - _textPresenter = textPresenter; - } - - private InlineCollection _inlineCollection; - private string _text; - - public string Text - { - get => _text; - set - { - _text = value; - RaisePropertyChanged(); - } - } - - public InlineCollection InlineCollection - { - get => _inlineCollection; - set - { - _inlineCollection = value; - RaisePropertyChanged(); - } - } - - public double Distance - { - get => _distance; - set - { - OnDistanceChanged(value); - RaisePropertyChanged(); - } - } - - private void OnDistanceChanged(double distance) - { - if(distance < 0) - { - distance = 0; - } - - if(distance > _textPresenter.TextLayout.Bounds.Width) - { - distance = _textPresenter.TextLayout.Bounds.Width; - } - - var height = _textPresenter.TextLayout.Bounds.Height; - - var distanceY = height / 2; - - _textPresenter.MoveCaretToPoint(new Point(distance, distanceY)); - - var caretIndex = _textPresenter.CaretIndex; - - Debug.WriteLine(caretIndex); - - _distance = distance; - } - } - - public class ViewModelBase : INotifyPropertyChanged - { - public event PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged([CallerMemberName]string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } + } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index ab72601c3e..6611357ee7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, + internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); @@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; @@ -76,7 +76,7 @@ namespace Avalonia.Media.TextFormatting { if (script == Script.Common && previousTypeface is not null) { - if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) + if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) { return new ShapeableTextCharacters(text.Take(fallbackCount), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); @@ -86,10 +86,10 @@ namespace Avalonia.Media.TextFormatting return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } - + if (previousTypeface is not null) { - if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) { return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); @@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting { continue; } - + codepoint = codepointEnumerator.Current; - + break; } - + //ToDo: Fix FontFamily fallback var matchFound = FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, @@ -157,14 +157,14 @@ namespace Avalonia.Media.TextFormatting /// /// protected static bool TryGetShapeableLength( - ReadOnlySlice text, - Typeface typeface, + ReadOnlySlice text, + Typeface typeface, Typeface? defaultTypeface, out int length, out Script script) { length = 0; - script = Script.Unknown; + script = Script.Unknown; if (text.Length == 0) { @@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; - if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { break; } - + if (currentScript != script) { if (script is Script.Unknown || currentScript != Script.Common && diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 4f9be86641..e540f58195 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -498,13 +498,13 @@ namespace Avalonia.Controls.Presenters IReadOnlyList>? textStyleOverrides = null; - if (length > 0) + if (length > 0 && SelectionForegroundBrush != null) { textStyleOverrides = new[] { new ValueSpan(start, length, new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: SelectionForegroundBrush ?? Brushes.Red)) + foregroundBrush: SelectionForegroundBrush)) }; } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 8490bfd3a0..1773163acc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1245,8 +1245,6 @@ namespace Avalonia.Controls var caretIndex = hit.TextPosition; - Debug.WriteLine($"TextPos: {caretIndex}, X: {point.X}"); - var text = Text; if (text != null && _wordSelectionStart >= 0) From ca84dda76b168c0181819df735df3bfa1512b924 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 16:59:46 +0200 Subject: [PATCH 451/481] Revert changes --- samples/Sandbox/MainWindow.axaml | 1 - samples/Sandbox/MainWindow.axaml.cs | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index d429777eeb..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,5 +1,4 @@ - diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 2d36ed6d28..3d54036d29 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -1,13 +1,7 @@ -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Documents; -using Avalonia.Controls.Presenters; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.VisualTree; +using Avalonia.Win32.WinRT.Composition; namespace Sandbox { @@ -16,12 +10,12 @@ namespace Sandbox public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); + this.AttachDevTools(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - } + } } From 6fefe5c8031a2aa24050c477535dff5297ca2f38 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 17:27:39 +0200 Subject: [PATCH 452/481] Align justified text to the natural start --- .../TextFormatting/InterWordJustification.cs | 17 ++++++++++++----- .../Media/TextFormatting/TextLineImpl.cs | 5 +++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index df83ada34a..a49e4ef13b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -15,6 +15,13 @@ namespace Avalonia.Media.TextFormatting public override void Justify(TextLine textLine) { + var lineImpl = textLine as TextLineImpl; + + if(lineImpl is null) + { + return; + } + var paragraphWidth = Width; if (double.IsInfinity(paragraphWidth)) @@ -22,12 +29,12 @@ namespace Avalonia.Media.TextFormatting return; } - if (textLine.NewLineLength > 0) + if (lineImpl.NewLineLength > 0) { return; } - var textLineBreak = textLine.TextLineBreak; + var textLineBreak = lineImpl.TextLineBreak; if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) { @@ -39,7 +46,7 @@ namespace Avalonia.Media.TextFormatting var breakOportunities = new Queue(); - foreach (var textRun in textLine.TextRuns) + foreach (var textRun in lineImpl.TextRuns) { var text = textRun.Text; @@ -68,10 +75,10 @@ namespace Avalonia.Media.TextFormatting return; } - var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace); + var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace); var spacing = remainingSpace / breakOportunities.Count; - foreach (var textRun in textLine.TextRuns) + foreach (var textRun in lineImpl.TextRuns) { var text = textRun.Text; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 94095cf82b..2c4e553355 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1536,6 +1536,11 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; + if(textAlignment == TextAlignment.Justify) + { + textAlignment = TextAlignment.Start; + } + switch (textAlignment) { case TextAlignment.Start: From 2cf660b601c69ad28656dfb36de0be96c5a4783b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 19:53:44 +0200 Subject: [PATCH 453/481] Second attempt to fix the font fallback --- src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 6611357ee7..42a9e61c36 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; - if (defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } From d5084eaf7582b83630c7a7b1c90b88f17503ce8e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 20:14:48 +0200 Subject: [PATCH 454/481] Fix GetCharacterHitFromDistance --- .../Media/TextFormatting/TextLineImpl.cs | 24 +++++++++---------- src/Avalonia.Controls/RichTextBlock.cs | 2 +- src/Avalonia.Controls/TextBox.cs | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 2c4e553355..b9147c0f65 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -260,18 +260,18 @@ namespace Avalonia.Media.TextFormatting { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); - //var offset = 0; - - //if (shapedRun.GlyphRun.IsLeftToRight) - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start); - //} - //else - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - //} - - //characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + var offset = 0; + + if (shapedRun.GlyphRun.IsLeftToRight) + { + offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + } + else + { + offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + } + + characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); break; } diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 2f8bed3be7..1f8abbc30d 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -276,7 +276,7 @@ namespace Avalonia.Controls protected override void SetText(string? text) { - var oldValue = _text ?? Inlines?.Text; + var oldValue = GetText(); AddText(text); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1773163acc..4c9e9327d4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1241,9 +1241,9 @@ namespace Avalonia.Controls MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)), MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0))); - var hit = _presenter.TextLayout.HitTestPoint(point); + _presenter.MoveCaretToPoint(point); - var caretIndex = hit.TextPosition; + var caretIndex = _presenter.CaretIndex; var text = Text; From adc81678ca6049def8124965642b3ad90b34e462 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 21:56:27 +0200 Subject: [PATCH 455/481] Only add offsets for LeftToRight runs --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b9147c0f65..6c6939d2a0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -266,10 +266,10 @@ namespace Avalonia.Media.TextFormatting { offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } - else - { - offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - } + //else + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); From c94120c065a69838399f87814b8bbcc0340e5789 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 30 Jul 2022 09:53:07 +0200 Subject: [PATCH 456/481] Try to fix tests on mac --- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index c457a96299..43948e9229 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1005,7 +1005,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 5); currentX += glyphAdvance; From 2a5760921761d819264d190a381a2314b09a0516 Mon Sep 17 00:00:00 2001 From: Evgeny Gorbovoy Date: Sat, 30 Jul 2022 17:16:16 +0200 Subject: [PATCH 457/481] + more `ContainsExclusive` for nodes --- .../Rendering/SceneGraph/ExperimentalAcrylicNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 59ebcf5109..12b67105e9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph if (Material != null) { var rect = Rect.Rect; - return rect.Contains(p); + return rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 9199611ed6..1f58111ecf 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -73,6 +73,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => Bounds.Contains(p); + public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index 23267166a5..339881e675 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -109,7 +109,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => Bounds.Contains(p); + public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); public override void Dispose() { From 86265f49aa8c9b13d2e420f505a65ad9df16f5b2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 30 Jul 2022 16:03:48 -0400 Subject: [PATCH 458/481] Update samples/ControlCatalog/App.xaml.cs --- samples/ControlCatalog/App.xaml.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index a5535ea52d..7ebb87094a 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -78,12 +78,9 @@ namespace ControlCatalog public override void Initialize() { - Styles.Insert(0, DefaultLight); - Styles.Insert(1, ColorPickerDefault); - Styles.Insert(2, DataGridDefault); - //Styles.Insert(0, Fluent); - //Styles.Insert(1, ColorPickerFluent); - //Styles.Insert(2, DataGridFluent); + Styles.Insert(0, Fluent); + Styles.Insert(1, ColorPickerFluent); + Styles.Insert(2, DataGridFluent); AvaloniaXamlLoader.Load(this); } From 33931e99bb9222339b50994b514f960d4b3c34b2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 30 Jul 2022 23:57:00 -0400 Subject: [PATCH 459/481] Fix unit tests --- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 0bb6c01041..29148e6f2e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -466,7 +466,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var xaml = @" - + "; var styles = AvaloniaRuntimeXamlLoader.Parse(xaml); From 195ef79e0a3151007eaa3d3b64a8a79d269ee915 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 31 Jul 2022 00:19:34 -0400 Subject: [PATCH 460/481] Convert ColorPicker default styles to ControlThemes --- .../Themes/Default/ColorPreviewer.xaml | 23 +- .../Themes/Default/ColorSlider.xaml | 346 +++++++++--------- .../Themes/Default/ColorSpectrum.xaml | 94 ++--- .../Themes/Default/Default.xaml | 76 ++-- 4 files changed, 274 insertions(+), 265 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index c3bc7df4a4..e067fe4ab5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,15 +1,14 @@ - + - - - 80 - 40 - + + 80 + 40 - + - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml index 35cd7a9faa..9aa2dcd9f9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -1,188 +1,190 @@ - + - + - + - + - - + + + + + + - - - - + + - - + - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 0e57f6b483..0e137c89c6 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,9 +1,10 @@ - + - - - - + + + - - - + + + - - - - - + + + + + - + - - - + + + - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml index db1fa3ee4e..b452e34394 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml @@ -3,42 +3,48 @@ xmlns:converters="using:Avalonia.Controls.Converters"> - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + - - - + + + + + + + + + + + From bdd451cdf96179dc9c28659ee21db3cec8b8ee0b Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 31 Jul 2022 00:39:45 -0400 Subject: [PATCH 461/481] Adjust ColorPicker tab background corner radius --- .../Themes/Fluent/ColorPicker.xaml | 15 ++++----------- .../Themes/Fluent/ColorView.xaml | 8 ++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 907b00dfff..50c5c06479 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,9 +3,6 @@ xmlns:controls="using:Avalonia.Controls" x:CompileBindings="True"> - - 5,5,0,0 - @@ -25,7 +22,7 @@ Padding="0,0,10,0" UseLayoutRounding="False"> - @@ -45,8 +42,10 @@ - + - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 993745b1e5..153b4422ea 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -77,6 +77,8 @@ 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z + + 5,5,0,0 @@ -644,6 +646,12 @@ + + + + From 9f031603424cbcd7b1f4f5bcc5ce38c0c2b93152 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:27:15 +0300 Subject: [PATCH 462/481] Update MacOS vmImage because current one is deprecated https://github.com/actions/virtual-environments/issues/5583 --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index edf3c3d819..dde1da1446 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macOS-10.15' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' From 8d3984b929e21e44cda15c420d5818ff925dbd4e Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:31:34 +0300 Subject: [PATCH 463/481] macos-12 is in beta,use macos-11 instead --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dde1da1446..78012b9041 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-12' + vmImage: 'macos-11' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' From 326fffc9fe14aafb571abc794331fe357423df7b Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:45:11 +0300 Subject: [PATCH 464/481] WIP --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 78012b9041..1d2fe88341 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-11' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,7 +91,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx11.1' + sdk: 'macosx13.1' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: '12' # Options: 8, 9, default, specifyPath From a62c7936690d28cb8a42ac2304b5a30de50b1afc Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:52:59 +0300 Subject: [PATCH 465/481] WIP --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d2fe88341..2a6e1b5475 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-12' + vmImage: 'macos-11' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,7 +91,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx13.1' + sdk: 'macosx12.2' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: '12' # Options: 8, 9, default, specifyPath From ebb8ffed1969c5d9156b6affa381a0ebf8c55e4f Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:10:25 +0300 Subject: [PATCH 466/481] WIP --- azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2a6e1b5475..7e4ba64fb9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-11' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,10 +91,10 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx12.2' + sdk: 'macosx12.3' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '12' # Options: 8, 9, default, specifyPath + xcodeVersion: '13.3' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From 3d845206cd8d6c7a4395000b0892015354e0bc99 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:31:40 +0300 Subject: [PATCH 467/481] WIP --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e4ba64fb9..52fc8db53c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -94,7 +94,7 @@ jobs: sdk: 'macosx12.3' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '13.3' # Options: 8, 9, default, specifyPath + xcodeVersion: '13' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From f87052311bd6e3448a198000fd1fe1915c5ab7c6 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 1 Aug 2022 22:01:38 -0400 Subject: [PATCH 468/481] Use resources to adjust tab background corner radius in ColorPicker --- .../Themes/Fluent/ColorPicker.xaml | 8 ++++++-- .../Themes/Fluent/ColorView.xaml | 12 +++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 50c5c06479..797d6c90d7 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -45,7 +45,6 @@ + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> + + + 5,5,0,0 + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 153b4422ea..59cc48975f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -77,8 +77,8 @@ 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z - - 5,5,0,0 + + 3 @@ -99,7 +99,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top" Background="{DynamicResource SystemControlBackgroundBaseLowBrush}" - CornerRadius="{TemplateBinding CornerRadius}" /> + CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" /> - - - - From 53e7a741c9a476e83a4a0c89956715bac8cf42cc Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 1 Aug 2022 22:01:55 -0400 Subject: [PATCH 469/481] Switch back to default flyout placement in ColorPicker --- .../Themes/Fluent/ColorPicker.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 797d6c90d7..74a1df4991 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -42,8 +42,7 @@ - + Date: Tue, 2 Aug 2022 11:48:20 +0200 Subject: [PATCH 470/481] fix: XML Comment --- src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs | 2 +- .../Rendering/Composition/Animations/CompositionAnimation.cs | 4 ++-- .../Rendering/Composition/Animations/ExpressionAnimation.cs | 2 +- .../Rendering/Composition/Animations/KeyFrameAnimation.cs | 4 ++-- src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs | 2 +- src/Avalonia.Base/Utilities/MathUtilities.cs | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 56a90f31ea..ab17263806 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -224,7 +224,7 @@ namespace Avalonia.Media.TextFormatting.Unicode } /// - /// Returns if is between + /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 19d316eb85..23f2aeaf90 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -16,10 +16,10 @@ namespace Avalonia.Rendering.Composition.Animations /// This is the base class for ExpressionAnimation and KeyFrameAnimation. /// /// - /// Use the method to start the animation. + /// Use the method to start the animation. /// Value parameters (as opposed to reference parameters which are set using ) /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called. - /// Changing the value of the variable after is called will not affect + /// Changing the value of the variable after is called will not affect /// the value of the ExpressionAnimation. /// See the remarks section of ExpressionAnimation for additional information. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index 577910d975..ec2972044e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -16,7 +16,7 @@ namespace Avalonia.Rendering.Composition.Animations /// This contrasts s, which use an interpolator to define how the animating /// property changes over time. The mathematical equation can be defined using references to properties /// of Composition objects, mathematical functions and operators and Input. - /// Use the method to start the animation. + /// Use the method to start the animation. /// public class ExpressionAnimation : CompositionAnimation { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs index 4692fde5e3..d21a4d06e3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition.Animations /// The delay behavior of the key frame animation. /// public AnimationDelayBehavior DelayBehavior { get; set; } - + /// - /// Delay before the animation starts after is called. + /// Delay before the animation starts after is called. /// public System.TimeSpan DelayTime { get; set; } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 4b43f93aee..e1d43b4268 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The geometry. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, IBrush? brush, IPen? pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1f58111ecf..b23b0684d3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IBrush foreground, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index ee5ec0a5fc..b027e43378 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The start point of the line. /// The end point of the line. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public LineNode( Matrix transform, IPen pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 549c1fd7de..5fd200ddff 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opacity mask to push. /// The bounds of the mask. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) : base(Rect.Empty, Matrix.Identity, aux) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index a9d1bf96e5..f2ffd7411c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The rectangle to draw. /// The box shadow parameters - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, IBrush? brush, diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 3d5be806e1..d381979c1e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -255,7 +255,7 @@ namespace Avalonia.Utilities /// /// Clamps a value between a minimum and maximum value. /// - /// The value. + /// The value. /// The minimum value. /// The maximum value. /// The clamped value. From 89c0ef56371bddf68590f8f21efe61672d1084fc Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 12:02:04 +0200 Subject: [PATCH 471/481] refactoring: removed some unused namespace --- .../Rendering/Composition/Animations/CompositionAnimation.cs | 3 --- src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs | 2 -- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 4 ---- src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs | 2 -- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 3 --- src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs | 2 -- 6 files changed, 16 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 19d316eb85..5499e1119a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -1,12 +1,9 @@ // ReSharper disable InconsistentNaming // ReSharper disable CheckNamespace -using System; -using System.Collections.Generic; using System.Numerics; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; // Special license applies License.md diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 4b43f93aee..747e96f130 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1f58111ecf..ed45e54f57 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; - using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index ee5ec0a5fc..973b28dead 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index a80f406989..6400d67fde 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -1,11 +1,8 @@ using System; using System.IO; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 7f1af46e97..81fa8c4bce 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -9,7 +8,6 @@ using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; From bf9a0d8656d787fc67a7ba8aea3f9c6097624f0c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 2 Aug 2022 13:17:01 +0200 Subject: [PATCH 472/481] Use a different arabic font for tests --- .../Assets/NotoKufiArabic-Regular.ttf | Bin 122736 -> 0 bytes .../Assets/NotoSansArabic-Regular.ttf | Bin 0 -> 177004 bytes .../Media/CustomFontManagerImpl.cs | 8 +++++++- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf create mode 100644 tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf diff --git a/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf deleted file mode 100644 index 6d2ad86f947a530de5dec09b9a230948d0f512c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122736 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 V75=m6xC;lbHm=3_}(L8AakEU|`p@iY%Zk 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 z�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 literal 0 HcmV?d00001 diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 13cc14b03e..7d0cf05b0b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -16,7 +16,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly Typeface _defaultTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); private readonly Typeface _arabicTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Kufi Arabic"); + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); private readonly Typeface _emojiTypeface = @@ -82,6 +82,12 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } + case "Noto Sans Arabic": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case FontFamily.DefaultFontFamilyName: case "Noto Mono": { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 43948e9229..c457a96299 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1005,7 +1005,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - Assert.Equal(currentX, distance, 5); + Assert.Equal(currentX, distance); currentX += glyphAdvance; From 4f79ac8a1c1cdecc71e89e4d94d98082ec54c8ea Mon Sep 17 00:00:00 2001 From: Benedikt Date: Tue, 2 Aug 2022 13:25:57 +0200 Subject: [PATCH 473/481] Fix precession errors --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 6 +++--- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 6c6939d2a0..fa1ab6fd29 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -357,7 +357,7 @@ namespace Avalonia.Media.TextFormatting if (currentPosition + currentRun.TextSourceLength >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { - return currentDistance + distance; + return Math.Max(0, currentDistance + distance); } //No hit hit found so we add the full width @@ -382,7 +382,7 @@ namespace Avalonia.Media.TextFormatting distance = currentGlyphRun.Size.Width - distance; } - return currentDistance - distance; + return Math.Max(0, currentDistance - distance); } //No hit hit found so we add the full width @@ -392,7 +392,7 @@ namespace Avalonia.Media.TextFormatting } } - return currentDistance; + return Math.Max(0, currentDistance); } private static bool TryGetDistanceFromCharacterHit( diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index c457a96299..43948e9229 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1005,7 +1005,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 5); currentX += glyphAdvance; From 1731f88822a6d0a4c2e5a58b69f866212e7ff7e4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 2 Aug 2022 20:37:55 -0400 Subject: [PATCH 474/481] Rename Default to Simple --- Avalonia.sln | 2 +- samples/BindingDemo/App.xaml | 2 +- samples/ControlCatalog/App.xaml.cs | 2 +- samples/ControlCatalog/ControlCatalog.csproj | 2 +- samples/ControlCatalog/MainView.xaml.cs | 4 +- samples/PlatformSanityChecks/App.xaml | 3 +- .../PlatformSanityChecks.csproj | 2 +- samples/Previewer/App.xaml | 5 +- samples/Previewer/Previewer.csproj | 2 +- samples/VirtualizationDemo/App.xaml | 4 +- .../VirtualizationDemo.csproj | 2 +- .../interop/Direct3DInteropSample/App.paml | 5 +- .../Direct3DInteropSample.csproj | 2 +- src/Avalonia.Controls/ProgressBar.cs | 4 - .../Avalonia.Diagnostics.csproj | 2 +- .../Diagnostics/Views/MainWindow.xaml | 2 +- .../Diagnostics/Views/MainWindow.xaml.cs | 2 +- src/Avalonia.Themes.Default/DefaultTheme.xaml | 75 ------------------- .../DefaultTheme.xaml.cs | 11 --- .../Accents/Base.xaml | 0 .../Accents/BaseDark.xaml | 0 .../Accents/BaseLight.xaml | 0 .../ApiCompatBaseline.txt | 0 .../Avalonia.Themes.Simple.csproj} | 0 .../Controls/AutoCompleteBox.xaml | 0 .../Controls/Button.xaml | 0 .../Controls/ButtonSpinner.xaml | 6 +- .../Controls/Calendar.xaml | 0 .../Controls/CalendarButton.xaml | 0 .../Controls/CalendarDatePicker.xaml | 0 .../Controls/CalendarDayButton.xaml | 0 .../Controls/CalendarItem.xaml | 0 .../Controls/CaptionButtons.xaml | 10 +-- .../Controls/Carousel.xaml | 0 .../Controls/CheckBox.xaml | 0 .../Controls/ComboBox.xaml | 0 .../Controls/ComboBoxItem.xaml | 0 .../Controls/ContentControl.xaml | 0 .../Controls/ContextMenu.xaml | 2 +- .../Controls/DataValidationErrors.xaml | 0 .../Controls/DatePicker.xaml | 22 +++--- .../Controls/DateTimePickerShared.xaml | 22 +++--- .../Controls/DropDownButton.xaml | 0 .../Controls/EmbeddableControlRoot.xaml | 0 .../Controls/Expander.xaml | 34 ++++----- .../Controls/FlyoutPresenter.xaml | 0 .../Controls/FocusAdorner.xaml | 0 .../Controls/GridSplitter.xaml | 0 .../Controls/ItemsControl.xaml | 0 .../Controls/Label.xaml | 0 .../Controls/ListBox.xaml | 0 .../Controls/ListBoxItem.xaml | 0 .../Controls/ManagedFileChooser.xaml | 0 .../Controls/Menu.xaml | 6 +- .../Controls/MenuFlyoutPresenter.xaml | 2 +- .../Controls/MenuItem.xaml | 2 +- .../Controls/NativeMenuBar.xaml | 10 +-- .../Controls/NotificationCard.xaml | 0 .../Controls/NumericUpDown.xaml | 0 .../Controls/OverlayPopupHost.xaml | 0 .../Controls/PathIcon.xaml | 0 .../Controls/PopupRoot.xaml | 0 .../Controls/ProgressBar.xaml | 0 .../Controls/RadioButton.xaml | 0 .../Controls/RepeatButton.xaml | 0 .../Controls/RichTextBlock.xaml | 0 .../Controls/ScrollBar.xaml | 0 .../Controls/ScrollViewer.xaml | 2 +- .../Controls/Separator.xaml | 0 .../Controls/SimpleControls.xaml | 74 ++++++++++++++++++ .../Controls/Slider.xaml | 0 .../Controls/SplitButton.xaml | 8 +- .../Controls/SplitView.xaml | 0 .../Controls/TabControl.xaml | 0 .../Controls/TabItem.xaml | 0 .../Controls/TabStrip.xaml | 0 .../Controls/TabStripItem.xaml | 0 .../Controls/TextBox.xaml | 14 ++-- .../Controls/TimePicker.xaml | 20 ++--- .../Controls/TitleBar.xaml | 0 .../Controls/ToggleButton.xaml | 0 .../Controls/ToggleSwitch.xaml | 0 .../Controls/ToolTip.xaml | 0 .../Controls/TransitioningContentControl.xaml | 0 .../Controls/TreeView.xaml | 0 .../Controls/TreeViewItem.xaml | 4 +- .../Controls/UserControl.xaml | 4 +- .../Controls/Window.xaml | 0 .../Controls/WindowNotificationManager.xaml | 0 .../IBitmapToImageConverter.cs | 2 +- .../InverseBooleanValueConverter.cs | 2 +- .../Properties/AssemblyInfo.cs | 3 + .../SimpleTheme.cs | 14 ++-- .../SimpleThemeMode.cs | 2 +- .../Avalonia.Designer.HostApp.csproj | 2 +- .../Utilities/UriExtensionsTests.cs | 6 +- .../Avalonia.Benchmarks.csproj | 2 +- .../Themes/ThemeBenchmark.cs | 6 +- .../Avalonia.DesignerSupport.TestApp/App.xaml | 6 +- .../Avalonia.DesignerSupport.TestApp.csproj | 2 +- .../Avalonia.Direct2D1.RenderTests.csproj | 2 +- .../Avalonia.LeakTests.csproj | 2 +- .../Avalonia.Markup.Xaml.UnitTests.csproj | 2 +- .../Xaml/BasicTests.cs | 2 +- .../Avalonia.Skia.RenderTests.csproj | 2 +- .../Avalonia.UnitTests.csproj | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 19 ++--- .../Avalonia.UnitTests/UnitTestApplication.cs | 8 +- 108 files changed, 220 insertions(+), 241 deletions(-) delete mode 100644 src/Avalonia.Themes.Default/DefaultTheme.xaml delete mode 100644 src/Avalonia.Themes.Default/DefaultTheme.xaml.cs rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Accents/Base.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Accents/BaseDark.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Accents/BaseLight.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/ApiCompatBaseline.txt (100%) rename src/{Avalonia.Themes.Default/Avalonia.Themes.Default.csproj => Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj} (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/AutoCompleteBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Button.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ButtonSpinner.xaml (94%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Calendar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarDatePicker.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarDayButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CaptionButtons.xaml (93%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Carousel.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CheckBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ComboBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ComboBoxItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ContentControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ContextMenu.xaml (95%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DataValidationErrors.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DatePicker.xaml (94%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DateTimePickerShared.xaml (87%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DropDownButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/EmbeddableControlRoot.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Expander.xaml (88%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/FlyoutPresenter.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/FocusAdorner.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/GridSplitter.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ItemsControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Label.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ListBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ListBoxItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ManagedFileChooser.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Menu.xaml (95%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/MenuFlyoutPresenter.xaml (96%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/MenuItem.xaml (98%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/NativeMenuBar.xaml (76%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/NotificationCard.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/NumericUpDown.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/OverlayPopupHost.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/PathIcon.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/PopupRoot.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ProgressBar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/RadioButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/RepeatButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/RichTextBlock.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ScrollBar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ScrollViewer.xaml (99%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Separator.xaml (100%) create mode 100644 src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Slider.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/SplitButton.xaml (98%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/SplitView.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabStrip.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabStripItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TextBox.xaml (96%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TimePicker.xaml (94%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TitleBar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ToggleButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ToggleSwitch.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ToolTip.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TransitioningContentControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TreeView.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TreeViewItem.xaml (96%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/UserControl.xaml (90%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Window.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/WindowNotificationManager.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/IBitmapToImageConverter.cs (95%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/InverseBooleanValueConverter.cs (94%) create mode 100644 src/Avalonia.Themes.Simple/Properties/AssemblyInfo.cs rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/SimpleTheme.cs (89%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/SimpleThemeMode.cs (67%) diff --git a/Avalonia.sln b/Avalonia.sln index 4999719676..35b6b2108a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Simple", "src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 3e312c8685..175e838616 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -4,6 +4,6 @@ x:Class="BindingDemo.App"> - + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 7ebb87094a..eb8ecc3af0 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.Themes.Fluent; using ControlCatalog.ViewModels; diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 8358fb3cd4..2654574a3e 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,7 +29,7 @@ - + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 47d11738bc..d8d58edaf6 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -60,14 +60,14 @@ namespace ControlCatalog } else if (theme == CatalogTheme.DefaultLight) { - App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; + App.Default.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; Application.Current.Styles[0] = App.DefaultLight; Application.Current.Styles[1] = App.ColorPickerDefault; Application.Current.Styles[2] = App.DataGridDefault; } else if (theme == CatalogTheme.DefaultDark) { - App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; + App.Default.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; Application.Current.Styles[0] = App.DefaultDark; Application.Current.Styles[1] = App.ColorPickerDefault; Application.Current.Styles[2] = App.DataGridDefault; diff --git a/samples/PlatformSanityChecks/App.xaml b/samples/PlatformSanityChecks/App.xaml index 25bab6ae35..1b9d64fca6 100644 --- a/samples/PlatformSanityChecks/App.xaml +++ b/samples/PlatformSanityChecks/App.xaml @@ -1,6 +1,5 @@ - - + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 4f7f06b529..5c743aabdb 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/Previewer/App.xaml b/samples/Previewer/App.xaml index 6bae1955af..1b9d64fca6 100644 --- a/samples/Previewer/App.xaml +++ b/samples/Previewer/App.xaml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 98560e9ab0..2cc84168dc 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,7 +10,7 @@ - + diff --git a/samples/VirtualizationDemo/App.xaml b/samples/VirtualizationDemo/App.xaml index 3ad1dce794..0b64d0c09b 100644 --- a/samples/VirtualizationDemo/App.xaml +++ b/samples/VirtualizationDemo/App.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="VirtualizationDemo.App"> - - + + \ No newline at end of file diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index bd6054327f..b27cfe77e8 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,7 +5,7 @@ - + diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml index d9630eef58..e6d77dfaf4 100644 --- a/samples/interop/Direct3DInteropSample/App.paml +++ b/samples/interop/Direct3DInteropSample/App.paml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index 81b94b4d09..f9ef4693d5 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 56b9caf085..91114628ee 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -119,14 +119,12 @@ namespace Avalonia.Controls nameof(Percentage), o => o.Percentage); - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateStartingOffset), p => p.IndeterminateStartingOffset, (p, o) => p.IndeterminateStartingOffset = o); - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateEndingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateEndingOffset), @@ -139,14 +137,12 @@ namespace Avalonia.Controls private set { SetAndRaise(PercentageProperty, ref _percentage, value); } } - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateStartingOffset { get => _indeterminateStartingOffset; set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); } - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateEndingOffset { get => _indeterminateEndingOffset; diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index adddf3f57b..d719135a7f 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 004518598c..63073952ac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:Avalonia.Diagnostics.Views" xmlns:diag="clr-namespace:Avalonia.Diagnostics" - xmlns:default="using:Avalonia.Themes.Default" + xmlns:default="using:Avalonia.Themes.Simple" Title="Avalonia DevTools" x:Class="Avalonia.Diagnostics.Views.MainWindow" Theme="{StaticResource {x:Type Window}}"> diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 96dc929434..c81997f2cb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -11,7 +11,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.Views diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml deleted file mode 100644 index 8f5bea557c..0000000000 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs deleted file mode 100644 index 598b418977..0000000000 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Styling; - -namespace Avalonia.Themes.Default -{ - /// - /// The default Avalonia theme. - /// - public class DefaultTheme : Styles - { - } -} diff --git a/src/Avalonia.Themes.Default/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/Base.xaml rename to src/Avalonia.Themes.Simple/Accents/Base.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/BaseDark.xaml rename to src/Avalonia.Themes.Simple/Accents/BaseDark.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/BaseLight.xaml rename to src/Avalonia.Themes.Simple/Accents/BaseLight.xaml diff --git a/src/Avalonia.Themes.Default/ApiCompatBaseline.txt b/src/Avalonia.Themes.Simple/ApiCompatBaseline.txt similarity index 100% rename from src/Avalonia.Themes.Default/ApiCompatBaseline.txt rename to src/Avalonia.Themes.Simple/ApiCompatBaseline.txt diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj similarity index 100% rename from src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj rename to src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj diff --git a/src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml b/src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml rename to src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Simple/Controls/Button.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Controls/Button.xaml rename to src/Avalonia.Themes.Simple/Controls/Button.xaml diff --git a/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml similarity index 94% rename from src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml rename to src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml index 4585fc8e56..9798e5290b 100644 --- a/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True"> - @@ -38,7 +38,7 @@ IsVisible="{TemplateBinding ShowButtonSpinner}" Rows="2"> + Theme="{StaticResource SimpleButtonSpinnerRepeatButton}"> + Theme="{StaticResource SimpleButtonSpinnerRepeatButton}"> - @@ -44,7 +44,7 @@ TextElement.FontSize="10">