diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6b910fc615..4e34d4b132 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve Avalonia title: '' labels: bug assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. Windows, Mac, Linux (State distribution)] - - Version [e.g. 0.10.0-rc1 or 0.9.12] + +- OS: [e.g. Windows, Mac, Linux (State distribution)] +- Version [e.g. 0.10.0-rc1 or 0.9.12] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..687355d825 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions, Discussions, Ideas + url: https://github.com/AvaloniaUI/Avalonia/discussions/new + about: Please ask and answer questions here. + - name: Avalonia Community Support on Gitter + url: https://gitter.im/AvaloniaUI/Avalonia + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491ef1..5f0a04cee3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: enhancement assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index b6f9ed5071..1732d6e71f 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -62,6 +62,28 @@ public: return S_OK; } + + virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override + { + if(bitmapData == nullptr || retOut == nullptr) + { + return E_POINTER; + } + + NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + + NSPoint hotSpot; + hotSpot.x = hotPixel.Width; + hotSpot.y = hotPixel.Height; + + *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); + + (*retOut)->AddRef(); + + return S_OK; + } }; extern IAvnCursorFactory* CreateCursorFactory() diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png new file mode 100644 index 0000000000..7b443e7a25 Binary files /dev/null and b/samples/ControlCatalog/Assets/avalonia-32.png differ diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f001425964..142c532d75 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,6 +22,10 @@ + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml new file mode 100644 index 0000000000..a28039ea3f --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml @@ -0,0 +1,29 @@ + + + + Cursor + Defines a cursor (mouse pointer) + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml.cs b/samples/ControlCatalog/Pages/CursorPage.xaml.cs new file mode 100644 index 0000000000..9e9e9ba8b9 --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class CursorPage : UserControl + { + public CursorPage() + { + this.InitializeComponent(); + DataContext = new CursorPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs new file mode 100644 index 0000000000..f1cc0637dc --- /dev/null +++ b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class CursorPageViewModel : ViewModelBase + { + public CursorPageViewModel() + { + StandardCursors = Enum.GetValues(typeof(StandardCursorType)) + .Cast() + .Select(x => new StandardCursorModel(x)) + .ToList(); + + var loader = AvaloniaLocator.Current.GetService(); + var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); + var bitmap = new Bitmap(s); + CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); + } + + public IEnumerable StandardCursors { get; } + + public Cursor CustomCursor { get; } + + public class StandardCursorModel + { + public StandardCursorModel(StandardCursorType type) + { + Type = type; + Cursor = new Cursor(type); + } + + public StandardCursorType Type { get; } + + public Cursor Cursor { get; } + } + } +} diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs index 1e4864283f..bc1f8d36a9 100644 --- a/src/Avalonia.Base/EnumExtensions.cs +++ b/src/Avalonia.Base/EnumExtensions.cs @@ -11,10 +11,32 @@ namespace Avalonia [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum { - var intValue = *(int*)&value; - var intFlag = *(int*)&flag; - - return (intValue & intFlag) == intFlag; + if (sizeof(T) == 1) + { + var byteValue = Unsafe.As(ref value); + var byteFlag = Unsafe.As(ref flag); + return (byteValue & byteFlag) == byteFlag; + } + else if (sizeof(T) == 2) + { + var shortValue = Unsafe.As(ref value); + var shortFlag = Unsafe.As(ref flag); + return (shortValue & shortFlag) == shortFlag; + } + else if (sizeof(T) == 4) + { + var intValue = Unsafe.As(ref value); + var intFlag = Unsafe.As(ref flag); + return (intValue & intFlag) == intFlag; + } + else if (sizeof(T) == 8) + { + var longValue = Unsafe.As(ref value); + var longFlag = Unsafe.As(ref flag); + return (longValue & longFlag) == longFlag; + } + else + throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf() + " are not supported"); } } } diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index d0d88166a7..097731bc60 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -372,8 +372,8 @@ namespace Avalonia.Utilities const string implicitName = "op_Implicit"; const string explicitName = "op_Explicit"; - bool allowImplicit = (operatorType & OperatorType.Implicit) != 0; - bool allowExplicit = (operatorType & OperatorType.Explicit) != 0; + bool allowImplicit = operatorType.HasFlagCustom(OperatorType.Implicit); + bool allowExplicit = operatorType.HasFlagCustom(OperatorType.Explicit); foreach (MethodInfo method in fromType.GetMethods()) { diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index 92734b128d..b97f2a2bcb 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -2595,7 +2595,7 @@ namespace Avalonia.Collections /// Whether the specified flag is set private bool CheckFlag(CollectionViewFlags flags) { - return (_flags & flags) != 0; + return _flags.HasFlagCustom(flags); } /// diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt new file mode 100644 index 0000000000..e5adc8c6ed --- /dev/null +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -0,0 +1,6 @@ +Compat issues with assembly Avalonia.Controls: +MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. +Total Issues: 4 diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 7f2acb58fe..20ca41bc57 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -188,7 +188,7 @@ namespace Avalonia.Controls return; if (e.Key == Key.F4 || - ((e.Key == Key.Down || e.Key == Key.Up) && ((e.KeyModifiers & KeyModifiers.Alt) != 0))) + ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))) { IsDropDownOpen = !IsDropDownOpen; e.Handled = true; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 522103c7bd..ca0e9d48b8 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public virtual PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1); - public virtual void SetCursor(IPlatformHandle cursor) + public virtual void SetCursor(ICursorImpl cursor) { } diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 6357ec98a8..66266c3b61 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -637,7 +637,7 @@ namespace Avalonia.Controls /// internal bool MeasureOverrideInProgress { - get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); } + get { return CheckFlags(Flags.MeasureOverrideInProgress); } set { SetFlags(value, Flags.MeasureOverrideInProgress); } } @@ -646,7 +646,7 @@ namespace Avalonia.Controls /// internal bool ArrangeOverrideInProgress { - get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); } + get { return CheckFlags(Flags.ArrangeOverrideInProgress); } set { SetFlags(value, Flags.ArrangeOverrideInProgress); } } @@ -2350,25 +2350,12 @@ namespace Avalonia.Controls } /// - /// CheckFlagsAnd returns true if all the flags in the + /// CheckFlags returns true if all the flags in the /// given bitmask are set on the object. /// - private bool CheckFlagsAnd(Flags flags) + private bool CheckFlags(Flags flags) { - return ((_flags & flags) == flags); - } - - /// - /// CheckFlagsOr returns true if at least one flag in the - /// given bitmask is set. - /// - /// - /// If no bits are set in the given bitmask, the method returns - /// true. - /// - private bool CheckFlagsOr(Flags flags) - { - return (flags == 0 || (_flags & flags) != 0); + return _flags.HasFlagCustom(flags); } private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) @@ -2535,7 +2522,7 @@ namespace Avalonia.Controls /// private bool CellsStructureDirty { - get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } + get { return !CheckFlags(Flags.ValidCellsStructure); } set { SetFlags(!value, Flags.ValidCellsStructure); } } @@ -2544,7 +2531,7 @@ namespace Avalonia.Controls /// private bool ListenToNotifications { - get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } + get { return CheckFlags(Flags.ListenToNotifications); } set { SetFlags(value, Flags.ListenToNotifications); } } @@ -2553,7 +2540,7 @@ namespace Avalonia.Controls /// private bool SizeToContentU { - get { return (CheckFlagsAnd(Flags.SizeToContentU)); } + get { return CheckFlags(Flags.SizeToContentU); } set { SetFlags(value, Flags.SizeToContentU); } } @@ -2562,7 +2549,7 @@ namespace Avalonia.Controls /// private bool SizeToContentV { - get { return (CheckFlagsAnd(Flags.SizeToContentV)); } + get { return CheckFlags(Flags.SizeToContentV); } set { SetFlags(value, Flags.SizeToContentV); } } @@ -2571,7 +2558,7 @@ namespace Avalonia.Controls /// private bool HasStarCellsU { - get { return (CheckFlagsAnd(Flags.HasStarCellsU)); } + get { return CheckFlags(Flags.HasStarCellsU); } set { SetFlags(value, Flags.HasStarCellsU); } } @@ -2580,7 +2567,7 @@ namespace Avalonia.Controls /// private bool HasStarCellsV { - get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } + get { return CheckFlags(Flags.HasStarCellsV); } set { SetFlags(value, Flags.HasStarCellsV); } } @@ -2589,7 +2576,7 @@ namespace Avalonia.Controls /// private bool HasGroup3CellsInAutoRows { - get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } + get { return CheckFlags(Flags.HasGroup3CellsInAutoRows); } set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } @@ -2803,10 +2790,10 @@ namespace Avalonia.Controls internal LayoutTimeSizeType SizeTypeU; internal LayoutTimeSizeType SizeTypeV; internal int Next; - internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } - internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } + internal bool IsStarU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Star); + internal bool IsAutoU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Auto); + internal bool IsStarV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Star); + internal bool IsAutoV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Auto); } /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index d1b8038581..b6b3cc786c 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -135,8 +135,8 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.KeyModifiers & KeyModifiers.Shift) != 0, - (e.KeyModifiers & KeyModifiers.Control) != 0); + e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift), + e.KeyModifiers.HasFlagCustom(KeyModifiers.Control)); } } @@ -154,8 +154,8 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.KeyModifiers & KeyModifiers.Shift) != 0, - (e.KeyModifiers & KeyModifiers.Control) != 0, + e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift), + e.KeyModifiers.HasFlagCustom(KeyModifiers.Control), point.Properties.IsRightButtonPressed); } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 71ac0fa523..94099a970e 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -103,6 +103,7 @@ namespace Avalonia.Controls private bool _commandCanExecute = true; private Popup? _popup; private KeyGesture _hotkey; + private bool _isEmbeddedInMenu; /// /// Initializes static members of the class. @@ -112,6 +113,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); CommandProperty.Changed.Subscribe(CommandChanged); + CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); FocusableProperty.OverrideDefaultValue(true); HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); IconProperty.Changed.AddClassHandler((x, e) => x.IconChanged(e)); @@ -146,7 +148,7 @@ namespace Avalonia.Controls { var parent = x as Control; return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? - Observable.Return(null); + Observable.Return(null); }); this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); @@ -274,7 +276,7 @@ namespace Avalonia.Controls public bool IsTopLevel => Parent is Menu; /// - bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; + bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; /// IMenuElement? IMenuItem.Parent => Parent as IMenuElement; @@ -309,7 +311,7 @@ namespace Avalonia.Controls .Select(x => x.ContainerControl) .OfType(); } - } + } /// /// Opens the submenu. @@ -336,6 +338,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (!_isEmbeddedInMenu) + { + //Normally the Menu's IMenuInteractionHandler is sending the click events for us + //However when the item is not embedded into a menu we need to send them ourselves. + RaiseEvent(new RoutedEventArgs(ClickEvent)); + } + } + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control @@ -349,6 +363,15 @@ namespace Avalonia.Controls { Command.CanExecuteChanged += CanExecuteChanged; } + + var parent = Parent; + + while (parent is MenuItem) + { + parent = parent.Parent; + } + + _isEmbeddedInMenu = parent is IMenu; } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) @@ -506,6 +529,18 @@ namespace Avalonia.Controls } } + /// + /// Called when the property changes. + /// + /// The event args. + private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is MenuItem menuItem) + { + menuItem.CanExecuteChanged(menuItem, EventArgs.Empty); + } + } + /// /// Called when the event fires. /// diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 7514f214aa..09f38042a1 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -98,7 +98,7 @@ namespace Avalonia.Platform /// Sets the cursor associated with the toplevel. /// /// The cursor. Use null for default cursor - void SetCursor(IPlatformHandle cursor); + void SetCursor(ICursorImpl cursor); /// /// Gets or sets a method called when the underlying implementation is destroyed. diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 9d644aaa00..7e1a7378c7 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -73,20 +73,20 @@ namespace Avalonia.Platform { if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None) return effect; // No need to check for the modifiers. - if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(RawInputModifiers.Alt)) + if (effect.HasFlagCustom(DragDropEffects.Link) && modifiers.HasFlagCustom(RawInputModifiers.Alt)) return DragDropEffects.Link; - if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(RawInputModifiers.Control)) + if (effect.HasFlagCustom(DragDropEffects.Copy) && modifiers.HasFlagCustom(RawInputModifiers.Control)) return DragDropEffects.Copy; return DragDropEffects.Move; } private StandardCursorType GetCursorForDropEffect(DragDropEffects effects) { - if (effects.HasFlag(DragDropEffects.Copy)) + if (effects.HasFlagCustom(DragDropEffects.Copy)) return StandardCursorType.DragCopy; - if (effects.HasFlag(DragDropEffects.Move)) + if (effects.HasFlagCustom(DragDropEffects.Move)) return StandardCursorType.DragMove; - if (effects.HasFlag(DragDropEffects.Link)) + if (effects.HasFlagCustom(DragDropEffects.Link)) return StandardCursorType.DragLink; return StandardCursorType.No; } @@ -161,7 +161,7 @@ namespace Avalonia.Platform void CheckDraggingAccepted(RawInputModifiers changedMouseButton) { - if (_initialInputModifiers.Value.HasFlag(changedMouseButton)) + if (_initialInputModifiers.Value.HasFlagCustom(changedMouseButton)) { var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers); UpdateCursor(null, DragDropEffects.None); diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index aed7dff0fe..545034239f 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -253,9 +253,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning { public static void ValidateEdge(this PopupAnchor edge) { - if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0) - || - ((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0)) + if (edge.HasFlagCustom(PopupAnchor.Left) && edge.HasFlagCustom(PopupAnchor.Right) || + edge.HasFlagCustom(PopupAnchor.Top) && edge.HasFlagCustom(PopupAnchor.Bottom)) throw new ArgumentException("Opposite edges specified"); } @@ -266,25 +265,25 @@ namespace Avalonia.Controls.Primitives.PopupPositioning public static PopupAnchor Flip(this PopupAnchor edge) { - var hmask = PopupAnchor.Left | PopupAnchor.Right; - var vmask = PopupAnchor.Top | PopupAnchor.Bottom; - if ((edge & hmask) != 0) - edge ^= hmask; - if ((edge & vmask) != 0) - edge ^= vmask; + if (edge.HasFlagCustom(PopupAnchor.HorizontalMask)) + edge ^= PopupAnchor.HorizontalMask; + + if (edge.HasFlagCustom(PopupAnchor.VerticalMask)) + edge ^= PopupAnchor.VerticalMask; + return edge; } public static PopupAnchor FlipX(this PopupAnchor edge) { - if ((edge & PopupAnchor.HorizontalMask) != 0) + if (edge.HasFlagCustom(PopupAnchor.HorizontalMask)) edge ^= PopupAnchor.HorizontalMask; return edge; } public static PopupAnchor FlipY(this PopupAnchor edge) { - if ((edge & PopupAnchor.VerticalMask) != 0) + if (edge.HasFlagCustom(PopupAnchor.VerticalMask)) edge ^= PopupAnchor.VerticalMask; return edge; } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 7f1dbdf592..80b32b5ef8 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -42,16 +42,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge) { double x, y; - if ((edge & PopupAnchor.Left) != 0) + if (edge.HasFlagCustom(PopupAnchor.Left)) x = anchorRect.X; - else if ((edge & PopupAnchor.Right) != 0) + else if (edge.HasFlagCustom(PopupAnchor.Right)) x = anchorRect.Right; else x = anchorRect.X + anchorRect.Width / 2; - if ((edge & PopupAnchor.Top) != 0) + if (edge.HasFlagCustom(PopupAnchor.Top)) y = anchorRect.Y; - else if ((edge & PopupAnchor.Bottom) != 0) + else if (edge.HasFlagCustom(PopupAnchor.Bottom)) y = anchorRect.Bottom; else y = anchorRect.Y + anchorRect.Height / 2; @@ -61,16 +61,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity) { double x, y; - if ((gravity & PopupGravity.Left) != 0) + if (gravity.HasFlagCustom(PopupGravity.Left)) x = -size.Width; - else if ((gravity & PopupGravity.Right) != 0) + else if (gravity.HasFlagCustom(PopupGravity.Right)) x = 0; else x = -size.Width / 2; - if ((gravity & PopupGravity.Top) != 0) + if (gravity.HasFlagCustom(PopupGravity.Top)) y = -size.Height; - else if ((gravity & PopupGravity.Bottom) != 0) + else if (gravity.HasFlagCustom(PopupGravity.Bottom)) y = 0; else y = -size.Height / 2; @@ -125,21 +125,13 @@ namespace Avalonia.Controls.Primitives.PopupPositioning bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask) { - if ((edge & PopupAnchor.Left) != 0 - && rc.X < bounds.X) - return false; - - if ((edge & PopupAnchor.Top) != 0 - && rc.Y < bounds.Y) - return false; - - if ((edge & PopupAnchor.Right) != 0 - && rc.Right > bounds.Right) - return false; - - if ((edge & PopupAnchor.Bottom) != 0 - && rc.Bottom > bounds.Bottom) + if (edge.HasFlagCustom(PopupAnchor.Left) && rc.X < bounds.X || + edge.HasFlagCustom(PopupAnchor.Top) && rc.Y < bounds.Y || + edge.HasFlagCustom(PopupAnchor.Right) && rc.Right > bounds.Right || + edge.HasFlagCustom(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom) + { return false; + } return true; } @@ -155,7 +147,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning // If flipping geometry and anchor is allowed and helps, use the flipped one, // otherwise leave it as is if (!FitsInBounds(geo, PopupAnchor.HorizontalMask) - && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) + && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipX)) { var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); if (FitsInBounds(flipped, PopupAnchor.HorizontalMask)) @@ -163,7 +155,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } // If sliding is allowed, try moving the rect into the bounds - if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0) + if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideX)) { geo = geo.WithX(Math.Max(geo.X, bounds.X)); if (geo.Right > bounds.Right) @@ -171,7 +163,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } // Resize the rect horizontally if allowed. - if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeX) != 0) + if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeX)) { var unconstrainedRect = geo; @@ -194,7 +186,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning // If flipping geometry and anchor is allowed and helps, use the flipped one, // otherwise leave it as is if (!FitsInBounds(geo, PopupAnchor.VerticalMask) - && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) + && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipY)) { var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); if (FitsInBounds(flipped, PopupAnchor.VerticalMask)) @@ -202,7 +194,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } // If sliding is allowed, try moving the rect into the bounds - if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0) + if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideY)) { geo = geo.WithY(Math.Max(geo.Y, bounds.Y)); if (geo.Bottom > bounds.Bottom) @@ -210,7 +202,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } // Resize the rect vertically if allowed. - if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeY) != 0) + if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeY)) { var unconstrainedRect = geo; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 280f46be9f..2cd69793dc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -321,7 +321,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets a value indicating whether is set. /// - protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; + protected bool AlwaysSelected => SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected); /// public override void BeginInit() @@ -487,7 +487,7 @@ namespace Avalonia.Controls.Primitives if (ItemCount > 0 && Match(keymap.SelectAll) && - SelectionMode.HasFlag(SelectionMode.Multiple)) + SelectionMode.HasFlagCustom(SelectionMode.Multiple)) { Selection.SelectAll(); e.Handled = true; @@ -577,8 +577,8 @@ namespace Avalonia.Controls.Primitives } var mode = SelectionMode; - var multi = (mode & SelectionMode.Multiple) != 0; - var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var multi = mode.HasFlagCustom(SelectionMode.Multiple); + var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle); var range = multi && rangeModifier; if (!select) diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs index fd33eeaf39..4eadced423 100644 --- a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -53,8 +53,8 @@ namespace Avalonia.Controls { return _owner.GetElementImpl( index, - (options & ElementRealizationOptions.ForceCreate) != 0, - (options & ElementRealizationOptions.SuppressAutoRecycle) != 0); + options.HasFlagCustom(ElementRealizationOptions.ForceCreate), + options.HasFlagCustom(ElementRealizationOptions.SuppressAutoRecycle)); } protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 90064fad57..54d3af9b59 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -585,7 +585,7 @@ namespace Avalonia.Controls var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers); + bool DetectSelection() => e.KeyModifiers.HasFlagCustom(keymap.SelectionModifiers); if (Match(keymap.SelectAll)) { @@ -703,7 +703,7 @@ namespace Avalonia.Controls } else { - bool hasWholeWordModifiers = modifiers.HasFlag(keymap.WholeWordTextActionModifiers); + bool hasWholeWordModifiers = modifiers.HasFlagCustom(keymap.WholeWordTextActionModifiers); switch (e.Key) { case Key.Left: diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 4e43ce13b7..7a92836ddf 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -165,7 +165,7 @@ namespace Avalonia.Controls this.GetObservable(PointerOverElementProperty) .Select( x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) - .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); + .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformImpl)); if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 09742412d9..c8150cc200 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -412,7 +412,7 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.KeyModifiers & KeyModifiers.Shift) != 0); + e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift)); } } @@ -521,8 +521,8 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.KeyModifiers & KeyModifiers.Shift) != 0, - (e.KeyModifiers & KeyModifiers.Control) != 0, + e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift), + e.KeyModifiers.HasFlagCustom(KeyModifiers.Control), point.Properties.IsRightButtonPressed); } } @@ -558,9 +558,9 @@ namespace Avalonia.Controls } var mode = SelectionMode; - var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; - var multi = (mode & SelectionMode.Multiple) != 0; - var range = multi && selectedContainer != null && rangeModifier; + var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle); + var multi = mode.HasFlagCustom(SelectionMode.Multiple); + var range = multi && rangeModifier && selectedContainer != null; if (rightButton) { diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index fe4c580bbb..67b832318a 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -47,7 +47,7 @@ namespace Avalonia.DesignerSupport.Remote var threading = new InternalPlatformThreadingInterface(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(threading) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index f6783dc0b7..eedfc52d9d 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -73,7 +73,7 @@ namespace Avalonia.DesignerSupport.Remote public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } @@ -192,9 +192,15 @@ namespace Avalonia.DesignerSupport.Remote public Task GetDataAsync(string format) => Task.FromResult((object)null); } - class CursorFactoryStub : IStandardCursorFactory + class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); + + private class CursorStub : ICursorImpl + { + public void Dispose() { } + } } class IconLoaderStub : IPlatformIconLoader diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index e93ca64d3a..2cf533195e 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -223,13 +223,13 @@ namespace Avalonia.FreeDesktop return null; var lst = new List(); var mod = item.Gesture; - if ((mod.KeyModifiers & KeyModifiers.Control) != 0) + if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Control)) lst.Add("Control"); - if ((mod.KeyModifiers & KeyModifiers.Alt) != 0) + if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) lst.Add("Alt"); - if ((mod.KeyModifiers & KeyModifiers.Shift) != 0) + if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) lst.Add("Shift"); - if ((mod.KeyModifiers & KeyModifiers.Meta) != 0) + if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) lst.Add("Super"); lst.Add(item.Gesture.Key.ToString()); return new[] { lst.ToArray() }; diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 32f9f99709..fb61f25cea 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -33,11 +33,11 @@ namespace Avalonia.Headless.Vnc { Window?.MouseMove(pt); foreach (var btn in CheckedButtons) - if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn)) + if (_previousButtons.HasFlagCustom(btn) && !buttons.HasFlagCustom(btn)) Window?.MouseUp(pt, TranslateButton(btn), modifiers); foreach (var btn in CheckedButtons) - if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn)) + if (!_previousButtons.HasFlagCustom(btn) && buttons.HasFlagCustom(btn)) Window?.MouseDown(pt, TranslateButton(btn), modifiers); _previousButtons = buttons; }, DispatcherPriority.Input); diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 1f750a0309..fca2a1336f 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -58,7 +58,7 @@ namespace Avalonia.Headless AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(new HeadlessPlatformSettingsStub()) .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 4c0e2982f4..ce4c31e27e 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -52,12 +52,14 @@ namespace Avalonia.Headless } } - class HeadlessCursorFactoryStub : IStandardCursorFactory + class HeadlessCursorFactoryStub : ICursorFactory { + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); - public IPlatformHandle GetCursor(StandardCursorType cursorType) + private class CursorStub : ICursorImpl { - return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); + public void Dispose() { } } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 3a1f3bdaf7..af522f3e36 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Headless public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 660584e2ed..731a409090 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -177,7 +177,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen) + if (e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt) || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fff2fb2806 --- /dev/null +++ b/src/Avalonia.Input/ApiCompatBaseline.txt @@ -0,0 +1,4 @@ +Compat issues with assembly Avalonia.Input: +MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract. +Total Issues: 2 diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursor.cs similarity index 58% rename from src/Avalonia.Input/Cursors.cs rename to src/Avalonia.Input/Cursor.cs index 920b598eac..2b99c51472 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -1,15 +1,11 @@ using System; +using Avalonia.Media.Imaging; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Input { - /* - ========================================================================================= - NOTE: Cursors are NOT disposable and are cached in platform implementation. - To support loading custom cursors some measures about that should be taken beforehand - ========================================================================================= - */ - public enum StandardCursorType { Arrow, @@ -46,21 +42,28 @@ namespace Avalonia.Input // SizeNorthEastSouthWest, } - public class Cursor + public class Cursor : IDisposable { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); - internal Cursor(IPlatformHandle platformCursor) + internal Cursor(ICursorImpl platformImpl) { - PlatformCursor = platformCursor; + PlatformImpl = platformImpl; } public Cursor(StandardCursorType cursorType) - : this(GetCursor(cursorType)) + : this(GetCursorFactory().GetCursor(cursorType)) + { + } + + public Cursor(IBitmap cursor, PixelPoint hotSpot) + : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot)) { } - public IPlatformHandle PlatformCursor { get; } + public ICursorImpl PlatformImpl { get; } + + public void Dispose() => PlatformImpl.Dispose(); public static Cursor Parse(string s) { @@ -69,16 +72,10 @@ namespace Avalonia.Input throw new ArgumentException($"Unrecognized cursor type '{s}'."); } - private static IPlatformHandle GetCursor(StandardCursorType type) + private static ICursorFactory GetCursorFactory() { - var platform = AvaloniaLocator.Current.GetService(); - - if (platform == null) - { - throw new Exception("Could not create Cursor: IStandardCursorFactory not registered."); - } - - return platform.GetCursor(type); + return AvaloniaLocator.Current.GetService() ?? + throw new Exception("Could not create Cursor: ICursorFactory not registered."); } } } diff --git a/src/Avalonia.Input/Platform/ICursorFactory.cs b/src/Avalonia.Input/Platform/ICursorFactory.cs new file mode 100644 index 0000000000..fff1f92d53 --- /dev/null +++ b/src/Avalonia.Input/Platform/ICursorFactory.cs @@ -0,0 +1,12 @@ +using Avalonia.Input; + +#nullable enable + +namespace Avalonia.Platform +{ + public interface ICursorFactory + { + ICursorImpl GetCursor(StandardCursorType cursorType); + ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); + } +} diff --git a/src/Avalonia.Input/Platform/ICursorImpl.cs b/src/Avalonia.Input/Platform/ICursorImpl.cs new file mode 100644 index 0000000000..14235869f7 --- /dev/null +++ b/src/Avalonia.Input/Platform/ICursorImpl.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Input; + +#nullable enable + +namespace Avalonia.Platform +{ + /// + /// Represents a platform implementation of a . + /// + public interface ICursorImpl : IDisposable + { + } +} diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs deleted file mode 100644 index 51845cf03e..0000000000 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.Input; - -namespace Avalonia.Platform -{ - public interface IStandardCursorFactory - { - IPlatformHandle GetCursor(StandardCursorType cursorType); - } -} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 6ce55e17b3..edcbf90ebc 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -97,7 +97,7 @@ namespace Avalonia.Native AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) - .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) + .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) .Bind().ToConstant(this) diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index 3c65367c12..ae218e270c 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -1,11 +1,12 @@ using System; +using System.IO; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Native.Interop; namespace Avalonia.Native { - class AvaloniaNativeCursor : IPlatformHandle, IDisposable + class AvaloniaNativeCursor : ICursorImpl, IDisposable { public IAvnCursor Cursor { get; private set; } public IntPtr Handle => IntPtr.Zero; @@ -24,7 +25,7 @@ namespace Avalonia.Native } } - class CursorFactory : IStandardCursorFactory + class CursorFactory : ICursorFactory { IAvnCursorFactory _native; @@ -33,10 +34,28 @@ namespace Avalonia.Native _native = native; } - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { var cursor = _native.GetCursor((AvnStandardCursorType)cursorType); return new AvaloniaNativeCursor( cursor ); } + + public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using(var ms = new MemoryStream()) + { + cursor.Save(ms); + + var imageData = ms.ToArray(); + + fixed(void* ptr = imageData) + { + var avnCursor = _native.CreateCustomCursor(ptr, new IntPtr(imageData.Length), + new AvnPixelSize { Width = hotSpot.X, Height = hotSpot.Y }); + + return new AvaloniaNativeCursor(avnCursor); + } + } + } } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 11d5801532..71359f733d 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -53,7 +53,7 @@ namespace Avalonia.Native private bool _gpu = false; private readonly MouseDevice _mouse; private readonly IKeyboardDevice _keyboard; - private readonly IStandardCursorFactory _cursorFactory; + private readonly ICursorFactory _cursorFactory; private Size _savedLogicalSize; private Size _lastRenderedLogicalSize; private double _savedScaling; @@ -68,7 +68,7 @@ namespace Avalonia.Native _keyboard = AvaloniaLocator.Current.GetService(); _mouse = new MouseDevice(); - _cursorFactory = AvaloniaLocator.Current.GetService(); + _cursorFactory = AvaloniaLocator.Current.GetService(); } protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) @@ -398,7 +398,7 @@ namespace Avalonia.Native public Action Deactivated { get; set; } public Action Activated { get; set; } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { if (_native == null) { diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 166046ca24..3627ff6894 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -619,6 +619,7 @@ interface IAvnCursor : IUnknown interface IAvnCursorFactory : IUnknown { HRESULT GetCursor(AvnStandardCursorType cursorType, IAvnCursor** retOut); + HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut); } [uuid(60452465-8616-40af-bc00-042e69828ce7)] diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs deleted file mode 100644 index 86d39a4283..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Avalonia.Media.TextFormatting.Unicode -{ - internal static class BreakPairTable - { - private static readonly byte[][] s_breakPairTable = - { - new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4,4}, - new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1,0}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - new byte[] {0,4,4,1,1,0,4,4,4,0,0,0,0,0,0,0,0,0,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, - }; - - public static PairBreakType Map(LineBreakClass first, LineBreakClass second) - { - return (PairBreakType)s_breakPairTable[(int)first][(int)second]; - } - } - - internal enum PairBreakType : byte - { - DI = 0, // Direct break opportunity - IN = 1, // Indirect break opportunity - CI = 2, // Indirect break opportunity for combining marks - CP = 3, // Prohibited break for combining marks - PR = 4 // Prohibited break - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs index 2f46fdd9d0..43a95310c6 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs @@ -9,37 +9,40 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); - private readonly int _value; - public Codepoint(int value) { - _value = value; + Value = value; } + /// + /// Get the codepoint's value. + /// + public int Value { get; } + /// /// Gets the . /// - public GeneralCategory GeneralCategory => UnicodeData.GetGeneralCategory(_value); + public GeneralCategory GeneralCategory => UnicodeData.GetGeneralCategory(Value); /// /// Gets the . /// - public Script Script => UnicodeData.GetScript(_value); + public Script Script => UnicodeData.GetScript(Value); /// /// Gets the . /// - public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(_value); + public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(Value); /// /// Gets the . /// - public LineBreakClass LineBreakClass => UnicodeData.GetLineBreakClass(_value); + public LineBreakClass LineBreakClass => UnicodeData.GetLineBreakClass(Value); /// /// Gets the . /// - public GraphemeBreakClass GraphemeBreakClass => UnicodeData.GetGraphemeClusterBreak(_value); + public GraphemeBreakClass GraphemeBreakClass => UnicodeData.GetGraphemeClusterBreak(Value); /// /// Determines whether this is a break char. @@ -51,7 +54,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { get { - switch (_value) + switch (Value) { case '\u000A': case '\u000B': @@ -93,12 +96,12 @@ namespace Avalonia.Media.TextFormatting.Unicode public static implicit operator int(Codepoint codepoint) { - return codepoint._value; + return codepoint.Value; } public static implicit operator uint(Codepoint codepoint) { - return (uint)codepoint._value; + return (uint)codepoint.Value; } /// @@ -112,7 +115,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { count = 1; - if (index > text.Length) + if (index >= text.Length) { return ReplacementCodepoint; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 76bb9ac44f..4d02f94cad 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -1,160 +1,460 @@ -// RichTextKit -// Copyright © 2019 Topten Software. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this product except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. -// -// Ported from: https://github.com/foliojs/linebreak -// Copied from: https://github.com/toptensoftware/RichTextKit +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/ using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { /// - /// Implementation of the Unicode Line Break Algorithm + /// Implementation of the Unicode Line Break Algorithm. UAX:14 + /// /// public ref struct LineBreakEnumerator { - // State private readonly ReadOnlySlice _text; - private int _pos; - private int _lastPos; - private LineBreakClass? _curClass; - private LineBreakClass? _nextClass; + private int _position; + private int _lastPosition; + private LineBreakClass _currentClass; + private LineBreakClass _nextClass; + private bool _first; + private int _alphaNumericCount; + private bool _lb8a; + private bool _lb21a; + private bool _lb22ex; + private bool _lb24ex; + private bool _lb25ex; + private bool _lb30; + private int _lb30a; + private bool _lb31; public LineBreakEnumerator(ReadOnlySlice text) + : this() { _text = text; - _pos = 0; - _lastPos = 0; - _curClass = null; - _nextClass = null; - Current = default; + _position = 0; + _currentClass = LineBreakClass.Unknown; + _nextClass = LineBreakClass.Unknown; + _first = true; + _lb8a = false; + _lb21a = false; + _lb22ex = false; + _lb24ex = false; + _lb25ex = false; + _alphaNumericCount = 0; + _lb31 = false; + _lb30 = false; + _lb30a = 0; } - + public LineBreak Current { get; private set; } - + public bool MoveNext() { - // get the first char if we're at the beginning of the string - if (!_curClass.HasValue) + // Get the first char if we're at the beginning of the string. + if (_first) { - _curClass = PeekCharClass() == LineBreakClass.Space ? LineBreakClass.WordJoiner : MapFirst(ReadCharClass()); + var firstClass = NextCharClass(); + _first = false; + _currentClass = MapFirst(firstClass); + _nextClass = firstClass; + _lb8a = firstClass == LineBreakClass.ZWJ; + _lb30a = 0; } - while (_pos < _text.Length) + while (_position < _text.Length) { - _lastPos = _pos; + _lastPosition = _position; var lastClass = _nextClass; - _nextClass = ReadCharClass(); + _nextClass = NextCharClass(); - // explicit newline - if (_curClass.HasValue && (_curClass == LineBreakClass.MandatoryBreak || _curClass == LineBreakClass.CarriageReturn && _nextClass != LineBreakClass.LineFeed)) + // Explicit newline + switch (_currentClass) { - _curClass = MapFirst(MapClass(_nextClass.Value)); - Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true); + case LineBreakClass.MandatoryBreak: + case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed: + { + _currentClass = MapFirst(_nextClass); + Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); + return true; + } + } + + var shouldBreak = GetSimpleBreak() ?? (bool?)GetPairTableBreak(lastClass); + + // Rule LB8a + _lb8a = _nextClass == LineBreakClass.ZWJ; + + if (shouldBreak.Value) + { + Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); return true; } + } - // handle classes not handled by the pair table - LineBreakClass? cur = null; - switch (_nextClass.Value) + if (_position >= _text.Length) + { + if (_lastPosition < _text.Length) { - case LineBreakClass.Space: - cur = _curClass; - break; + _lastPosition = _text.Length; + + var required = false; + + switch (_currentClass) + { + case LineBreakClass.MandatoryBreak: + case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed: + required = true; + break; + } + + Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); + return true; + } + } + + Current = default; + + return false; + } + + private static LineBreakClass MapClass(Codepoint cp) + { + if (cp.Value == 327685) + { + return LineBreakClass.Alphabetic; + } + + // LB 1 + // ========================================== + // Resolved Original General_Category + // ========================================== + // AL AI, SG, XX Any + // CM SA Only Mn or Mc + // AL SA Any except Mn and Mc + // NS CJ Any + switch (cp.LineBreakClass) + { + case LineBreakClass.Ambiguous: + case LineBreakClass.Surrogate: + case LineBreakClass.Unknown: + return LineBreakClass.Alphabetic; + case LineBreakClass.ComplexContext: + return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark + ? LineBreakClass.CombiningMark + : LineBreakClass.Alphabetic; + + case LineBreakClass.ConditionalJapaneseStarter: + return LineBreakClass.Nonstarter; + + default: + return cp.LineBreakClass; + } + } + + private static LineBreakClass MapFirst(LineBreakClass c) + { + switch (c) + { + case LineBreakClass.LineFeed: + case LineBreakClass.NextLine: + return LineBreakClass.MandatoryBreak; + + case LineBreakClass.Space: + return LineBreakClass.WordJoiner; + + default: + return c; + } + } + + private static bool IsAlphaNumeric(LineBreakClass cls) + => cls == LineBreakClass.Alphabetic + || cls == LineBreakClass.HebrewLetter + || cls == LineBreakClass.Numeric; + + private LineBreakClass PeekNextCharClass() + { + var cp = Codepoint.ReadAt(_text, _position, out _); + + return MapClass(cp); + } + + // Get the next character class + private LineBreakClass NextCharClass() + { + var cp = Codepoint.ReadAt(_text, _position, out var count); + var cls = MapClass(cp); + _position += count; + + // Keep track of alphanumeric + any combining marks. + // This is used for LB22 and LB30. + if (IsAlphaNumeric(_currentClass) || _alphaNumericCount > 0 && cls == LineBreakClass.CombiningMark) + { + _alphaNumericCount++; + } + + // Track combining mark exceptions. LB22 + if (cls == LineBreakClass.CombiningMark) + { + switch (_currentClass) + { case LineBreakClass.MandatoryBreak: + case LineBreakClass.ContingentBreak: + case LineBreakClass.Exclamation: case LineBreakClass.LineFeed: case LineBreakClass.NextLine: - cur = LineBreakClass.MandatoryBreak; - break; - + case LineBreakClass.Space: + case LineBreakClass.ZWSpace: case LineBreakClass.CarriageReturn: - cur = LineBreakClass.CarriageReturn; + _lb22ex = true; break; + } + } + // Track combining mark exceptions. LB31 + if (_first && cls == LineBreakClass.CombiningMark) + { + _lb31 = true; + } + + if (cls == LineBreakClass.CombiningMark) + { + switch (_currentClass) + { + case LineBreakClass.MandatoryBreak: case LineBreakClass.ContingentBreak: - cur = LineBreakClass.BreakAfter; + case LineBreakClass.Exclamation: + case LineBreakClass.LineFeed: + case LineBreakClass.NextLine: + case LineBreakClass.Space: + case LineBreakClass.ZWSpace: + case LineBreakClass.CarriageReturn: + case LineBreakClass.ZWJ: + _lb31 = true; break; } + } + + if (_first + && (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) + { + _lb31 = true; + } + + if (_currentClass == LineBreakClass.Alphabetic && + (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) + { + _lb31 = true; + } + + // Reset LB31 if next is U+0028 (Left Opening Parenthesis) + if (_lb31 + && _currentClass != LineBreakClass.PostfixNumeric + && _currentClass != LineBreakClass.PrefixNumeric + && cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028) + { + _lb31 = false; + } + + // Rule LB24 + if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis)) + { + _lb24ex = true; + } - if (cur != null) + // Rule LB25 + if (_first + && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols)) + { + _lb25ex = true; + } + + if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic) + { + var next = PeekNextCharClass(); + if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols) { - _curClass = cur; + _lb25ex = true; + } + } + + // AlphaNumeric + and combining marks can break for OP except. + // - U+0028 (Left Opening Parenthesis) + // - U+005B (Opening Square Bracket) + // - U+007B (Left Curly Bracket) + // See custom colums|rules in the text pair table. + // https://www.unicode.org/Public/13.0.0/ucd/auxiliary/LineBreakTest.html + _lb30 = _alphaNumericCount > 0 + && cls == LineBreakClass.OpenPunctuation + && cp.Value != 0x0028 + && cp.Value != 0x005B + && cp.Value != 0x007B; + + return cls; + } + + private bool? GetSimpleBreak() + { + // handle classes not handled by the pair table + switch (_nextClass) + { + case LineBreakClass.Space: + return false; - if (_nextClass.Value == LineBreakClass.MandatoryBreak) + case LineBreakClass.MandatoryBreak: + case LineBreakClass.LineFeed: + case LineBreakClass.NextLine: + _currentClass = LineBreakClass.MandatoryBreak; + return false; + + case LineBreakClass.CarriageReturn: + _currentClass = LineBreakClass.CarriageReturn; + return false; + } + + return null; + } + + private bool GetPairTableBreak(LineBreakClass lastClass) + { + // If not handled already, use the pair table + bool shouldBreak = false; + switch (LineBreakPairTable.Table[(int)_currentClass][(int)_nextClass]) + { + case LineBreakPairTable.DIBRK: // Direct break + shouldBreak = true; + break; + + // TODO: Rewrite this so that it defaults to true and rules are set as exceptions. + case LineBreakPairTable.INBRK: // Possible indirect break + + // LB31 + if (_lb31 && _nextClass == LineBreakClass.OpenPunctuation) { - _lastPos = _pos; - Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true); - return true; + shouldBreak = true; + _lb31 = false; + break; } - continue; - } - - // if not handled already, use the pair table - var shouldBreak = false; - switch (BreakPairTable.Map(_curClass.Value,_nextClass.Value)) - { - case PairBreakType.DI: // Direct break + // LB30 + if (_lb30) + { shouldBreak = true; + _lb30 = false; + _alphaNumericCount = 0; break; + } - case PairBreakType.IN: // possible indirect break - shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space; + // LB25 + if (_lb25ex && (_nextClass == LineBreakClass.PrefixNumeric || _nextClass == LineBreakClass.Numeric)) + { + shouldBreak = true; + _lb25ex = false; break; + } - case PairBreakType.CI: - shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space; - if (!shouldBreak) - { - continue; - } + // LB24 + if (_lb24ex && (_nextClass == LineBreakClass.PostfixNumeric || _nextClass == LineBreakClass.PrefixNumeric)) + { + shouldBreak = true; + _lb24ex = false; break; + } + + // LB18 + shouldBreak = lastClass == LineBreakClass.Space; + break; + + case LineBreakPairTable.CIBRK: + shouldBreak = lastClass == LineBreakClass.Space; + if (!shouldBreak) + { + return false; + } - case PairBreakType.CP: // prohibited for combining marks - if (!lastClass.HasValue || lastClass.Value != LineBreakClass.Space) + break; + + case LineBreakPairTable.CPBRK: // prohibited for combining marks + if (lastClass != LineBreakClass.Space) + { + return false; + } + + break; + + case LineBreakPairTable.PRBRK: + break; + } + + // Rule LB22 + if (_nextClass == LineBreakClass.Inseparable) + { + switch (lastClass) + { + case LineBreakClass.MandatoryBreak: + case LineBreakClass.ContingentBreak: + case LineBreakClass.Exclamation: + case LineBreakClass.LineFeed: + case LineBreakClass.NextLine: + case LineBreakClass.Space: + case LineBreakClass.ZWSpace: + + // Allow break + break; + case LineBreakClass.CombiningMark: + if (_lb22ex) { - continue; + // Allow break + _lb22ex = false; + break; } + + shouldBreak = false; + break; + default: + shouldBreak = false; break; } + } - _curClass = _nextClass; + if (_lb8a) + { + shouldBreak = false; + } - if (shouldBreak) - { - Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); - return true; - } + // Rule LB21a + if (_lb21a && (_currentClass == LineBreakClass.Hyphen || _currentClass == LineBreakClass.BreakAfter)) + { + shouldBreak = false; + _lb21a = false; + } + else + { + _lb21a = _currentClass == LineBreakClass.HebrewLetter; } - if (_pos >= _text.Length) + // Rule LB30a + if (_currentClass == LineBreakClass.RegionalIndicator) { - if (_lastPos < _text.Length) + _lb30a++; + if (_lb30a == 2 && _nextClass == LineBreakClass.RegionalIndicator) { - _lastPos = _text.Length; - var cls = Codepoint.ReadAt(_text, _text.Length - 1, out _).LineBreakClass; - bool required = cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn; - Current = new LineBreak(FindPriorNonWhitespace(_text.Length), _text.Length, required); - return true; + shouldBreak = true; + _lb30a = 0; } } + else + { + _lb30a = 0; + } - return false; - } + _currentClass = _nextClass; + return shouldBreak; + } + private int FindPriorNonWhitespace(int from) { if (from > 0) @@ -163,7 +463,8 @@ namespace Avalonia.Media.TextFormatting.Unicode var cls = cp.LineBreakClass; - if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn) + if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || + cls == LineBreakClass.CarriageReturn) { from -= count; } @@ -184,61 +485,8 @@ namespace Avalonia.Media.TextFormatting.Unicode break; } } - return from; - } - // Get the next character class - private LineBreakClass ReadCharClass() - { - var cp = Codepoint.ReadAt(_text, _pos, out var count); - - _pos += count; - - return MapClass(cp.LineBreakClass); - } - - private LineBreakClass PeekCharClass() - { - return MapClass(Codepoint.ReadAt(_text, _pos, out _).LineBreakClass); - } - - private static LineBreakClass MapClass(LineBreakClass c) - { - switch (c) - { - case LineBreakClass.Ambiguous: - return LineBreakClass.Alphabetic; - - case LineBreakClass.ComplexContext: - case LineBreakClass.Surrogate: - case LineBreakClass.Unknown: - return LineBreakClass.Alphabetic; - - case LineBreakClass.ConditionalJapaneseStarter: - return LineBreakClass.Nonstarter; - - default: - return c; - } - } - - private static LineBreakClass MapFirst(LineBreakClass c) - { - switch (c) - { - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - return LineBreakClass.MandatoryBreak; - - case LineBreakClass.ContingentBreak: - return LineBreakClass.BreakAfter; - - case LineBreakClass.Space: - return LineBreakClass.WordJoiner; - - default: - return c; - } + return from; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs new file mode 100644 index 0000000000..fd37eed68d --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/ + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal static class LineBreakPairTable + { + /// + /// Direct break opportunity + /// + public const byte DIBRK = 0; + + /// + /// Indirect break opportunity + /// + public const byte INBRK = 1; + + /// + /// Indirect break opportunity for combining marks + /// + public const byte CIBRK = 2; + + /// + /// Prohibited break for combining marks + /// + public const byte CPBRK = 3; + + /// + /// Prohibited break + /// + public const byte PRBRK = 4; + + // Based on example pair table from https://www.unicode.org/reports/tr14/tr14-37.html#Table2 + // - ZWJ special processing for LB8a + // - CB manually added as per Rule LB20 + public static byte[][] Table { get; } = { + // . OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ CB + new[] { PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, CPBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK }, // OP + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // CL + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // CP + new[] { PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK }, // QU + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK }, // GL + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // NS + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // EX + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, DIBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // SY + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // IS + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK }, // PR + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // PO + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // NU + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // AL + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // HL + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // ID + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // IN + new[] { DIBRK, PRBRK, PRBRK, INBRK, DIBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // HY + new[] { DIBRK, PRBRK, PRBRK, INBRK, DIBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // BA + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK }, // BB + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, PRBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // B2 + new[] { DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK }, // ZW + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // CM + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK }, // WJ + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // H2 + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // H3 + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // JL + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // JV + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // JT + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, INBRK, DIBRK }, // RI + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK }, // EB + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // EM + new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // ZWJ + new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, DIBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK } // CB + }; + } +} diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 1c995475ae..f95d4320fe 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.X11 { - class X11CursorFactory : IStandardCursorFactory + class X11CursorFactory : ICursorFactory { private static readonly byte[] NullCursorData = new byte[] { 0 }; @@ -51,7 +56,7 @@ namespace Avalonia.X11 .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { IntPtr handle; if (cursorType == StandardCursorType.None) @@ -64,7 +69,12 @@ namespace Avalonia.X11 ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; } - return new PlatformHandle(handle, "XCURSOR"); + return new CursorImpl(handle); + } + + public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new XImageCursor(_display, cursor, hotSpot); } private static IntPtr GetNullCursor(IntPtr display) @@ -74,5 +84,62 @@ namespace Avalonia.X11 IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, NullCursorData, 1, 1); return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } + + private unsafe class XImageCursor : CursorImpl, IFramebufferPlatformSurface, IPlatformHandle + { + private readonly PixelSize _pixelSize; + private readonly IUnmanagedBlob _blob; + + public XImageCursor(IntPtr display, IBitmapImpl bitmap, PixelPoint hotSpot) + { + var size = Marshal.SizeOf() + + (bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4); + + _pixelSize = bitmap.PixelSize; + _blob = AvaloniaLocator.Current.GetService().AllocBlob(size); + + var image = (XcursorImage*)_blob.Address; + image->version = 1; + image->size = Marshal.SizeOf(); + image->width = bitmap.PixelSize.Width; + image->height = bitmap.PixelSize.Height; + image->xhot = hotSpot.X; + image->yhot = hotSpot.Y; + image->pixels = (IntPtr)(image + 1); + + using (var renderTarget = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[] { this })) + using (var ctx = renderTarget.CreateDrawingContext(null)) + { + var r = new Rect(_pixelSize.ToSize(1)); + ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); + } + + Handle = XLib.XcursorImageLoadCursor(display, _blob.Address); + } + + public string HandleDescriptor => "XCURSOR"; + + public override void Dispose() + { + XLib.XcursorImageDestroy(Handle); + _blob.Dispose(); + } + + public ILockedFramebuffer Lock() + { + return new LockedFramebuffer( + _blob.Address + Marshal.SizeOf(), + _pixelSize, _pixelSize.Width * 4, + new Vector(96, 96), PixelFormat.Bgra8888, null); + } + } + } + + class CursorImpl : ICursorImpl + { + public CursorImpl() { } + public CursorImpl(IntPtr handle) => Handle = handle; + public IntPtr Handle { get; protected set; } + public virtual void Dispose() { } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index b871aa6fcf..a57bdbdf87 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -71,7 +71,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) - .Bind().ToConstant(new X11CursorFactory(Display)) + .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index e62aeefc5d..b1006b43ee 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1693,7 +1693,7 @@ namespace Avalonia.X11 { [StructLayout (LayoutKind.Sequential)] internal struct XcursorImage { - private int version; + public int version; public int size; /* nominal size for matching */ public int width; /* actual width */ public int height; /* actual height */ diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs index f469ff7455..ac626f5825 100644 --- a/src/Avalonia.X11/X11Window.Ime.cs +++ b/src/Avalonia.X11/X11Window.Ime.cs @@ -96,14 +96,14 @@ namespace Avalonia.X11 void HandleKeyEvent(ref XEvent ev) { - var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); + var index = ev.KeyEvent.state.HasFlagCustom(XModifierMask.ShiftMask); // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); // Manually switch the Shift index for the keypad, // there should be a proper way to do this - if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) + if (ev.KeyEvent.state.HasFlagCustom(XModifierMask.Mod2Mask) && key > X11Key.Num_Lock && key <= X11Key.KP_9) key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index aa83b9f114..8f3f412578 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -639,23 +639,23 @@ namespace Avalonia.X11 RawInputModifiers TranslateModifiers(XModifierMask state) { var rv = default(RawInputModifiers); - if (state.HasFlag(XModifierMask.Button1Mask)) + if (state.HasFlagCustom(XModifierMask.Button1Mask)) rv |= RawInputModifiers.LeftMouseButton; - if (state.HasFlag(XModifierMask.Button2Mask)) + if (state.HasFlagCustom(XModifierMask.Button2Mask)) rv |= RawInputModifiers.RightMouseButton; - if (state.HasFlag(XModifierMask.Button3Mask)) + if (state.HasFlagCustom(XModifierMask.Button3Mask)) rv |= RawInputModifiers.MiddleMouseButton; - if (state.HasFlag(XModifierMask.Button4Mask)) + if (state.HasFlagCustom(XModifierMask.Button4Mask)) rv |= RawInputModifiers.XButton1MouseButton; - if (state.HasFlag(XModifierMask.Button5Mask)) + if (state.HasFlagCustom(XModifierMask.Button5Mask)) rv |= RawInputModifiers.XButton2MouseButton; - if (state.HasFlag(XModifierMask.ShiftMask)) + if (state.HasFlagCustom(XModifierMask.ShiftMask)) rv |= RawInputModifiers.Shift; - if (state.HasFlag(XModifierMask.ControlMask)) + if (state.HasFlagCustom(XModifierMask.ControlMask)) rv |= RawInputModifiers.Control; - if (state.HasFlag(XModifierMask.Mod1Mask)) + if (state.HasFlagCustom(XModifierMask.Mod1Mask)) rv |= RawInputModifiers.Alt; - if (state.HasFlag(XModifierMask.Mod4Mask)) + if (state.HasFlagCustom(XModifierMask.Mod4Mask)) rv |= RawInputModifiers.Meta; return rv; } @@ -872,15 +872,13 @@ namespace Avalonia.X11 UpdateSizeHints(null); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { if (cursor == null) XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor); - else + else if (cursor is CursorImpl impl) { - if (cursor.HandleDescriptor != "XCURSOR") - throw new ArgumentException("Expected XCURSOR handle type"); - XDefineCursor(_x11.Display, _handle, cursor.Handle); + XDefineCursor(_x11.Display, _handle, impl.Handle); } } diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 8cdf24cc7b..2874c517a9 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -342,13 +342,13 @@ namespace Avalonia.X11 Type = ev->evtype; Timestamp = (ulong)ev->time.ToInt64(); var state = (XModifierMask)ev->mods.Effective; - if (state.HasFlag(XModifierMask.ShiftMask)) + if (state.HasFlagCustom(XModifierMask.ShiftMask)) Modifiers |= RawInputModifiers.Shift; - if (state.HasFlag(XModifierMask.ControlMask)) + if (state.HasFlagCustom(XModifierMask.ControlMask)) Modifiers |= RawInputModifiers.Control; - if (state.HasFlag(XModifierMask.Mod1Mask)) + if (state.HasFlagCustom(XModifierMask.Mod1Mask)) Modifiers |= RawInputModifiers.Alt; - if (state.HasFlag(XModifierMask.Mod4Mask)) + if (state.HasFlagCustom(XModifierMask.Mod4Mask)) Modifiers |= RawInputModifiers.Meta; Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask); @@ -364,7 +364,7 @@ namespace Avalonia.X11 if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease) Button = ev->detail; Detail = ev->detail; - Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated); + Emulated = ev->flags.HasFlagCustom(XiDeviceEventFlags.XIPointerEmulated); } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a1ee8d188..e2b370821f 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -20,6 +20,7 @@ namespace Avalonia.X11 const string libX11Randr = "libXrandr.so.2"; const string libX11Ext = "libXext.so.6"; const string libXInput = "libXi.so.6"; + const string libXCursor = "libXcursor.so.1"; [DllImport(libX11)] public static extern IntPtr XOpenDisplay(IntPtr display); @@ -569,6 +570,12 @@ namespace Avalonia.X11 [DllImport(libXInput)] public static extern void XIFreeDeviceInfo(XIDeviceInfo* info); + [DllImport(libXCursor)] + public static extern IntPtr XcursorImageLoadCursor(IntPtr display, IntPtr image); + + [DllImport(libXCursor)] + public static extern IntPtr XcursorImageDestroy(IntPtr image); + public static void XISetMask(ref int mask, XiEventType ev) { mask |= (1 << (int)ev); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 0a101eec7a..4bbb58e53e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -57,7 +57,7 @@ namespace Avalonia.LinuxFramebuffer public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 8801f71f9a..3c8d3b5e06 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading; using Avalonia.Controls; @@ -38,7 +38,7 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToTransient() + .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() .Bind().ToConstant(new RenderLoop()) @@ -79,6 +79,11 @@ namespace Avalonia.LinuxFramebuffer tl.Prepare(); _topLevel = tl; _topLevel.Renderer.Start(); + + if (_topLevel is IFocusScope scope) + { + FocusManager.Instance?.SetFocusScope(scope); + } } _topLevel.Content = value; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs index b5ebc4bcb7..34cc261187 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs @@ -54,7 +54,7 @@ namespace Avalonia.LinuxFramebuffer.Output } public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay); - public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED); + public bool IsPreferred => Mode.type.HasFlagCustom(DrmModeType.DRM_MODE_TYPE_PREFERRED); public string Name { get; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index 7a257da0dd..642be28c69 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -4,11 +4,14 @@ using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer { - internal class CursorFactoryStub : IStandardCursorFactory + internal class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); + + private class CursorStub : ICursorImpl { - return new PlatformHandle(IntPtr.Zero, null); + public void Dispose() { } } } internal class PlatformSettings : IPlatformSettings diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 89e2c096ee..7b9cd0212e 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -2,6 +2,8 @@ netstandard2.0 Avalonia + Enable + CS8600;CS8602;CS8603 diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index bf43730481..50be598d7f 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -39,17 +39,17 @@ namespace Avalonia.Data /// /// Gets or sets the name of the element to use as the binding source. /// - public string ElementName { get; set; } + public string? ElementName { get; set; } /// /// Gets or sets the relative source for the binding. /// - public RelativeSource RelativeSource { get; set; } + public RelativeSource? RelativeSource { get; set; } /// /// Gets or sets the source for the binding. /// - public object Source { get; set; } + public object? Source { get; set; } /// /// Gets or sets the binding path. @@ -59,24 +59,36 @@ namespace Avalonia.Data /// /// Gets or sets a function used to resolve types from names in the binding path. /// - public Func TypeResolver { get; set; } + public Func? TypeResolver { get; set; } - protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object? anchor, bool enableDataValidation) { - Contract.Requires(target != null); - anchor = anchor ?? DefaultAnchor?.Target; - + _ = target ?? throw new ArgumentNullException(nameof(target)); + + anchor ??= DefaultAnchor?.Target; enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - INameScope nameScope = null; + INameScope? nameScope = null; NameScope?.TryGetTarget(out nameScope); var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope); + if (node is null) + { + throw new InvalidOperationException("Could not parse binding expression."); + } + + IStyledElement GetSource() + { + return target as IStyledElement ?? + anchor as IStyledElement ?? + throw new ArgumentException("Could not find binding source: either target or anchor must be an IStyledElement."); + } + if (ElementName != null) { return CreateElementObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), + GetSource(), ElementName, node); } @@ -96,9 +108,7 @@ namespace Avalonia.Data } else { - return CreateSourceObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - node); + return CreateSourceObserver(GetSource(), node); } } else if (RelativeSource.Mode == RelativeSourceMode.DataContext) @@ -111,15 +121,11 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - return CreateSourceObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - node); + return CreateSourceObserver(GetSource(), node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - return CreateTemplatedParentObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - node); + return CreateTemplatedParentObserver(GetSource(), node); } else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) { @@ -128,10 +134,7 @@ namespace Avalonia.Data throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } - return CreateFindAncestorObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - RelativeSource, - node); + return CreateFindAncestorObserver(GetSource(), RelativeSource, node); } else { diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 3dbc83a7df..c25ef49167 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -38,22 +38,22 @@ namespace Avalonia.Data /// /// Gets or sets the to use. /// - public IValueConverter Converter { get; set; } + public IValueConverter? Converter { get; set; } /// /// Gets or sets a parameter to pass to . /// - public object ConverterParameter { get; set; } + public object? ConverterParameter { get; set; } /// /// Gets or sets the value to use when the binding is unable to produce a value. /// - public object FallbackValue { get; set; } + public object? FallbackValue { get; set; } /// /// Gets or sets the value to use when the binding result is null. /// - public object TargetNullValue { get; set; } + public object? TargetNullValue { get; set; } /// /// Gets or sets the binding mode. @@ -68,26 +68,27 @@ namespace Avalonia.Data /// /// Gets or sets the string format. /// - public string StringFormat { get; set; } + public string? StringFormat { get; set; } - public WeakReference DefaultAnchor { get; set; } + public WeakReference? DefaultAnchor { get; set; } - public WeakReference NameScope { get; set; } + public WeakReference? NameScope { get; set; } protected abstract ExpressionObserver CreateExpressionObserver( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor, + object? anchor, bool enableDataValidation); /// public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null, + object? anchor = null, bool enableDataValidation = false) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); + anchor = anchor ?? DefaultAnchor?.Target; enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; @@ -133,18 +134,13 @@ namespace Avalonia.Data IAvaloniaObject target, ExpressionNode node, bool targetIsDataContext, - object anchor) + object? anchor) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); if (!(target is IDataContextProvider)) { - target = anchor as IDataContextProvider; - - if (target == null) - { - throw new InvalidOperationException("Cannot find a DataContext to bind to."); - } + target = anchor as IDataContextProvider ?? throw new InvalidOperationException("Cannot find a DataContext to bind to."); } if (!targetIsDataContext) @@ -171,10 +167,9 @@ namespace Avalonia.Data string elementName, ExpressionNode node) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); - NameScope.TryGetTarget(out var scope); - if (scope == null) + if (NameScope is null || !NameScope.TryGetTarget(out var scope) || scope is null) throw new InvalidOperationException("Name scope is null or was already collected"); var result = new ExpressionObserver( NameScopeLocator.Track(scope, elementName), @@ -188,9 +183,9 @@ namespace Avalonia.Data RelativeSource relativeSource, ExpressionNode node) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); - IObservable controlLocator; + IObservable controlLocator; switch (relativeSource.Tree) { @@ -220,7 +215,7 @@ namespace Avalonia.Data object source, ExpressionNode node) { - Contract.Requires(source != null); + _ = source ?? throw new ArgumentNullException(nameof(source)); return new ExpressionObserver(source, node); } @@ -229,7 +224,7 @@ namespace Avalonia.Data IAvaloniaObject target, ExpressionNode node) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); var result = new ExpressionObserver( () => target.GetValue(StyledElement.TemplatedParentProperty), @@ -240,7 +235,7 @@ namespace Avalonia.Data return result; } - protected IObservable GetParentDataContext(IAvaloniaObject target) + protected IObservable GetParentDataContext(IAvaloniaObject target) { // The DataContext is based on the visual parent and not the logical parent: this may // seem counter intuitive considering the fact that property inheritance works on the logical @@ -252,7 +247,7 @@ namespace Avalonia.Data .Select(x => { return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? - Observable.Return((object)null); + Observable.Return((object?)null); }).Switch(); } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index cbc5f414f2..17b033f238 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -22,12 +22,12 @@ namespace Avalonia.Data /// /// Gets or sets the to use. /// - public IMultiValueConverter Converter { get; set; } + public IMultiValueConverter? Converter { get; set; } /// /// Gets or sets a parameter to pass to . /// - public object ConverterParameter { get; set; } + public object? ConverterParameter { get; set; } /// /// Gets or sets the value to use when the binding is unable to produce a value. @@ -52,12 +52,12 @@ namespace Avalonia.Data /// /// Gets or sets the relative source for the binding. /// - public RelativeSource RelativeSource { get; set; } + public RelativeSource? RelativeSource { get; set; } /// /// Gets or sets the string format. /// - public string StringFormat { get; set; } + public string? StringFormat { get; set; } public MultiBinding() { @@ -69,7 +69,7 @@ namespace Avalonia.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null, + object? anchor = null, bool enableDataValidation = false) { var targetType = targetProperty?.PropertyType ?? typeof(object); @@ -105,7 +105,7 @@ namespace Avalonia.Data } } - private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) + private object ConvertValue(IList values, Type targetType, IMultiValueConverter? converter) { for (var i = 0; i < values.Count; ++i) { @@ -116,7 +116,7 @@ namespace Avalonia.Data } var culture = CultureInfo.CurrentCulture; - values = new System.Collections.ObjectModel.ReadOnlyCollection(values); + values = new System.Collections.ObjectModel.ReadOnlyCollection(values); object converted; if (converter != null) { diff --git a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs index e3d2dd4aaa..037175a137 100644 --- a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs @@ -94,7 +94,7 @@ namespace Avalonia.Data /// /// Gets the type of ancestor to look for when in mode. /// - public Type AncestorType { get; set; } + public Type? AncestorType { get; set; } /// /// Gets or sets a value that describes the type of relative source lookup. diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index 83f02c52aa..b6c723d68c 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -17,8 +17,8 @@ namespace Avalonia.Data ISetterValue { private bool _isSetterValue; - private IStyledElement _target; - private Type _targetType; + private IStyledElement _target = default!; + private Type? _targetType; public TemplateBinding() { @@ -33,7 +33,7 @@ namespace Avalonia.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null, + object? anchor = null, bool enableDataValidation = false) { // Usually each `TemplateBinding` will only be instantiated once; in this case we can @@ -68,12 +68,12 @@ namespace Avalonia.Data /// /// Gets or sets the to use. /// - public IValueConverter Converter { get; set; } + public IValueConverter? Converter { get; set; } /// /// Gets or sets a parameter to pass to . /// - public object ConverterParameter { get; set; } + public object? ConverterParameter { get; set; } /// /// Gets or sets the binding mode. @@ -83,7 +83,7 @@ namespace Avalonia.Data /// /// Gets or sets the name of the source property on the templated parent. /// - public AvaloniaProperty Property { get; set; } + public AvaloniaProperty? Property { get; set; } /// public string Description => "TemplateBinding: " + Property; @@ -164,10 +164,7 @@ namespace Avalonia.Data { if (e.Property == StyledElement.TemplatedParentProperty) { - var oldValue = (IAvaloniaObject)e.OldValue; - var newValue = (IAvaloniaObject)e.OldValue; - - if (oldValue != null) + if (e.OldValue is IAvaloniaObject oldValue) { oldValue.PropertyChanged -= TemplatedParentPropertyChanged; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 7c362e24cc..439bc15243 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -6,6 +6,8 @@ using Avalonia.Utilities; using System; using System.Collections.Generic; +#nullable enable + namespace Avalonia.Markup.Parsers { internal enum SourceMode @@ -271,8 +273,8 @@ namespace Avalonia.Markup.Parsers } else if (mode.SequenceEqual("parent".AsSpan())) { - string ancestorNamespace = null; - string ancestorType = null; + string? ancestorNamespace = null; + string? ancestorType = null; var ancestorLevel = 0; if (PeekOpenBracket(ref r)) { @@ -424,19 +426,19 @@ namespace Avalonia.Markup.Parsers public class PropertyNameNode : INode { - public string PropertyName { get; set; } + public string PropertyName { get; set; } = string.Empty; } public class AttachedPropertyNameNode : INode { - public string Namespace { get; set; } - public string TypeName { get; set; } - public string PropertyName { get; set; } + public string Namespace { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; + public string PropertyName { get; set; } = string.Empty; } public class IndexerNode : INode { - public IList Arguments { get; set; } + public IList Arguments { get; set; } = Array.Empty(); } public class NotNode : INode, ITransformNode { } @@ -447,20 +449,20 @@ namespace Avalonia.Markup.Parsers public class NameNode : INode { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; } public class AncestorNode : INode { - public string Namespace { get; set; } - public string TypeName { get; set; } + public string? Namespace { get; set; } + public string? TypeName { get; set; } public int Level { get; set; } } public class TypeCastNode : INode { - public string Namespace { get; set; } - public string TypeName { get; set; } + public string Namespace { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index f957bcab1e..ebdad881c3 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -8,8 +8,8 @@ namespace Avalonia.Markup.Parsers { public static class ExpressionObserverBuilder { - internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null, - INameScope nameScope = null) + internal static (ExpressionNode? Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func? typeResolver = null, + INameScope? nameScope = null) { if (string.IsNullOrWhiteSpace(expression)) { @@ -32,8 +32,8 @@ namespace Avalonia.Markup.Parsers object root, string expression, bool enableDataValidation = false, - string description = null, - Func typeResolver = null) + string? description = null, + Func? typeResolver = null) { return new ExpressionObserver( root, @@ -45,10 +45,11 @@ namespace Avalonia.Markup.Parsers IObservable rootObservable, string expression, bool enableDataValidation = false, - string description = null, - Func typeResolver = null) + string? description = null, + Func? typeResolver = null) { - Contract.Requires(rootObservable != null); + _ = rootObservable ?? throw new ArgumentNullException(nameof(rootObservable)); + return new ExpressionObserver( rootObservable, Parse(expression, enableDataValidation, typeResolver).Node, @@ -61,10 +62,10 @@ namespace Avalonia.Markup.Parsers string expression, IObservable update, bool enableDataValidation = false, - string description = null, - Func typeResolver = null) + string? description = null, + Func? typeResolver = null) { - Contract.Requires(rootGetter != null); + _ = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter)); return new ExpressionObserver( rootGetter, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 558130e23f..0a6f3f82ba 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -11,25 +11,25 @@ namespace Avalonia.Markup.Parsers internal class ExpressionParser { private readonly bool _enableValidation; - private readonly Func _typeResolver; - private readonly INameScope _nameScope; + private readonly Func? _typeResolver; + private readonly INameScope? _nameScope; - public ExpressionParser(bool enableValidation, Func typeResolver, INameScope nameScope) + public ExpressionParser(bool enableValidation, Func? typeResolver, INameScope? nameScope) { _typeResolver = typeResolver; _nameScope = nameScope; _enableValidation = enableValidation; } - public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) + public (ExpressionNode? Node, SourceMode Mode) Parse(ref CharacterReader r) { - ExpressionNode rootNode = null; - ExpressionNode node = null; + ExpressionNode? rootNode = null; + ExpressionNode? node = null; var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); foreach (var astNode in astNodes) { - ExpressionNode nextNode = null; + ExpressionNode? nextNode = null; switch (astNode) { case BindingExpressionGrammar.EmptyExpressionNode _: @@ -57,13 +57,13 @@ namespace Avalonia.Markup.Parsers nextNode = ParseFindAncestor(ancestor); break; case BindingExpressionGrammar.NameNode elementName: - nextNode = new ElementNameNode(_nameScope, elementName.Name); + nextNode = new ElementNameNode(_nameScope ?? throw new NotSupportedException("Invalid element name binding with null name scope!"), elementName.Name); break; case BindingExpressionGrammar.TypeCastNode typeCast: nextNode = ParseTypeCastNode(typeCast); break; } - if (rootNode is null) + if (node is null) { rootNode = node = nextNode; } @@ -79,7 +79,7 @@ namespace Avalonia.Markup.Parsers private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - Type ancestorType = null; + Type? ancestorType = null; var ancestorLevel = node.Level; if (!(node.Namespace is null) && !(node.TypeName is null)) @@ -97,7 +97,7 @@ namespace Avalonia.Markup.Parsers private TypeCastNode ParseTypeCastNode(BindingExpressionGrammar.TypeCastNode node) { - Type castType = null; + Type? castType = null; if (!(node.Namespace is null) && !(node.TypeName is null)) { if (_typeResolver == null) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 97198145a8..d6068a15d4 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -9,7 +9,7 @@ namespace Avalonia.Markup.Parsers.Nodes { private readonly WeakReference _nameScope; private readonly string _name; - private IDisposable _subscription; + private IDisposable? _subscription; public ElementNameNode(INameScope nameScope, string name) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index f304d1e9a2..4124032113 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -7,10 +7,10 @@ namespace Avalonia.Markup.Parsers.Nodes public class FindAncestorNode : ExpressionNode { private readonly int _level; - private readonly Type _ancestorType; - private IDisposable _subscription; + private readonly Type? _ancestorType; + private IDisposable? _subscription; - public FindAncestorNode(Type ancestorType, int level) + public FindAncestorNode(Type? ancestorType, int level) { _level = level; _ancestorType = ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index f3abd6a5c5..2f1756fc55 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -29,15 +29,15 @@ namespace Avalonia.Markup.Parsers.Nodes var list = target as IList; var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); - var indexerParameters = indexerProperty?.GetIndexParameters(); + ParameterInfo[] indexerParameters; - if (indexerProperty != null && indexerParameters.Length == Arguments.Count) + if (indexerProperty != null && (indexerParameters = indexerProperty.GetIndexParameters()).Length == Arguments.Count) { var convertedObjectArray = new object[indexerParameters.Length]; for (int i = 0; i < Arguments.Count; i++) { - object temp = null; + object? temp = null; if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp)) { @@ -125,7 +125,7 @@ namespace Avalonia.Markup.Parsers.Nodes public IList Arguments { get; } - public override Type PropertyType + public override Type? PropertyType { get { @@ -144,15 +144,15 @@ namespace Avalonia.Markup.Parsers.Nodes var list = target as IList; var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); - var indexerParameters = indexerProperty?.GetIndexParameters(); + ParameterInfo[] indexerParameters; - if (indexerProperty != null && indexerParameters.Length == Arguments.Count) + if (indexerProperty != null && (indexerParameters = indexerProperty.GetIndexParameters()).Length == Arguments.Count) { var convertedObjectArray = new object[indexerParameters.Length]; for (int i = 0; i < Arguments.Count; i++) { - object temp = null; + object? temp = null; if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp)) { @@ -246,7 +246,7 @@ namespace Avalonia.Markup.Parsers.Nodes return true; } - private static PropertyInfo GetIndexer(TypeInfo typeInfo) + private static PropertyInfo? GetIndexer(TypeInfo? typeInfo) { PropertyInfo indexer; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs index c5953b514c..250eca1852 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using Avalonia.Data.Core; using Avalonia.Utilities; +#nullable enable + namespace Avalonia.Markup.Parsers { #if !BUILDTASK @@ -30,7 +32,7 @@ namespace Avalonia.Markup.Parsers var parsed = new List(); while (state != State.End) { - ISyntax syntax = null; + ISyntax? syntax = null; if (state == State.Start) (state, syntax) = ParseStart(ref r); else if (state == State.Next) @@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers return parsed; } - private static (State, ISyntax) ParseNext(ref CharacterReader r) + private static (State, ISyntax?) ParseNext(ref CharacterReader r) { r.SkipWhitespace(); if (r.End) @@ -106,7 +108,7 @@ namespace Avalonia.Markup.Parsers }); } - static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r) + static (string? ns, string name) ParseXamlIdentifier(ref CharacterReader r) { var ident = r.ParseIdentifier(); if (ident.IsEmpty) @@ -147,7 +149,7 @@ namespace Avalonia.Markup.Parsers return true; } - private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) + private static (State, ISyntax?) ParseAfterProperty(ref CharacterReader r) { if (TryParseCasts(ref r, out var rv)) return rv; @@ -184,20 +186,20 @@ namespace Avalonia.Markup.Parsers public class PropertySyntax : ISyntax { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is PropertySyntax other && other.Name == Name; } public class TypeQualifiedPropertySyntax : ISyntax { - public string Name { get; set; } - public string TypeName { get; set; } - public string TypeNamespace { get; set; } + public string Name { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; + public string? TypeNamespace { get; set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is TypeQualifiedPropertySyntax other && other.Name == Name && other.TypeName == TypeName @@ -207,14 +209,14 @@ namespace Avalonia.Markup.Parsers public class ChildTraversalSyntax : ISyntax { public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); - public override bool Equals(object obj) => obj is ChildTraversalSyntax; + public override bool Equals(object? obj) => obj is ChildTraversalSyntax; } public class EnsureTypeSyntax : ISyntax { - public string TypeName { get; set; } - public string TypeNamespace { get; set; } - public override bool Equals(object obj) + public string TypeName { get; set; } = string.Empty; + public string? TypeNamespace { get; set; } + public override bool Equals(object? obj) => obj is EnsureTypeSyntax other && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; @@ -222,9 +224,9 @@ namespace Avalonia.Markup.Parsers public class CastTypeSyntax : ISyntax { - public string TypeName { get; set; } - public string TypeNamespace { get; set; } - public override bool Equals(object obj) + public string TypeName { get; set; } = string.Empty; + public string? TypeNamespace { get; set; } + public override bool Equals(object? obj) => obj is CastTypeSyntax other && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index b25e9490cd..9d03341f92 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -4,6 +4,8 @@ using System.Linq; using Avalonia.Data.Core; using Avalonia.Utilities; +#nullable enable + // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the // only reason they have overridden Equals methods is for unit testing. #pragma warning disable 659 @@ -39,7 +41,7 @@ namespace Avalonia.Markup.Parsers var selector = new List(); while (!r.End && state != State.End) { - ISyntax syntax = null; + ISyntax? syntax = null; switch (state) { case State.Start: @@ -110,7 +112,7 @@ namespace Avalonia.Markup.Parsers return State.TypeName; } - private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end) + private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) { if (r.TakeIf(':')) { @@ -190,7 +192,7 @@ namespace Avalonia.Markup.Parsers } } - private static (State, ISyntax) ParseTraversal(ref CharacterReader r) + private static (State, ISyntax?) ParseTraversal(ref CharacterReader r) { r.SkipWhitespace(); if (r.TakeIf('>')) @@ -325,11 +327,11 @@ namespace Avalonia.Markup.Parsers public class OfTypeSyntax : ISyntax, ITypeSyntax { - public string TypeName { get; set; } + public string TypeName { get; set; } = string.Empty; public string Xmlns { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as OfTypeSyntax; return other != null && other.TypeName == TypeName && other.Xmlns == Xmlns; @@ -338,11 +340,11 @@ namespace Avalonia.Markup.Parsers public class IsSyntax : ISyntax, ITypeSyntax { - public string TypeName { get; set; } + public string TypeName { get; set; } = string.Empty; public string Xmlns { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as IsSyntax; return other != null && other.TypeName == TypeName && other.Xmlns == Xmlns; @@ -351,9 +353,9 @@ namespace Avalonia.Markup.Parsers public class ClassSyntax : ISyntax { - public string Class { get; set; } + public string Class { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ClassSyntax && ((ClassSyntax)obj).Class == Class; } @@ -361,9 +363,9 @@ namespace Avalonia.Markup.Parsers public class NameSyntax : ISyntax { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is NameSyntax && ((NameSyntax)obj).Name == Name; } @@ -371,11 +373,11 @@ namespace Avalonia.Markup.Parsers public class PropertySyntax : ISyntax { - public string Property { get; set; } + public string Property { get; set; } = string.Empty; - public string Value { get; set; } + public string Value { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is PropertySyntax && ((PropertySyntax)obj).Property == Property && @@ -385,7 +387,7 @@ namespace Avalonia.Markup.Parsers public class ChildSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ChildSyntax; } @@ -393,7 +395,7 @@ namespace Avalonia.Markup.Parsers public class DescendantSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is DescendantSyntax; } @@ -401,7 +403,7 @@ namespace Avalonia.Markup.Parsers public class TemplateSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is TemplateSyntax; } @@ -409,9 +411,9 @@ namespace Avalonia.Markup.Parsers public class NotSyntax : ISyntax { - public IEnumerable Argument { get; set; } + public IEnumerable Argument { get; set; } = Enumerable.Empty(); - public override bool Equals(object obj) + public override bool Equals(object? obj) { return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument); } @@ -419,7 +421,7 @@ namespace Avalonia.Markup.Parsers public class CommaSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is CommaSyntax or; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 04519bf2bb..92ba744ee1 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -31,13 +31,13 @@ namespace Avalonia.Markup.Parsers /// /// The string. /// The parsed selector. - public Selector Parse(string s) + public Selector? Parse(string s) { var syntax = SelectorGrammar.Parse(s); return Create(syntax); } - private Selector Create(IEnumerable syntax) + private Selector? Create(IEnumerable syntax) { var result = default(Selector); var results = default(List); @@ -110,7 +110,7 @@ namespace Avalonia.Markup.Parsers results = new List(); } - results.Add(result); + results.Add(result ?? throw new NotSupportedException("Invalid selector!")); result = null; break; default: diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 71deb1235f..7c4ff4edc0 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -32,7 +32,7 @@ namespace Avalonia.Skia weight -= weight % 100; // make sure we start at a full weight - for (var i = (int)key.Style; i < 2; i++) + for (var i = 0; i < 2; i++) { // only try 2 font weights in each direction for (var j = 0; j < 200; j += 100) @@ -57,8 +57,8 @@ namespace Avalonia.Skia } } - //Nothing was found so we use the first typeface we can get. - return typefaces.Values.FirstOrDefault(); + //Nothing was found so we try to get a regular typeface. + return typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface) ? typeface : null; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 136ff63f3d..47a19aad8c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Direct2D1.Media private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; + private readonly bool _ownsDeviceContext; private readonly SharpDX.DXGI.SwapChain1 _swapChain; private readonly Action _finishedCallback; @@ -51,10 +52,12 @@ namespace Avalonia.Direct2D1.Media if (_renderTarget is DeviceContext deviceContext) { _deviceContext = deviceContext; + _ownsDeviceContext = false; } else { _deviceContext = _renderTarget.QueryInterface(); + _ownsDeviceContext = true; } _deviceContext.BeginDraw(); @@ -96,6 +99,13 @@ namespace Avalonia.Direct2D1.Media { throw new RenderTargetCorruptedException(ex); } + finally + { + if (_ownsDeviceContext) + { + _deviceContext.Dispose(); + } + } } /// @@ -151,7 +161,7 @@ namespace Avalonia.Direct2D1.Media using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) - using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_deviceContext.Factory, destRect.ToDirect2D())) + using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D())) { if (d2dOpacityMask.PlatformBrush != null) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 636309ad1a..d04e2b3110 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -33,13 +33,13 @@ namespace Avalonia.Direct2D1.Media /// public IGeometryImpl Intersect(IGeometryImpl geometry) { - var result = new PathGeometry(Geometry.Factory); - + var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory); using (var sink = result.Open()) { Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink); - return new StreamGeometryImpl(result); + sink.Close(); } + return new StreamGeometryImpl(result); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 49193afd78..90592ea806 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Direct2D1.Media /// public class WicBitmapImpl : BitmapImpl { - private BitmapDecoder _decoder; + private readonly BitmapDecoder _decoder; private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) { @@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.Media /// The filename of the bitmap to load. public WicBitmapImpl(string fileName) { - using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) + using (var decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); Dpi = new Vector(96, 96); @@ -177,7 +177,7 @@ namespace Avalonia.Direct2D1.Media /// The Direct2D bitmap. public override OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) { - FormatConverter converter = new FormatConverter(Direct2D1Platform.ImagingFactory); + using var converter = new FormatConverter(Direct2D1Platform.ImagingFactory); converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA); return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index 9104be64b2..2bc2b2db71 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -29,9 +29,12 @@ namespace Avalonia.Direct2D1.Media public IStreamGeometryImpl Clone() { var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory); - var sink = result.Open(); - ((PathGeometry)Geometry).Stream(sink); - sink.Close(); + using (var sink = result.Open()) + { + ((PathGeometry)Geometry).Stream(sink); + sink.Close(); + } + return new StreamGeometryImpl(result); } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 31e9c260e0..669e139d8f 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -111,7 +111,7 @@ namespace Avalonia.Direct2D1 /// The Direct2D brush. public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, SharpDX.Direct2D1.RenderTarget renderTarget) { - return pen.ToDirect2DStrokeStyle(renderTarget.Factory); + return pen.ToDirect2DStrokeStyle(Direct2D1Platform.Direct2D1Factory); } /// diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 3467a33d16..7d86116f38 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -147,13 +147,13 @@ namespace Avalonia.Win32.Interop.Wpf { var state = Keyboard.Modifiers; var rv = default(RawInputModifiers); - if (state.HasFlag(ModifierKeys.Windows)) + if (state.HasFlagCustom(ModifierKeys.Windows)) rv |= RawInputModifiers.Meta; - if (state.HasFlag(ModifierKeys.Alt)) + if (state.HasFlagCustom(ModifierKeys.Alt)) rv |= RawInputModifiers.Alt; - if (state.HasFlag(ModifierKeys.Control)) + if (state.HasFlagCustom(ModifierKeys.Control)) rv |= RawInputModifiers.Control; - if (state.HasFlag(ModifierKeys.Shift)) + if (state.HasFlagCustom(ModifierKeys.Shift)) rv |= RawInputModifiers.Shift; if (e != null) { @@ -225,12 +225,12 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); - void ITopLevelImpl.SetCursor(IPlatformHandle cursor) + void ITopLevelImpl.SetCursor(ICursorImpl cursor) { if (cursor == null) Cursor = Cursors.Arrow; - else if (cursor.HandleDescriptor == "HCURSOR") - Cursor = CursorShim.FromHCursor(cursor.Handle); + else if (cursor is IPlatformHandle handle) + Cursor = CursorShim.FromHCursor(handle.Handle); } Action ITopLevelImpl.Input { get; set; } //TODO diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 8c40eaa7b8..0a0e45b03a 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Win32.Interop; +using SdBitmap = System.Drawing.Bitmap; +using SdPixelFormat = System.Drawing.Imaging.PixelFormat; namespace Avalonia.Win32 { - internal class CursorFactory : IStandardCursorFactory + internal class CursorFactory : ICursorFactory { public static CursorFactory Instance { get; } = new CursorFactory(); @@ -29,8 +34,7 @@ namespace Avalonia.Win32 IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id)); if (cursor != IntPtr.Zero) { - PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType); - Cache.Add(cursorType, phCursor); + Cache.Add(cursorType, new CursorImpl(cursor, false)); } } } @@ -70,22 +74,119 @@ namespace Avalonia.Win32 {StandardCursorType.DragLink, 32516}, }; - private static readonly Dictionary Cache = - new Dictionary(); + private static readonly Dictionary Cache = + new Dictionary(); - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { - IPlatformHandle rv; - if (!Cache.TryGetValue(cursorType, out rv)) + if (!Cache.TryGetValue(cursorType, out var rv)) { - Cache[cursorType] = - rv = - new PlatformHandle( - UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), - PlatformConstants.CursorHandleType); + rv = new CursorImpl( + UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), + false); + Cache.Add(cursorType, rv); } return rv; } + + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var source = LoadSystemDrawingBitmap(cursor); + using var mask = AlphaToMask(source); + + var info = new UnmanagedMethods.ICONINFO + { + IsIcon = false, + xHotspot = hotSpot.X, + yHotspot = hotSpot.Y, + MaskBitmap = mask.GetHbitmap(), + ColorBitmap = source.GetHbitmap(), + }; + + return new CursorImpl(UnmanagedMethods.CreateIconIndirect(ref info), true); + } + + private SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap) + { + using var memoryStream = new MemoryStream(); + bitmap.Save(memoryStream); + return new SdBitmap(memoryStream); + } + + private unsafe SdBitmap AlphaToMask(SdBitmap source) + { + var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed); + + if (source.PixelFormat == SdPixelFormat.Format32bppPArgb) + { + throw new NotSupportedException( + "Images with premultiplied alpha not yet supported as cursor images."); + } + + if (source.PixelFormat != SdPixelFormat.Format32bppArgb) + { + return dest; + } + + var sourceData = source.LockBits( + new Rectangle(default, source.Size), + ImageLockMode.ReadOnly, + SdPixelFormat.Format32bppArgb); + var destData = dest.LockBits( + new Rectangle(default, source.Size), + ImageLockMode.ReadOnly, + SdPixelFormat.Format1bppIndexed); + + try + { + var pSource = (byte*)sourceData.Scan0.ToPointer(); + var pDest = (byte*)destData.Scan0.ToPointer(); + + for (var y = 0; y < dest.Height; ++y) + { + for (var x = 0; x < dest.Width; ++x) + { + if (pSource[x * 4] == 0) + { + pDest[x / 8] |= (byte)(1 << (x % 8)); + } + } + + pSource += sourceData.Stride; + pDest += destData.Stride; + } + + return dest; + } + finally + { + source.UnlockBits(sourceData); + dest.UnlockBits(destData); + } + } + } + + internal class CursorImpl : ICursorImpl, IPlatformHandle + { + private readonly bool _isCustom; + + public CursorImpl(IntPtr handle, bool isCustom) + { + Handle = handle; + _isCustom = isCustom; + } + + public IntPtr Handle { get; private set; } + public string HandleDescriptor => PlatformConstants.CursorHandleType; + + public void Dispose() + { + if (_isCustom && Handle != IntPtr.Zero) + { + UnmanagedMethods.DestroyIcon(Handle); + Handle = IntPtr.Zero; + } + } } } diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 6c1b4ef5d9..5f02796d30 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -181,7 +181,7 @@ namespace Avalonia.Win32 ole.GetData(ref format, out medium); return; } - if(!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + if(!format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL)) Marshal.ThrowExceptionForHR(DV_E_TYMED); if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) @@ -205,7 +205,7 @@ namespace Avalonia.Win32 return; } - if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL)) Marshal.ThrowExceptionForHR(DV_E_TYMED); if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) @@ -228,7 +228,7 @@ namespace Avalonia.Win32 return ole.QueryGetData(ref format); if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) return DV_E_DVASPECT; - if (!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + if (!format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL)) return DV_E_TYMED; string dataFormat = ClipboardFormats.GetFormat(format.cfFormat); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 1559c7e794..c137926e4c 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1035,6 +1035,12 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName); + [DllImport("user32.dll")] + public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo); + + [DllImport("user32.dll")] + public static extern bool DestroyIcon(IntPtr hIcon); + [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); @@ -1762,6 +1768,16 @@ namespace Avalonia.Win32.Interop public int CyContact; } + [StructLayout(LayoutKind.Sequential)] + public struct ICONINFO + { + public bool IsIcon; + public int xHotspot; + public int yHotspot; + public IntPtr MaskBitmap; + public IntPtr ColorBitmap; + }; + [Flags] public enum TouchInputFlags { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 37d047689c..d038e341b8 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -24,11 +24,11 @@ namespace Avalonia.Win32 public static DropEffect ConvertDropEffect(DragDropEffects operation) { DropEffect result = DropEffect.None; - if (operation.HasFlag(DragDropEffects.Copy)) + if (operation.HasFlagCustom(DragDropEffects.Copy)) result |= DropEffect.Copy; - if (operation.HasFlag(DragDropEffects.Move)) + if (operation.HasFlagCustom(DragDropEffects.Move)) result |= DropEffect.Move; - if (operation.HasFlag(DragDropEffects.Link)) + if (operation.HasFlagCustom(DragDropEffects.Link)) result |= DropEffect.Link; return result; } @@ -36,11 +36,11 @@ namespace Avalonia.Win32 public static DragDropEffects ConvertDropEffect(DropEffect effect) { DragDropEffects result = DragDropEffects.None; - if (effect.HasFlag(DropEffect.Copy)) + if (effect.HasFlagCustom(DropEffect.Copy)) result |= DragDropEffects.Copy; - if (effect.HasFlag(DropEffect.Move)) + if (effect.HasFlagCustom(DropEffect.Move)) result |= DragDropEffects.Move; - if (effect.HasFlag(DropEffect.Link)) + if (effect.HasFlagCustom(DropEffect.Link)) result |= DragDropEffects.Link; return result; } @@ -50,17 +50,17 @@ namespace Avalonia.Win32 var modifiers = RawInputModifiers.None; var state = (UnmanagedMethods.ModifierKeys)grfKeyState; - if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) + if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) modifiers |= RawInputModifiers.LeftMouseButton; - if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) + if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) modifiers |= RawInputModifiers.MiddleMouseButton; - if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) + if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) modifiers |= RawInputModifiers.RightMouseButton; - if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_SHIFT)) + if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_SHIFT)) modifiers |= RawInputModifiers.Shift; - if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_CONTROL)) + if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_CONTROL)) modifiers |= RawInputModifiers.Control; - if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_ALT)) + if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_ALT)) modifiers |= RawInputModifiers.Alt; return modifiers; } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 45aad77efe..d92bd08d01 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -104,7 +104,7 @@ namespace Avalonia.Win32 Options = options; AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToConstant(CursorFactory.Instance) + .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index 68bd40da79..0a8648aa9a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -23,13 +23,13 @@ namespace Avalonia.Win32 AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); var borderThickness = new RECT(); - if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + if (GetStyle().HasFlagCustom(WindowStyles.WS_THICKFRAME)) { AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle()), false, 0); borderThickness.left *= -1; borderThickness.top *= -1; } - else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + else if (GetStyle().HasFlagCustom(WindowStyles.WS_BORDER)) { borderThickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 3c09d2b7a0..2f980cfe86 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -608,14 +608,19 @@ namespace Avalonia.Win32 SetWindowText(_hwnd, title); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { - var hCursor = cursor?.Handle ?? DefaultCursor; - SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + var impl = cursor as CursorImpl; - if (_owner.IsPointerOver) + if (cursor is null || impl is object) { - UnmanagedMethods.SetCursor(hCursor); + var hCursor = impl?.Handle ?? DefaultCursor; + SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + + if (_owner.IsPointerOver) + { + UnmanagedMethods.SetCursor(hCursor); + } } } @@ -831,7 +836,7 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; - bool wantsTitleBar = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1; + bool wantsTitleBar = _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1; if (!wantsTitleBar) { @@ -848,7 +853,7 @@ namespace Avalonia.Win32 borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); } - margins.cyTopHeight = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; + margins.cyTopHeight = _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; if (WindowState == WindowState.Maximized) { @@ -896,8 +901,8 @@ namespace Avalonia.Win32 _extendedMargins = new Thickness(); } - if(!_isClientAreaExtended || (_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && - !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome))) + if(!_isClientAreaExtended || (_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) && + !_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome))) { EnableCloseButton(_hwnd); } @@ -1234,7 +1239,7 @@ namespace Avalonia.Win32 public Action ExtendClientAreaToDecorationsChanged { get; set; } /// - public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome); + public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome); /// public Thickness ExtendedMargins => _extendedMargins; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 7d367c99d1..36a70ea410 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -74,7 +74,7 @@ namespace Avalonia.iOS public PixelPoint PointToScreen(Point point) => new PixelPoint((int) point.X, (int) point.Y); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl _) { // no-op } @@ -136,4 +136,4 @@ namespace Avalonia.iOS set => _topLevel.Content = value; } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 28bccb6637..2cac5e6bcf 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -27,7 +27,7 @@ namespace Avalonia.iOS var softKeyboard = new SoftKeyboardHelper(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(GlFeature) - .Bind().ToConstant(new CursorFactoryStub()) + .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) .Bind().ToConstant(new PlatformSettings()) diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs index a35b301a7f..c2526d7d9f 100644 --- a/src/iOS/Avalonia.iOS/Stubs.cs +++ b/src/iOS/Avalonia.iOS/Stubs.cs @@ -5,9 +5,15 @@ using Avalonia.Platform; namespace Avalonia.iOS { - class CursorFactoryStub : IStandardCursorFactory + class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "NULL"); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorImplStub(); + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new CursorImplStub(); + + private class CursorImplStub : ICursorImpl + { + public void Dispose() { } + } } class WindowingPlatformStub : IWindowingPlatform @@ -57,4 +63,4 @@ namespace Avalonia.iOS _ms.CopyTo(outputStream); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Benchmarks/NullCursorFactory.cs b/tests/Avalonia.Benchmarks/NullCursorFactory.cs index 012adce0f2..9aeb353151 100644 --- a/tests/Avalonia.Benchmarks/NullCursorFactory.cs +++ b/tests/Avalonia.Benchmarks/NullCursorFactory.cs @@ -4,11 +4,14 @@ using Avalonia.Platform; namespace Avalonia.Benchmarks { - internal class NullCursorFactory : IStandardCursorFactory + internal class NullCursorFactory : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new NullCursorImpl(); + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new NullCursorImpl(); + + private class NullCursorImpl : ICursorImpl { - return new PlatformHandle(IntPtr.Zero, "null"); + public void Dispose() { } } } } diff --git a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs index f41a3e7581..d77c7b87fa 100644 --- a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs @@ -74,7 +74,7 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private CalendarDatePicker CreateControl() { diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index da02cccdc5..ee4264e6b9 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -1,14 +1,25 @@ -using System; using Avalonia.Input; using Avalonia.Platform; namespace Avalonia.Controls.UnitTests { - public class CursorFactoryMock : IStandardCursorFactory + public class CursorFactoryMock : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { - return new PlatformHandle(IntPtr.Zero, cursorType.ToString()); + return new MockCursorImpl(); + } + + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new MockCursorImpl(); + } + + private class MockCursorImpl : ICursorImpl + { + public void Dispose() + { + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 7bcb120850..3105263290 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -204,7 +204,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl()); private IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index f2b6b0db4b..72ccead783 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -11,8 +11,8 @@ namespace Avalonia.Controls.UnitTests { public GridSplitterTests() { - var cursorFactoryImpl = new Mock(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); + var cursorFactoryImpl = new Mock(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 34371916df..ebe471f303 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -156,15 +156,35 @@ namespace Avalonia.Controls.UnitTests root.Child = null; Assert.Equal(0, command.SubscriptionCount); } + + [Fact] + public void MenuItem_Invokes_CanExecute_When_CommandParameter_Changed() + { + var command = new TestCommand(p => p is bool value && value); + var target = new MenuItem { Command = command }; + + target.CommandParameter = true; + Assert.True(target.IsEffectivelyEnabled); + + target.CommandParameter = false; + Assert.False(target.IsEffectivelyEnabled); + } private class TestCommand : ICommand { - private bool _enabled; + private readonly Func _canExecute; + private readonly Action _execute; private EventHandler _canExecuteChanged; public TestCommand(bool enabled = true) + : this(_ => enabled, _ => { }) + { + } + + public TestCommand(Func canExecute, Action execute = null) { - _enabled = enabled; + _canExecute = canExecute; + _execute = execute ?? (_ => { }); } public int SubscriptionCount { get; private set; } @@ -175,11 +195,9 @@ namespace Avalonia.Controls.UnitTests remove { _canExecuteChanged -= value; --SubscriptionCount; } } - public bool CanExecute(object parameter) => _enabled; + public bool CanExecute(object parameter) => _canExecute(parameter); - public void Execute(object parameter) - { - } + public void Execute(object parameter) => _execute(parameter); } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index fe25fa7346..6ac7799828 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -698,10 +698,10 @@ namespace Avalonia.Controls.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 78b6bf01ec..570b9ee4ea 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index 682f0eaadb..c39a963dae 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -100,7 +100,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl()); private IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index a0fe348166..cc4b727bd9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight); + typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight); return true; } @@ -83,7 +83,7 @@ namespace Avalonia.Skia.UnitTests.Media case "Noto Mono": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); - skTypeface = typefaceCollection.Get(typeface); + skTypeface = typefaceCollection.Get(_defaultTypeface); break; } default: diff --git a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs index f9f924e782..68813f28ab 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs @@ -7,25 +7,33 @@ namespace Avalonia.Skia.UnitTests.Media public class SKTypefaceCollectionCacheTests { [Fact] - public void Should_Load_Typefaces_From_Invalid_Name() + public void Should_Get_Near_Matching_Typeface() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { var notoMono = new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - var colorEmoji = - new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); - var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - var typeface = new Typeface("ABC", FontStyle.Italic, FontWeight.Bold); - - Assert.Equal("Noto Mono", notoMonoCollection.Get(typeface).FamilyName); - - var notoColorEmojiCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(colorEmoji); + Assert.Equal("Noto Mono", + notoMonoCollection.Get(new Typeface(notoMono, weight: FontWeight.Bold)).FamilyName); + } + } + + [Fact] + public void Should_Get_Null_For_Invalid_FamilyName() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var notoMono = + new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); + + var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - Assert.Equal("Twitter Color Emoji", notoColorEmojiCollection.Get(typeface).FamilyName); + var typeface = notoMonoCollection.Get(new Typeface("ABC")); + + Assert.Null(typeface); } } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 012cab23dc..8d27562146 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -23,7 +23,7 @@ namespace Avalonia.UnitTests assetLoader: new AssetLoader(), platform: new AppBuilder().RuntimePlatform, renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), styler: new Styler(), theme: () => CreateDefaultTheme(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), @@ -70,7 +70,7 @@ namespace Avalonia.UnitTests IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, IScheduler scheduler = null, - IStandardCursorFactory standardCursorFactory = null, + ICursorFactory standardCursorFactory = null, IStyler styler = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, @@ -111,7 +111,7 @@ namespace Avalonia.UnitTests public IFontManagerImpl FontManagerImpl { get; } public ITextShaperImpl TextShaperImpl { get; } public IScheduler Scheduler { get; } - public IStandardCursorFactory StandardCursorFactory { get; } + public ICursorFactory StandardCursorFactory { get; } public IStyler Styler { get; } public Func Theme { get; } public IPlatformThreadingInterface ThreadingInterface { get; } @@ -130,7 +130,7 @@ namespace Avalonia.UnitTests IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, IScheduler scheduler = null, - IStandardCursorFactory standardCursorFactory = null, + ICursorFactory standardCursorFactory = null, IStyler styler = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index e4a65f105d..6fc82088e9 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -62,7 +62,7 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.TextShaperImpl) .Bind().ToConstant(Services.ThreadingInterface) .Bind().ToConstant(Services.Scheduler) - .Bind().ToConstant(Services.StandardCursorFactory) + .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.Styler) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton(); diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt index 814ce15d0a..93d531c700 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt @@ -1,7 +1,7 @@ OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ CB OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ -CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ -CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs new file mode 100644 index 0000000000..a90be6d519 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using Avalonia.Media.TextFormatting.Unicode; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.Visuals.UnitTests.Media.TextFormatting +{ + public class LineBreakEnumeratorTests + { + private readonly ITestOutputHelper _outputHelper; + + public LineBreakEnumeratorTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Fact] + public void BasicLatinTest() + { + var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.".AsMemory()); + + Assert.True(lineBreaker.MoveNext()); + Assert.Equal(6, lineBreaker.Current.PositionWrap); + Assert.False(lineBreaker.Current.Required); + + Assert.True(lineBreaker.MoveNext()); + Assert.Equal(13, lineBreaker.Current.PositionWrap); + Assert.True(lineBreaker.Current.Required); + + Assert.True(lineBreaker.MoveNext()); + Assert.Equal(18, lineBreaker.Current.PositionWrap); + Assert.False(lineBreaker.Current.Required); + + Assert.True(lineBreaker.MoveNext()); + Assert.Equal(21, lineBreaker.Current.PositionWrap); + Assert.False(lineBreaker.Current.Required); + + Assert.True(lineBreaker.MoveNext()); + Assert.Equal(23, lineBreaker.Current.PositionWrap); + Assert.False(lineBreaker.Current.Required); + + Assert.True(lineBreaker.MoveNext()); + Assert.Equal(28, lineBreaker.Current.PositionWrap); + Assert.False(lineBreaker.Current.Required); + + Assert.False(lineBreaker.MoveNext()); + } + + + [Fact] + public void ForwardTextWithOuterWhitespace() + { + var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ".AsMemory()); + var positionsF = GetBreaks(lineBreaker); + Assert.Equal(1, positionsF[0].PositionWrap); + Assert.Equal(0, positionsF[0].PositionMeasure); + Assert.Equal(8, positionsF[1].PositionWrap); + Assert.Equal(7, positionsF[1].PositionMeasure); + Assert.Equal(14, positionsF[2].PositionWrap); + Assert.Equal(13, positionsF[2].PositionMeasure); + Assert.Equal(24, positionsF[3].PositionWrap); + Assert.Equal(21, positionsF[3].PositionMeasure); + } + + private static List GetBreaks(LineBreakEnumerator lineBreaker) + { + var breaks = new List(); + + while (lineBreaker.MoveNext()) + { + breaks.Add(lineBreaker.Current); + } + + return breaks; + } + + [Fact] + public void ForwardTest() + { + var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas".AsMemory()); + + var positionsF = GetBreaks(lineBreaker); + Assert.Equal(7, positionsF[0].PositionWrap); + Assert.Equal(6, positionsF[0].PositionMeasure); + Assert.Equal(13, positionsF[1].PositionWrap); + Assert.Equal(12, positionsF[1].PositionMeasure); + Assert.Equal(20, positionsF[2].PositionWrap); + Assert.Equal(20, positionsF[2].PositionMeasure); + } + + [Theory(Skip = "Only run when the Unicode spec changes.")] + [ClassData(typeof(LineBreakTestDataGenerator))] + public void ShouldFindBreaks(int lineNumber, int[] codePoints, int[] breakPoints) + { + var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); + + var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + + var foundBreaks = new List(); + + while (lineBreaker.MoveNext()) + { + foundBreaks.Add(lineBreaker.Current.PositionWrap); + } + + // Check the same + var pass = true; + + if (foundBreaks.Count != breakPoints.Length) + { + pass = false; + } + else + { + for (var i = 0; i < foundBreaks.Count; i++) + { + if (foundBreaks[i] != breakPoints[i]) + { + pass = false; + } + } + } + + if (!pass) + { + _outputHelper.WriteLine($"Failed test on line {lineNumber}"); + _outputHelper.WriteLine(""); + _outputHelper.WriteLine($" Code Points: {string.Join(" ", codePoints)}"); + _outputHelper.WriteLine($"Expected Breaks: {string.Join(" ", breakPoints)}"); + _outputHelper.WriteLine($" Actual Breaks: {string.Join(" ", foundBreaks)}"); + _outputHelper.WriteLine($" Text: {text}"); + _outputHelper.WriteLine($" Char Props: {string.Join(" ", codePoints.Select(x => new Codepoint(x).LineBreakClass))}"); + _outputHelper.WriteLine(""); + } + + Assert.True(pass); + } + + private class LineBreakTestDataGenerator : IEnumerable + { + private readonly List _testData; + + public LineBreakTestDataGenerator() + { + _testData = GenerateTestData(); + } + + public IEnumerator GetEnumerator() + { + return _testData.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private static List GenerateTestData() + { + // Process each line + var tests = new List(); + + // Read the test file + var url = Path.Combine(UnicodeDataGenerator.Ucd, "auxiliary/LineBreakTest.txt"); + + using (var client = new HttpClient()) + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) + { + if (!result.IsSuccessStatusCode) + { + return tests; + } + + using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + using (var reader = new StreamReader(stream)) + { + var lineNumber = 1; + + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + + if (line is null) + { + break; + } + + // Get the line, remove comments + line = line.Split('#')[0].Trim(); + + // Ignore blank/comment only lines + if (string.IsNullOrWhiteSpace(line)) + { + lineNumber++; + continue; + } + + var codePoints = new List(); + var breakPoints = new List(); + + // Parse the test + var p = 0; + + while (p < line.Length) + { + // Ignore white space + if (char.IsWhiteSpace(line[p])) + { + p++; + continue; + } + + if (line[p] == '×') + { + p++; + continue; + } + + if (line[p] == '÷') + { + breakPoints.Add(codePoints.Select(x=> x > ushort.MaxValue ? 2 : 1).Sum()); + p++; + continue; + } + + var codePointPos = p; + + while (p < line.Length && IsHexDigit(line[p])) + { + p++; + } + + var codePointStr = line.Substring(codePointPos, p - codePointPos); + var codePoint = Convert.ToInt32(codePointStr, 16); + codePoints.Add(codePoint); + } + + tests.Add(new object[] { lineNumber, codePoints.ToArray(), breakPoints.ToArray() }); + + lineNumber++; + } + } + } + + return tests; + } + + private static bool IsHexDigit(char ch) + { + return char.IsDigit(ch) || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs deleted file mode 100644 index 3d489af3a2..0000000000 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; -using Xunit; - -namespace Avalonia.Visuals.UnitTests.Media.TextFormatting -{ - public class LineBreakerTests - { - [Fact] - public void Should_Split_Text_By_Explicit_Breaks() - { - //ABC [0 3] - //DEF\r[4 7] - //\r[8] - //Hello\r\n[9 15] - const string text = "ABC DEF\r\rHELLO\r\n"; - - var buffer = new ReadOnlySlice(text.AsMemory()); - - var lineBreaker = new LineBreakEnumerator(buffer); - - var current = 0; - - Assert.True(lineBreaker.MoveNext()); - - var a = text.Substring(current, lineBreaker.Current.PositionMeasure - current + 1); - - Assert.Equal("ABC ", a); - - current += a.Length; - - Assert.True(lineBreaker.MoveNext()); - - var b = text.Substring(current, lineBreaker.Current.PositionMeasure - current + 1); - - Assert.Equal("DEF\r", b); - - current += b.Length; - - Assert.True(lineBreaker.MoveNext()); - - var c = text.Substring(current, lineBreaker.Current.PositionMeasure - current + 1); - - Assert.Equal("\r", c); - - current += c.Length; - - Assert.True(lineBreaker.MoveNext()); - - var d = text.Substring(current, text.Length - current); - - Assert.Equal("HELLO\r\n", d); - } - } -} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs index 5c705ba0c7..47aef84533 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs @@ -1,6 +1,4 @@ -using System; -using Avalonia.Media.TextFormatting.Unicode; -using Xunit; +using Xunit; namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { @@ -15,26 +13,5 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { UnicodeDataGenerator.Execute(); } - [Theory(Skip = "Only run when we update the trie.")] - [ClassData(typeof(LineBreakTestDataGenerator))] - - public void Should_Enumerate_LineBreaks(string text, int expectedLength) - { - var textMemory = text.AsMemory(); - - var enumerator = new LineBreakEnumerator(textMemory); - - Assert.True(enumerator.MoveNext()); - - Assert.Equal(expectedLength, enumerator.Current.PositionWrap); - } - - private class LineBreakTestDataGenerator : TestDataGenerator - { - public LineBreakTestDataGenerator() - : base("auxiliary/LineBreakTest.txt") - { - } - } } }