diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e3532ee23..92e4afdca8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -134,3 +134,4 @@ jobs: pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip' artifactName: 'Samples' condition: succeeded() + diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index b491cf2a92..b62fe8a968 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -11,14 +11,26 @@ class AvnStringImpl : public virtual ComSingleObject { private: - NSString* _string; + int _length; + const char* _cstring; public: FORWARD_IUNKNOWN() AvnStringImpl(NSString* string) + { + auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding]; + _length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + _cstring = (const char*)malloc(_length + 5); + + memset((void*)_cstring, 0, _length + 5); + memcpy((void*)_cstring, (void*)cstring, _length); + } + + virtual ~AvnStringImpl() { - _string = string; + free((void*)_cstring); } virtual HRESULT Pointer(void**retOut) override @@ -30,7 +42,7 @@ public: return E_POINTER; } - *retOut = (void*)_string.UTF8String; + *retOut = (void*)_cstring; return S_OK; } @@ -43,7 +55,7 @@ public: return E_POINTER; } - *retOut = (int)_string.length; + *retOut = _length; return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 53c1fe3c2c..6e4d3ce668 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject { public: FORWARD_IUNKNOWN() + + Clipboard() + { + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard stringForType:NSPasteboardTypeString]; + } + virtual HRESULT GetText (IAvnString**ppv) override { @autoreleasepool @@ -39,7 +46,9 @@ public: { @autoreleasepool { - [[NSPasteboard generalPasteboard] clearContents]; + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard clearContents]; + [pasteBoard setString:@"" forType:NSPasteboardTypeString]; } return S_OK; diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index de9ca02ed1..09d2612ac3 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; using Avalonia; @@ -28,15 +29,24 @@ namespace ControlCatalog.NetCore } var builder = BuildAvaloniaApp(); + + double GetScaling() + { + var idx = Array.IndexOf(args, "--scaling"); + if (idx != 0 && args.Length > idx + 1 && + double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling)) + return scaling; + return 1; + } if (args.Contains("--fbdev")) { SilenceConsole(); - return builder.StartLinuxFbDev(args); + return builder.StartLinuxFbDev(args, scaling: GetScaling()); } else if (args.Contains("--drm")) { SilenceConsole(); - return builder.StartLinuxDrm(args); + return builder.StartLinuxDrm(args, scaling: GetScaling()); } else return builder.StartWithClassicDesktopLifetime(args); diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index 749f83c1ab..2e7218b956 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -34,7 +34,7 @@ namespace ControlCatalog SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North); - SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South); + SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South); SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest); SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast); SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 5f24c8062e..c35f8a3c0c 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -6,10 +6,13 @@ Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> - - Light - Dark - + + + @@ -21,7 +24,12 @@ - + + + + @@ -42,6 +50,12 @@ + + + Light + Dark + + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6a9e865e26..9527ac3b4e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -1,4 +1,5 @@ ItemsRepeater A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization. - + Stack - Vertical Stack - Horizontal UniformGrid - Vertical UniformGrid - Horizontal + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 214de89253..1a607342f3 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -1,8 +1,11 @@ +using System; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -16,7 +19,8 @@ namespace ControlCatalog.Pages this.InitializeComponent(); _repeater = this.FindControl("repeater"); _scroller = this.FindControl("scroller"); - DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); + _repeater.PointerPressed += RepeaterClick; + DataContext = new ItemsRepeaterPageViewModel(); } private void InitializeComponent() @@ -67,5 +71,11 @@ namespace ControlCatalog.Pages break; } } + + private void RepeaterClick(object sender, PointerPressedEventArgs e) + { + var item = (e.Source as TextBlock)?.DataContext as string; + ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item; + } } } diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 4783c8cfb8..49e9aafc4a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -9,7 +9,20 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index dbe6c74800..8a67766c76 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -11,9 +11,8 @@ namespace ControlCatalog.Pages { public ListBoxPage() { - this.InitializeComponent(); - DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" ) - .ToArray(); + InitializeComponent(); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -21,5 +20,46 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + private class PageViewModel : ReactiveObject + { + private int _counter; + private SelectionMode _selectionMode; + + public PageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + SelectedItems = new ObservableCollection(); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (SelectedItems.Count > 0) + { + Items.Remove(SelectedItems[0]); + } + }); + } + + public ObservableCollection Items { get; } + + public ObservableCollection SelectedItems { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++}"; + } } } diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index a1359519e6..60e946dfbe 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -69,16 +69,25 @@ namespace ControlCatalog.Pages { UpdatePointer(e); e.Pointer.Capture(this); + e.Handled = true; base.OnPointerPressed(e); } protected override void OnPointerMoved(PointerEventArgs e) { UpdatePointer(e); + e.Handled = true; base.OnPointerMoved(e); } protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { _pointers.Remove(e.Pointer); InvalidateVisual(); diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b9b384e8fe..13c1667ed2 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -22,7 +22,10 @@ namespace ControlCatalog.Pages public override void Render(DrawingContext context) { base.Render(context); - Window w = (Window)VisualRoot; + if (!(VisualRoot is Window w)) + { + return; + } var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c03edb8b03..3a81e2ed02 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -6,16 +6,29 @@ Displays a hierachical tree of data. - - - - - - - + Margin="0,16,0,0" + HorizontalAlignment="Center" + Spacing="16"> + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index a83f9cf43f..1f35f05f1d 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,8 +1,9 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -10,8 +11,8 @@ namespace ControlCatalog.Pages { public TreeViewPage() { - this.InitializeComponent(); - DataContext = new Node().Children; + InitializeComponent(); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -19,22 +20,96 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - public class Node + private class PageViewModel : ReactiveObject { - private IList _children; + private SelectionMode _selectionMode; + + public PageViewModel() + { + Node root = new Node(); + Items = root.Children; + SelectedItems = new ObservableCollection(); + + AddItemCommand = ReactiveCommand.Create(() => + { + Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root; + parentItem.AddNewItem(); + }); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (SelectedItems.Count > 0) + { + Node lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + SelectedItems.Remove(lastItem); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + }); + } + + public ObservableCollection Items { get; } + + public ObservableCollection SelectedItems { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + } + + private class Node + { + private int _counter; + private ObservableCollection _children; + public string Header { get; private set; } - public IList Children + + public bool AreChildrenInitialized => _children != null; + + public ObservableCollection Children { get { if (_children == null) { - _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"}) - .ToArray(); + _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); } return _children; } } + + public void AddNewItem() => Children.Add(CreateNewNode()); + + public override string ToString() => Header; + + private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"}; } } } diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3513e94107..26d25a6266 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -24,7 +24,8 @@ Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" - Background="{TemplateBinding Background}"> + Background="{TemplateBinding Background}" + DockPanel.Dock="Left"> - - + + + + + @@ -58,6 +64,8 @@ + + - - - + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index 5417d5fb0b..89fbb9d64d 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -1,5 +1,6 @@ \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml new file mode 100644 index 0000000000..35d3a8cff4 --- /dev/null +++ b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml @@ -0,0 +1,14 @@ + diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index cc23367ac0..71042f2a98 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -2,11 +2,13 @@ - + + + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml index 41f366fdf9..9e05c38eef 100644 --- a/src/Avalonia.Themes.Default/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/ToggleButton.xaml @@ -22,17 +22,17 @@ - - - - + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 2514422ce8..2a8b5d0fca 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -5,14 +5,14 @@ - + - + diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs index 522953eb04..87e698e705 100644 --- a/src/Avalonia.Visuals/Media/BrushExtensions.cs +++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.Immutable; namespace Avalonia.Media { @@ -23,27 +24,33 @@ namespace Avalonia.Media } /// - /// Converts a pen to a pen with an immutable brush + /// Converts a dash style to an immutable dash style. + /// + /// The dash style. + /// + /// The result of calling if the style is mutable, + /// otherwise . + /// + public static ImmutableDashStyle ToImmutable(this IDashStyle style) + { + Contract.Requires(style != null); + + return style as ImmutableDashStyle ?? ((DashStyle)style).ToImmutable(); + } + + /// + /// Converts a pen to an immutable pen. /// /// The pen. /// - /// A copy of the pen with an immutable brush, or if the pen's brush - /// is already immutable or null. + /// The result of calling if the brush is mutable, + /// otherwise . /// - public static Pen ToImmutable(this Pen pen) + public static ImmutablePen ToImmutable(this IPen pen) { Contract.Requires(pen != null); - var brush = pen.Brush?.ToImmutable(); - return ReferenceEquals(pen.Brush, brush) ? - pen : - new Pen( - brush, - thickness: pen.Thickness, - dashStyle: pen.DashStyle, - lineCap: pen.LineCap, - lineJoin: pen.LineJoin, - miterLimit: pen.MiterLimit); + return pen as ImmutablePen ?? ((Pen)pen).ToImmutable(); } } } diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index c7e1db57b2..7784c73736 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -1,72 +1,114 @@ namespace Avalonia.Media { + using System; using System.Collections.Generic; + using System.Linq; using Avalonia.Animation; + using Avalonia.Media.Immutable; - public class DashStyle : Animatable + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + public class DashStyle : Animatable, IDashStyle, IAffectsRender { - private static DashStyle dash; - public static DashStyle Dash - { - get - { - if (dashDotDot == null) - { - dash = new DashStyle(new double[] { 2, 2 }, 1); - } - - return dash; - } - } + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty> DashesProperty = + AvaloniaProperty.Register>(nameof(Dashes)); + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty OffsetProperty = + AvaloniaProperty.Register(nameof(Offset)); + private static ImmutableDashStyle s_dash; + private static ImmutableDashStyle s_dot; + private static ImmutableDashStyle s_dashDot; + private static ImmutableDashStyle s_dashDotDot; - private static DashStyle dot; - public static DashStyle Dot + /// + /// Initializes a new instance of the class. + /// + public DashStyle() + : this(null, 0) { - get { return dot ?? (dot = new DashStyle(new double[] {0, 2}, 0)); } } - private static DashStyle dashDot; - public static DashStyle DashDot + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public DashStyle(IEnumerable dashes, double offset) { - get - { - if (dashDot == null) - { - dashDot = new DashStyle(new double[] { 2, 2, 0, 2 }, 1); - } - - return dashDot; - } + Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + Offset = offset; } - private static DashStyle dashDotDot; - public static DashStyle DashDotDot + static DashStyle() { - get + void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e) { - if (dashDotDot == null) - { - dashDotDot = new DashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1); - } - - return dashDotDot; + ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty); } + + DashesProperty.Changed.Subscribe(RaiseInvalidated); + OffsetProperty.Changed.Subscribe(RaiseInvalidated); } + /// + /// Represents a dashed . + /// + public static IDashStyle Dash => + s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1)); + + /// + /// Represents a dotted . + /// + public static IDashStyle Dot => + s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0)); + + /// + /// Represents a dashed dotted . + /// + public static IDashStyle DashDot => + s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1)); + + /// + /// Represents a dashed double dotted . + /// + public static IDashStyle DashDotDot => + s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1)); - public DashStyle(IReadOnlyList dashes = null, double offset = 0.0) + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + public IReadOnlyList Dashes { - this.Dashes = dashes; - this.Offset = offset; + get => GetValue(DashesProperty); + set => SetValue(DashesProperty, value); } /// - /// Gets and sets the length of alternating dashes and gaps. + /// Gets or sets how far in the dash sequence the stroke will start. /// - public IReadOnlyList Dashes { get; } + public double Offset + { + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } - public double Offset { get; } + /// + /// Raised when the dash style changes. + /// + public event EventHandler Invalidated; + + /// + /// Returns an immutable clone of the . + /// + /// + public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset); } } diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index d3af71ffcb..4c9bf9ebd4 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -94,7 +94,7 @@ namespace Avalonia.Media /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) { @@ -108,7 +108,7 @@ namespace Avalonia.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) + public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry) { Contract.Requires(geometry != null); @@ -124,7 +124,7 @@ namespace Avalonia.Media /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) { if (PenIsVisible(pen)) { @@ -328,7 +328,7 @@ namespace Avalonia.Media PlatformImpl.Dispose(); } - private static bool PenIsVisible(Pen pen) + private static bool PenIsVisible(IPen pen) { return pen?.Brush != null && pen.Thickness > 0; } diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index ac0cc1c17d..3dad10fb8f 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -23,7 +23,7 @@ public static readonly StyledProperty PenProperty = AvaloniaProperty.Register(nameof(Pen)); - public Pen Pen + public IPen Pen { get => GetValue(PenProperty); set => SetValue(PenProperty, value); diff --git a/src/Avalonia.Visuals/Media/IDashStyle.cs b/src/Avalonia.Visuals/Media/IDashStyle.cs new file mode 100644 index 0000000000..7835c7a1e9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IDashStyle.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Avalonia.Media +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + public interface IDashStyle + { + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + IReadOnlyList Dashes { get; } + + /// + /// Gets or sets how far in the dash sequence the stroke will start. + /// + double Offset { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IPen.cs b/src/Avalonia.Visuals/Media/IPen.cs new file mode 100644 index 0000000000..0cdac312cc --- /dev/null +++ b/src/Avalonia.Visuals/Media/IPen.cs @@ -0,0 +1,39 @@ +namespace Avalonia.Media +{ + /// + /// Describes how a stroke is drawn. + /// + public interface IPen + { + /// + /// Gets the brush used to draw the stroke. + /// + IBrush Brush { get; } + + /// + /// Gets the style of dashed lines drawn with a object. + /// + IDashStyle DashStyle { get; } + + /// + /// Gets the type of shape to use on both ends of a line. + /// + PenLineCap LineCap { get; } + + /// + /// Gets a value describing how to join consecutive line or curve segments in a + /// contained in a object. + /// + PenLineJoin LineJoin { get; } + + /// + /// Gets the limit of the thickness of the join on a mitered corner. + /// + double MiterLimit { get; } + + /// + /// Gets the stroke thickness. + /// + double Thickness { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs new file mode 100644 index 0000000000..e9a52fe6ed --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Media.Immutable +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by an + /// . + /// + public class ImmutableDashStyle : IDashStyle, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public ImmutableDashStyle(IEnumerable dashes, double offset) + { + Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + Offset = offset; + } + + /// + public IReadOnlyList Dashes { get; } + + /// + public double Offset { get; } + + /// + public override bool Equals(object obj) => Equals(obj as IDashStyle); + + /// + public bool Equals(IDashStyle other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + else if (other is null) + { + return false; + } + + if (Offset != other.Offset) + { + return false; + } + + return SequenceEqual(Dashes, other.Dashes); + } + + /// + public override int GetHashCode() + { + var hashCode = 717868523; + hashCode = hashCode * -1521134295 + Offset.GetHashCode(); + + if (Dashes != null) + { + foreach (var i in Dashes) + { + hashCode = hashCode * -1521134295 + i.GetHashCode(); + } + } + + return hashCode; + } + + private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) + { + if (left == right) + { + return true; + } + + if (left == null || right == null || left.Count != right.Count) + { + return false; + } + + for (var c = 0; c < left.Count; c++) + { + if (left[c] != right[c]) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs new file mode 100644 index 0000000000..4b3bd640cb --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -0,0 +1,118 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Media.Immutable +{ + /// + /// Describes how a stroke is drawn. + /// + public class ImmutablePen : IPen, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The stroke color. + /// The stroke thickness. + /// The dash style. + /// Specifies the type of graphic shape to use on both ends of a line. + /// The line join. + /// The miter limit. + public ImmutablePen( + uint color, + double thickness = 1.0, + ImmutableDashStyle dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to draw. + /// The stroke thickness. + /// The dash style. + /// The line cap. + /// The line join. + /// The miter limit. + public ImmutablePen( + IBrush brush, + double thickness = 1.0, + ImmutableDashStyle dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) + { + Brush = brush; + Thickness = thickness; + LineCap = lineCap; + LineJoin = lineJoin; + MiterLimit = miterLimit; + DashStyle = dashStyle; + } + + /// + /// Gets the brush used to draw the stroke. + /// + public IBrush Brush { get; } + + /// + /// Gets the stroke thickness. + /// + public double Thickness { get; } + + /// + /// Specifies the style of dashed lines drawn with a object. + /// + public IDashStyle DashStyle { get; } + + /// + /// Specifies the type of graphic shape to use on both ends of a line. + /// + public PenLineCap LineCap { get; } + + /// + /// Specifies how to join consecutive line or curve segments in a + /// (subpaths) contained in a object. + /// + public PenLineJoin LineJoin { get; } + + /// + /// The limit on the ratio of the miter length to half this pen's Thickness. + /// + public double MiterLimit { get; } + + /// + public override bool Equals(object obj) => Equals(obj as IPen); + + /// + public bool Equals(IPen other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + else if (other is null) + { + return false; + } + + return EqualityComparer.Default.Equals(Brush, other.Brush) && + Thickness == other.Thickness && + EqualityComparer.Default.Equals(DashStyle, other.DashStyle) && + LineCap == other.LineCap && + LineJoin == other.LineJoin && + MiterLimit == other.MiterLimit; + } + + /// + public override int GetHashCode() + { + return (Brush, Thickness, DashStyle, LineCap, LineJoin, MiterLimit).GetHashCode(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index ee427c913b..b88fae28ff 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -1,13 +1,61 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.Generic; +using Avalonia.Media.Immutable; +using Avalonia.Utilities; + namespace Avalonia.Media { /// /// Describes how a stroke is drawn. /// - public class Pen + public class Pen : AvaloniaObject, IPen { + /// + /// Defines the property. + /// + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), 1.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DashStyleProperty = + AvaloniaProperty.Register(nameof(DashStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineCapProperty = + AvaloniaProperty.Register(nameof(LineCap)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineJoinProperty = + AvaloniaProperty.Register(nameof(LineJoin)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MiterLimitProperty = + AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + + /// + /// Initializes a new instance of the class. + /// + public Pen() + { + } + /// /// Initializes a new instance of the class. /// @@ -20,7 +68,7 @@ namespace Avalonia.Media public Pen( uint color, double thickness = 1.0, - DashStyle dashStyle = null, + IDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) @@ -39,7 +87,7 @@ namespace Avalonia.Media public Pen( IBrush brush, double thickness = 1.0, - DashStyle dashStyle = null, + IDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) @@ -52,34 +100,139 @@ namespace Avalonia.Media DashStyle = dashStyle; } + static Pen() + { + AffectsRender( + BrushProperty, + ThicknessProperty, + DashStyleProperty, + LineCapProperty, + LineJoinProperty, + MiterLimitProperty); + } + + /// + /// Gets or sets the brush used to draw the stroke. + /// + public IBrush Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + /// + /// Gets or sets the stroke thickness. + /// + public double Thickness + { + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); + } + + /// + /// Gets or sets the style of dashed lines drawn with a object. + /// + public IDashStyle DashStyle + { + get => GetValue(DashStyleProperty); + set => SetValue(DashStyleProperty, value); + } + + /// + /// Gets or sets the type of shape to use on both ends of a line. + /// + public PenLineCap LineCap + { + get => GetValue(LineCapProperty); + set => SetValue(LineCapProperty, value); + } + /// - /// Gets the brush used to draw the stroke. + /// Gets or sets the join style for the ends of two consecutive lines drawn with this + /// . /// - public IBrush Brush { get; } + public PenLineJoin LineJoin + { + get => GetValue(LineJoinProperty); + set => SetValue(LineJoinProperty, value); + } /// - /// Gets the stroke thickness. + /// Gets or sets the limit of the thickness of the join on a mitered corner. /// - public double Thickness { get; } + public double MiterLimit + { + get => GetValue(MiterLimitProperty); + set => SetValue(MiterLimitProperty, value); + } /// - /// Specifies the style of dashed lines drawn with a object. + /// Raised when the pen changes. /// - public DashStyle DashStyle { get; } + public event EventHandler Invalidated; /// - /// Specifies the type of graphic shape to use on both ends of a line. + /// Creates an immutable clone of the brush. /// - public PenLineCap LineCap { get; } + /// The immutable clone. + public ImmutablePen ToImmutable() + { + return new ImmutablePen( + Brush?.ToImmutable(), + Thickness, + DashStyle?.ToImmutable(), + LineCap, + LineJoin, + MiterLimit); + } /// - /// Specifies how to join consecutive line or curve segments in a (subpath) contained in a object. + /// Marks a property as affecting the pen's visual representation. /// - public PenLineJoin LineJoin { get; } + /// The properties. + /// + /// After a call to this method in a pen's static constructor, any change to the + /// property will cause the event to be raised on the pen. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : Pen + { + void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + if (e.OldValue is IAffectsRender oldValue) + { + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(oldValue.Invalidated), + sender.AffectsRenderInvalidated); + } + + if (e.NewValue is IAffectsRender newValue) + { + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.Invalidated), + sender.AffectsRenderInvalidated); + } + + sender.RaiseInvalidated(EventArgs.Empty); + } + } + + foreach (var property in properties) + { + property.Changed.Subscribe(Invalidate); + } + } /// - /// The limit on the ratio of the miter length to half this pen's Thickness. + /// Raises the event. /// - public double MiterLimit { get; } + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); } } diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index 995781ee9f..d62c2a2e55 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -59,6 +59,59 @@ namespace Avalonia { return !(left == right); } + + /// + /// Converts the to a . + /// + /// The point. + public static implicit operator PixelVector(PixelPoint p) + { + return new PixelVector(p.X, p.Y); + } + + /// + /// Adds two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Adds a vector to a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Subtracts two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } + + /// + /// Subtracts a vector from a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } /// /// Parses a string. @@ -106,7 +159,7 @@ namespace Avalonia return hash; } } - + /// /// Returns a new with the same Y co-ordinate and the specified X co-ordinate. /// diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 9c8e5ad1c4..0e2094da07 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -261,6 +261,16 @@ namespace Avalonia { return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); } + + /// + /// Translates the rectangle by an offset. + /// + /// The offset. + /// The translated rectangle. + public PixelRect Translate(PixelVector offset) + { + return new PixelRect(Position + offset, Size); + } /// /// Gets the union of two rectangles. diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Visuals/Media/PixelVector.cs new file mode 100644 index 0000000000..4a623e3bc2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/PixelVector.cs @@ -0,0 +1,203 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using Avalonia.Animation.Animators; +using JetBrains.Annotations; + +namespace Avalonia +{ + /// + /// Defines a vector. + /// + public readonly struct PixelVector + { + /// + /// The X vector. + /// + private readonly int _x; + + /// + /// The Y vector. + /// + private readonly int _y; + + /// + /// Initializes a new instance of the structure. + /// + /// The X vector. + /// The Y vector. + public PixelVector(int x, int y) + { + _x = x; + _y = y; + } + + /// + /// Gets the X vector. + /// + public int X => _x; + + /// + /// Gets the Y vector. + /// + public int Y => _y; + + /// + /// Converts the to a . + /// + /// The vector. + public static explicit operator PixelPoint(PixelVector a) + { + return new PixelPoint(a._x, a._y); + } + + /// + /// Calculates the dot product of two vectors + /// + /// First vector + /// Second vector + /// The dot product + public static int operator *(PixelVector a, PixelVector b) + { + return a.X * b.X + a.Y * b.Y; + } + + /// + /// Scales a vector. + /// + /// The vector + /// The scaling factor. + /// The scaled vector. + public static PixelVector operator *(PixelVector vector, int scale) + { + return new PixelVector(vector._x * scale, vector._y * scale); + } + + /// + /// Scales a vector. + /// + /// The vector + /// The divisor. + /// The scaled vector. + public static PixelVector operator /(PixelVector vector, int scale) + { + return new PixelVector(vector._x / scale, vector._y / scale); + } + + /// + /// Length of the vector + /// + public double Length => Math.Sqrt(X * X + Y * Y); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static PixelVector operator -(PixelVector a) + { + return new PixelVector(-a._x, -a._y); + } + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the addition. + public static PixelVector operator +(PixelVector a, PixelVector b) + { + return new PixelVector(a._x + b._x, a._y + b._y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the subtraction. + public static PixelVector operator -(PixelVector a, PixelVector b) + { + return new PixelVector(a._x - b._x, a._y - b._y); + } + + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(PixelVector other) + { + return _x == other._x && _y == other._y; + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(PixelVector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is PixelVector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(PixelVector left, PixelVector right) + { + return left.Equals(right); + } + + public static bool operator !=(PixelVector left, PixelVector right) + { + return !left.Equals(right); + } + + /// + /// Returns the string representation of the point. + /// + /// The string representation of the point. + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); + } + + /// + /// Returns a new vector with the specified X coordinate. + /// + /// The X coordinate. + /// The new vector. + public PixelVector WithX(int x) + { + return new PixelVector(x, _y); + } + + /// + /// Returns a new vector with the specified Y coordinate. + /// + /// The Y coordinate. + /// The new vector. + public PixelVector WithY(int y) + { + return new PixelVector(_x, y); + } + } +} diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index e5be04ebf9..f74c551fe0 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Platform /// The stroke pen. /// The first point of the line. /// The second point of the line. - void DrawLine(Pen pen, Point p1, Point p2); + void DrawLine(IPen pen, Point p1, Point p2); /// /// Draws a geometry. @@ -58,7 +58,7 @@ namespace Avalonia.Platform /// The fill brush. /// The stroke pen. /// The geometry. - void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry); + void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// /// Draws the outline of a rectangle. @@ -66,7 +66,7 @@ namespace Avalonia.Platform /// The pen. /// The rectangle bounds. /// The corner radius. - void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f); + void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f); /// /// Draws text. diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index 4e8e6521bd..b762859d1d 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.Platform /// /// The pen to use. May be null. /// The bounding rectangle. - Rect GetRenderBounds(Pen pen); + Rect GetRenderBounds(IPen pen); /// /// Indicates whether the geometry's fill contains the specified point. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(Pen pen, Point point); + bool StrokeContains(IPen pen, Point point); /// /// Makes a clone of the geometry with the specified transform. diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index ebaf62b2c0..bf1799bbdc 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -30,6 +30,7 @@ namespace Avalonia.Rendering private bool _disposed; private volatile IRef _scene; private DirtyVisuals _dirty; + private HashSet _recalculateChildren; private IRef _overlay; private int _lastSceneId = -1; private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); @@ -135,6 +136,8 @@ namespace Avalonia.Rendering DisposeRenderTarget(); } + public void RecalculateChildren(IVisual visual) => _recalculateChildren?.Add(visual); + void DisposeRenderTarget() { using (var l = _lock.TryLock()) @@ -229,6 +232,8 @@ namespace Avalonia.Rendering internal void UnitTestRender() => Render(false); + internal Scene UnitTestScene() => _scene.Item; + private void Render(bool forceComposite) { using (var l = _lock.TryLock()) @@ -516,10 +521,19 @@ namespace Avalonia.Rendering if (_dirty == null) { _dirty = new DirtyVisuals(); + _recalculateChildren = new HashSet(); _sceneBuilder.UpdateAll(scene); } - else if (_dirty.Count > 0) + else { + foreach (var visual in _recalculateChildren) + { + var node = scene.FindNode(visual); + ((VisualNode)node)?.SortChildren(scene); + } + + _recalculateChildren.Clear(); + foreach (var visual in _dirty) { _sceneBuilder.Update(scene, visual); diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 36a1f7d220..9ad7186dca 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -50,6 +50,12 @@ namespace Avalonia.Rendering /// The visuals at the specified point, topmost first. IEnumerable HitTest(Point p, IVisual root, Func filter); + /// + /// Informs the renderer that the z-ordering of a visual's children has changed. + /// + /// The visual. + void RecalculateChildren(IVisual visual); + /// /// Called when a resize notification is received by the control being rendered. /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 21129e38af..b2d242d4af 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -163,6 +163,9 @@ namespace Avalonia.Rendering return HitTest(root, p, filter); } + /// + public void RecalculateChildren(IVisual visual) => AddDirty(visual); + /// public void Start() { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index 4c09dc2ddd..b2c0581388 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen) + public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen) : base(bounds, transform, pen) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 0b33851911..3af56f5215 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var next = NextDrawAs(); @@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { var next = NextDrawAs(); @@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index 1a5a6fad3f..d9dfd8bd55 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class DrawOperation : IDrawOperation { - public DrawOperation(Rect bounds, Matrix transform, Pen pen) + public DrawOperation(Rect bounds, Matrix transform, IPen pen) { bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform); Bounds = new Rect( diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 2d01b117d9..d5aa1251f3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -24,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph public GeometryNode( Matrix transform, IBrush brush, - Pen pen, + IPen pen, IGeometryImpl geometry, IDictionary childScenes = null) : base(geometry.GetRenderBounds(pen), transform, null) @@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// Gets the geometry to draw. @@ -71,11 +72,11 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry) + public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry) { return transform == Transform && Equals(brush, Brush) && - pen == Pen && + Equals(Pen, pen) && Equals(geometry, Geometry); } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 11c763fcc9..9a65fac078 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -23,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph /// Child scenes for drawing visual brushes. public LineNode( Matrix transform, - Pen pen, + IPen pen, Point p1, Point p2, IDictionary childScenes = null) @@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// Gets the start point of the line. @@ -71,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, Pen pen, Point p1, Point p2) + public bool Equals(Matrix transform, IPen pen, Point p1, Point p2) { - return transform == Transform && pen == Pen && p1 == P1 && p2 == P2; + return transform == Transform && Equals(Pen, pen) && p1 == P1 && p2 == P2; } public override void Render(IDrawingContextImpl context) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index c622dc8a43..0f3581b84c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -25,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph public RectangleNode( Matrix transform, IBrush brush, - Pen pen, + IPen pen, Rect rect, float cornerRadius, IDictionary childScenes = null) @@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// Gets the rectangle to draw. @@ -80,11 +81,11 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius) + public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius) { return transform == Transform && Equals(brush, Brush) && - pen == Pen && + Equals(Pen, pen) && rect == Rect && cornerRadius == CornerRadius; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 4e95d21a48..f579bf0a62 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -172,6 +172,42 @@ namespace Avalonia.Rendering.SceneGraph old.Dispose(); } + /// + /// Sorts the collection according to the order of the visual's + /// children and their z-index. + /// + /// The scene that the node is a part of. + public void SortChildren(Scene scene) + { + if (_children == null || _children.Count <= 1) + { + return; + } + + var keys = new List(Visual.VisualChildren.Count); + + for (var i = 0; i < Visual.VisualChildren.Count; ++i) + { + var child = Visual.VisualChildren[i]; + var zIndex = child.ZIndex; + keys.Add(((long)zIndex << 32) + i); + } + + keys.Sort(); + _children.Clear(); + + foreach (var i in keys) + { + var child = Visual.VisualChildren[(int)(i & 0xffffffff)]; + var node = scene.FindNode(child); + + if (node != null) + { + _children.Add(node); + } + } + } + /// /// Removes items in the collection from the specified index /// to the end. @@ -236,7 +272,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var operation in DrawOperations) { - if (operation.Item.HitTest(p)) + if (operation?.Item?.HitTest(p) == true) { return true; } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 9e088cb136..1f2d67b69e 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -111,6 +111,7 @@ namespace Avalonia IsVisibleProperty, OpacityProperty); RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); + ZIndexProperty.Changed.Subscribe(ZIndexChanged); } /// @@ -345,6 +346,12 @@ namespace Avalonia } } + protected override void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.LogicalChildrenCollectionChanged(sender, e); + VisualRoot?.Renderer?.RecalculateChildren(this); + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -501,6 +508,18 @@ namespace Avalonia } } + /// + /// Called when the property changes on any control. + /// + /// The event args. + private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) + { + var sender = e.Sender as IVisual; + var parent = sender?.VisualParent; + sender?.InvalidateVisual(); + parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); + } + /// /// Called when the 's event /// is fired. diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 0a8b1ee9c4..bed6f4693b 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -24,7 +24,7 @@ namespace Avalonia.X11 {StandardCursorType.No, CursorFontShape.XC_X_cursor}, {StandardCursorType.Wait, CursorFontShape.XC_watch}, {StandardCursorType.AppStarting, CursorFontShape.XC_watch}, - {StandardCursorType.BottomSize, CursorFontShape.XC_bottom_side}, + {StandardCursorType.BottomSide, CursorFontShape.XC_bottom_side}, {StandardCursorType.DragCopy, CursorFontShape.XC_center_ptr}, {StandardCursorType.DragLink, CursorFontShape.XC_fleur}, {StandardCursorType.DragMove, CursorFontShape.XC_diamond_cross}, diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7bdc61eb28..e88a7d8db2 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -74,18 +74,13 @@ namespace Avalonia.X11 public IntPtr Display { get; set; } public IWindowImpl CreateWindow() { - return new X11Window(this, false); + return new X11Window(this, null); } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } - - public IPopupImpl CreatePopup() - { - return new X11Window(this, true); - } } } @@ -96,6 +91,7 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 18c23aa31e..975b3d11d7 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -21,6 +22,7 @@ namespace Avalonia.X11 unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client { private readonly AvaloniaX11Platform _platform; + private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private bool _invalidated; @@ -38,6 +40,7 @@ namespace Avalonia.X11 private bool _mapped; private HashSet _transientChildren = new HashSet(); private X11Window _transientParent; + private double? _scalingOverride; public object SyncRoot { get; } = new object(); class InputEventContainer @@ -47,10 +50,10 @@ namespace Avalonia.X11 private readonly Queue _inputQueue = new Queue(); private InputEventContainer _lastEvent; private bool _useRenderWindow = false; - public X11Window(AvaloniaX11Platform platform, bool popup) + public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) { _platform = platform; - _popup = popup; + _popup = popupParent != null; _x11 = platform.Info; _mouse = platform.MouseDevice; _keyboard = platform.KeyboardDevice; @@ -66,7 +69,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (popup) + if (_popup) { attr.override_redirect = true; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -150,6 +153,8 @@ namespace Avalonia.X11 _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); + if(_popup) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -453,22 +458,28 @@ namespace Avalonia.X11 } } - private bool UpdateScaling() + private bool UpdateScaling(bool skipResize = false) { lock (SyncRoot) { - var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) - .FirstOrDefault(m => m.Bounds.Contains(Position)); - var newScaling = monitor?.PixelDensity ?? Scaling; + double newScaling; + if (_scalingOverride.HasValue) + newScaling = _scalingOverride.Value; + else + { + var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) + .FirstOrDefault(m => m.Bounds.Contains(Position)); + newScaling = monitor?.PixelDensity ?? Scaling; + } + if (Scaling != newScaling) { - Console.WriteLine( - $"Updating scaling from {Scaling} to {newScaling} as a response to position change to {Position}"); var oldScaledSize = ClientSize; Scaling = newScaling; ScalingChanged?.Invoke(Scaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); - Resize(oldScaledSize, true); + if(!skipResize) + Resize(oldScaledSize, true); return true; } @@ -547,24 +558,23 @@ namespace Avalonia.X11 } - - InputModifiers TranslateModifiers(XModifierMask state) + RawInputModifiers TranslateModifiers(XModifierMask state) { - var rv = default(InputModifiers); + var rv = default(RawInputModifiers); if (state.HasFlag(XModifierMask.Button1Mask)) - rv |= InputModifiers.LeftMouseButton; + rv |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) - rv |= InputModifiers.RightMouseButton; + rv |= RawInputModifiers.RightMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) - rv |= InputModifiers.MiddleMouseButton; + rv |= RawInputModifiers.MiddleMouseButton; if (state.HasFlag(XModifierMask.ShiftMask)) - rv |= InputModifiers.Shift; + rv |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) - rv |= InputModifiers.Control; + rv |= RawInputModifiers.Control; if (state.HasFlag(XModifierMask.Mod1Mask)) - rv |= InputModifiers.Alt; + rv |= RawInputModifiers.Alt; if (state.HasFlag(XModifierMask.Mod4Mask)) - rv |= InputModifiers.Windows; + rv |= RawInputModifiers.Meta; return rv; } @@ -730,6 +740,14 @@ namespace Avalonia.X11 public void Resize(Size clientSize) => Resize(clientSize, false); + public void Move(PixelPoint point) => Position = point; + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + _scalingOverride = scaling; + UpdateScaling(true); + Resize(size, true); + } PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling)); @@ -793,7 +811,9 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; - + public IPopupImpl CreatePopup() + => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); + public void Activate() { if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero) @@ -937,6 +957,8 @@ namespace Avalonia.X11 { SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero); - } + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 0a78c0dfd9..cf75572601 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -247,7 +247,7 @@ namespace Avalonia.X11 unsafe class ParsedDeviceEvent { public XiEventType Type { get; } - public InputModifiers Modifiers { get; } + public RawInputModifiers Modifiers { get; } public ulong Timestamp { get; } public Point Position { get; } public int Button { get; set; } @@ -260,25 +260,25 @@ namespace Avalonia.X11 Timestamp = (ulong)ev->time.ToInt64(); var state = (XModifierMask)ev->mods.Effective; if (state.HasFlag(XModifierMask.ShiftMask)) - Modifiers |= InputModifiers.Shift; + Modifiers |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) - Modifiers |= InputModifiers.Control; + Modifiers |= RawInputModifiers.Control; if (state.HasFlag(XModifierMask.Mod1Mask)) - Modifiers |= InputModifiers.Alt; + Modifiers |= RawInputModifiers.Alt; if (state.HasFlag(XModifierMask.Mod4Mask)) - Modifiers |= InputModifiers.Windows; + Modifiers |= RawInputModifiers.Meta; if (ev->buttons.MaskLen > 0) { var buttons = ev->buttons.Mask; if (XIMaskIsSet(buttons, 1)) - Modifiers |= InputModifiers.LeftMouseButton; + Modifiers |= RawInputModifiers.LeftMouseButton; if (XIMaskIsSet(buttons, 2)) - Modifiers |= InputModifiers.MiddleMouseButton; + Modifiers |= RawInputModifiers.MiddleMouseButton; if (XIMaskIsSet(buttons, 3)) - Modifiers |= InputModifiers.RightMouseButton; + Modifiers |= RawInputModifiers.RightMouseButton; } Valuators = new Dictionary(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 5e2ba51caf..2dc112f3d3 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -59,7 +59,9 @@ namespace Avalonia.LinuxFramebuffer public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); - public double Scaling => 1; + public IPopupImpl CreatePopup() => null; + + public double Scaling => _outputBackend.Scaling; public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } public Action Paint { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 723028c666..432344955a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -125,7 +125,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_UP ? RawPointerEventType.TouchEnd : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchCancel, - pt, InputModifiers.None, slot)); + pt, RawInputModifiers.None, slot)); } } @@ -140,7 +140,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width), libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height)); ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition, - InputModifiers.None)); + RawInputModifiers.None)); } else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON) { @@ -162,7 +162,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput ScheduleInput( - new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, InputModifiers.None)); + new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, RawInputModifiers.None)); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 2cc1f65202..8fc555aac2 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -107,11 +107,12 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev)); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) + where T : AppBuilderBase, new() => + StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling}); - public static int StartLinuxDrm(this T builder, string[] args, string card = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card)); + public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) where T : AppBuilderBase, new() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 6a76977352..273265a6dc 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -14,7 +14,7 @@ namespace Avalonia.LinuxFramebuffer.Output private DrmCard _card; private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; - + public double Scaling { get; set; } public DrmOutput(string path = null) { var card = new DrmCard(path); @@ -233,7 +233,7 @@ namespace Avalonia.LinuxFramebuffer.Output public PixelSize Size => _parent._mode.Resolution; - public double Scaling => 1; + public double Scaling => _parent.Scaling; } public IGlPlatformSurfaceRenderingSession BeginDraw() @@ -241,6 +241,8 @@ namespace Avalonia.LinuxFramebuffer.Output _parent._deferredContext.MakeCurrent(_parent._eglSurface); return new RenderSession(_parent); } + + } IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 3021c29015..b83fe6cbe8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -9,16 +9,15 @@ namespace Avalonia.LinuxFramebuffer { public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend { - private readonly Vector _dpi; private int _fd; private fb_fix_screeninfo _fixedInfo; private fb_var_screeninfo _varInfo; private IntPtr _mappedLength; private IntPtr _mappedAddress; + public double Scaling { get; set; } - public FbdevOutput(string fileName = null, Vector? dpi = null) + public FbdevOutput(string fileName = null) { - _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) @@ -101,7 +100,7 @@ namespace Avalonia.LinuxFramebuffer { if (_fd <= 0) throw new ObjectDisposedException("LinuxFramebuffer"); - return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _dpi); + return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index 01690f07ac..17a39b0219 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -3,5 +3,6 @@ namespace Avalonia.LinuxFramebuffer.Output public interface IOutputBackend { PixelSize PixelSize { get; } + double Scaling { get; set; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 262d87d8b6..47e651ce91 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -154,7 +154,7 @@ namespace Avalonia.Skia } /// - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { @@ -163,7 +163,7 @@ namespace Avalonia.Skia } /// - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; @@ -184,7 +184,7 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) { using (var paint = CreatePaint(pen, rect.Size)) { @@ -561,7 +561,7 @@ namespace Avalonia.Skia /// Source pen. /// Target size. /// - private PaintWrapper CreatePaint(Pen pen, Size targetSize) + private PaintWrapper CreatePaint(IPen pen, Size targetSize) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 5940de418e..23980fb913 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.Skia } /// - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { // Skia requires to compute stroke path to check for point containment. // Due to that we are caching using stroke width. @@ -89,7 +89,7 @@ namespace Avalonia.Skia } /// - public Rect GetRenderBounds(Pen pen) + public Rect GetRenderBounds(IPen pen) { var strokeWidth = (float)(pen?.Thickness ?? 0); diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index a7c1d0a38b..61ccf09e52 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -26,51 +26,64 @@ namespace Avalonia.Skia public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); - var disp = session.Display; - var gl = disp.GlInterface; - gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - - var size = session.Size; - var scaling = session.Scaling; - if (size.Width <= 0 || size.Height <= 0 || scaling < 0) - { - throw new InvalidOperationException( - $"Can't create drawing context for surface with {size} size and {scaling} scaling"); - } - - gl.Viewport(0, 0, size.Width, size.Height); - gl.ClearStencil(0); - gl.ClearColor(0, 0, 0, 0); - gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - lock (_grContext) + bool success = false; + try { - _grContext.ResetContext(); - - GRBackendRenderTarget renderTarget = - new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, - new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); - var surface = SKSurface.Create(_grContext, renderTarget, - GRSurfaceOrigin.BottomLeft, - GRPixelConfig.Rgba8888.ToColorType()); + var disp = session.Display; + var gl = disp.GlInterface; + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - var nfo = new DrawingContextImpl.CreateInfo + var size = session.Size; + var scaling = session.Scaling; + if (size.Width <= 0 || size.Height <= 0 || scaling < 0) { - GrContext = _grContext, - Canvas = surface.Canvas, - Dpi = SkiaPlatform.DefaultDpi * scaling, - VisualBrushRenderer = visualBrushRenderer, - DisableTextLcdRendering = true - }; + session.Dispose(); + throw new InvalidOperationException( + $"Can't create drawing context for surface with {size} size and {scaling} scaling"); + } - return new DrawingContextImpl(nfo, Disposable.Create(() => + gl.Viewport(0, 0, size.Width, size.Height); + gl.ClearStencil(0); + gl.ClearColor(0, 0, 0, 0); + gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + lock (_grContext) { + _grContext.ResetContext(); + + GRBackendRenderTarget renderTarget = + new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, + new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); + var surface = SKSurface.Create(_grContext, renderTarget, + GRSurfaceOrigin.BottomLeft, + GRPixelConfig.Rgba8888.ToColorType()); + + var nfo = new DrawingContextImpl.CreateInfo + { + GrContext = _grContext, + Canvas = surface.Canvas, + Dpi = SkiaPlatform.DefaultDpi * scaling, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + - surface.Canvas.Flush(); - surface.Dispose(); - renderTarget.Dispose(); - _grContext.Flush(); + var ctx = new DrawingContextImpl(nfo, Disposable.Create(() => + { + + surface.Canvas.Flush(); + surface.Dispose(); + renderTarget.Dispose(); + _grContext.Flush(); + session.Dispose(); + })); + success = true; + return ctx; + } + } + finally + { + if(!success) session.Dispose(); - })); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e90d444c44..39d801eb2f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -174,7 +174,7 @@ namespace Avalonia.Direct2D1.Media /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { if (pen != null) { @@ -202,7 +202,7 @@ namespace Avalonia.Direct2D1.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { if (brush != null) { @@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius) { using (var brush = CreateBrush(pen.Brush, rect.Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 7c8ddaca3f..51ca2520ad 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media public Geometry Geometry { get; } /// - public Rect GetRenderBounds(Avalonia.Media.Pen pen) + public Rect GetRenderBounds(Avalonia.Media.IPen pen) { return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia(); } @@ -46,7 +46,7 @@ namespace Avalonia.Direct2D1.Media } /// - public bool StrokeContains(Avalonia.Media.Pen pen, Point point) + public bool StrokeContains(Avalonia.Media.IPen pen, Point point) { return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0)); } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 6b0d30f250..065895859d 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The render target. /// The Direct2D brush. - public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget) + public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, SharpDX.Direct2D1.RenderTarget renderTarget) { return pen.ToDirect2DStrokeStyle(renderTarget.Factory); } @@ -120,7 +120,7 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The factory associated with this resource. /// The Direct2D brush. - public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory) + public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, Factory factory) { var d2dLineCap = pen.LineCap.ToDirect2D(); diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index c89d0a15cf..71c398481b 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -142,27 +142,33 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnLostFocus(RoutedEventArgs e) => LostFocus?.Invoke(); - InputModifiers GetModifiers() + RawInputModifiers GetModifiers(MouseEventArgs e) { var state = Keyboard.Modifiers; - var rv = default(InputModifiers); + var rv = default(RawInputModifiers); if (state.HasFlag(ModifierKeys.Windows)) - rv |= InputModifiers.Windows; + rv |= RawInputModifiers.Meta; if (state.HasFlag(ModifierKeys.Alt)) - rv |= InputModifiers.Alt; + rv |= RawInputModifiers.Alt; if (state.HasFlag(ModifierKeys.Control)) - rv |= InputModifiers.Control; + rv |= RawInputModifiers.Control; if (state.HasFlag(ModifierKeys.Shift)) - rv |= InputModifiers.Shift; - //TODO: mouse modifiers - - + rv |= RawInputModifiers.Shift; + if (e != null) + { + if (e.LeftButton == MouseButtonState.Pressed) + rv |= RawInputModifiers.LeftMouseButton; + if (e.RightButton == MouseButtonState.Pressed) + rv |= RawInputModifiers.RightMouseButton; + if (e.MiddleButton == MouseButtonState.Pressed) + rv |= RawInputModifiers.MiddleMouseButton; + } return rv; } void MouseEvent(RawPointerEventType type, MouseEventArgs e) => _ttl.Input?.Invoke(new RawPointerEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type, - e.GetPosition(this).ToAvaloniaPoint(), GetModifiers())); + e.GetPosition(this).ToAvaloniaPoint(), GetModifiers(e))); protected override void OnMouseDown(MouseButtonEventArgs e) { @@ -201,19 +207,19 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseWheel(MouseWheelEventArgs e) => _ttl.Input?.Invoke(new RawMouseWheelEventArgs(_mouse, (uint) e.Timestamp, _inputRoot, - e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers())); + e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers(e))); protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e); protected override void OnKeyDown(KeyEventArgs e) => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, (Key) e.Key, - GetModifiers())); + GetModifiers(null))); protected override void OnKeyUp(KeyEventArgs e) => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, RawKeyEventType.KeyUp, (Key)e.Key, - GetModifiers())); + GetModifiers(null))); protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, e.Text)); @@ -240,5 +246,7 @@ namespace Avalonia.Win32.Interop.Wpf return new Vector(1, 1); return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22); } + + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index f1fd74f931..b45138c27a 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -56,7 +56,7 @@ namespace Avalonia.Win32 {StandardCursorType.Wait, 32514}, //Same as SizeNorthSouth {StandardCursorType.TopSide, 32645}, - {StandardCursorType.BottomSize, 32645}, + {StandardCursorType.BottomSide, 32645}, //Same as SizeWestEast {StandardCursorType.LeftSide, 32644}, {StandardCursorType.RightSide, 32644}, diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs index a1bc5023a5..a8d74571a1 100644 --- a/src/Windows/Avalonia.Win32/DragSource.cs +++ b/src/Windows/Avalonia.Win32/DragSource.cs @@ -8,10 +8,11 @@ namespace Avalonia.Win32 { class DragSource : IPlatformDragSource { - public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + public Task DoDragDrop(PointerEventArgs triggerEvent, + IDataObject data, DragDropEffects allowedEffects) { Dispatcher.UIThread.VerifyAccess(); - + triggerEvent.Pointer.Capture(null); OleDragSource src = new OleDragSource(); DataObject dataObject = new DataObject(data); int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index fee1fe2ae6..fda5483b00 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -14,31 +14,31 @@ namespace Avalonia.Win32.Input public new static WindowsKeyboardDevice Instance { get; } = new WindowsKeyboardDevice(); - public InputModifiers Modifiers + public RawInputModifiers Modifiers { get { UpdateKeyStates(); - InputModifiers result = 0; + RawInputModifiers result = 0; if (IsDown(Key.LeftAlt) || IsDown(Key.RightAlt)) { - result |= InputModifiers.Alt; + result |= RawInputModifiers.Alt; } if (IsDown(Key.LeftCtrl) || IsDown(Key.RightCtrl)) { - result |= InputModifiers.Control; + result |= RawInputModifiers.Control; } if (IsDown(Key.LeftShift) || IsDown(Key.RightShift)) { - result |= InputModifiers.Shift; + result |= RawInputModifiers.Shift; } if (IsDown(Key.LWin) || IsDown(Key.RWin)) { - result |= InputModifiers.Windows; + result |= RawInputModifiers.Meta; } return result; diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 28065566d0..8ac0f25598 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -45,23 +45,23 @@ namespace Avalonia.Win32 return result; } - private static InputModifiers ConvertKeyState(int grfKeyState) + private static RawInputModifiers ConvertKeyState(int grfKeyState) { - InputModifiers modifiers = InputModifiers.None; + var modifiers = RawInputModifiers.None; var state = (UnmanagedMethods.ModifierKeys)grfKeyState; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) - modifiers |= InputModifiers.LeftMouseButton; + modifiers |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) - modifiers |= InputModifiers.MiddleMouseButton; + modifiers |= RawInputModifiers.MiddleMouseButton; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) - modifiers |= InputModifiers.RightMouseButton; + modifiers |= RawInputModifiers.RightMouseButton; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_SHIFT)) - modifiers |= InputModifiers.Shift; + modifiers |= RawInputModifiers.Shift; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_CONTROL)) - modifiers |= InputModifiers.Control; + modifiers |= RawInputModifiers.Control; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_ALT)) - modifiers |= InputModifiers.Alt; + modifiers |= RawInputModifiers.Alt; return modifiers; } @@ -127,7 +127,7 @@ namespace Avalonia.Win32 default(Point), null, DragDropEffects.None, - InputModifiers.None + RawInputModifiers.None )); return UnmanagedMethods.HRESULT.S_OK; } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 39f1a95466..c9aa1ce4e7 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -57,5 +58,19 @@ namespace Avalonia.Win32 return base.WndProc(hWnd, msg, wParam, lParam); } } + + public PopupImpl(IWindowBaseImpl parent) + { + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + Resize(size); + //TODO: We ignore the scaling override for now + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Windows/Avalonia.Win32/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs deleted file mode 100644 index 7dbb745a23..0000000000 --- a/src/Windows/Avalonia.Win32/RenderTimer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Threading; -using Avalonia.Rendering; -using Avalonia.Win32.Interop; - -namespace Avalonia.Win32 -{ - internal class RenderTimer : DefaultRenderTimer - { - private UnmanagedMethods.WaitOrTimerCallback timerDelegate; - - private static IntPtr _timerQueue; - - private static void EnsureTimerQueueCreated() - { - if (Volatile.Read(ref _timerQueue) == null) - { - var queue = UnmanagedMethods.CreateTimerQueue(); - if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero) - { - UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero); - } - } - } - - public RenderTimer(int framesPerSecond) - : base(framesPerSecond) - { - } - - protected override IDisposable StartCore(Action tick) - { - EnsureTimerQueueCreated(); - var msPerFrame = 1000 / FramesPerSecond; - - timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount)); - - UnmanagedMethods.CreateTimerQueueTimer( - out var timer, - _timerQueue, - timerDelegate, - IntPtr.Zero, - (uint)msPerFrame, - (uint)msPerFrame, - 0 - ); - - return Disposable.Create(() => - { - timerDelegate = null; - UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero); - }); - } - } -} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c45bf6389e..bc40ec2ff7 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -41,6 +41,7 @@ namespace Avalonia public bool UseDeferredRendering { get; set; } = true; public bool AllowEglInitialization { get; set; } public bool? EnableMultitouch { get; set; } + public bool OverlayPopups { get; set; } } } @@ -61,6 +62,7 @@ namespace Avalonia.Win32 } public static bool UseDeferredRendering => Options.UseDeferredRendering; + internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size( @@ -84,7 +86,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new RenderTimer(60)) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToSingleton() @@ -210,11 +212,6 @@ namespace Avalonia.Win32 return embedded; } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } - public IWindowIconImpl LoadIcon(string fileName) { using (var stream = File.OpenRead(fileName)) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2f7805884d..9bd58c10bc 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -131,6 +131,8 @@ namespace Avalonia.Win32 } } + public void Move(PixelPoint point) => Position = point; + public void SetMinMaxSize(Size minSize, Size maxSize) { _minSize = minSize; @@ -248,10 +250,7 @@ namespace Avalonia.Win32 UnmanagedMethods.SetActiveWindow(_hwnd); } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } + public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); public void Dispose() { @@ -763,16 +762,16 @@ namespace Avalonia.Win32 return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } - static InputModifiers GetMouseModifiers(IntPtr wParam) + static RawInputModifiers GetMouseModifiers(IntPtr wParam) { var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam); var modifiers = WindowsKeyboardDevice.Instance.Modifiers; if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) - modifiers |= InputModifiers.LeftMouseButton; + modifiers |= RawInputModifiers.LeftMouseButton; if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) - modifiers |= InputModifiers.RightMouseButton; + modifiers |= RawInputModifiers.RightMouseButton; if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) - modifiers |= InputModifiers.MiddleMouseButton; + modifiers |= RawInputModifiers.MiddleMouseButton; return modifiers; } diff --git a/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs b/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs index be32d12315..cdf244d330 100644 --- a/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs +++ b/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs @@ -76,7 +76,7 @@ namespace Avalonia.iOS.Specific private void HandleKey(Key key, RawKeyEventType type) { - var rawKeyEvent = new RawKeyEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, type, key, InputModifiers.None); + var rawKeyEvent = new RawKeyEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, type, key, RawInputModifiers.None); _view.Input(rawKeyEvent); } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 15e8b35056..a5342b227f 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -92,7 +92,7 @@ namespace Avalonia.iOS _inputRoot, RawPointerEventType.LeftButtonUp, location, - InputModifiers.None)); + RawInputModifiers.None)); } } @@ -105,10 +105,10 @@ namespace Avalonia.iOS var location = touch.LocationInView(this).ToAvalonia(); _touchLastPoint = location; Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawPointerEventType.Move, location, InputModifiers.None)); + RawPointerEventType.Move, location, RawInputModifiers.None)); Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawPointerEventType.LeftButtonDown, location, InputModifiers.None)); + RawPointerEventType.LeftButtonDown, location, RawInputModifiers.None)); } } @@ -120,19 +120,21 @@ namespace Avalonia.iOS var location = touch.LocationInView(this).ToAvalonia(); if (iOSPlatform.MouseDevice.Captured != null) Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawPointerEventType.Move, location, InputModifiers.LeftMouseButton)); + RawPointerEventType.Move, location, RawInputModifiers.LeftMouseButton)); else { //magic number based on test - correction of 0.02 is working perfect double correction = 0.02; Input?.Invoke(new RawMouseWheelEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, - _inputRoot, location, (location - _touchLastPoint) * correction, InputModifiers.LeftMouseButton)); + _inputRoot, location, (location - _touchLastPoint) * correction, RawInputModifiers.LeftMouseButton)); } _touchLastPoint = location; } } public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); + + public IPopupImpl CreatePopup() => null; } } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 015a122677..ef7dc33f76 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -982,6 +982,8 @@ namespace Avalonia.Controls.UnitTests AutoCompleteBox control = CreateControl(); control.Items = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); + var window = new Window {Content = control}; + window.ApplyTemplate(); Dispatcher.UIThread.RunJobs(); test.Invoke(control, textBox); } @@ -1027,7 +1029,8 @@ namespace Avalonia.Controls.UnitTests var popup = new Popup { - Name = "PART_Popup" + Name = "PART_Popup", + PlacementTarget = control }.RegisterInNameScope(scope); var panel = new Panel(); diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 58d205deaa..522afc9546 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); int openedCount = 0; @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests openedCount++; }; - sut.Open(null); + sut.Open(target); Assert.Equal(1, openedCount); } @@ -53,9 +53,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); - sut.Open(null); + sut.Open(target); int closedCount = 0; @@ -84,7 +84,8 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + var window = new Window {Content = target}; + window.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -112,7 +113,8 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - var window = new Window { Content = target }; + var window = new Window {Content = target}; + window.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -151,7 +153,7 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] + [Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")] public void Cancelling_Closing_Leaves_ContextMenuOpen() { using (Application()) @@ -165,7 +167,9 @@ namespace Avalonia.Controls.UnitTests { ContextMenu = sut }; - new Window { Content = target }; + + var window = new Window {Content = target}; + window.ApplyTemplate(); sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; @@ -190,12 +194,12 @@ namespace Avalonia.Controls.UnitTests screenImpl.Setup(x => x.ScreenCount).Returns(1); screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) }); - var windowImpl = new Mock(); - windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); - - popupImpl = new Mock(); + popupImpl = MockWindowingPlatform.CreatePopupMock(); popupImpl.SetupGet(x => x.Scaling).Returns(1); + var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object); + windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); + var services = TestServices.StyledWindow.With( inputManager: new InputManager(), windowImpl: windowImpl.Object, diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index df804d5d8c..2b9197e20b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1357,5 +1357,36 @@ namespace Avalonia.Controls.UnitTests PrintColumnDefinitions(grid); Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); } + + [Fact] + public void Correct_Grid_Bounds_When_Child_Control_Has_DesiredSize_Larger_Than_Available_Space() + { + // Issue #2746 + var grid = new Grid + { + RowDefinitions = RowDefinitions.Parse("Auto"), + Children = + { + new TestControl + { + MeasureSize = new Size(150, 150), + } + } + }; + + var parent = new Decorator { Child = grid }; + + parent.Measure(new Size(100, 100)); + parent.Arrange(new Rect(grid.DesiredSize)); + + Assert.Equal(new Size(100, 100), grid.Bounds.Size); + } + + private class TestControl : Control + { + public Size MeasureSize { get; set; } + + protected override Size MeasureOverride(Size availableSize) => MeasureSize; + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 2a61ff1566..27ddd95d20 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -9,8 +9,8 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.Markup.Data; using Avalonia.Styling; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index ba4d6ca9c5..ff11bc513d 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,14 +11,14 @@ namespace Avalonia.Controls.UnitTests.Platform public class DefaultMenuInteractionHandlerTests { static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) - => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default); + => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, PointerPointProperties.None, default); static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source, - new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true}, + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), default); static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, - new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left); + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), default); public class TopLevel { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 7d05547799..6ab9c345d4 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -1,7 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -256,7 +260,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Child); } - [Fact] public void Should_Not_Bind_Old_Child_To_New_DataContext() { @@ -281,6 +284,65 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = 42; } + [Fact] + public void Should_Not_Bind_Child_To_Wrong_DataContext_When_Removing() + { + // Test for issue #2823 + var canvas = new Canvas(); + var (target, host) = CreateTarget(); + var viewModel = new TestViewModel { Content = "foo" }; + var dataContexts = new List(); + + target.Bind(ContentPresenter.ContentProperty, (IBinding)new TemplateBinding(ContentControl.ContentProperty)); + canvas.GetObservable(ContentPresenter.DataContextProperty).Subscribe(x => dataContexts.Add(x)); + + host.DataTemplates.Add(new FuncDataTemplate((_, __) => canvas)); + host.Bind(ContentControl.ContentProperty, new Binding(nameof(TestViewModel.Content))); + host.DataContext = viewModel; + + Assert.Same(canvas, target.Child); + + viewModel.Content = 42; + + Assert.Equal(new object[] + { + null, + "foo", + null, + }, dataContexts); + } + + [Fact] + public void Should_Set_InheritanceParent_Even_When_LogicalParent_Is_Already_Set() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var (target, host) = CreateTarget(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + + Assert.Same(logicalParent, child.Parent); + + // InheritanceParent is exposed via StylingParent. + Assert.Same(target, ((IStyledElement)child).StylingParent); + } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var (target, _) = CreateTarget(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.Content = null; + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl @@ -302,5 +364,25 @@ namespace Avalonia.Controls.UnitTests.Presenters { public IControl Child { get; set; } } + + private class TestViewModel : INotifyPropertyChanged + { + private object _content; + + public object Content + { + get => _content; + set + { + if (_content != value) + { + _content = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Content))); + } + } + } + + public event PropertyChangedEventHandler PropertyChanged; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index ab75a87110..59f3ae44c2 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -251,5 +251,21 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = 42; } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var target = new ContentPresenter(); + var root = new TestRoot(target); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.Content = null; + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs index 09970926fa..c30e81a1cb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs @@ -98,5 +98,22 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); Assert.IsType(target.Child); } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var target = new ContentPresenter(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.UpdateChild(); + target.Content = null; + target.UpdateChild(); + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 059146f17d..0ebe6833d3 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(((ILogical)target).IsAttachedToLogicalTree); } @@ -32,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(target.Presenter.IsAttachedToLogicalTree); } @@ -43,28 +43,70 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); - Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent); + Assert.Equal(target.Popup, ((IStyleHost)target.Popup.Host).StylingParent); } } + [Fact] + public void PopupRoot_Should_Have_Template_Applied() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var target = new Popup {PlacementMode = PlacementMode.Pointer}; + var child = new Control(); + + window.Content = target; + window.ApplyTemplate(); + target.Open(); + + + Assert.Single(((Visual)target.Host).GetVisualChildren()); + + var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); + + Assert.IsType(templatedChild); + var contentPresenter = templatedChild.VisualChildren.Single(); + Assert.IsType(contentPresenter); + + + Assert.Equal((PopupRoot)target.Host, ((IControl)templatedChild).TemplatedParent); + Assert.Equal((PopupRoot)target.Host, ((IControl)contentPresenter).TemplatedParent); + } + } + + [Fact] + public void PopupRoot_Should_Have_Null_VisualParent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Popup() {PlacementTarget = new Window()}; + + target.Open(); + + Assert.Null(((Visual)target.Host).GetVisualParent()); + } + } + [Fact] public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -88,8 +130,8 @@ namespace Avalonia.Controls.UnitTests.Primitives using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -117,22 +159,23 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); target.PopupContent = null; } } - private PopupRoot CreateTarget() + private PopupRoot CreateTarget(TopLevel popupParent) { - var result = new PopupRoot + var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup()) { Template = new FuncControlTemplate((parent, scope) => new ContentPresenter @@ -158,6 +201,7 @@ namespace Avalonia.Controls.UnitTests.Primitives new Popup { [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], + PlacementTarget = parent }); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 2e22725125..7cb9fccee8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class PopupTests { + protected bool UsePopupHost; + [Fact] public void Setting_Child_Should_Set_Child_Controls_LogicalParent() { @@ -137,20 +139,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new Popup(); - Assert.Null(target.PopupRoot); - } - } - - [Fact] - public void PopupRoot_Should_Have_Null_VisualParent() - { - using (CreateServices()) - { - var target = new Popup(); - - target.Open(); - - Assert.Null(target.PopupRoot.GetVisualParent()); + Assert.Null(((Visual)target.Host)); } } @@ -159,12 +148,12 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); + var target = new Popup() {PlacementTarget = PreparedWindow()}; target.Open(); - Assert.Equal(target, target.PopupRoot.Parent); - Assert.Equal(target, target.PopupRoot.GetLogicalParent()); + Assert.Equal(target, ((Visual)target.Host).Parent); + Assert.Equal(target, ((Visual)target.Host).GetLogicalParent()); } } @@ -173,15 +162,15 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); - var root = new TestRoot { Child = target }; + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; + var root = PreparedWindow(target); target.Open(); - var popupRoot = (ILogical)target.PopupRoot; + var popupRoot = (ILogical)((Visual)target.Host); Assert.True(popupRoot.IsAttachedToLogicalTree); - root.Child = null; + root.Content = null; Assert.False(((ILogical)target).IsAttachedToLogicalTree); } } @@ -191,8 +180,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); - var target = new Popup(); + var window = PreparedWindow(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; @@ -214,10 +203,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); - var target = new Popup(); + var window = PreparedWindow(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; + window.ApplyTemplate(); target.Open(); int closedCount = 0; @@ -233,46 +223,28 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - [Fact] - public void PopupRoot_Should_Have_Template_Applied() - { - using (CreateServices()) - { - var window = new Window(); - var target = new Popup(); - var child = new Control(); - - window.Content = target; - target.Open(); - - Assert.Single(target.PopupRoot.GetVisualChildren()); - - var templatedChild = target.PopupRoot.GetVisualChildren().Single(); - Assert.IsType(templatedChild); - Assert.Equal(target.PopupRoot, ((IControl)templatedChild).TemplatedParent); - } - } - + [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { using (CreateServices()) { PopupContentControl target; - var root = new TestRoot + var root = PreparedWindow(target = new PopupContentControl { - Child = target = new PopupContentControl - { - Content = new Border(), - Template = new FuncControlTemplate(PopupContentControlTemplate), - }, - StylingParent = AvaloniaLocator.Current.GetService() - }; + Content = new Border(), + Template = new FuncControlTemplate(PopupContentControlTemplate), + }); + root.Show(); target.ApplyTemplate(); + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); popup.Open(); - var popupRoot = popup.PopupRoot; + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); var children = popupRoot.GetVisualDescendants().ToList(); var types = children.Select(x => x.GetType().Name).ToList(); @@ -280,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "VisualLayerManager", "ContentPresenter", "ContentPresenter", "Border", @@ -293,6 +266,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new object[] { + popupRoot, popupRoot, target, null, @@ -301,6 +275,13 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + Window PreparedWindow(object content = null) + { + var w = new Window {Content = content}; + w.ApplyTemplate(); + return w; + } + [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -311,6 +292,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = child = new TestControl(), DataContext = "foo", + PlacementTarget = PreparedWindow() }; var beginCalled = false; @@ -330,46 +312,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(beginCalled); } } - - - private static IDisposable CreateServices() + + [Fact] + public void Popup_Host_Type_Should_Match_Platform_Preference() { - var result = AvaloniaLocator.EnterScope(); - - var styles = new Styles + using (CreateServices()) { - new Style(x => x.OfType()) - { - Setters = new[] - { - new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(PopupRootTemplate)), - } - }, - }; - - var globalStyles = new Mock(); - globalStyles.Setup(x => x.IsStylesInitialized).Returns(true); - globalStyles.Setup(x => x.Styles).Returns(styles); - - var renderInterface = new Mock(); - - AvaloniaLocator.CurrentMutable - .Bind().ToFunc(() => globalStyles.Object) - .Bind().ToConstant(new WindowingPlatformMock()) - .Bind().ToTransient() - .Bind().ToFunc(() => renderInterface.Object) - .Bind().ToConstant(new InputManager()); - - return result; + var target = new Popup() {PlacementTarget = PreparedWindow()}; + + target.Open(); + if (UsePopupHost) + Assert.IsType(target.Host); + else + Assert.IsType(target.Host); + } } - private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) + private IDisposable CreateServices() { - return new ContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }.RegisterInNameScope(scope); + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + () => + { + if(UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock().Object; + }))); } private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) @@ -377,6 +345,7 @@ namespace Avalonia.Controls.UnitTests.Primitives return new Popup { Name = "popup", + PlacementTarget = control, Child = new ContentPresenter { [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], @@ -401,4 +370,12 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } + + public class PopupTestsWithPopupRoot : PopupTests + { + public PopupTestsWithPopupRoot() + { + UsePopupHost = true; + } + } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 4bcfeb6d03..be0f4272a5 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -13,7 +13,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Markup.Data; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index 722ad1c8ee..db113f0569 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -210,13 +210,13 @@ namespace Avalonia.Controls.UnitTests new[] { new Rect(0, 0, 50, 10), - new Rect(0, 10, 150, 10), + new Rect(0, 10, 100, 10), new Rect(25, 20, 50, 10), - new Rect(-25, 30, 150, 10), + new Rect(0, 30, 100, 10), new Rect(50, 40, 50, 10), - new Rect(-50, 50, 150, 10), + new Rect(0, 50, 100, 10), new Rect(0, 60, 100, 10), - new Rect(0, 70, 150, 10), + new Rect(0, 70, 100, 10), }, bounds); } @@ -283,13 +283,13 @@ namespace Avalonia.Controls.UnitTests new[] { new Rect(0, 0, 10, 50), - new Rect(10, 0, 10, 150), + new Rect(10, 0, 10, 100), new Rect(20, 25, 10, 50), - new Rect(30, -25, 10, 150), + new Rect(30, 0, 10, 100), new Rect(40, 50, 10, 50), - new Rect(50, -50, 10, 150), + new Rect(50, 0, 10, 100), new Rect(60, 0, 10, 100), - new Rect(70, 0, 10, 150), + new Rect(70, 0, 10, 100), }, bounds); } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 35f0b39210..febc1de5f9 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests Text = "1234" }; - RaiseKeyEvent(target, Key.A, InputModifiers.Control); + RaiseKeyEvent(target, Key.A, KeyModifiers.Control); Assert.Equal(0, target.SelectionStart); Assert.Equal(4, target.SelectionEnd); @@ -72,7 +72,7 @@ namespace Avalonia.Controls.UnitTests Template = CreateTemplate() }; - RaiseKeyEvent(target, Key.A, InputModifiers.Control); + RaiseKeyEvent(target, Key.A, KeyModifiers.Control); Assert.Equal(0, target.SelectionStart); Assert.Equal(0, target.SelectionEnd); @@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests Text = "1234" }; - RaiseKeyEvent(target, Key.Z, InputModifiers.Control); + RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); Assert.Equal("1234", target.Text); } @@ -136,29 +136,29 @@ namespace Avalonia.Controls.UnitTests }; // (First| Second Third Fourth) - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" Second Third Fourth", textBox.Text); // ( Second |Third Fourth) textBox.CaretIndex = 8; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" Third Fourth", textBox.Text); // ( Thi|rd Fourth) textBox.CaretIndex = 4; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" rd Fourth", textBox.Text); // ( rd F[ou]rth) textBox.SelectionStart = 5; textBox.SelectionEnd = 7; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" rd Frth", textBox.Text); // ( |rd Frth) textBox.CaretIndex = 1; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal("rd Frth", textBox.Text); } } @@ -175,30 +175,30 @@ namespace Avalonia.Controls.UnitTests }; // (First Second Third |Fourth) - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("First Second Third ", textBox.Text); // (First Second |Third ) textBox.CaretIndex = 13; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("First Second ", textBox.Text); // (First Sec|ond ) textBox.CaretIndex = 9; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("First Sec", textBox.Text); // (Fi[rs]t Sec ) textBox.SelectionStart = 2; textBox.SelectionEnd = 4; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("Fit Sec", textBox.Text); // (Fit Sec| ) textBox.Text += " "; textBox.CaretIndex = 7; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("Fit Sec", textBox.Text); } } @@ -486,12 +486,12 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } - private void RaiseKeyEvent(TextBox textBox, Key key, InputModifiers inputModifiers) + private void RaiseKeyEvent(TextBox textBox, Key key, KeyModifiers inputModifiers) { textBox.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, - Modifiers = inputModifiers, + KeyModifiers = inputModifiers, Key = key }); } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 0ee772425b..c744543f99 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -183,7 +183,7 @@ namespace Avalonia.Controls.UnitTests new Mock().Object, 0, RawKeyEventType.KeyDown, - Key.A, InputModifiers.None); + Key.A, RawInputModifiers.None); impl.Object.Input(input); inputManagerMock.Verify(x => x.ProcessInput(input)); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 5646e86f7a..f51a50c0d5 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -619,7 +619,7 @@ namespace Avalonia.Controls.UnitTests { RoutedEvent = InputElement.KeyDownEvent, Key = selectAllGesture.Key, - Modifiers = selectAllGesture.Modifiers + KeyModifiers = selectAllGesture.KeyModifiers }; target.RaiseEvent(keyEvent); @@ -665,7 +665,7 @@ namespace Avalonia.Controls.UnitTests { RoutedEvent = InputElement.KeyDownEvent, Key = selectAllGesture.Key, - Modifiers = selectAllGesture.Modifiers + KeyModifiers = selectAllGesture.KeyModifiers }; target.RaiseEvent(keyEvent); @@ -711,7 +711,7 @@ namespace Avalonia.Controls.UnitTests { RoutedEvent = InputElement.KeyDownEvent, Key = selectAllGesture.Key, - Modifiers = selectAllGesture.Modifiers + KeyModifiers = selectAllGesture.KeyModifiers }; target.RaiseEvent(keyEvent); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 3ee6a50e69..55e8ae0115 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -20,33 +20,6 @@ namespace Avalonia.Controls.UnitTests { public class WindowBaseTests { - [Fact] - public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = Mock.Of(x => x.Scaling == 1); - - Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { }); - - var target = new TestWindowBase(impl) - { - Template = CreateTemplate(), - Content = new TextBlock - { - Width = 321, - Height = 432, - }, - IsVisible = true, - }; - - target.LayoutManager.ExecuteInitialLayoutPass(target); - - Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); - } - } - - [Fact] public void Activate_Should_Call_Impl_Activate() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f4d9a91d0c..75239f014f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -277,8 +277,7 @@ namespace Avalonia.Controls.UnitTests var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); windowImpl.Setup(x => x.Scaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(screens.Object); @@ -302,14 +301,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() { - var parentWindowImpl = new Mock(); - parentWindowImpl.SetupProperty(x => x.Position); + var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); parentWindowImpl.Setup(x => x.Scaling).Returns(1); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); windowImpl.Setup(x => x.Scaling).Returns(1); diff --git a/tests/Avalonia.Input.UnitTests/GesturesTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs new file mode 100644 index 0000000000..39c219a773 --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -0,0 +1,208 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Interactivity.UnitTests +{ + public class GesturesTests + { + private MouseTestHelper _mouse = new MouseTestHelper(); + + [Fact] + public void Tapped_Should_Follow_Pointer_Pressed_Released() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, false); + + _mouse.Click(border); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_Even_When_Pressed_Released_Handled() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, true); + + _mouse.Click(border); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_For_Middle_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Middle); + + Assert.True(raised); + } + + [Fact] + public void Tapped_Should_Not_Be_Raised_For_Right_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Right); + + Assert.False(raised); + } + + [Fact] + public void RightTapped_Should_Be_Raised_For_Right_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.RightTappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Right); + + Assert.True(raised); + } + + [Fact] + public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, false); + + _mouse.Click(border); + _mouse.Down(border, clickCount: 2); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Be_Raised_Even_When_Pressed_Released_Handled() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, true); + + _mouse.Click(border); + _mouse.Down(border, clickCount: 2); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Be_Raised_For_Middle_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Middle); + _mouse.Down(border, MouseButton.Middle, clickCount: 2); + + Assert.True(raised); + } + + [Fact] + public void DoubleTapped_Should_Not_Be_Raised_For_Right_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Right); + _mouse.Down(border, MouseButton.Right, clickCount: 2); + + Assert.False(raised); + } + + private void AddHandlers( + Decorator decorator, + Border border, + IList result, + bool markHandled) + { + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => + { + result.Add("dp"); + + if (markHandled) + { + e.Handled = true; + } + }); + + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => + { + result.Add("dr"); + + if (markHandled) + { + e.Handled = true; + } + }); + + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + } + } +} diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 983f541c2a..214aead521 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -231,7 +231,7 @@ namespace Avalonia.Input.UnitTests root, RawPointerEventType.Move, p, - InputModifiers.None)); + RawInputModifiers.None)); } private void SetHit(Mock renderer, IControl hit) diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 7316b1de3d..2bde78ad63 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -20,7 +20,7 @@ - + diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs deleted file mode 100644 index 69bdf58f9d..0000000000 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.UnitTests; -using Avalonia.Input; -using Xunit; - -namespace Avalonia.Interactivity.UnitTests -{ - public class GestureTests - { - private MouseTestHelper _mouse = new MouseTestHelper(); - - [Fact] - public void Tapped_Should_Follow_Pointer_Pressed_Released() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - - _mouse.Click(border); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); - } - - [Fact] - public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - border.AddHandler(Border.PointerPressedEvent, (s, e) => e.Handled = true); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - - _mouse.Click(border); - - Assert.Equal(new[] { "bt", "dt" }, result); - } - - [Fact] - public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); - - _mouse.Click(border); - _mouse.Down(border, clickCount: 2); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); - } - - [Fact] - public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => - { - result.Add("dp"); - e.Handled = true; - }); - - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); - - _mouse.Click(border); - _mouse.Down(border, clickCount: 2); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result); - } - } -} diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index a841174d2d..1da4746516 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -401,6 +401,10 @@ namespace Avalonia.LeakTests { } + public void RecalculateChildren(IVisual visual) + { + } + public void Resized(Size size) { } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs index 6ffaaaee5c..b424003ed6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs @@ -3,7 +3,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Converters { - public class ConverterTests + public class ConverterTests : XamlTestBase { [Fact] public void Bug_2228_Relative_Uris_Should_Be_Correctly_Parsed() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs index bb44d069b5..cdd40ed80f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs @@ -11,7 +11,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters public Orientation? Orientation { get; set; } } - public class NullableConverterTests + public class NullableConverterTests : XamlTestBase { [Fact] public void Nullable_Types_Should_Still_Be_Converted_Properly() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs index 6f2c4363e2..5e698117c3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Converters { - public class ValueConverterTests + public class ValueConverterTests : XamlTestBase { [Fact] public void ValueConverter_Special_Values_Work() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index e412657711..5972920af3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data { - public class BindingTests + public class BindingTests : XamlTestBase { [Fact] public void Binding_With_Null_Path_Works() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index 0d96df8eb8..db45f1989b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -10,7 +10,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data { - public class BindingTests_Method + public class BindingTests_Method : XamlTestBase { [Fact] public void Binding_Method_To_Command_Works() @@ -102,4 +102,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data public string Value { get; private set; } = "Not called"; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs index a9bea01fde..86ca351d67 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -10,7 +10,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data { - public class BindingTests_TemplatedParent + public class BindingTests_TemplatedParent : XamlTestBase { [Fact] public void TemplateBinding_With_Null_Path_Works() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index dcecfe3b22..c3bc649abb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Styling; using Avalonia.UnitTests; @@ -10,7 +11,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions { - public class BindingExtensionTests + public class BindingExtensionTests : XamlTestBase { [Fact] @@ -59,11 +60,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions new Setter( Window.TemplateProperty, new FuncControlTemplate((x, scope) => - new ContentPresenter + new VisualLayerManager { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], - }.RegisterInNameScope(scope))) + Child = + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], + }.RegisterInNameScope(scope) + })) } }; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index ed70cd6fe8..96955539c1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -15,7 +15,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions { - public class DynamicResourceExtensionTests + public class DynamicResourceExtensionTests : XamlTestBase { [Fact] public void DynamicResource_Can_Be_Assigned_To_Property() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs index a35c7bdd9b..7ab6c2de40 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions { public class ResourceIncludeTests { - public class StaticResourceExtensionTests + public class StaticResourceExtensionTests : XamlTestBase { [Fact] public void ResourceInclude_Loads_ResourceDictionary() @@ -52,4 +52,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index 7a96b9f989..58985af0ad 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -14,7 +14,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions { - public class StaticResourceExtensionTests + public class StaticResourceExtensionTests : XamlTestBase { [Fact] public void StaticResource_Can_Be_Assigned_To_Property() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs index f4c3302d52..2dc6c4a7fb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs @@ -12,7 +12,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests { - public class StyleTests + public class StyleTests : XamlTestBase { [Fact] public void Binding_Should_Be_Assigned_To_Setter_Value_Instead_Of_Bound() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index d74eed992e..f4d4a9dd2a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -22,7 +22,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class BasicTests + public class BasicTests : XamlTestBase { [Fact] public void Simple_Property_Is_Set() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 3930608515..7281542bc1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class BindingTests + public class BindingTests : XamlTestBase { [Fact] public void Binding_To_DataContext_Works() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index c6fe79bc0c..86b874f75c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class BindingTests_RelativeSource + public class BindingTests_RelativeSource : XamlTestBase { [Fact] public void Binding_To_DataContext_Works() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index bd9d99ff23..0850f3fa78 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -4,14 +4,13 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Layout; using Avalonia.Logging; using Avalonia.UnitTests; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class ControlBindingTests + public class ControlBindingTests : XamlTestBase { [Fact] public void Binding_ProgressBar_Value_To_Invalid_Value_Uses_FallbackValue() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 6b67303b07..4f2886582d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class DataTemplateTests + public class DataTemplateTests : XamlTestBase { [Fact] public void DataTemplate_Can_Contain_Name() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs index 44697f5937..dcb6533b5e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs @@ -9,7 +9,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class EventTests + public class EventTests : XamlTestBase { [Fact] public void Event_Is_Attached() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 8dd1d24dd6..b76022852c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -12,7 +12,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class StyleTests + public class StyleTests : XamlTestBase { [Fact] public void Color_Can_Be_Added_To_Style_Resources() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs index 4134f5be23..f5fed02899 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs @@ -4,14 +4,13 @@ using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Templates; using Avalonia.UnitTests; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { - public class TreeDataTemplateTests + public class TreeDataTemplateTests : XamlTestBase { [Fact] public void Binding_Should_Be_Assigned_To_ItemsSource_Instead_Of_Bound() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 1f135f8e76..4ff9e3db38 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -5,10 +5,7 @@ using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls; -using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; -using Avalonia.Input; -using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; using Avalonia.UnitTests; @@ -18,7 +15,7 @@ using Xunit; namespace Avalonia.Markup.Xaml.UnitTests { - public class XamlIlTests + public class XamlIlTests : XamlTestBase { [Fact] public void Binding_Button_IsPressed_ShouldWork() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs new file mode 100644 index 0000000000..5172b2e830 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Markup.Xaml.UnitTests +{ + public class XamlTestBase + { + public XamlTestBase() + { + // Ensure necessary assemblies are loaded. + var _ = typeof(TemplateBinding); + } + } +} diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index 876f37cc9e..56b14c3936 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -60,6 +60,28 @@ namespace Avalonia.ReactiveUI.UnitTests } } + [Fact] + public void AutoSuspendHelper_Should_Throw_When_Not_Supported_Lifetime_Is_Used() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) + { + var application = AvaloniaLocator.Current.GetService(); + application.ApplicationLifetime = lifetime; + Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime)); + } + } + + [Fact] + public void AutoSuspendHelper_Should_Throw_When_Lifetime_Is_Null() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var application = AvaloniaLocator.Current.GetService(); + Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime)); + } + } + [Fact] public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() { @@ -82,17 +104,5 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().Example); } } - - [Fact] - public void AutoSuspendHelper_Should_Throw_For_Not_Supported_Lifetimes() - { - using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) - using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) - { - var application = AvaloniaLocator.Current.GetService(); - application.ApplicationLifetime = lifetime; - Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime)); - } - } } } \ No newline at end of file diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index f065fcb63d..272b1fc489 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -1,9 +1,11 @@  netstandard2.0 + latest false Library false + latest diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs index 63da9ed3f0..4fa3fbf523 100644 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs @@ -47,12 +47,12 @@ namespace Avalonia.UnitTests return _context.FillContains(point); } - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { return false; } - public Rect GetRenderBounds(Pen pen) => Bounds; + public Rect GetRenderBounds(IPen pen) => Bounds; public IGeometryImpl Intersect(IGeometryImpl geometry) { diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 36297bf58b..c33ec72141 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Moq; using Avalonia.Platform; @@ -15,16 +17,48 @@ namespace Avalonia.UnitTests _popupImpl = popupImpl; } + public static Mock CreateWindowMock(Func popupImpl = null) + { + var win = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(win); + mock.Setup(x => x.CreatePopup()).Returns(() => + { + if (popupImpl != null) + return popupImpl(); + return CreatePopupMock().Object; + + }); + PixelPoint pos = default; + mock.SetupGet(x => x.Position).Returns(() => pos); + mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np)); + SetupToplevel(mock); + return mock; + } + + static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + { + mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + } + + public static Mock CreatePopupMock() + { + var positioner = Mock.Of(); + var popup = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(popup); + mock.SetupGet(x => x.PopupPositioner).Returns(positioner); + SetupToplevel(mock); + + return mock; + } + public IWindowImpl CreateWindow() { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object; } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs similarity index 75% rename from tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs rename to tests/Avalonia.UnitTests/MouseTestHelper.cs index 373bbaed75..d6e64936c7 100644 --- a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -1,9 +1,8 @@ -using System.Reactive; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; -namespace Avalonia.Controls.UnitTests +namespace Avalonia.UnitTests { public class MouseTestHelper { @@ -33,7 +32,8 @@ namespace Avalonia.Controls.UnitTests private MouseButton _pressedButton; - InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons; + KeyModifiers GetModifiers(InputModifiers modifiers) => + (KeyModifiers)((int)modifiers & (int)RawInputModifiers.KeyboardMask); public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, InputModifiers modifiers = default, int clickCount = 1) @@ -45,7 +45,11 @@ namespace Avalonia.Controls.UnitTests Point position = default, InputModifiers modifiers = default, int clickCount = 1) { _pressedButtons |= Convert(mouseButton); - var props = new PointerPointProperties(_pressedButtons); + var props = new PointerPointProperties((RawInputModifiers)_pressedButtons, + mouseButton == MouseButton.Left ? PointerUpdateKind.LeftButtonPressed + : mouseButton == MouseButton.Middle ? PointerUpdateKind.MiddleButtonPressed + : mouseButton == MouseButton.Right ? PointerUpdateKind.RightButtonPressed : PointerUpdateKind.Other + ); if (ButtonCount(props) > 1) Move(target, source, position); else @@ -61,7 +65,7 @@ namespace Avalonia.Controls.UnitTests public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position, - Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers))); + Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), GetModifiers(modifiers))); } public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, @@ -73,13 +77,17 @@ namespace Avalonia.Controls.UnitTests { var conv = Convert(mouseButton); _pressedButtons = (_pressedButtons | conv) ^ conv; - var props = new PointerPointProperties(_pressedButtons); + var props = new PointerPointProperties((RawInputModifiers)_pressedButtons, + mouseButton == MouseButton.Left ? PointerUpdateKind.LeftButtonReleased + : mouseButton == MouseButton.Middle ? PointerUpdateKind.MiddleButtonReleased + : mouseButton == MouseButton.Right ? PointerUpdateKind.RightButtonReleased : PointerUpdateKind.Other + ); if (ButtonCount(props) == 0) { _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, Timestamp(), props, - GetModifiers(modifiers), _pressedButton)); + GetModifiers(modifiers))); } else Move(target, source, position); @@ -98,13 +106,13 @@ namespace Avalonia.Controls.UnitTests public void Enter(IInteractive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, - Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); + Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), KeyModifiers.None)); } public void Leave(IInteractive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, - Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); + Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), KeyModifiers.None)); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs new file mode 100644 index 0000000000..418ac7576b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs @@ -0,0 +1,91 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class PenTests + { + [Fact] + public void Changing_Thickness_Raises_Invalidated() + { + var target = new Pen(); + var raised = false; + + target.Invalidated += (s, e) => raised = true; + target.Thickness = 18; + + Assert.True(raised); + } + + [Fact] + public void Changing_Brush_Color_Raises_Invalidated() + { + var brush = new SolidColorBrush(Colors.Red); + var target = new Pen { Brush = brush }; + var raised = false; + + target.Invalidated += (s, e) => raised = true; + brush.Color = Colors.Green; + + Assert.True(raised); + } + + [Fact] + public void Changing_DashStyle_Dashes_Raises_Invalidated() + { + var dashes = new DashStyle(); + var target = new Pen { DashStyle = dashes }; + var raised = false; + + target.Invalidated += (s, e) => raised = true; + dashes.Dashes = new[] { 0.1, 0.2 }; + + Assert.True(raised); + } + + [Fact] + public void Equality_Is_Implemented_Between_Immutable_And_Mmutable_Pens() + { + var brush = new SolidColorBrush(Colors.Red); + var target1 = new ImmutablePen( + brush: brush, + thickness: 2, + dashStyle: (ImmutableDashStyle)DashStyle.Dash, + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + var target2 = new Pen( + brush: brush, + thickness: 2, + dashStyle: DashStyle.Dash, + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + + Assert.True(Equals(target1, target2)); + } + + [Fact] + public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles() + { + var brush = new SolidColorBrush(Colors.Red); + var target1 = new ImmutablePen( + brush: brush, + thickness: 2, + dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5), + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + var target2 = new Pen( + brush: brush, + thickness: 2, + dashStyle: new DashStyle(new[] { 0.1, 0.2 }, 5), + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + + Assert.True(Equals(target1, target2)); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index f094d9c78d..4c302a24a2 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -96,6 +96,180 @@ namespace Avalonia.Visuals.UnitTests.Rendering Assert.Equal(new List { root, decorator, border, canvas }, result); } + [Fact] + public void Should_Update_VisualNode_Order_On_Child_Remove_Insert() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children= + { + (canvas1 = new Canvas()), + (canvas2 = new Canvas()), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + stack.Children.Remove(canvas2); + stack.Children.Insert(0, canvas2); + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + + [Fact] + public void Should_Update_VisualNode_Order_On_Child_Move() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children = + { + (canvas1 = new Canvas()), + (canvas2 = new Canvas()), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + stack.Children.Move(1, 0); + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + + [Fact] + public void Should_Update_VisualNode_Order_On_ZIndex_Change() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children = + { + (canvas1 = new Canvas { ZIndex = 1 }), + (canvas2 = new Canvas { ZIndex = 2 }), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + canvas1.ZIndex = 3; + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + + [Fact] + public void Should_Update_VisualNode_Order_On_ZIndex_Change_With_Dirty_Ancestor() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children = + { + (canvas1 = new Canvas { ZIndex = 1 }), + (canvas2 = new Canvas { ZIndex = 2 }), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + root.InvalidateVisual(); + canvas1.ZIndex = 3; + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + [Fact] public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() { diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs index 1101ccacba..24ba2d1c48 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs @@ -92,5 +92,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Same(node1.DrawOperations[0].Item, node2.DrawOperations[0].Item); Assert.NotSame(node1.DrawOperations[0], node2.DrawOperations[0]); } + + [Fact] + public void SortChildren_Does_Not_Throw_On_Null_Children() + { + var node = new VisualNode(Mock.Of(), null); + var scene = new Scene(Mock.Of()); + + node.SortChildren(scene); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs index 504f0ada86..936a5d16a2 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs @@ -282,5 +282,52 @@ namespace Avalonia.Visuals.UnitTests Assert.True(called); } + + [Fact] + public void Changing_ZIndex_Should_InvalidateVisual() + { + Canvas canvas1; + var renderer = new Mock(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + (canvas1 = new Canvas()), + new Canvas(), + }, + }, + }; + + root.Renderer = renderer.Object; + canvas1.ZIndex = 10; + + renderer.Verify(x => x.AddDirty(canvas1)); + } + + [Fact] + public void Changing_ZIndex_Should_Recalculate_Parent_Children() + { + Canvas canvas1; + StackPanel stackPanel; + var renderer = new Mock(); + var root = new TestRoot + { + Child = stackPanel = new StackPanel + { + Children = + { + (canvas1 = new Canvas()), + new Canvas(), + }, + }, + }; + + root.Renderer = renderer.Object; + canvas1.ZIndex = 10; + + renderer.Verify(x => x.RecalculateChildren(stackPanel)); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 03470670d2..d31210bc71 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -96,7 +96,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl.FillContains(point); } - public Rect GetRenderBounds(Pen pen) + public Rect GetRenderBounds(IPen pen) { throw new NotImplementedException(); } @@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl; } - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { throw new NotImplementedException(); }