From acab1102081e3d09c359b79174e8706ac127f34b Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 8 Sep 2020 16:57:38 -0500 Subject: [PATCH 01/32] PopupRoot IFocusScope --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index da7352b77f..2721ab879f 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -14,7 +15,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost + public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost, IFocusScope { private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; From 4e2dc7f5dbb9a2c38ce9e24a75318b5b32b38758 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 8 Sep 2020 17:41:25 -0500 Subject: [PATCH 02/32] Add test --- .../Primitives/PopupTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index d9176ca55d..a0b7368a4e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -395,6 +395,34 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Focusable_Controls_In_Popup_Should_Get_Focus() + { + using (CreateServicesWithFocus()) + { + var tb = new TextBox(); + var b = new Button(); + var p = new Popup + { + PlacementTarget = PreparedWindow(), + Child = new StackPanel + { + Children = + { + tb, + b + } + } + }; + ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + + p.Open(); + tb.Focus(); + + Assert.True(FocusManager.Instance?.Current == tb); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -407,6 +435,21 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private IDisposable CreateServicesWithFocus() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + x => + { + if (UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock(x).Object; + }), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); + } + + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p) { var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); From 05c6978617d87c30ae7cfb6a541c397b1547e400 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 10 Sep 2020 16:29:16 -0500 Subject: [PATCH 03/32] Try to get test to pass on CI --- tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index a0b7368a4e..e5dcba9912 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -416,8 +416,12 @@ namespace Avalonia.Controls.UnitTests.Primitives }; ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + p.Opened += (s, e) => + { + tb.Focus(); + }; + p.Open(); - tb.Focus(); Assert.True(FocusManager.Instance?.Current == tb); } From 7782261ec3b3148fac846e1b0e77c36672b4f0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 11 Sep 2020 03:26:58 +0100 Subject: [PATCH 04/32] Added typed AvaloniaProperty.Changed. --- src/Avalonia.Base/ApiCompatBaseline.txt | 3 +++ src/Avalonia.Base/AvaloniaProperty.cs | 17 +++--------- src/Avalonia.Base/AvaloniaProperty`1.cs | 27 +++++++++++++++++++ .../Mixins/SelectableMixin.cs | 4 +-- src/Avalonia.Controls/NativeMenu.Export.cs | 2 +- src/Avalonia.Controls/NativeMenuItem.cs | 2 +- .../Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- .../AvaloniaObjectTests_Direct.cs | 2 +- .../AvaloniaPropertyTests.cs | 4 +-- 10 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/ApiCompatBaseline.txt diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt new file mode 100644 index 0000000000..4668a572c5 --- /dev/null +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Base: +CannotAddAbstractMembers : Member 'protected System.IObservable Avalonia.AvaloniaProperty.GetChanged()' is abstract in the implementation but is missing in the contract. +Total Issues: 1 diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 39391490b0..3ae0445e9b 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -18,7 +17,6 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); @@ -50,7 +48,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _changed = new Subject(); _metadata = new Dictionary(); Name = name; @@ -77,7 +74,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _changed = source._changed; _metadata = new Dictionary(); Name = source.Name; @@ -139,7 +135,7 @@ namespace Avalonia /// An observable that is fired when this property changes on any /// instance. /// - public IObservable Changed => _changed; + public IObservable Changed => GetChanged(); /// /// Gets a method that gets called before and after the property starts being notified on an @@ -474,15 +470,6 @@ namespace Avalonia public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) where TData : struct; - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e) - { - _changed.OnNext(e); - } - /// /// Routes an untyped ClearValue call to a typed call. /// @@ -553,6 +540,8 @@ namespace Avalonia _hasMetadataOverrides = true; } + protected abstract IObservable GetChanged(); + private PropertyMetadata GetMetadataWithOverrides(Type type) { if (type is null) diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 2f26d855f2..7480d9c9c5 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Utilities; @@ -10,6 +11,8 @@ namespace Avalonia /// The value type of the property. public abstract class AvaloniaProperty : AvaloniaProperty { + private readonly Subject> _changed; + /// /// Initializes a new instance of the class. /// @@ -24,6 +27,7 @@ namespace Avalonia Action notifying = null) : base(name, typeof(TValue), ownerType, metadata, notifying) { + _changed = new Subject>(); } /// @@ -38,8 +42,31 @@ namespace Avalonia PropertyMetadata metadata) : base(source, ownerType, metadata) { + _changed = source is AvaloniaProperty p ? p._changed : new Subject>(); } + /// + /// Gets an observable that is fired when this property changes on any + /// instance. + /// + /// + /// An observable that is fired when this property changes on any + /// instance. + /// + + public new IObservable> Changed => _changed; + + /// + /// Notifies the observable. + /// + /// The observable arguments. + internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e) + { + _changed.OnNext(e); + } + + protected override IObservable GetChanged() => Changed; + protected BindingValue TryConvert(object value) { if (value == UnsetValue) diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index d2586ab6e8..e7dbecb06e 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Mixins { Contract.Requires(isSelected != null); - isSelected.Changed.Subscribe(x => + isSelected.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => { var sender = x.Sender as TControl; @@ -58,4 +58,4 @@ namespace Avalonia.Controls.Mixins }); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 776e9d2171..89e4c9e492 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("IsNativeMenuExported property is read-only"); info.ChangingIsExported = false; }); - MenuProperty.Changed.Subscribe(args => + MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => { if (args.Sender is TopLevel tl) { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 4c94d82eb4..d4badbc559 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls static NativeMenuItem() { - MenuProperty.Changed.Subscribe(args => + MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => { var item = (NativeMenuItem)args.Sender; var value = (NativeMenu)args.NewValue; diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index cb7bee1d33..f5115a2f7c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls.Presenters TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); - Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, + Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 3b9e9c4751..d8477840af 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -138,7 +138,7 @@ namespace Avalonia.Controls FontStyleProperty, TextWrappingProperty, FontFamilyProperty, TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty); - Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, + Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, FontWeightProperty.Changed, diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 81a8de1046..83ae663419 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -92,7 +92,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); bool raised = false; - Class1.FooProperty.Changed.Subscribe(e => + Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs e) => raised = e.Property == Class1.FooProperty && (string)e.OldValue == "initial" && (string)e.NewValue == "newvalue" && diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index d7f927372e..19040ff584 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); string value = null; - Class1.FooProperty.Changed.Subscribe(x => value = (string)x.NewValue); + Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => value = (string)x.NewValue); target.SetValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", value); @@ -95,7 +95,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var result = new List(); - Class1.FooProperty.Changed.Subscribe(x => result.Add((string)x.NewValue)); + Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => result.Add((string)x.NewValue)); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "local"); From 5bf12ac0f0be4d1624ccf22776f5ad7a7e8cf82d Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 12:58:05 +0300 Subject: [PATCH 05/32] Added TextSelector and AutoCompleteMode properties to the AutoCompleteBox Now it's possible to change the logic for modifying the text of the control after choosing the autocomplete option --- src/Avalonia.Controls/AutoCompleteBox.cs | 222 ++++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..16b7d09e8a 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -225,6 +225,52 @@ namespace Avalonia.Controls Custom = 13, } + /// + /// Represents the selector used by the + /// control to + /// determine how the specified text should be modified with an item. + /// + /// + /// Modified text that will be used by the + /// . + /// + /// The string used as the basis for filtering. + /// + /// The selected item that should be combined with the + /// parameter. + /// + /// + /// The type used for filtering the + /// . + /// At the moment this type known only as a string. + /// + public delegate string AutoCompleteSelector(string search, T item); + + /// + /// Specifies how the selected autocomplete result should be treated. + /// + public enum AutoCompleteMode + { + /// + /// Specifies that the text will be replaced + /// with the selected autocomplete result. + /// + Replace = 0, + + /// + /// Specifies that the selected autocomplete result + /// will be appended to the text. + /// + Append = 1, + + /// + /// Specifies that a custom selector is used. This mode is used when + /// the + /// property is set. + /// + Custom = 2 + } + /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -362,6 +408,8 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _textSelector = AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace); + public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -499,6 +547,17 @@ namespace Avalonia.Controls defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); + /// + /// Gets the identifier for the + /// + /// dependency property. + /// + public static readonly StyledProperty AutoCompleteModeProperty = + AvaloniaProperty.Register( + nameof(AutoCompleteMode), + defaultValue: AutoCompleteMode.Replace, + validate: IsValidAutoCompleteMode); + /// /// Identifies the /// @@ -528,6 +587,21 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextSelector), + o => o.TextSelector, + (o, v) => o.TextSelector = v, + unsetValue: AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace)); + /// /// Identifies the /// @@ -578,6 +652,19 @@ namespace Avalonia.Controls } } + private static bool IsValidAutoCompleteMode(AutoCompleteMode mode) + { + switch (mode) + { + case AutoCompleteMode.Replace: + case AutoCompleteMode.Append: + case AutoCompleteMode.Custom: + return true; + default: + return false; + } + } + /// /// Handle the change of the IsEnabled property. /// @@ -728,6 +815,19 @@ namespace Avalonia.Controls TextFilter = AutoCompleteSearch.GetFilter(mode); } + /// + /// AutoCompleteModeProperty property changed handler. + /// + /// Event arguments. + private void OnAutoCompleteModePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteMode mode = (AutoCompleteMode)e.NewValue; + + // Sets the text selector for the new value + if (mode != AutoCompleteMode.Custom) + TextSelector = AutoCompleteSelection.GetSelector(mode); + } + /// /// ItemFilterProperty property changed handler. /// @@ -748,6 +848,25 @@ namespace Avalonia.Controls } } + /// + /// TextSelectorProperty property changed handler. + /// + /// Event arguments. + private void OnTextSelectorPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteSelector value = e.NewValue as AutoCompleteSelector; + + // If null, revert to the "Replace" predicate + if (value == null) + { + AutoCompleteMode = AutoCompleteMode.Replace; + } + else if (value.Method.DeclaringType != typeof(AutoCompleteSelection)) + { + AutoCompleteMode = AutoCompleteMode.Custom; + } + } + /// /// ItemsSourceProperty property changed handler. /// @@ -793,6 +912,8 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); + AutoCompleteModeProperty.Changed.AddClassHandler((x,e) => x.OnAutoCompleteModePropertyChanged(e)); + TextSelectorProperty.Changed.AddClassHandler((x,e) => x.OnTextSelectorPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -1015,6 +1136,31 @@ namespace Avalonia.Controls set { SetValue(FilterModeProperty, value); } } + /// + /// Gets or sets how the text in the text box will be modified + /// with the selected autocomplete item. + /// + /// + /// One of the + /// values. The default is + /// . + /// + /// The specified value is not a valid + /// . + /// + /// + /// Use the AutoCompleteMode property to specify the way the text will + /// be modified with the selected autocomplete item. For example, text + /// can be modified in a predefined or custom way. The autocomplete + /// mode is automatically set to Custom if you set the TextSelector + /// property. + /// + public AutoCompleteMode AutoCompleteMode + { + get { return GetValue(AutoCompleteModeProperty); } + set { SetValue(AutoCompleteModeProperty, value); } + } + public string Watermark { get { return GetValue(WatermarkProperty); } @@ -1061,6 +1207,26 @@ namespace Avalonia.Controls set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } + /// + /// Gets or sets the custom method that combines the user-entered + /// text to and one of the items specified by the + /// . + /// + /// + /// The custom method that combines the user-entered + /// text to and one of the items specified by the + /// . + /// + /// + /// The AutoCompleteMode is automatically set to Custom if you set + /// the TextSelector property. + /// + public AutoCompleteSelector TextSelector + { + get { return _textSelector; } + set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); } + } + public Func>> AsyncPopulator { get { return _asyncPopulator; } @@ -2331,7 +2497,7 @@ namespace Avalonia.Controls } else { - text = FormatValue(newItem, true); + text = TextSelector(SearchText, FormatValue(newItem, true)); } // Update the Text property and the TextBox values @@ -2590,6 +2756,60 @@ namespace Avalonia.Controls } } + /// + /// A predefined set of selector functions for the known, built-in + /// AutoCompleteMode enumeration values. + /// + private static class AutoCompleteSelection + { + /// + /// Index function that retrieves the selector for the provided + /// AutoCompleteMode. + /// + /// The built-in autocomplete mode. + /// Returns the string-based selector function. + public static AutoCompleteSelector GetSelector(AutoCompleteMode completeMode) + { + switch (completeMode) + { + case AutoCompleteMode.Replace: + return Replace; + case AutoCompleteMode.Append: + return Append; + case AutoCompleteMode.Custom: + default: + return null; + } + } + + /// + /// Implements AutoCompleteMode.Replace. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// + /// Return the and ignores the + /// . + /// + private static string Replace(string text, string value) + { + return value ?? String.Empty; + } + + /// + /// Implements AutoCompleteMode.Append. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// + /// Returns the concatenated string. + /// + private static string Append(string text, string value) + { + return text + value; + } + } + /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. From 9a1dd58273ff33d3c53cfb9d9f288d21e8e6bf38 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 12:59:58 +0300 Subject: [PATCH 06/32] Well, default AutoCompleteMode.Append isn't doing well, tbh --- src/Avalonia.Controls/AutoCompleteBox.cs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 16b7d09e8a..22b09ef110 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -257,18 +257,12 @@ namespace Avalonia.Controls /// Replace = 0, - /// - /// Specifies that the selected autocomplete result - /// will be appended to the text. - /// - Append = 1, - /// /// Specifies that a custom selector is used. This mode is used when /// the /// property is set. /// - Custom = 2 + Custom = 1 } /// @@ -657,7 +651,6 @@ namespace Avalonia.Controls switch (mode) { case AutoCompleteMode.Replace: - case AutoCompleteMode.Append: case AutoCompleteMode.Custom: return true; default: @@ -2774,8 +2767,6 @@ namespace Avalonia.Controls { case AutoCompleteMode.Replace: return Replace; - case AutoCompleteMode.Append: - return Append; case AutoCompleteMode.Custom: default: return null; @@ -2795,19 +2786,6 @@ namespace Avalonia.Controls { return value ?? String.Empty; } - - /// - /// Implements AutoCompleteMode.Append. - /// - /// The AutoCompleteBox prefix text. - /// The item's string value. - /// - /// Returns the concatenated string. - /// - private static string Append(string text, string value) - { - return text + value; - } } /// From 0fa3350a9d704d8d94b6635b0db80d59c9518d87 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 13:25:38 +0300 Subject: [PATCH 07/32] Made tests for the AutoCompleteMode and TextSelector properties --- .../AutoCompleteBoxTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 57cea91834..2205384542 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,6 +363,47 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Test_Selectors() + { + Assert.Equal(GetSelector(AutoCompleteMode.Replace)("Never", "gonna"), "gonna"); + Assert.Equal(GetSelector(AutoCompleteMode.Replace)("give", "you"), "you"); + Assert.NotEqual(GetSelector(AutoCompleteMode.Replace)("up", "!"), "42"); + } + + [Fact] + public void AutoCompleteMode_Changes_To_Custom_And_Back() + { + RunTest((control, textbox) => + { + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Custom); + + control.AutoCompleteMode = AutoCompleteMode.Replace; + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); + Assert.Equal(control.TextSelector, GetSelector(AutoCompleteMode.Replace)); + }); + } + + [Fact] + public void Custom_TextSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.TextSelector("4", "2"), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); + }); + } + /// /// Retrieves a defined predicate filter through a new AutoCompleteBox /// control instance. @@ -375,6 +416,17 @@ namespace Avalonia.Controls.UnitTests .TextFilter; } + /// + /// Retrieves a defined selector through a new AutoCompleteBox + /// control instance. + /// + /// The AutoCompleteMode of interest. + /// Returns the selector instance. + private static AutoCompleteSelector GetSelector(AutoCompleteMode mode) + { + return new AutoCompleteBox { AutoCompleteMode = mode }.TextSelector; + } + /// /// Creates a large list of strings for AutoCompleteBox testing. /// From 82240033dc12d476deb333cff99f9239abb06662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 16 Sep 2020 20:09:52 +0100 Subject: [PATCH 08/32] Changed property copy constructors to require same property type. --- src/Avalonia.Base/AvaloniaProperty`1.cs | 23 +++++++++++++++++++---- src/Avalonia.Base/DirectPropertyBase.cs | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 7480d9c9c5..d5549e979b 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -31,18 +31,33 @@ namespace Avalonia } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + [Obsolete("Use constructor with AvaloniaProperty instead.", true)] + protected AvaloniaProperty( + AvaloniaProperty source, + Type ownerType, + PropertyMetadata metadata) + : this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata) + { + } + + /// + /// Initializes a new instance of the class. /// /// The property to copy. /// The new owner type. /// Optional overridden metadata. protected AvaloniaProperty( - AvaloniaProperty source, - Type ownerType, + AvaloniaProperty source, + Type ownerType, PropertyMetadata metadata) : base(source, ownerType, metadata) { - _changed = source is AvaloniaProperty p ? p._changed : new Subject>(); + _changed = source._changed; } /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index dbc2625b86..a2f113adb7 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -32,15 +32,30 @@ namespace Avalonia } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The property to copy. /// The new owner type. /// Optional overridden metadata. + [Obsolete("Use constructor with DirectPropertyBase instead.", true)] protected DirectPropertyBase( AvaloniaProperty source, Type ownerType, PropertyMetadata metadata) + : this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + protected DirectPropertyBase( + DirectPropertyBase source, + Type ownerType, + PropertyMetadata metadata) : base(source, ownerType, metadata) { } From 32fe0e60a7f9ff20d1c3173e4471638c64f6f0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Thu, 17 Sep 2020 23:30:34 +0100 Subject: [PATCH 09/32] Use typed property changed args instead of changing handler args type. --- src/Avalonia.Controls/Mixins/SelectableMixin.cs | 4 ++-- src/Avalonia.Controls/NativeMenu.Export.cs | 4 ++-- src/Avalonia.Controls/NativeMenuItem.cs | 4 ++-- tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs | 6 +++--- tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index e7dbecb06e..c9e2b684cb 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -42,13 +42,13 @@ namespace Avalonia.Controls.Mixins { Contract.Requires(isSelected != null); - isSelected.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => + isSelected.Changed.Subscribe(x => { var sender = x.Sender as TControl; if (sender != null) { - ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); + ((IPseudoClasses)sender.Classes).Set(":selected", x.NewValue.GetValueOrDefault()); sender.RaiseEvent(new RoutedEventArgs { diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 89e4c9e492..0349df842b 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -73,11 +73,11 @@ namespace Avalonia.Controls throw new InvalidOperationException("IsNativeMenuExported property is read-only"); info.ChangingIsExported = false; }); - MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => + MenuProperty.Changed.Subscribe(args => { if (args.Sender is TopLevel tl) { - GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); + GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); } }); } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index d4badbc559..a0fec9e677 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -20,10 +20,10 @@ namespace Avalonia.Controls static NativeMenuItem() { - MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => + MenuProperty.Changed.Subscribe(args => { var item = (NativeMenuItem)args.Sender; - var value = (NativeMenu)args.NewValue; + var value = args.NewValue.GetValueOrDefault(); if (value.Parent != null && value.Parent != item) throw new InvalidOperationException("NativeMenu already has a parent"); value.Parent = item; diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 83ae663419..20172eea88 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -92,10 +92,10 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); bool raised = false; - Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs e) => + Class1.FooProperty.Changed.Subscribe(e => raised = e.Property == Class1.FooProperty && - (string)e.OldValue == "initial" && - (string)e.NewValue == "newvalue" && + e.OldValue.GetValueOrDefault() == "initial" && + e.NewValue.GetValueOrDefault() == "newvalue" && e.Priority == BindingPriority.LocalValue); target.SetValue(Class1.FooProperty, "newvalue"); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 19040ff584..8e5d8b7be2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); string value = null; - Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => value = (string)x.NewValue); + Class1.FooProperty.Changed.Subscribe(x => value = x.NewValue.GetValueOrDefault()); target.SetValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", value); @@ -95,7 +95,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var result = new List(); - Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => result.Add((string)x.NewValue)); + Class1.FooProperty.Changed.Subscribe(x => result.Add(x.NewValue.GetValueOrDefault())); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "local"); From 58c333b6f16eb13581bed38f9f337204727ada9b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 16:43:01 +0200 Subject: [PATCH 10/32] Added PseudoClassesAttribute. And apply it to our controls. Can be used by designers to add auto-completion support for pseudoclasses. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 ++ src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 ++ .../DataGridColumnHeader.cs | 2 ++ src/Avalonia.Controls.DataGrid/DataGridRow.cs | 2 ++ .../DataGridRowGroupHeader.cs | 2 ++ .../DataGridRowHeader.cs | 2 ++ src/Avalonia.Controls/AutoCompleteBox.cs | 2 ++ src/Avalonia.Controls/Button.cs | 2 ++ src/Avalonia.Controls/ButtonSpinner.cs | 2 ++ .../Calendar/CalendarButton.cs | 2 ++ .../Calendar/CalendarDayButton.cs | 2 ++ src/Avalonia.Controls/Calendar/CalendarItem.cs | 2 ++ src/Avalonia.Controls/Chrome/CaptionButtons.cs | 2 ++ src/Avalonia.Controls/Chrome/TitleBar.cs | 2 ++ src/Avalonia.Controls/DataValidationErrors.cs | 2 ++ .../DateTimePickers/DatePicker.cs | 4 +++- .../DateTimePickers/TimePicker.cs | 4 +++- src/Avalonia.Controls/Expander.cs | 3 ++- src/Avalonia.Controls/ItemsControl.cs | 2 ++ src/Avalonia.Controls/ListBoxItem.cs | 2 ++ src/Avalonia.Controls/MenuItem.cs | 2 ++ .../Notifications/NotificationCard.cs | 2 ++ .../Notifications/WindowNotificationManager.cs | 2 ++ src/Avalonia.Controls/Primitives/ScrollBar.cs | 2 ++ src/Avalonia.Controls/Primitives/Thumb.cs | 2 ++ .../Primitives/ToggleButton.cs | 2 ++ src/Avalonia.Controls/Primitives/Track.cs | 2 ++ src/Avalonia.Controls/ProgressBar.cs | 2 ++ src/Avalonia.Controls/Slider.cs | 2 ++ src/Avalonia.Controls/SplitView.cs | 7 ++++++- src/Avalonia.Controls/TabItem.cs | 2 ++ src/Avalonia.Controls/TextBox.cs | 2 ++ src/Avalonia.Controls/ToggleSwitch.cs | 4 +++- src/Avalonia.Controls/ToolTip.cs | 2 ++ src/Avalonia.Controls/TreeViewItem.cs | 2 ++ src/Avalonia.Input/InputElement.cs | 2 ++ .../Metadata/PseudoClassesAttribute.cs | 18 ++++++++++++++++++ 37 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 9ca0b91523..7c57ea3db9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,12 +24,14 @@ using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Displays data in a customizable grid. /// + [PseudoClasses(":invalid")] public partial class DataGrid : TemplatedControl { private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e5fbfa1a81..445dc541a7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 25aae99942..856d1f6566 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -14,12 +14,14 @@ using Avalonia.Utilities; using System; using Avalonia.Controls.Utils; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Represents an individual column header. /// + [PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")] public class DataGridColumnHeader : ContentControl { private enum DragMode diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index d5ce8dba75..c3562c53a4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 0833247439..1e03b134b1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -13,6 +14,7 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 8f8b1742ba..0cd3589a57 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Media; using System.Diagnostics; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..6bbb4f0b75 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -14,6 +14,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; @@ -30,6 +31,7 @@ namespace Avalonia.Controls /// /// event. /// + [PseudoClasses(":dropdownopen")] public class PopulatedEventArgs : EventArgs { /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index b54eb2ac57..e94d00b2ff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -28,6 +29,7 @@ namespace Avalonia.Controls /// /// A button control. /// + [PseudoClasses(":pressed")] public class Button : ContentControl { /// diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 44f66d397a..5fe2cf3704 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -15,6 +16,7 @@ namespace Avalonia.Controls /// /// Represents a spinner control that includes two Buttons. /// + [PseudoClasses(":left", ":right")] public class ButtonSpinner : Spinner { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 80370df145..76af933b55 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using System; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// Represents a button on a /// . /// + [PseudoClasses(":selected", ":inactive", ":btnfocused")] public sealed class CalendarButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 3a39bd10fa..d5748bb9e4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -5,10 +5,12 @@ using System; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Input; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")] public sealed class CalendarDayButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 0be7c4f67e..e9ea942142 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives /// Represents the currently displayed month or year on a /// . /// + [PseudoClasses(":calendardisabled")] public sealed class CalendarItem : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index a86cbc271b..cd60130c5b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index c0c8076dd8..fbddb06952 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws a titlebar when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class TitleBar : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index dfe9a16532..3c64691816 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// /// You will probably only want to create instances inside of control templates. /// + [PseudoClasses(":error")] public class DataValidationErrors : ContentControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index a41c159980..8d893154eb 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Interactivity; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// + [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index e54da1fb3a..e4ff5e9e5b 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using System; @@ -9,6 +10,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a time /// + [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 43882b70c8..9ff2e41fa9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,6 @@ using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; namespace Avalonia.Controls { @@ -12,6 +12,7 @@ namespace Avalonia.Controls Right } + [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { public static readonly StyledProperty ContentTransitionProperty = diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a3dfe33641..3aec06e4eb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -18,6 +19,7 @@ namespace Avalonia.Controls /// /// Displays a collection of items. /// + [PseudoClasses(":empty", ":singleitem")] public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener { /// diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index e04c79987f..4fe5f4de40 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Input; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// A selectable item in a . /// + [PseudoClasses(":pressed", ":selected")] public class ListBoxItem : ContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index b4d3272471..3d8ab3ae48 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index f90746bf06..cdbace3ced 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications /// /// Control that represents and displays a notification. /// + [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { private bool _isClosed; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 6d9f6b8b77..8f5c6faf40 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives; using Avalonia.Rendering; using Avalonia.Data; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Notifications { /// /// An that displays notifications in a . /// + [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 477d24dc99..a7fb7ae08c 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -4,6 +4,7 @@ using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Primitives { @@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives /// /// A scrollbar control. /// + [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { /// diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 96810ed01b..348922b71d 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,9 +1,11 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed")] public class Thumb : TemplatedControl { public static readonly RoutedEvent DragStartedEvent = diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 13031ddad8..f96ca9310d 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. /// + [PseudoClasses(":checked", ":unchecked", ":indeterminate")] public class ToggleButton : Button { /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 29e7f28b44..9399f5fb31 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -12,6 +13,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { public static readonly DirectProperty MinimumProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a92f24a050..161f09d9b6 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// + [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { public class ProgressBarTemplateProperties : AvaloniaObject diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 293cbac82f..6e08e78813 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -39,6 +40,7 @@ namespace Avalonia.Controls /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// + [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { /// diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index b71858f796..8267efc466 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -73,6 +74,10 @@ namespace Avalonia.Controls /// /// A control with two views: A collapsible pane and an area for content /// + [PseudoClasses(":open", ":closed")] + [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] + [PseudoClasses(":left", ":right")] + [PseudoClasses(":lightdismiss")] public class SplitView : TemplatedControl { /* diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 6320443a13..593643a1eb 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// An item in a or . /// + [PseudoClasses(":pressed", ":selected")] public class TabItem : HeaderedContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 73a1ae3335..0fe3ac62e4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -13,9 +13,11 @@ using Avalonia.Metadata; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { + [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index c32f2d8102..662a355dac 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A Toggle Switch control. /// + [PseudoClasses(":dragging")] public class ToggleSwitch : ToggleButton { private Panel _knobsPanel; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index d56ff5752f..71bd0726d4 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// To add a tooltip to a control, use the attached property, /// assigning the content that you want displayed. /// + [PseudoClasses(":open")] public class ToolTip : ContentControl { /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4942d4d313..8ce258b546 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,5 +1,6 @@ using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// An item in a . /// + [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { /// diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 25f2d553d7..9ace7fd92d 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; @@ -12,6 +13,7 @@ namespace Avalonia.Input /// /// Implements input-related functionality for a control. /// + [PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")] public class InputElement : Interactive, IInputElement { /// diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs new file mode 100644 index 0000000000..66d0282b92 --- /dev/null +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Controls.Metadata +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class PseudoClassesAttribute : Attribute + { + public PseudoClassesAttribute(params string[] pseudoClasses) + { + PseudoClasses = pseudoClasses; + } + + public IReadOnlyList PseudoClasses { get; } + } +} From c2fad766e52eb47531e848c745e4325d9f53069c Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sat, 19 Sep 2020 15:46:44 +0200 Subject: [PATCH 11/32] Rework GlyphRun BaselineOrigin --- samples/RenderDemo/Pages/GlyphRunPage.xaml.cs | 3 +- .../Primitives/AccessText.cs | 4 +- src/Avalonia.Controls/TextBlock.cs | 5 +- .../HeadlessPlatformRenderInterface.cs | 2 +- src/Avalonia.Visuals/ApiCompatBaseline.txt | 20 +++++++- src/Avalonia.Visuals/Media/DrawingContext.cs | 5 +- src/Avalonia.Visuals/Media/GlyphRun.cs | 49 ++++++++++++++----- src/Avalonia.Visuals/Media/GlyphRunDrawing.cs | 13 +---- src/Avalonia.Visuals/Media/TextDecoration.cs | 17 ++++--- .../Media/TextFormatting/DrawableTextRun.cs | 7 ++- .../TextFormatting/ShapedTextCharacters.cs | 11 ++--- .../Media/TextFormatting/TextFormatterImpl.cs | 6 +-- .../Media/TextFormatting/TextLayout.cs | 17 ++++--- .../Media/TextFormatting/TextLine.cs | 3 +- .../Media/TextFormatting/TextLineImpl.cs | 29 +++++------ .../Media/TextFormatting/TextLineMetrics.cs | 2 +- .../Platform/IDrawingContextImpl.cs | 3 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 4 +- .../Rendering/SceneGraph/GlyphRunNode.cs | 13 ++--- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 8 +-- .../Media/DrawingContextImpl.cs | 7 +-- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 22 files changed, 132 insertions(+), 98 deletions(-) diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 7f15845596..ddee880288 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -61,7 +61,6 @@ namespace RenderDemo.Pages { Foreground = Brushes.Black, GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), - BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale) }; drawingGroup.Children.Add(glyphRunDrawing); @@ -69,7 +68,7 @@ namespace RenderDemo.Pages var geometryDrawing = new GeometryDrawing { Pen = new Pen(Brushes.Black), - Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } + Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) } }; drawingGroup.Children.Add(geometryDrawing); diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 89f672deaa..7a5e6ce426 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -126,7 +126,7 @@ namespace Avalonia.Controls.Primitives if (shapedTextCharacters.GlyphRun.Characters.End < textPosition) { - currentX += shapedTextCharacters.GlyphRun.Bounds.Width; + currentX += shapedTextCharacters.Size.Width; continue; } @@ -143,7 +143,7 @@ namespace Avalonia.Controls.Primitives width = 0.0; } - return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height); + return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height); } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 3b9e9c4751..6f978ce86a 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -434,7 +434,10 @@ namespace Avalonia.Controls var padding = Padding; - TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top)); + using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, padding.Top))) + { + TextLayout.Draw(context); + } } /// diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 3ae6c8c30e..4f6af0a41b 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -385,7 +385,7 @@ namespace Avalonia.Headless } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 5aa497861d..148916932f 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,15 +1,33 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.GlyphRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Media.GlyphRunDrawing.BaselineOriginProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Point Avalonia.Media.GlyphRunDrawing.BaselineOrigin.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.GlyphRunDrawing.BaselineOrigin.set(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract. CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract. CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract. TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.DrawableTextRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size.get()' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.ShapedTextCharacters.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. -Total Issues: 13 +Total Issues: 31 diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index ba7191d7a6..ae4c927ae2 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -206,14 +206,13 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - /// The baseline origin of the glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { Contract.Requires(glyphRun != null); if (foreground != null) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + PlatformImpl.DrawGlyphRun(foreground, glyphRun); } } diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index da3a1f721c..14ab083b4f 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -16,8 +16,9 @@ namespace Avalonia.Media private IGlyphRunImpl _glyphRunImpl; private GlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; - private Rect? _bounds; + private Size? _size; private int _biDiLevel; + private Point? _baselineOrigin; private ReadOnlySlice _glyphIndices; private ReadOnlySlice _glyphAdvances; @@ -89,6 +90,20 @@ namespace Avalonia.Media set => Set(ref _fontRenderingEmSize, value); } + /// + /// Gets or sets the baseline origin of the. + /// + public Point BaselineOrigin + { + get + { + _baselineOrigin ??= CalculateBaselineOrigin(); + + return _baselineOrigin.Value; + } + set => Set(ref _baselineOrigin, value); + } + /// /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. /// @@ -156,16 +171,13 @@ namespace Avalonia.Media /// /// Gets or sets the conservative bounding box of the . /// - public Rect Bounds + public Size Size { get { - if (_bounds == null) - { - _bounds = CalculateBounds(); - } + _size ??= CalculateSize(); - return _bounds.Value; + return _size.Value; } } @@ -200,7 +212,7 @@ namespace Avalonia.Media if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End) { - return Bounds.Width; + return Size.Width; } var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); @@ -257,7 +269,7 @@ namespace Avalonia.Media } //After - if (distance > Bounds.Size.Width) + if (distance > Size.Width) { isInside = false; @@ -529,12 +541,21 @@ namespace Avalonia.Media } /// - /// Calculates the bounds of the . + /// Calculates the default baseline origin of the . + /// + /// The baseline origin. + private Point CalculateBaselineOrigin() + { + return new Point(0, -GlyphTypeface.Ascent * Scale); + } + + /// + /// Calculates the size of the . /// /// /// The calculated bounds. /// - private Rect CalculateBounds() + private Size CalculateSize() { var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; @@ -555,7 +576,7 @@ namespace Avalonia.Media } } - return new Rect(0, GlyphTypeface.Ascent * Scale, width, height); + return new Size(width, height); } private void Set(ref T field, T value) @@ -590,11 +611,15 @@ namespace Avalonia.Media throw new InvalidOperationException(); } + _baselineOrigin = new Point(0, -GlyphTypeface.Ascent * Scale); + var platformRenderInterface = AvaloniaLocator.Current.GetService(); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + _size = new Size(width, height); } void IDisposable.Dispose() diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs index d0ea113a6f..7e0d5c3c81 100644 --- a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs @@ -8,9 +8,6 @@ public static readonly StyledProperty GlyphRunProperty = AvaloniaProperty.Register(nameof(GlyphRun)); - public static readonly StyledProperty BaselineOriginProperty = - AvaloniaProperty.Register(nameof(BaselineOrigin)); - public IBrush Foreground { get => GetValue(ForegroundProperty); @@ -23,12 +20,6 @@ set => SetValue(GlyphRunProperty, value); } - public Point BaselineOrigin - { - get => GetValue(BaselineOriginProperty); - set => SetValue(BaselineOriginProperty, value); - } - public override void Draw(DrawingContext context) { if (GlyphRun == null) @@ -36,12 +27,12 @@ return; } - context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + context.DrawGlyphRun(Foreground, GlyphRun); } public override Rect GetBounds() { - return GlyphRun?.Bounds ?? default; + return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; } } } diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs index 681fc5d499..d9b3f664ce 100644 --- a/src/Avalonia.Visuals/Media/TextDecoration.cs +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -155,8 +155,7 @@ namespace Avalonia.Media /// /// The drawing context. /// The shaped characters that are decorated. - /// The origin. - internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters, Point origin) + internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters) { var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize; var fontMetrics = shapedTextCharacters.FontMetrics; @@ -181,16 +180,20 @@ namespace Avalonia.Media break; } + var origin = new Point(); + switch (Location) { - case TextDecorationLocation.Overline: - origin += new Point(0, fontMetrics.Ascent); + case TextDecorationLocation.Baseline: + origin += shapedTextCharacters.GlyphRun.BaselineOrigin; break; case TextDecorationLocation.Strikethrough: - origin += new Point(0, -fontMetrics.StrikethroughPosition); + origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, + shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.StrikethroughPosition); break; case TextDecorationLocation.Underline: - origin += new Point(0, -fontMetrics.UnderlinePosition); + origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, + shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.UnderlinePosition); break; } @@ -207,7 +210,7 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); - drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Bounds.Width, 0)); + drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Size.Width, 0)); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs index 56790cc0db..338c92f6b1 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs @@ -6,15 +6,14 @@ public abstract class DrawableTextRun : TextRun { /// - /// Gets the bounds. + /// Gets the size. /// - public abstract Rect Bounds { get; } + public abstract Size Size { get; } /// /// Draws the at the given origin. /// /// The drawing context. - /// The origin. - public abstract void Draw(DrawingContext drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 9e67a03f45..09ecc0a026 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting public override int TextSourceLength { get; } /// - public override Rect Bounds => GlyphRun.Bounds; + public override Size Size => GlyphRun.Size; /// /// Gets the font metrics. @@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting public GlyphRun GlyphRun { get; } /// - public override void Draw(DrawingContext drawingContext, Point origin) + public override void Draw(DrawingContext drawingContext) { if (GlyphRun.GlyphIndices.Length == 0) { @@ -64,11 +64,10 @@ namespace Avalonia.Media.TextFormatting if (Properties.BackgroundBrush != null) { - drawingContext.DrawRectangle(Properties.BackgroundBrush, null, - new Rect(origin.X, origin.Y + FontMetrics.Ascent, Bounds.Width, Bounds.Height)); + drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size)); } - drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun, origin); + drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun); if (Properties.TextDecorations == null) { @@ -77,7 +76,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textDecoration in Properties.TextDecorations) { - textDecoration.Draw(drawingContext, this, origin); + textDecoration.Draw(drawingContext, this); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index b116249fd4..3e85f0f6f0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = textCharacters.GlyphRun; - if (glyphRun.Bounds.Width < availableWidth) + if (glyphRun.Size.Width < availableWidth) { return glyphRun.Characters.Length; } @@ -348,7 +348,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[runIndex]; - if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) + if (currentWidth + currentRun.Size.Width > availableWidth) { var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); @@ -421,7 +421,7 @@ namespace Avalonia.Media.TextFormatting return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); } - currentWidth += currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.Size.Width; currentLength += currentRun.GlyphRun.Characters.Length; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index df1ecb4067..0c5179f88b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -115,22 +115,27 @@ namespace Avalonia.Media.TextFormatting /// Draws the text layout. /// /// The drawing context. - /// The origin. - public void Draw(DrawingContext context, Point origin) + public void Draw(DrawingContext context) { if (!TextLines.Any()) { return; } - var currentY = origin.Y; + var currentY = 0.0; foreach (var textLine in TextLines) { - var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, - _paragraphProperties.TextAlignment); + using (context.PushPostTransform(Matrix.CreateTranslation(0, currentY))) + { + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); - textLine.Draw(context, new Point(origin.X + offsetX, currentY)); + using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, 0))) + { + textLine.Draw(context); + } + } currentY += textLine.LineMetrics.Size.Height; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index c052fb8948..8a1efa0611 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -51,8 +51,7 @@ namespace Avalonia.Media.TextFormatting /// Draws the at the given origin. /// /// The drawing context. - /// The origin. - public abstract void Draw(DrawingContext drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext); /// /// Create a collapsed line based on collapsed text properties. diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 51092cddda..f5e87d097b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -33,17 +33,18 @@ namespace Avalonia.Media.TextFormatting public override bool HasCollapsed { get; } /// - public override void Draw(DrawingContext drawingContext, Point origin) + public override void Draw(DrawingContext drawingContext) { - var currentX = origin.X; + var currentX = 0.0; foreach (var textRun in _textRuns) { - var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline); - - textRun.Draw(drawingContext, baselineOrigin); + using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, 0))) + { + textRun.Draw(drawingContext); + } - currentX += textRun.Bounds.Width; + currentX += textRun.Size.Width; } } @@ -64,13 +65,13 @@ namespace Avalonia.Media.TextFormatting var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); - var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width; + var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width; while (runIndex < _textRuns.Count) { var currentRun = _textRuns[runIndex]; - currentWidth += currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.Size.Width; if (currentWidth > availableWidth) { @@ -125,7 +126,7 @@ namespace Avalonia.Media.TextFormatting return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true); } - availableWidth -= currentRun.GlyphRun.Bounds.Width; + availableWidth -= currentRun.Size.Width; collapsedLength += currentRun.GlyphRun.Characters.Length; @@ -133,7 +134,7 @@ namespace Avalonia.Media.TextFormatting } textLineMetrics = - new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width), + new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width), LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed); return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null, @@ -156,12 +157,12 @@ namespace Avalonia.Media.TextFormatting { characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); - if (distance <= run.Bounds.Width) + if (distance <= run.Size.Width) { break; } - distance -= run.Bounds.Width; + distance -= run.Size.Width; } return characterHit; @@ -229,7 +230,7 @@ namespace Avalonia.Media.TextFormatting { if (codepointIndex > textRun.Text.End) { - currentDistance += textRun.Bounds.Width; + currentDistance += textRun.Size.Width; continue; } @@ -405,7 +406,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < shapedTextCharacters.Count; i++) { - shapedWidth += shapedTextCharacters[i].Bounds.Width; + shapedWidth += shapedTextCharacters[i].Size.Width; } return shapedWidth; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 6875cc1c04..c4d7527659 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media.TextFormatting var fontMetrics = new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize); - lineWidth += shapedRun.Bounds.Width; + lineWidth += shapedRun.Size.Width; if (ascent > fontMetrics.Ascent) { diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index c87946b3ea..019614ae80 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -84,8 +84,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - /// The baseline origin of the glyph run. - void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 4a364998fd..cb6b1f59d4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -204,13 +204,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) { - Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground))); + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); } else diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs index bdf05c4f86..a6dba1bd32 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,20 +18,17 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// The baseline origin of the glyph run. /// Child scenes for drawing visual brushes. public GlyphRunNode( Matrix transform, IBrush foreground, GlyphRun glyphRun, - Point baselineOrigin, IDictionary childScenes = null) - : base(glyphRun.Bounds.Translate(baselineOrigin), transform) + : base(new Rect(glyphRun.Size), transform) { Transform = transform; Foreground = foreground?.ToImmutable(); GlyphRun = glyphRun; - BaselineOrigin = baselineOrigin; ChildScenes = childScenes; } @@ -49,11 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - /// Gets the baseline origin. - /// - public Point BaselineOrigin { get; set; } - /// public override IDictionary ChildScenes { get; } @@ -61,7 +54,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + context.DrawGlyphRun(Foreground, GlyphRun); } /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a155fd863b..98528a128a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -401,16 +401,16 @@ namespace Avalonia.Skia } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; ConfigureTextRendering(paintWrapper); - Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, - (float)baselineOrigin.Y, paintWrapper.Paint); + Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.BaselineOrigin.X, + (float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e0de40525f..258a51db5a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -324,13 +324,14 @@ namespace Avalonia.Direct2D1.Media /// The foreground. /// The glyph run. /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; - _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); + _renderTarget.DrawGlyphRun(glyphRun.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, + brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index bf41381b52..f3e1c37705 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = shapedRun.GlyphRun; - var width = glyphRun.Bounds.Width; + var width = glyphRun.Size.Width; var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _); From 0042e82db2766931acde34de6e994abed235698f Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Sat, 19 Sep 2020 13:42:25 -0400 Subject: [PATCH 12/32] Add resizing ui test for RenderDemo --- samples/RenderDemo/MainWindow.xaml | 8 +++- .../ViewModels/MainWindowViewModel.cs | 48 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 770960d7c4..93fbe5e412 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -3,8 +3,8 @@ x:Class="RenderDemo.MainWindow" Title="AvaloniaUI Rendering Test" xmlns:pages="clr-namespace:RenderDemo.Pages" - Width="800" - Height="600"> + Width="{Binding Width, Mode=TwoWay}" + Height="{Binding Height, Mode=TwoWay}"> @@ -24,6 +24,10 @@ + + + diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index d2d789a687..eda5e80530 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ -using System; -using System.Reactive; +using System.Reactive; +using System.Threading.Tasks; + using ReactiveUI; namespace RenderDemo.ViewModels @@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels { private bool drawDirtyRects = false; private bool drawFps = true; + private double width = 800; + private double height = 600; public MainWindowViewModel() { ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); + ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync); } public bool DrawDirtyRects { - get { return drawDirtyRects; } - set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } + get => drawDirtyRects; + set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } public bool DrawFps { - get { return drawFps; } - set { this.RaiseAndSetIfChanged(ref drawFps, value); } + get => drawFps; + set => this.RaiseAndSetIfChanged(ref drawFps, value); + } + + public double Width + { + get => width; + set => this.RaiseAndSetIfChanged(ref width, value); + } + + public double Height + { + get => height; + set => this.RaiseAndSetIfChanged(ref height, value); } public ReactiveCommand ToggleDrawDirtyRects { get; } public ReactiveCommand ToggleDrawFps { get; } + public ReactiveCommand ResizeWindow { get; } + + private async Task ResizeWindowAsync() + { + for (int i = 0; i < 30; i++) + { + Width += 10; + Height += 5; + await Task.Delay(10); + } + + await Task.Delay(10); + + for (int i = 0; i < 30; i++) + { + Width -= 10; + Height -= 5; + await Task.Delay(10); + } + } } } From 7481aea60620b05a915fdb03b20ee56dc731908c Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 00:57:08 +0300 Subject: [PATCH 13/32] Removed AutoCompleteMode enum --- src/Avalonia.Controls/AutoCompleteBox.cs | 148 +----------------- .../AutoCompleteBoxTests.cs | 35 ----- 2 files changed, 4 insertions(+), 179 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 22b09ef110..c9b50a46fd 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -246,25 +246,6 @@ namespace Avalonia.Controls /// public delegate string AutoCompleteSelector(string search, T item); - /// - /// Specifies how the selected autocomplete result should be treated. - /// - public enum AutoCompleteMode - { - /// - /// Specifies that the text will be replaced - /// with the selected autocomplete result. - /// - Replace = 0, - - /// - /// Specifies that a custom selector is used. This mode is used when - /// the - /// property is set. - /// - Custom = 1 - } - /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -402,7 +383,7 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); - private AutoCompleteSelector _textSelector = AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace); + private AutoCompleteSelector _textSelector; public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -541,17 +522,6 @@ namespace Avalonia.Controls defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); - /// - /// Gets the identifier for the - /// - /// dependency property. - /// - public static readonly StyledProperty AutoCompleteModeProperty = - AvaloniaProperty.Register( - nameof(AutoCompleteMode), - defaultValue: AutoCompleteMode.Replace, - validate: IsValidAutoCompleteMode); - /// /// Identifies the /// @@ -593,8 +563,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect>( nameof(TextSelector), o => o.TextSelector, - (o, v) => o.TextSelector = v, - unsetValue: AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace)); + (o, v) => o.TextSelector = v); /// /// Identifies the @@ -646,18 +615,6 @@ namespace Avalonia.Controls } } - private static bool IsValidAutoCompleteMode(AutoCompleteMode mode) - { - switch (mode) - { - case AutoCompleteMode.Replace: - case AutoCompleteMode.Custom: - return true; - default: - return false; - } - } - /// /// Handle the change of the IsEnabled property. /// @@ -808,19 +765,6 @@ namespace Avalonia.Controls TextFilter = AutoCompleteSearch.GetFilter(mode); } - /// - /// AutoCompleteModeProperty property changed handler. - /// - /// Event arguments. - private void OnAutoCompleteModePropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - AutoCompleteMode mode = (AutoCompleteMode)e.NewValue; - - // Sets the text selector for the new value - if (mode != AutoCompleteMode.Custom) - TextSelector = AutoCompleteSelection.GetSelector(mode); - } - /// /// ItemFilterProperty property changed handler. /// @@ -841,25 +785,6 @@ namespace Avalonia.Controls } } - /// - /// TextSelectorProperty property changed handler. - /// - /// Event arguments. - private void OnTextSelectorPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - AutoCompleteSelector value = e.NewValue as AutoCompleteSelector; - - // If null, revert to the "Replace" predicate - if (value == null) - { - AutoCompleteMode = AutoCompleteMode.Replace; - } - else if (value.Method.DeclaringType != typeof(AutoCompleteSelection)) - { - AutoCompleteMode = AutoCompleteMode.Custom; - } - } - /// /// ItemsSourceProperty property changed handler. /// @@ -905,8 +830,6 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); - AutoCompleteModeProperty.Changed.AddClassHandler((x,e) => x.OnAutoCompleteModePropertyChanged(e)); - TextSelectorProperty.Changed.AddClassHandler((x,e) => x.OnTextSelectorPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -1129,31 +1052,6 @@ namespace Avalonia.Controls set { SetValue(FilterModeProperty, value); } } - /// - /// Gets or sets how the text in the text box will be modified - /// with the selected autocomplete item. - /// - /// - /// One of the - /// values. The default is - /// . - /// - /// The specified value is not a valid - /// . - /// - /// - /// Use the AutoCompleteMode property to specify the way the text will - /// be modified with the selected autocomplete item. For example, text - /// can be modified in a predefined or custom way. The autocomplete - /// mode is automatically set to Custom if you set the TextSelector - /// property. - /// - public AutoCompleteMode AutoCompleteMode - { - get { return GetValue(AutoCompleteModeProperty); } - set { SetValue(AutoCompleteModeProperty, value); } - } - public string Watermark { get { return GetValue(WatermarkProperty); } @@ -2490,7 +2388,8 @@ namespace Avalonia.Controls } else { - text = TextSelector(SearchText, FormatValue(newItem, true)); + string formattedValue = FormatValue(newItem, true); + text = TextSelector == null ? formattedValue : TextSelector(SearchText, formattedValue); } // Update the Text property and the TextBox values @@ -2749,45 +2648,6 @@ namespace Avalonia.Controls } } - /// - /// A predefined set of selector functions for the known, built-in - /// AutoCompleteMode enumeration values. - /// - private static class AutoCompleteSelection - { - /// - /// Index function that retrieves the selector for the provided - /// AutoCompleteMode. - /// - /// The built-in autocomplete mode. - /// Returns the string-based selector function. - public static AutoCompleteSelector GetSelector(AutoCompleteMode completeMode) - { - switch (completeMode) - { - case AutoCompleteMode.Replace: - return Replace; - case AutoCompleteMode.Custom: - default: - return null; - } - } - - /// - /// Implements AutoCompleteMode.Replace. - /// - /// The AutoCompleteBox prefix text. - /// The item's string value. - /// - /// Return the and ignores the - /// . - /// - private static string Replace(string text, string value) - { - return value ?? String.Empty; - } - } - /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 2205384542..2e609132d6 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,30 +363,6 @@ namespace Avalonia.Controls.UnitTests }); } - [Fact] - public void Test_Selectors() - { - Assert.Equal(GetSelector(AutoCompleteMode.Replace)("Never", "gonna"), "gonna"); - Assert.Equal(GetSelector(AutoCompleteMode.Replace)("give", "you"), "you"); - Assert.NotEqual(GetSelector(AutoCompleteMode.Replace)("up", "!"), "42"); - } - - [Fact] - public void AutoCompleteMode_Changes_To_Custom_And_Back() - { - RunTest((control, textbox) => - { - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); - - control.TextSelector = (text, item) => text + item; - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Custom); - - control.AutoCompleteMode = AutoCompleteMode.Replace; - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); - Assert.Equal(control.TextSelector, GetSelector(AutoCompleteMode.Replace)); - }); - } - [Fact] public void Custom_TextSelector() { @@ -416,17 +392,6 @@ namespace Avalonia.Controls.UnitTests .TextFilter; } - /// - /// Retrieves a defined selector through a new AutoCompleteBox - /// control instance. - /// - /// The AutoCompleteMode of interest. - /// Returns the selector instance. - private static AutoCompleteSelector GetSelector(AutoCompleteMode mode) - { - return new AutoCompleteBox { AutoCompleteMode = mode }.TextSelector; - } - /// /// Creates a large list of strings for AutoCompleteBox testing. /// From bc87efc65fbae7ab338716f1e89a43b48fea6428 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 01:56:05 +0300 Subject: [PATCH 14/32] Added ItemSelector property to the AutoCompleteBox --- src/Avalonia.Controls/AutoCompleteBox.cs | 54 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c9b50a46fd..c119fd1964 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -242,7 +242,7 @@ namespace Avalonia.Controls /// /// The type used for filtering the /// . - /// At the moment this type known only as a string. + /// This type can be either a string or an object. /// public delegate string AutoCompleteSelector(string search, T item); @@ -383,6 +383,7 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _itemSelector; private AutoCompleteSelector _textSelector; public static readonly RoutedEvent SelectionChangedEvent = @@ -551,6 +552,20 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemSelector), + o => o.ItemSelector, + (o, v) => o.ItemSelector = v); + /// /// Identifies the /// @@ -1100,18 +1115,32 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that combines the user-entered - /// text to and one of the items specified by the + /// text and one of the items specified by the /// . /// /// /// The custom method that combines the user-entered - /// text to and one of the items specified by the + /// text and one of the items specified by the /// . /// - /// - /// The AutoCompleteMode is automatically set to Custom if you set - /// the TextSelector property. - /// + public AutoCompleteSelector ItemSelector + { + get { return _itemSelector; } + set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); } + } + + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// public AutoCompleteSelector TextSelector { get { return _textSelector; } @@ -2386,10 +2415,17 @@ namespace Avalonia.Controls { text = SearchText; } + else if (TextSelector != null) + { + text = TextSelector(SearchText, FormatValue(newItem, true)); + } + else if (ItemSelector != null) + { + text = ItemSelector(SearchText, newItem); + } else { - string formattedValue = FormatValue(newItem, true); - text = TextSelector == null ? formattedValue : TextSelector(SearchText, formattedValue); + text = FormatValue(newItem, true); } // Update the Text property and the TextBox values From 997385eab7307201943dc94b88d417b5e3207fbd Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 01:56:34 +0300 Subject: [PATCH 15/32] Made test for AutoCompleteBox.ItemSelector --- .../AutoCompleteBoxTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 2e609132d6..3e78e951e2 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -379,6 +379,23 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); }); } + + [Fact] + public void Custom_ItemSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.ItemSelector = (text, item) => text + item; + Assert.Equal(control.ItemSelector("4", 2), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); + }); + } /// /// Retrieves a defined predicate filter through a new AutoCompleteBox From 14aff78be5e40a92f2c58431e2472d882f0488c5 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Mon, 21 Sep 2020 03:16:14 -0700 Subject: [PATCH 16/32] use sealed for attribute class. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dariusz Komosiński --- .../Controls/Metadata/PseudoClassesAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs index 66d0282b92..0060767565 100644 --- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Avalonia.Controls.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class PseudoClassesAttribute : Attribute + public sealed class PseudoClassesAttribute : Attribute { public PseudoClassesAttribute(params string[] pseudoClasses) { From 3f3ec4b8351a1f034f71584751ca71ba03fe089b Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Tue, 22 Sep 2020 03:40:10 +0300 Subject: [PATCH 17/32] Added custom TextSelector example --- .../Pages/AutoCompleteBoxPage.xaml | 5 ++ .../Pages/AutoCompleteBoxPage.xaml.cs | 58 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index f90a0c4658..a49616e543 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -51,6 +51,11 @@ Width="200" Margin="0,0,0,8" FilterMode="None"/> + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index f9d6a72a3a..574cc79a7d 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -92,13 +92,28 @@ namespace ControlCatalog.Pages } public StateData[] States { get; private set; } + private LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + public AutoCompleteBoxPage() { this.InitializeComponent(); States = BuildAllStates(); + Sentences = BuildAllSentences(); - foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) { box.Items = States; } @@ -116,6 +131,11 @@ namespace ControlCatalog.Pages var asyncBox = this.FindControl("AsyncBox"); asyncBox.AsyncPopulator = PopulateAsync; + + var customAutocompleteBox = this.FindControl("CustomAutocompleteBox"); + customAutocompleteBox.Items = Sentences.SelectMany(x => x); + customAutocompleteBox.TextFilter = LastWordContains; + customAutocompleteBox.TextSelector = AppendWord; } private IEnumerable GetAllAutoCompleteBox() { @@ -137,6 +157,42 @@ namespace ControlCatalog.Pages .ToList(); } + private bool LastWordContains(string searchText, string item) + { + var words = searchText.Split(' '); + var options = Sentences.Select(x => x.First).ToArray(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; j < options.Length; ++j) + { + var option = options[j]; + if (option == null) + continue; + + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string text, string item) + { + string[] parts = text.Split(' '); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); From ab354827244116f18b331d8cf022c151ccf44493 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 22 Sep 2020 00:27:51 -0400 Subject: [PATCH 18/32] Make Fluent buttons animation consistent --- src/Avalonia.Themes.Fluent/Button.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index e58e8758d2..021c6eae1d 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - From 95f07a9f9687adc93c0783defd1aebba7fbf47d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 23 Sep 2020 01:04:03 -0400 Subject: [PATCH 19/32] Only specific buttons should have pressing animation --- src/Avalonia.Themes.Fluent/Button.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 021c6eae1d..8522c933ae 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - From 1430bf86e32b820aac26a159c3ee98a33b58f5dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Sep 2020 12:13:38 +0200 Subject: [PATCH 20/32] Use screen bounds Bottom instead of Height. Secondard screens on Windows can have a Y offset (i.e. `bounds.Y != 0`) if they're offset vertically from the primary screen, and when this was the case the popup wasn't getting properly constrained as the bottom is not equal to the height. Fixes #4726 --- .../Primitives/PopupPositioning/ManagedPopupPositioner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 8c464c7aad..7f1dbdf592 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom)) { - unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y); + unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y); } if (IsValid(unconstrainedRect)) From 6c52a7cb64ca660d1b03abfca22918e0a4704199 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 23 Sep 2020 16:16:09 -0500 Subject: [PATCH 21/32] Move IFocusScope to IPopupHost --- src/Avalonia.Controls/Primitives/IPopupHost.cs | 3 ++- src/Avalonia.Controls/Primitives/PopupRoot.cs | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index e424bf683d..82a49c4189 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -13,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// () or an which is created /// on an . /// - public interface IPopupHost : IDisposable + public interface IPopupHost : IDisposable, IFocusScope { /// /// Sets the control to display in the popup. diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 2721ab879f..da7352b77f 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -15,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost, IFocusScope + public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; From 52075d6cd98527d7eb75c890c3d1f0a91fe6e203 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 23 Sep 2020 16:16:54 -0500 Subject: [PATCH 22/32] Fix Test --- .../Primitives/PopupTests.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index e5dcba9912..53a8db2176 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -400,11 +400,13 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServicesWithFocus()) { + var window = PreparedWindow(); + var tb = new TextBox(); var b = new Button(); var p = new Popup { - PlacementTarget = PreparedWindow(), + PlacementTarget = window, Child = new StackPanel { Children = @@ -415,15 +417,28 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + window.Show(); - p.Opened += (s, e) => + p.Open(); + + if(p.Host is OverlayPopupHost host) { - tb.Focus(); - }; + //Need to measure/arrange for visual children to show up + //in OverlayPopupHost + host.Measure(Size.Infinity); + host.Arrange(new Rect(host.DesiredSize)); + } - p.Open(); + tb.Focus(); Assert.True(FocusManager.Instance?.Current == tb); + + //Ensure focus remains in the popup + var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next); + + Assert.True(nextFocus == b); + + p.Close(); } } From 888dd2b61b2bc1b1deadae4ed2c325874e5558a7 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 25 Sep 2020 16:30:51 +0200 Subject: [PATCH 23/32] Simplify PushPostTransform usage --- .../Media/TextFormatting/TextLayout.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 0c5179f88b..daa8807bf6 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -126,15 +126,12 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in TextLines) { - using (context.PushPostTransform(Matrix.CreateTranslation(0, currentY))) - { - var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, - _paragraphProperties.TextAlignment); + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); - using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, 0))) - { - textLine.Draw(context); - } + using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, currentY))) + { + textLine.Draw(context); } currentY += textLine.LineMetrics.Size.Height; From b9985a8fa0fde48ae98af48bd3f60c4bbdde90d4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Sep 2020 14:47:24 +0300 Subject: [PATCH 24/32] Refactorings for opengl context and surface management --- .../ControlCatalog/Pages/OpenGlPage.xaml.cs | 1 + src/Avalonia.Native/AvaloniaNativePlatform.cs | 8 +- ... AvaloniaNativePlatformOpenGlInterface.cs} | 41 ++-- src/Avalonia.Native/PopupImpl.cs | 6 +- src/Avalonia.Native/WindowImpl.cs | 6 +- src/Avalonia.Native/WindowImplBase.cs | 2 +- .../Angle/AngleEglInterface.cs | 1 + .../Angle/AngleWin32EglDisplay.cs | 10 +- .../{ => Controls}/OpenGlControlBase.cs | 192 +++++++++------- src/Avalonia.OpenGL/{ => Egl}/EglConsts.cs | 2 +- src/Avalonia.OpenGL/{ => Egl}/EglContext.cs | 69 ++++-- src/Avalonia.OpenGL/{ => Egl}/EglDisplay.cs | 53 ++--- src/Avalonia.OpenGL/{ => Egl}/EglErrors.cs | 2 +- .../Egl/EglGlPlatformSurface.cs | 54 +++++ .../{ => Egl}/EglGlPlatformSurfaceBase.cs | 47 ++-- src/Avalonia.OpenGL/{ => Egl}/EglInterface.cs | 2 +- .../Egl/EglPlatformOpenGlInterface.cs | 72 ++++++ src/Avalonia.OpenGL/{ => Egl}/EglSurface.cs | 11 +- src/Avalonia.OpenGL/EglGlPlatformFeature.cs | 43 ---- src/Avalonia.OpenGL/EglGlPlatformSurface.cs | 51 ----- src/Avalonia.OpenGL/GlInterface.cs | 13 ++ src/Avalonia.OpenGL/IGlContext.cs | 2 + .../IOpenGlAwarePlatformRenderInterface.cs | 2 +- .../IPlatformOpenGlInterface.cs | 13 ++ .../IWindowingPlatformGlFeature.cs | 8 - .../Imaging/IOpenGlBitmapImpl.cs | 17 ++ .../Imaging/IOpenGlTextureBitmapImpl.cs | 13 -- ...OpenGlTextureBitmap.cs => OpenGlBitmap.cs} | 34 ++- src/Avalonia.OpenGL/OpenGlException.cs | 1 + .../{ => Surfaces}/IGlPlatformSurface.cs | 2 +- .../IGlPlatformSurfaceRenderTarget.cs | 2 +- .../IGlPlatformSurfaceRenderingSession.cs | 2 +- src/Avalonia.X11/Glx/GlxContext.cs | 53 ++++- src/Avalonia.X11/Glx/GlxDisplay.cs | 8 +- src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs | 25 +-- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 13 +- src/Avalonia.X11/X11Platform.cs | 5 +- src/Avalonia.X11/X11Window.cs | 9 +- .../LinuxFramebufferPlatform.cs | 4 +- .../Output/DrmOutput.cs | 18 +- .../Output/IGlOutputBackend.cs | 9 + src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 2 +- .../Gpu/OpenGl/GlRenderTarget.cs | 1 + .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 9 +- .../Gpu/OpenGl/OpenGlBitmapImpl.cs | 207 ++++++++++++++++++ .../Gpu/OpenGlTextureBitmapImpl.cs | 81 ------- .../Avalonia.Skia/PlatformRenderInterface.cs | 6 +- src/Windows/Avalonia.Win32/Win32GlManager.cs | 9 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 +- src/iOS/Avalonia.iOS/EaglDisplay.cs | 20 +- src/iOS/Avalonia.iOS/EaglLayerSurface.cs | 3 +- src/iOS/Avalonia.iOS/Platform.cs | 2 +- 52 files changed, 786 insertions(+), 486 deletions(-) rename src/Avalonia.Native/{GlPlatformFeature.cs => AvaloniaNativePlatformOpenGlInterface.cs} (76%) rename src/Avalonia.OpenGL/{ => Controls}/OpenGlControlBase.cs (50%) rename src/Avalonia.OpenGL/{ => Egl}/EglConsts.cs (99%) rename src/Avalonia.OpenGL/{ => Egl}/EglContext.cs (55%) rename src/Avalonia.OpenGL/{ => Egl}/EglDisplay.cs (79%) rename src/Avalonia.OpenGL/{ => Egl}/EglErrors.cs (96%) create mode 100644 src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs rename src/Avalonia.OpenGL/{ => Egl}/EglGlPlatformSurfaceBase.cs (67%) rename src/Avalonia.OpenGL/{ => Egl}/EglInterface.cs (99%) create mode 100644 src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs rename src/Avalonia.OpenGL/{ => Egl}/EglSurface.cs (57%) delete mode 100644 src/Avalonia.OpenGL/EglGlPlatformFeature.cs delete mode 100644 src/Avalonia.OpenGL/EglGlPlatformSurface.cs create mode 100644 src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs delete mode 100644 src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs create mode 100644 src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs delete mode 100644 src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs rename src/Avalonia.OpenGL/Imaging/{OpenGlTextureBitmap.cs => OpenGlBitmap.cs} (54%) rename src/Avalonia.OpenGL/{ => Surfaces}/IGlPlatformSurface.cs (77%) rename src/Avalonia.OpenGL/{ => Surfaces}/IGlPlatformSurfaceRenderTarget.cs (89%) rename src/Avalonia.OpenGL/{ => Surfaces}/IGlPlatformSurfaceRenderingSession.cs (86%) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs delete mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index 6c13a5ac22..cb79bf219a 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; using Avalonia.Platform.Interop; using Avalonia.Threading; using static Avalonia.OpenGL.GlConsts; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 804cf7f8ac..e8b2f065c7 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -16,7 +16,7 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; - private GlPlatformFeature _glFeature; + private AvaloniaNativePlatformOpenGlInterface _platformGl; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); @@ -116,8 +116,8 @@ namespace Avalonia.Native { try { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(_platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay())); } catch (Exception) { @@ -128,7 +128,7 @@ namespace Avalonia.Native public IWindowImpl CreateWindow() { - return new WindowImpl(_factory, _options, _glFeature); + return new WindowImpl(_factory, _options, _platformGl); } public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Native/GlPlatformFeature.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs similarity index 76% rename from src/Avalonia.Native/GlPlatformFeature.cs rename to src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index e321db6eda..dbe968b82f 100644 --- a/src/Avalonia.Native/GlPlatformFeature.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -2,21 +2,20 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; +using Avalonia.OpenGL.Surfaces; using Avalonia.Threading; namespace Avalonia.Native { - class GlPlatformFeature : IWindowingPlatformGlFeature + class AvaloniaNativePlatformOpenGlInterface : IPlatformOpenGlInterface { private readonly IAvnGlDisplay _display; - public GlPlatformFeature(IAvnGlDisplay display) + public AvaloniaNativePlatformOpenGlInterface(IAvnGlDisplay display) { _display = display; var immediate = display.CreateContext(null); - var deferred = display.CreateContext(immediate); - int major, minor; GlInterface glInterface; using (immediate.MakeCurrent()) @@ -33,19 +32,22 @@ namespace Avalonia.Native } GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); - - ImmediateContext = new GlContext(GlDisplay, immediate, _version); - DeferredContext = new GlContext(GlDisplay, deferred, _version); + MainContext = new GlContext(GlDisplay, null, immediate, _version); } - internal IGlContext ImmediateContext { get; } - public IGlContext MainContext => DeferredContext; - internal GlContext DeferredContext { get; } + internal GlContext MainContext { get; } + public IGlContext PrimaryContext => MainContext; + + public bool CanShareContexts => true; + public bool CanCreateContexts => true; internal GlDisplay GlDisplay; private readonly GlVersion _version; + public IGlContext CreateSharedContext() => new GlContext(GlDisplay, + MainContext, _display.CreateContext(MainContext.Context), _version); + public IGlContext CreateContext() => new GlContext(GlDisplay, - _display.CreateContext(((GlContext)ImmediateContext).Context), _version); + null, _display.CreateContext(null), _version); } class GlDisplay @@ -72,11 +74,13 @@ namespace Avalonia.Native class GlContext : IGlContext { private readonly GlDisplay _display; + private readonly GlContext _sharedWith; public IAvnGlContext Context { get; private set; } - public GlContext(GlDisplay display, IAvnGlContext context, GlVersion version) + public GlContext(GlDisplay display, GlContext sharedWith, IAvnGlContext context, GlVersion version) { _display = display; + _sharedWith = sharedWith; Context = context; Version = version; } @@ -86,6 +90,17 @@ namespace Avalonia.Native public int SampleCount => _display.SampleCount; public int StencilSize => _display.StencilSize; public IDisposable MakeCurrent() => Context.MakeCurrent(); + public IDisposable EnsureCurrent() => MakeCurrent(); + + public bool IsSharedWith(IGlContext context) + { + var c = (GlContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + public void Dispose() { @@ -108,7 +123,7 @@ namespace Avalonia.Native public IGlPlatformSurfaceRenderingSession BeginDraw() { - var feature = (GlPlatformFeature)AvaloniaLocator.Current.GetService(); + var feature = (AvaloniaNativePlatformOpenGlInterface)AvaloniaLocator.Current.GetService(); return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 2d246e08d2..2f98385038 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - private readonly GlPlatformFeature _glFeature; + private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - GlPlatformFeature glFeature, + AvaloniaNativePlatformOpenGlInterface glFeature, IWindowBaseImpl parent) : base(opts, glFeature) { _factory = factory; @@ -23,7 +23,7 @@ namespace Avalonia.Native _parent = parent; using (var e = new PopupEvents(this)) { - var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + var context = _opts.UseGpu ? glFeature?.MainContext : null; Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); } PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 885591495b..11a0ebce61 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,19 +14,19 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - private readonly GlPlatformFeature _glFeature; + private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; IAvnWindow _native; private double _extendTitleBarHeight = -1; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - GlPlatformFeature glFeature) : base(opts, glFeature) + AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature) { _factory = factory; _opts = opts; _glFeature = glFeature; using (var e = new WindowEvents(this)) { - var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + var context = _opts.UseGpu ? glFeature?.MainContext : null; Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 56cf544d9d..5d35c773d7 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -61,7 +61,7 @@ namespace Avalonia.Native private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) + internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) { _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs index 8565d99b45..8c9b028164 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Platform.Interop; diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs index 1a42ed90c2..191fb53204 100644 --- a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs +++ b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; - -using static Avalonia.OpenGL.EglConsts; +using Avalonia.OpenGL.Egl; +using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Angle { @@ -52,7 +52,7 @@ namespace Avalonia.OpenGL.Angle } } - private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, info.Display) + private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, false, info.Display) { PlatformApi = info.PlatformApi; } @@ -78,11 +78,11 @@ namespace Avalonia.OpenGL.Angle return d3dDeviceHandle; } - public EglSurface WrapDirect3D11Texture(IntPtr handle) + public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle) { if (PlatformApi != AngleOptions.PlatformApi.DirectX11) throw new InvalidOperationException("Current platform API is " + PlatformApi); - return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); } } } diff --git a/src/Avalonia.OpenGL/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs similarity index 50% rename from src/Avalonia.OpenGL/OpenGlControlBase.cs rename to src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 8567dcae20..33773ed8e2 100644 --- a/src/Avalonia.OpenGL/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -3,44 +3,83 @@ using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.OpenGL.Imaging; -using Avalonia.Rendering; -using Avalonia.VisualTree; using static Avalonia.OpenGL.GlConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { private IGlContext _context; - private int _fb, _texture, _renderBuffer; - private OpenGlTextureBitmap _bitmap; - private PixelSize _oldSize; + private int _fb, _depthBuffer; + private OpenGlBitmap _bitmap; + private IOpenGlBitmapAttachment _attachment; + private PixelSize _depthBufferSize; private bool _glFailed; + private bool _initialized; protected GlVersion GlVersion { get; private set; } public sealed override void Render(DrawingContext context) { if(!EnsureInitialized()) return; - + using (_context.MakeCurrent()) { - using (_bitmap.Lock()) - { - var gl = _context.GlInterface; - gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - if (_oldSize != GetPixelSize()) - ResizeTexture(gl); - - OnOpenGlRender(gl, _fb); - gl.Flush(); - } + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); + EnsureTextureAttachment(); + EnsureDepthBufferAttachment(_context.GlInterface); + if(!CheckFramebufferStatus(_context.GlInterface)) + return; + + OnOpenGlRender(_context.GlInterface, _fb); + _attachment.Present(); } context.DrawImage(_bitmap, new Rect(_bitmap.Size), Bounds); base.Render(context); } + + private void CheckError(GlInterface gl) + { + int err; + while ((err = gl.GetError()) != GL_NO_ERROR) + Console.WriteLine(err); + } + + void EnsureTextureAttachment() + { + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); + if (_bitmap == null || _attachment == null || _bitmap.PixelSize != GetPixelSize()) + { + _attachment?.Dispose(); + _attachment = null; + _bitmap?.Dispose(); + _bitmap = null; + _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _attachment = _bitmap.CreateFramebufferAttachment(_context); + } + } + + void EnsureDepthBufferAttachment(GlInterface gl) + { + var size = GetPixelSize(); + if (size == _depthBufferSize && _depthBuffer != 0) + return; + + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + if (_depthBuffer != 0) gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + + var oneArr = new int[1]; + gl.GenRenderbuffers(1, oneArr); + _depthBuffer = oneArr[0]; + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + } - void DoCleanup(bool callUserDeinit) + void DoCleanup() { if (_context != null) { @@ -50,16 +89,19 @@ namespace Avalonia.OpenGL gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.DeleteFramebuffers(1, new[] { _fb }); - using (_bitmap.Lock()) - _bitmap.SetTexture(0, 0, new PixelSize(1, 1), 1); - gl.DeleteTextures(1, new[] { _texture }); - gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); - _bitmap.Dispose(); + gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + _attachment?.Dispose(); + _attachment = null; + _bitmap?.Dispose(); + _bitmap = null; try { - if (callUserDeinit) + if (_initialized) + { + _initialized = false; OnOpenGlDeinit(_context.GlInterface, _fb); + } } finally { @@ -72,11 +114,11 @@ namespace Avalonia.OpenGL protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - DoCleanup(true); + DoCleanup(); base.OnDetachedFromVisualTree(e); } - bool EnsureInitialized() + private bool EnsureInitializedCore() { if (_context != null) return true; @@ -84,34 +126,43 @@ namespace Avalonia.OpenGL if (_glFailed) return false; - var feature = AvaloniaLocator.Current.GetService(); + var feature = AvaloniaLocator.Current.GetService(); if (feature == null) return false; + if (!feature.CanShareContexts) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); + return false; + } try { - _context = feature.CreateContext(); - + _context = feature.CreateSharedContext(); } catch (Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - _glFailed = true; return false; } GlVersion = _context.Version; try { - _bitmap = new OpenGlTextureBitmap(); + _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + if (!_bitmap.SupportsContext(_context)) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create OpenGlBitmap: OpenGL context is not compatible"); + return false; + } } catch (Exception e) { _context.Dispose(); _context = null; Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e); - _glFailed = true; + "Unable to initialize OpenGL: unable to create OpenGlBitmap: {exception}", e); return false; } @@ -119,80 +170,55 @@ namespace Avalonia.OpenGL { try { - _oldSize = GetPixelSize(); + _depthBufferSize = GetPixelSize(); var gl = _context.GlInterface; var oneArr = new int[1]; gl.GenFramebuffers(1, oneArr); _fb = oneArr[0]; gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - - gl.GenTextures(1, oneArr); - _texture = oneArr[0]; - ResizeTexture(gl); - - gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + EnsureDepthBufferAttachment(gl); + EnsureTextureAttachment(); - var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) - { - int code; - while ((code = gl.GetError()) != 0) - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {code}", code); - - _glFailed = true; - return false; - } + return CheckFramebufferStatus(gl); } catch(Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to initialize OpenGL FBO: {exception}", e); - _glFailed = true; + return false; } - - if (!_glFailed) - OnOpenGlInit(_context.GlInterface, _fb); } + } - if (_glFailed) + private bool CheckFramebufferStatus(GlInterface gl) + { + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { - DoCleanup(false); + int code; + while ((code = gl.GetError()) != 0) + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL FBO: {code}", code); + return false; } return true; } - void ResizeTexture(GlInterface gl) + private bool EnsureInitialized() { - var size = GetPixelSize(); - - gl.GetIntegerv( GL_TEXTURE_BINDING_2D, out var oldTexture); - gl.BindTexture(GL_TEXTURE_2D, _texture); - gl.TexImage2D(GL_TEXTURE_2D, 0, - GlVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8, - size.Width, size.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - - gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); - var oneArr = new int[1]; - gl.GenRenderbuffers(1, oneArr); - _renderBuffer = oneArr[0]; - gl.BindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); - gl.RenderbufferStorage(GL_RENDERBUFFER, - GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, - size.Width, size.Height); - gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderBuffer); - gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); - using (_bitmap.Lock()) - _bitmap.SetTexture(_texture, GL_RGBA8, size, 1); + if (_initialized) + return true; + _glFailed = !(_initialized = EnsureInitializedCore()); + if (_glFailed) + return false; + using (_context.MakeCurrent()) + OnOpenGlInit(_context.GlInterface, _fb); + return true; } - PixelSize GetPixelSize() + private PixelSize GetPixelSize() { var scaling = VisualRoot.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs similarity index 99% rename from src/Avalonia.OpenGL/EglConsts.cs rename to src/Avalonia.OpenGL/Egl/EglConsts.cs index 8e44004f2d..58f5f1cef5 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -1,6 +1,6 @@ // ReSharper disable UnusedMember.Global // ReSharper disable IdentifierTypo -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public static class EglConsts { diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs similarity index 55% rename from src/Avalonia.OpenGL/EglContext.cs rename to src/Avalonia.OpenGL/Egl/EglContext.cs index 871665e857..5365354418 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,23 +1,25 @@ using System; using System.Reactive.Disposables; using System.Threading; -using static Avalonia.OpenGL.EglConsts; +using static Avalonia.OpenGL.Egl.EglConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglContext : IGlContext { private readonly EglDisplay _disp; private readonly EglInterface _egl; + private readonly EglContext _sharedWith; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface, + public EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, Func offscreenSurface, GlVersion version, int sampleCount, int stencilSize) { _disp = display; _egl = egl; + _sharedWith = sharedWith; Context = ctx; - OffscreenSurface = offscreenSurface; + OffscreenSurface = offscreenSurface(this); Version = version; SampleCount = sampleCount; StencilSize = stencilSize; @@ -33,21 +35,17 @@ namespace Avalonia.OpenGL public int StencilSize { get; } public EglDisplay Display => _disp; - public IDisposable Lock() - { - Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); - } - class RestoreContext : IDisposable { private readonly EglInterface _egl; + private readonly object _l; private readonly IntPtr _display; private IntPtr _context, _read, _draw; - public RestoreContext(EglInterface egl, IntPtr defDisplay) + public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { _egl = egl; + _l = l; _display = _egl.GetCurrentDisplay(); if (_display == IntPtr.Zero) _display = defDisplay; @@ -59,29 +57,52 @@ namespace Avalonia.OpenGL public void Dispose() { _egl.MakeCurrent(_display, _draw, _read, _context); + Monitor.Exit(_l); } } - public IDisposable MakeCurrent() + public IDisposable MakeCurrent() => MakeCurrent(OffscreenSurface); + + public IDisposable MakeCurrent(EglSurface surface) { - var old = new RestoreContext(_egl, _disp.Handle); - _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - return old; + Monitor.Enter(_lock); + var success = false; + try + { + var old = new RestoreContext(_egl, _disp.Handle, _lock); + var surf = surface ?? OffscreenSurface; + _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) + throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + success = true; + return old; + } + finally + { + if(!success) + Monitor.Enter(_lock); + } } - public IDisposable MakeCurrent(EglSurface surface) + public IDisposable EnsureCurrent() { - var old = new RestoreContext(_egl, _disp.Handle); - var surf = surface ?? OffscreenSurface; - _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - return old; + if(IsCurrent) + return Disposable.Empty; + return MakeCurrent(); } + public bool IsSharedWith(IGlContext context) + { + var c = (EglContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + + public bool IsCurrent => _egl.GetCurrentDisplay() == _disp.Handle && _egl.GetCurrentContext() == Context; + public void Dispose() { _egl.DestroyContext(_disp.Handle, Context); diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs similarity index 79% rename from src/Avalonia.OpenGL/EglDisplay.cs rename to src/Avalonia.OpenGL/Egl/EglDisplay.cs index 7f41e75d6a..fd3de854f5 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,26 +1,25 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -using Avalonia.Platform.Interop; -using static Avalonia.OpenGL.EglConsts; +using static Avalonia.OpenGL.Egl.EglConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglDisplay { private readonly EglInterface _egl; + public bool SupportsSharing { get; } private readonly IntPtr _display; private readonly IntPtr _config; private readonly int[] _contextAttributes; private readonly int _surfaceType; public IntPtr Handle => _display; + public IntPtr Config => _config; private int _sampleCount; private int _stencilSize; private GlVersion _version; - public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) + public EglDisplay(EglInterface egl, bool supportsSharing) : this(egl, supportsSharing, -1, IntPtr.Zero, null) { } @@ -45,15 +44,16 @@ namespace Avalonia.OpenGL return display; } - public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) - : this(egl, CreateDisplay(egl, platformType, platformDisplay, attrs)) + public EglDisplay(EglInterface egl, bool supportsSharing, int platformType, IntPtr platformDisplay, int[] attrs) + : this(egl, supportsSharing, CreateDisplay(egl, platformType, platformDisplay, attrs)) { } - public EglDisplay(EglInterface egl, IntPtr display) + public EglDisplay(EglInterface egl, bool supportsSharing, IntPtr display) { _egl = egl; + SupportsSharing = supportsSharing; _display = display; if(_display == IntPtr.Zero) throw new ArgumentException(); @@ -136,7 +136,12 @@ namespace Avalonia.OpenGL throw new OpenGlException("No suitable EGL config was found"); } - public EglDisplay() : this(new EglInterface()) + public EglDisplay() : this(false) + { + + } + + public EglDisplay(bool supportsSharing) : this(new EglInterface(), supportsSharing) { } @@ -144,6 +149,9 @@ namespace Avalonia.OpenGL public EglInterface EglInterface => _egl; public EglContext CreateContext(IGlContext share) { + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); + if((_surfaceType|EGL_PBUFFER_BIT) == 0) throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); var shareCtx = (EglContext)share; @@ -158,37 +166,22 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf), + var rv = new EglContext(this, _egl, shareCtx, ctx, context => new EglSurface(this, context, surf), _version, _sampleCount, _stencilSize); return rv; } public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) { + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); + var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, ctx, offscreenSurface, _version, _sampleCount, _stencilSize); + var rv = new EglContext(this, _egl, share, ctx, _ => offscreenSurface, _version, _sampleCount, _stencilSize); rv.MakeCurrent(null); return rv; } - - public EglSurface CreateWindowSurface(IntPtr window) - { - var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE}); - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); - return new EglSurface(this, _egl, s); - } - - public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) - { - var s = _egl.CreatePbufferFromClientBuffer(_display, bufferType, handle, - _config, attribs); - - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", _egl); - return new EglSurface(this, _egl, s); - } } } diff --git a/src/Avalonia.OpenGL/EglErrors.cs b/src/Avalonia.OpenGL/Egl/EglErrors.cs similarity index 96% rename from src/Avalonia.OpenGL/EglErrors.cs rename to src/Avalonia.OpenGL/Egl/EglErrors.cs index bfe46f2b69..d89bbb499f 100644 --- a/src/Avalonia.OpenGL/EglErrors.cs +++ b/src/Avalonia.OpenGL/Egl/EglErrors.cs @@ -1,4 +1,4 @@ -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public enum EglErrors { diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs new file mode 100644 index 0000000000..3d58660d47 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -0,0 +1,54 @@ +using Avalonia.OpenGL.Surfaces; + +namespace Avalonia.OpenGL.Egl +{ + public class EglGlPlatformSurface : EglGlPlatformSurfaceBase + { + private readonly EglPlatformOpenGlInterface _egl; + private readonly IEglWindowGlPlatformSurfaceInfo _info; + + public EglGlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) : base() + { + _egl = egl; + _info = info; + } + + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + var glSurface = _egl.CreateWindowSurface(_info.Handle); + return new RenderTarget(_egl, glSurface, _info); + } + + class RenderTarget : EglPlatformSurfaceRenderTargetBase + { + private readonly EglPlatformOpenGlInterface _egl; + private EglSurface _glSurface; + private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _currentSize; + + public RenderTarget(EglPlatformOpenGlInterface egl, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(egl) + { + _egl = egl; + _glSurface = glSurface; + _info = info; + _currentSize = info.Size; + } + + public override void Dispose() => _glSurface.Dispose(); + + public override IGlPlatformSurfaceRenderingSession BeginDraw() + { + if (_info.Size != _currentSize || _glSurface == null) + { + _glSurface?.Dispose(); + _glSurface = null; + _glSurface = _egl.CreateWindowSurface(_info.Handle); + _currentSize = _info.Size; + } + return base.BeginDraw(_glSurface, _info); + } + } + } +} + diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs similarity index 67% rename from src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs rename to src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index 00c7c4796c..4ea6766de2 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -1,6 +1,7 @@ using System; +using Avalonia.OpenGL.Surfaces; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface { @@ -14,19 +15,15 @@ namespace Avalonia.OpenGL public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); } - public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTarget { - private readonly EglDisplay _display; - private readonly EglContext _context; + private readonly EglPlatformOpenGlInterface _egl; - protected EglPlatformSurfaceRenderTargetBase(EglDisplay display, EglContext context) + protected EglPlatformSurfaceRenderTargetBase(EglPlatformOpenGlInterface egl) { - _display = display; - _context = context; + _egl = egl; } - public abstract bool IsCorrupted { get; } - public virtual void Dispose() { @@ -37,22 +34,25 @@ namespace Avalonia.OpenGL protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) { - var l = _context.Lock(); + + var restoreContext = _egl.PrimaryEglContext.MakeCurrent(surface); + var success = false; try { - if (IsCorrupted) - throw new RenderTargetCorruptedException(); - var restoreContext = _context.MakeCurrent(surface); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - - return new Session(_display, _context, surface, info, l, restoreContext, onFinish, isYFlipped); + var egli = _egl.Display.EglInterface; + egli.WaitClient(); + egli.WaitGL(); + egli.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + + _egl.PrimaryContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + + success = true; + return new Session(_egl.Display, _egl.PrimaryEglContext, surface, info, restoreContext, onFinish, isYFlipped); } - catch + finally { - l.Dispose(); - throw; + if(!success) + restoreContext.Dispose(); } } @@ -62,21 +62,19 @@ namespace Avalonia.OpenGL private readonly EglSurface _glSurface; private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglDisplay _display; - private readonly IDisposable _lock; private readonly IDisposable _restoreContext; private readonly Action _onFinish; public Session(EglDisplay display, EglContext context, EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable restoreContext, Action onFinish, bool isYFlipped) + IDisposable restoreContext, Action onFinish, bool isYFlipped) { IsYFlipped = isYFlipped; _context = context; _display = display; _glSurface = glSurface; _info = info; - _lock = @lock; _restoreContext = restoreContext; _onFinish = onFinish; } @@ -90,7 +88,6 @@ namespace Avalonia.OpenGL _display.EglInterface.WaitGL(); _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); _restoreContext.Dispose(); - _lock.Dispose(); _onFinish?.Invoke(); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs similarity index 99% rename from src/Avalonia.OpenGL/EglInterface.cs rename to src/Avalonia.OpenGL/Egl/EglInterface.cs index 666c0d8351..8055226042 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglInterface : GlInterfaceBase { diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..476f65a774 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -0,0 +1,72 @@ +using System; +using Avalonia.Logging; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Egl +{ + public class EglPlatformOpenGlInterface : IPlatformOpenGlInterface + { + public EglDisplay Display { get; private set; } + public bool CanCreateContexts => true; + public bool CanShareContexts => Display.SupportsSharing; + + public EglContext PrimaryEglContext { get; } + public IGlContext PrimaryContext => PrimaryEglContext; + + public EglPlatformOpenGlInterface(EglDisplay display) + { + Display = display; + PrimaryEglContext = display.CreateContext(null); + } + + public static void TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + } + + public static EglPlatformOpenGlInterface TryCreate() => TryCreate(() => new EglDisplay()); + public static EglPlatformOpenGlInterface TryCreate(Func displayFactory) + { + try + { + return new EglPlatformOpenGlInterface(displayFactory()); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); + return null; + } + } + + public IGlContext CreateContext() => Display.CreateContext(null); + public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryEglContext); + + + public EglSurface CreateWindowSurface(IntPtr window) + { + using (PrimaryContext.MakeCurrent()) + { + var s = Display.EglInterface.CreateWindowSurface(Display.Handle, Display.Config, window, + new[] { EGL_NONE, EGL_NONE }); + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateWindowSurface", Display.EglInterface); + return new EglSurface(Display, PrimaryEglContext, s); + } + } + + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) + { + using (PrimaryContext.MakeCurrent()) + { + var s = Display.EglInterface.CreatePbufferFromClientBuffer(Display.Handle, bufferType, handle, + Display.Config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", Display.EglInterface); + return new EglSurface(Display, PrimaryEglContext, s); + } + } + } +} diff --git a/src/Avalonia.OpenGL/EglSurface.cs b/src/Avalonia.OpenGL/Egl/EglSurface.cs similarity index 57% rename from src/Avalonia.OpenGL/EglSurface.cs rename to src/Avalonia.OpenGL/Egl/EglSurface.cs index 5ac56a00e3..a93751ca9e 100644 --- a/src/Avalonia.OpenGL/EglSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglSurface.cs @@ -1,22 +1,25 @@ using System; using System.Runtime.InteropServices; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglSurface : SafeHandle { private readonly EglDisplay _display; + private readonly EglContext _context; private readonly EglInterface _egl; - public EglSurface(EglDisplay display, EglInterface egl, IntPtr surface) : base(surface, true) + public EglSurface(EglDisplay display, EglContext context, IntPtr surface) : base(surface, true) { _display = display; - _egl = egl; + _context = context; + _egl = display.EglInterface; } protected override bool ReleaseHandle() { - _egl.DestroySurface(_display.Handle, handle); + using (_context.MakeCurrent()) + _egl.DestroySurface(_display.Handle, handle); return true; } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs deleted file mode 100644 index 7e9383432c..0000000000 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Avalonia.Logging; - -namespace Avalonia.OpenGL -{ - public class EglGlPlatformFeature : IWindowingPlatformGlFeature - { - private EglDisplay _display; - public EglDisplay Display => _display; - public IGlContext CreateContext() - { - return _display.CreateContext(DeferredContext); - } - public EglContext DeferredContext { get; private set; } - public IGlContext MainContext => DeferredContext; - - public static void TryInitialize() - { - var feature = TryCreate(); - if (feature != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); - } - - public static EglGlPlatformFeature TryCreate() => TryCreate(() => new EglDisplay()); - public static EglGlPlatformFeature TryCreate(Func displayFactory) - { - try - { - var disp = displayFactory(); - return new EglGlPlatformFeature - { - _display = disp, - DeferredContext = disp.CreateContext(null) - }; - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); - return null; - } - } - } -} diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs deleted file mode 100644 index 21fadff19e..0000000000 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading; - -namespace Avalonia.OpenGL -{ - public class EglGlPlatformSurface : EglGlPlatformSurfaceBase - { - private readonly EglDisplay _display; - private readonly EglContext _context; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base() - { - _display = context.Display; - _context = context; - _info = info; - } - - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() - { - var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_display, _context, glSurface, _info); - } - - class RenderTarget : EglPlatformSurfaceRenderTargetBase - { - private readonly EglDisplay _display; - private readonly EglContext _context; - private readonly EglSurface _glSurface; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - private PixelSize _initialSize; - - public RenderTarget(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(display, context) - { - _display = display; - _context = context; - _glSurface = glSurface; - _info = info; - _initialSize = info.Size; - } - - public override void Dispose() => _glSurface.Dispose(); - - public override bool IsCorrupted => _initialSize != _info.Size; - - public override IGlPlatformSurfaceRenderingSession BeginDraw() => base.BeginDraw(_glSurface, _info); - } - } -} - diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 23188e7dbf..ea2fe0a99c 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -82,6 +82,9 @@ namespace Avalonia.OpenGL [GlEntryPoint("glFlush")] public Action Flush { get; } + + [GlEntryPoint("glFinish")] + public Action Finish { get; } public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] @@ -144,6 +147,10 @@ namespace Avalonia.OpenGL [GlEntryPoint("glBindTexture")] public GlBindTexture BindTexture { get; } + public delegate void GlActiveTexture(int texture); + [GlEntryPoint("glActiveTexture")] + public GlActiveTexture ActiveTexture { get; } + public delegate void GlDeleteTextures(int count, int[] textures); [GlEntryPoint("glDeleteTextures")] public GlDeleteTextures DeleteTextures { get; } @@ -154,6 +161,12 @@ namespace Avalonia.OpenGL [GlEntryPoint("glTexImage2D")] public GlTexImage2D TexImage2D { get; } + public delegate void GlCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, + int width, int height); + + [GlEntryPoint("glCopyTexSubImage2D")] + public GlCopyTexSubImage2D CopyTexSubImage2D { get; } + public delegate void GlTexParameteri(int target, int name, int value); [GlEntryPoint("glTexParameteri")] public GlTexParameteri TexParameteri { get; } diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index eb4313fba9..50868db873 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -9,5 +9,7 @@ namespace Avalonia.OpenGL int SampleCount { get; } int StencilSize { get; } IDisposable MakeCurrent(); + IDisposable EnsureCurrent(); + bool IsSharedWith(IGlContext context); } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs index 30f83745ad..fdb9162164 100644 --- a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs +++ b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs @@ -4,6 +4,6 @@ namespace Avalonia.OpenGL { public interface IOpenGlAwarePlatformRenderInterface { - IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); } } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..5ee5df1e85 --- /dev/null +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -0,0 +1,13 @@ +namespace Avalonia.OpenGL +{ + public interface IPlatformOpenGlInterface + { + IGlContext PrimaryContext { get; } + IGlContext CreateSharedContext(); + bool CanShareContexts { get; } + bool CanCreateContexts { get; } + IGlContext CreateContext(); + /*IGlContext TryCreateContext(GlVersion version); + */ + } +} diff --git a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs b/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs deleted file mode 100644 index b91496f42b..0000000000 --- a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.OpenGL -{ - public interface IWindowingPlatformGlFeature - { - IGlContext CreateContext(); - IGlContext MainContext { get; } - } -} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs new file mode 100644 index 0000000000..aef4f601be --- /dev/null +++ b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL.Imaging +{ + public interface IOpenGlBitmapImpl : IBitmapImpl + { + IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); + bool SupportsContext(IGlContext context); + } + + public interface IOpenGlBitmapAttachment : IDisposable + { + void Present(); + } +} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs deleted file mode 100644 index e5f3691569..0000000000 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Avalonia.Media.Imaging; -using Avalonia.Platform; - -namespace Avalonia.OpenGL.Imaging -{ - public interface IOpenGlTextureBitmapImpl : IBitmapImpl - { - IDisposable Lock(); - void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling); - void SetDirty(); - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs similarity index 54% rename from src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs rename to src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs index 558eae8fdf..7af44cd624 100644 --- a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs +++ b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs @@ -6,32 +6,30 @@ using Avalonia.Threading; namespace Avalonia.OpenGL.Imaging { - public class OpenGlTextureBitmap : Bitmap, IAffectsRender + public class OpenGlBitmap : Bitmap, IAffectsRender { - private IOpenGlTextureBitmapImpl _impl; - static IOpenGlTextureBitmapImpl CreateOrThrow() + private IOpenGlBitmapImpl _impl; + + public OpenGlBitmap(PixelSize size, Vector dpi) + : base(CreateOrThrow(size, dpi)) { - if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface - glAware)) - throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); - return glAware.CreateOpenGlTextureBitmap(); + _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; } - public OpenGlTextureBitmap() - : base(CreateOrThrow()) + static IOpenGlBitmapImpl CreateOrThrow(PixelSize size, Vector dpi) { - _impl = (IOpenGlTextureBitmapImpl)PlatformImpl.Item; + if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface + glAware)) + throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); + return glAware.CreateOpenGlBitmap(size, dpi); } - public IDisposable Lock() => _impl.Lock(); + public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => + _impl.CreateFramebufferAttachment(context, SetIsDirty); - public void SetTexture(int textureId, int internalFormat, PixelSize size, double dpiScaling) - { - _impl.SetBackBuffer(textureId, internalFormat, size, dpiScaling); - SetIsDirty(); - } - - public void SetIsDirty() + public bool SupportsContext(IGlContext context) => _impl.SupportsContext(context); + + void SetIsDirty() { if (Dispatcher.UIThread.CheckAccess()) CallInvalidated(); diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index d3cd7d059e..196f507ad8 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.OpenGL.Egl; namespace Avalonia.OpenGL { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurface.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs similarity index 77% rename from src/Avalonia.OpenGL/IGlPlatformSurface.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs index 22d36b4472..875c215336 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs @@ -1,4 +1,4 @@ -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurface { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs similarity index 89% rename from src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs index d198d46e5c..f89b6f04f5 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurfaceRenderTarget : IDisposable { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs similarity index 86% rename from src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs index 89911a20a8..da06eab1e7 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurfaceRenderingSession : IDisposable { diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index 0349a6e26e..e9cb88cb8f 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -8,18 +8,21 @@ namespace Avalonia.X11.Glx { public IntPtr Handle { get; } public GlxInterface Glx { get; } + private readonly GlxContext _sharedWith; private readonly X11Info _x11; private readonly IntPtr _defaultXid; private readonly bool _ownsPBuffer; private readonly object _lock = new object(); - public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + GlxContext sharedWith, GlVersion version, int sampleCount, int stencilSize, X11Info x11, IntPtr defaultXid, bool ownsPBuffer) { Handle = handle; Glx = glx; + _sharedWith = sharedWith; _x11 = x11; _defaultXid = defaultXid; _ownsPBuffer = ownsPBuffer; @@ -37,25 +40,21 @@ namespace Avalonia.X11.Glx public int SampleCount { get; } public int StencilSize { get; } - public IDisposable Lock() - { - Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); - } - class RestoreContext : IDisposable { private GlxInterface _glx; private IntPtr _defaultDisplay; + private readonly object _l; private IntPtr _display; private IntPtr _context; private IntPtr _read; private IntPtr _draw; - public RestoreContext(GlxInterface glx, IntPtr defaultDisplay) + public RestoreContext(GlxInterface glx, IntPtr defaultDisplay, object l) { _glx = glx; _defaultDisplay = defaultDisplay; + _l = l; _display = _glx.GetCurrentDisplay(); _context = _glx.GetCurrentContext(); _read = _glx.GetCurrentReadDrawable(); @@ -66,19 +65,49 @@ namespace Avalonia.X11.Glx { var disp = _display == IntPtr.Zero ? _defaultDisplay : _display; _glx.MakeContextCurrent(disp, _draw, _read, _context); + Monitor.Exit(_l); } } public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); + public IDisposable EnsureCurrent() + { + if(IsCurrent) + return Disposable.Empty; + return MakeCurrent(); + } + + public bool IsSharedWith(IGlContext context) + { + var c = (GlxContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } public IDisposable MakeCurrent(IntPtr xid) { - var old = new RestoreContext(Glx, _x11.Display); - if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) - throw new OpenGlException("glXMakeContextCurrent failed "); - return old; + Monitor.Enter(_lock); + var success = false; + try + { + var old = new RestoreContext(Glx, _x11.Display, _lock); + if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) + throw new OpenGlException("glXMakeContextCurrent failed "); + + success = true; + return old; + } + finally + { + if (!success) + Monitor.Exit(_lock); + } } + public bool IsCurrent => Glx.GetCurrentContext() == Handle; + public void Dispose() { Glx.DestroyContext(_x11.Display, Handle); diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index b82895d12c..fa8c866c09 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -113,9 +113,9 @@ namespace Avalonia.X11.Glx } - public GlxContext CreateContext() => CreateContext(DeferredContext); - - GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, + public GlxContext CreateContext() => CreateContext(); + + public GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, share.SampleCount, share.StencilSize, true); GlxContext CreateContext(IntPtr defaultXid, IGlContext share, @@ -144,7 +144,7 @@ namespace Avalonia.X11.Glx if (handle != IntPtr.Zero) { _version = profile; - return new GlxContext(new GlxInterface(), handle, this, profile, + return new GlxContext(new GlxInterface(), handle, this, (GlxContext)share, profile, sampleCount, stencilSize, _x11, defaultXid, ownsPBuffer); } diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index ae6b0eb353..cb4ab4aca0 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -1,5 +1,8 @@ using System; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.X11.Glx { @@ -40,33 +43,26 @@ namespace Avalonia.X11.Glx public IGlPlatformSurfaceRenderingSession BeginDraw() { - var l = _context.Lock(); - try - { - - return new Session(_context, _info, l, _context.MakeCurrent(_info.Handle)); - } - catch - { - l.Dispose(); - throw; - } + var oldContext = _context.MakeCurrent(_info.Handle); + + // Reset to default FBO first + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, 0); + + return new Session(_context, _info, oldContext); } class Session : IGlPlatformSurfaceRenderingSession { private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - private IDisposable _lock; private readonly IDisposable _clearContext; public IGlContext Context => _context; public Session(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable clearContext) + IDisposable clearContext) { _context = context; _info = info; - _lock = @lock; _clearContext = clearContext; } @@ -77,7 +73,6 @@ namespace Avalonia.X11.Glx _context.Display.SwapBuffers(_info.Handle); _context.Glx.WaitX(); _clearContext.Dispose(); - _lock.Dispose(); } public PixelSize Size => _info.Size; diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index ad3a54bcc1..6735a32ffe 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -5,31 +5,34 @@ using Avalonia.OpenGL; namespace Avalonia.X11.Glx { - class GlxGlPlatformFeature : IWindowingPlatformGlFeature + class GlxPlatformOpenGlInterface : IPlatformOpenGlInterface { public GlxDisplay Display { get; private set; } + public bool CanCreateContexts => true; + public bool CanShareContexts => true; public IGlContext CreateContext() => Display.CreateContext(); + public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } - public IGlContext MainContext => DeferredContext; + public IGlContext PrimaryContext => DeferredContext; public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); return true; } return false; } - public static GlxGlPlatformFeature TryCreate(X11Info x11, IList glProfiles) + public static GlxPlatformOpenGlInterface TryCreate(X11Info x11, IList glProfiles) { try { var disp = new GlxDisplay(x11, glProfiles); - return new GlxGlPlatformFeature + return new GlxPlatformOpenGlInterface { Display = disp, DeferredContext = disp.DeferredContext diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d7bd81db98..c6db146f7b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -7,6 +7,7 @@ using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.X11; @@ -70,9 +71,9 @@ namespace Avalonia.X11 if (options.UseGpu) { if (options.UseEGL) - EglGlPlatformFeature.TryInitialize(); + EglPlatformOpenGlInterface.TryInitialize(); else - GlxGlPlatformFeature.TryInitialize(Info, Options.GlProfiles); + GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 0c0b942bcd..2cd3b973d8 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -12,6 +12,7 @@ using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -65,7 +66,7 @@ namespace Avalonia.X11 _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; - var glfeature = AvaloniaLocator.Current.GetService(); + var glfeature = AvaloniaLocator.Current.GetService(); XSetWindowAttributes attr = new XSetWindowAttributes(); var valueMask = default(SetWindowValuemask); @@ -87,13 +88,13 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - var glx = glfeature as GlxGlPlatformFeature; + var glx = glfeature as GlxPlatformOpenGlInterface; if (glx != null) visualInfo = *glx.Display.VisualInfo; else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglGlPlatformFeature; + var egl = glfeature as EglPlatformOpenGlInterface; var visual = IntPtr.Zero; var depth = 24; @@ -168,7 +169,7 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface(egl.DeferredContext, + new EglGlPlatformSurface(egl, new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index db37e4af0b..8801f71f9a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -32,8 +32,8 @@ namespace Avalonia.LinuxFramebuffer void Initialize() { Threading = new InternalPlatformThreadingInterface(); - if (_fb is IWindowingPlatformGlFeature glFeature) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature); + if (_fb is IGlOutputBackend gl) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 7a5d20fc83..72eed9e543 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -4,6 +4,8 @@ using System.ComponentModel; using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform.Interop; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; @@ -11,13 +13,16 @@ using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats; namespace Avalonia.LinuxFramebuffer.Output { - public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature + public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmCard _card; private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } - public IGlContext MainContext => _deferredContext; + public IGlContext PrimaryContext => _deferredContext; + + private EglPlatformOpenGlInterface _platformGl; + public IPlatformOpenGlInterface PlatformOpenGlInterface => _platformGl; public DrmOutput(string path = null) { @@ -132,10 +137,9 @@ namespace Avalonia.LinuxFramebuffer.Output if(_gbmTargetSurface == null) throw new InvalidOperationException("Unable to create GBM surface"); - - - _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null); - _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface); + _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); + _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); + _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); EglContext CreateContext(EglContext share) @@ -144,7 +148,7 @@ namespace Avalonia.LinuxFramebuffer.Output GbmBoFlags.GBM_BO_USE_RENDERING); if (offSurf == null) throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); - return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); + return _eglDisplay.CreateContext(share, _platformGl.CreateWindowSurface(offSurf)); } _deferredContext = CreateContext(null); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs new file mode 100644 index 0000000000..7bc73d590c --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs @@ -0,0 +1,9 @@ +using Avalonia.OpenGL; + +namespace Avalonia.LinuxFramebuffer.Output +{ + public interface IGlOutputBackend : IOutputBackend + { + public IPlatformOpenGlInterface PlatformOpenGlInterface { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 987e2c089c..1a7a9b75cf 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -19,6 +19,6 @@ namespace Avalonia.Skia public interface IOpenGlAwareSkiaGpu : ISkiaGpu { - IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 081db5d26a..6df8df9a4c 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 9278de2137..46d42dfdab 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; +using Avalonia.OpenGL.Surfaces; using SkiaSharp; namespace Avalonia.Skia @@ -8,10 +9,12 @@ namespace Avalonia.Skia class GlSkiaGpu : IOpenGlAwareSkiaGpu { private GRContext _grContext; + private IGlContext _glContext; - public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes) + public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) { - var context = gl.MainContext; + var context = openGl.PrimaryContext; + _glContext = context; using (context.MakeCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? @@ -40,6 +43,6 @@ namespace Avalonia.Skia return null; } - public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() => new OpenGlTextureBitmapImpl(); + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs new file mode 100644 index 0000000000..2ebf7c680b --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Imaging; +using Avalonia.Utilities; +using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.Skia +{ + class GlOpenGlBitmapImpl : IOpenGlBitmapImpl, IDrawableBitmapImpl + { + private readonly IGlContext _context; + private readonly object _lock = new object(); + private IGlPresentableOpenGlSurface _surface; + + public GlOpenGlBitmapImpl(IGlContext context, PixelSize pixelSize, Vector dpi) + { + _context = context; + PixelSize = pixelSize; + Dpi = dpi; + } + + public Vector Dpi { get; } + public PixelSize PixelSize { get; } + public int Version { get; private set; } + public void Save(string fileName) => throw new NotSupportedException(); + + public void Save(Stream stream) => throw new NotSupportedException(); + + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + lock (_lock) + { + if (_surface == null) + return; + using (_surface.Lock()) + { + using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, + new GRGlTextureInfo( + GlConsts.GL_TEXTURE_2D, (uint)_surface.GetTextureId(), + (uint)_surface.InternalFormat))) + using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888)) + { + // Again, silently ignore, if something went wrong it's not our fault + if (surface == null) + return; + + using (var snapshot = surface.Snapshot()) + context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); + } + + } + } + } + + public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback) + { + if (!SupportsContext(context)) + throw new OpenGlException("Context is not supported for texture sharing"); + return new SharedOpenGlBitmapAttachment(this, context, presentCallback); + } + + public bool SupportsContext(IGlContext context) + { + // TODO: negotiated platform surface sharing + return _context.IsSharedWith(context); + } + + public void Dispose() + { + + } + + internal void Present(IGlPresentableOpenGlSurface surface) + { + lock (_lock) + { + _surface = surface; + } + } + } + + interface IGlPresentableOpenGlSurface : IDisposable + { + int GetTextureId(); + int InternalFormat { get; } + IDisposable Lock(); + } + + class SharedOpenGlBitmapAttachment : IOpenGlBitmapAttachment, IGlPresentableOpenGlSurface + { + private readonly GlOpenGlBitmapImpl _bitmap; + private readonly IGlContext _context; + private readonly Action _presentCallback; + private readonly int _fbo; + private readonly int _texture; + private readonly int _frontBuffer; + private bool _disposed; + private readonly DisposableLock _lock = new DisposableLock(); + + public SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) + { + _bitmap = bitmap; + _context = context; + _presentCallback = presentCallback; + using (_context.EnsureCurrent()) + { + var glVersion = _context.Version; + InternalFormat = glVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + + _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); + if (_fbo == 0) + throw new OpenGlException("Current FBO is 0"); + + { + var gl = _context.GlInterface; + + var textures = new int[2]; + gl.GenTextures(2, textures); + _texture = textures[0]; + _frontBuffer = textures[1]; + + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + foreach (var t in textures) + { + gl.BindTexture(GL_TEXTURE_2D, t); + gl.TexImage2D(GL_TEXTURE_2D, 0, + InternalFormat, + _bitmap.PixelSize.Width, _bitmap.PixelSize.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + } + + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + } + } + } + + public void Present() + { + using (_context.MakeCurrent()) + { + if (_disposed) + throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); + + var gl = _context.GlInterface; + + gl.Finish(); + using (Lock()) + { + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + gl.GetIntegerv(GL_ACTIVE_TEXTURE, out var oldActive); + + gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); + gl.BindTexture(GL_TEXTURE_2D, _frontBuffer); + gl.ActiveTexture(GL_TEXTURE0); + + gl.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, _bitmap.PixelSize.Width, + _bitmap.PixelSize.Height); + + gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + gl.ActiveTexture(oldActive); + + gl.Finish(); + } + } + + _bitmap.Present(this); + _presentCallback(); + } + + public void Dispose() + { + var gl = _context.GlInterface; + _bitmap.Present(null); + + if(_disposed) + return; + using (_context.MakeCurrent()) + using (Lock()) + { + if(_disposed) + return; + _disposed = true; + gl.DeleteTextures(2, new[] { _texture, _frontBuffer }); + } + } + + int IGlPresentableOpenGlSurface.GetTextureId() + { + return _frontBuffer; + } + + public int InternalFormat { get; } + + public IDisposable Lock() => _lock.Lock(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs deleted file mode 100644 index 8d007e35f3..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.IO; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; -using Avalonia.Skia.Helpers; -using Avalonia.Utilities; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class OpenGlTextureBitmapImpl : IOpenGlTextureBitmapImpl, IDrawableBitmapImpl - { - private DisposableLock _lock = new DisposableLock(); - private int _textureId; - private int _internalFormat; - - public void Dispose() - { - using (Lock()) - { - _textureId = 0; - PixelSize = new PixelSize(1, 1); - Version++; - } - } - - public Vector Dpi { get; private set; } = new Vector(96, 96); - public PixelSize PixelSize { get; private set; } = new PixelSize(1, 1); - public int Version { get; private set; } = 0; - - public void Save(string fileName) => throw new System.NotSupportedException(); - public void Save(Stream stream) => throw new System.NotSupportedException(); - - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) - { - // For now silently ignore - if (context.GrContext == null) - return; - - using (Lock()) - { - if (_textureId == 0) - return; - using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, - new GRGlTextureInfo( - GlConsts.GL_TEXTURE_2D, (uint)_textureId, - (uint)_internalFormat))) - using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, - SKColorType.Rgba8888)) - { - // Again, silently ignore, if something went wrong it's not our fault - if (surface == null) - return; - - using (var snapshot = surface.Snapshot()) - context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); - } - } - } - - public IDisposable Lock() => _lock.Lock(); - - public void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling) - { - using (_lock.Lock()) - { - _textureId = textureId; - _internalFormat = internalFormat; - PixelSize = pixelSize; - Dpi = new Vector(96 * dpiScaling, 96 * dpiScaling); - Version++; - } - } - - public void SetDirty() - { - using (_lock.Lock()) - Version++; - } - } -} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index c4f70df7c0..b9c1cbc673 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia return; } - var gl = AvaloniaLocator.Current.GetService(); + var gl = AvaloniaLocator.Current.GetService(); if (gl != null) _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); } @@ -256,10 +256,10 @@ namespace Avalonia.Skia } - public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) { if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) - return glAware.CreateOpenGlTextureBitmap(); + return glAware.CreateOpenGlBitmap(size, dpi); if (_skiaGpu == null) throw new PlatformNotSupportedException("GPU acceleration is not available"); throw new PlatformNotSupportedException( diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index bd188ad53a..fbc56e7703 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,26 +1,27 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; namespace Avalonia.Win32 { static class Win32GlManager { /// This property is initialized if drawing platform requests OpenGL support - public static EglGlPlatformFeature EglFeature { get; private set; } + public static EglPlatformOpenGlInterface EglPlatformInterface { get; private set; } private static bool s_attemptedToInitialize; public static void Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => + AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => { if (!s_attemptedToInitialize) { - EglFeature = EglGlPlatformFeature.TryCreate(() => new AngleWin32EglDisplay()); + EglPlatformInterface = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); s_attemptedToInitialize = true; } - return EglFeature; + return EglPlatformInterface; }); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9c6bce1c90..cb85e14e5a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Win32.Input; @@ -103,8 +105,8 @@ namespace Avalonia.Win32 CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - if (Win32GlManager.EglFeature != null) - _gl = new EglGlPlatformSurface(Win32GlManager.EglFeature.DeferredContext, this); + if (Win32GlManager.EglPlatformInterface != null) + _gl = new EglGlPlatformSurface(Win32GlManager.EglPlatformInterface, this); Screen = new ScreenImpl(); diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 635df43407..f9c787b6a8 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,15 +1,18 @@ using System; +using System.Reactive.Disposables; using Avalonia.OpenGL; using OpenGLES; using OpenTK.Graphics.ES30; namespace Avalonia.iOS { - class EaglFeature : IWindowingPlatformGlFeature + class EaglFeature : IPlatformOpenGlInterface { + public IGlContext PrimaryContext => Context; + public IGlContext CreateSharedContext() => throw new NotSupportedException(); + public bool CanShareContexts => false; + public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); - - public IGlContext MainContext => Context; public GlContext Context { get; } = new GlContext(); } @@ -61,9 +64,18 @@ namespace Avalonia.iOS return new ResetContext(old); } + public IDisposable EnsureCurrent() + { + if(EAGLContext.CurrentContext == Context) + return Disposable.Empty; + return MakeCurrent(); + } + + public bool IsSharedWith(IGlContext context) => false; + public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); public GlInterface GlInterface { get; } public int SampleCount { get; } = 0; public int StencilSize { get; } = 9; } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 64912b8ae3..5e5e1da949 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using Avalonia.OpenGL; +using Avalonia.OpenGL.Surfaces; using CoreAnimation; using OpenTK.Graphics.ES30; @@ -91,4 +92,4 @@ namespace Avalonia.iOS } } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index b484559ff3..28bccb6637 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -26,7 +26,7 @@ namespace Avalonia.iOS var keyboard = new KeyboardDevice(); var softKeyboard = new SoftKeyboardHelper(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(GlFeature) + .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) From 48c6a170518d18a1395c2aff683314c0291d85c7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Sep 2020 10:25:53 +0300 Subject: [PATCH 25/32] Skip surface snapshot when doing a blit --- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27b29c6e1e..428087ac56 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -109,10 +109,16 @@ namespace Avalonia.Skia /// public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) { - using (var image = SnapshotImage()) + if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) { - context.Canvas.DrawImage(image, sourceRect, destRect, paint); + _surface.Canvas.Flush(); + _surface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); } + else + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } } /// From eb9ddd90013d3017340107fb083f0130a388b35b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Sep 2020 15:52:28 +0100 Subject: [PATCH 26/32] implement a simple PathIcon control to allow simple mono-tone icons to easily be used. --- src/Avalonia.Controls/PathIcon.cs | 22 ++++++++++++++++++++++ src/Avalonia.Themes.Default/PathIcon.xaml | 17 +++++++++++++++++ src/Avalonia.Themes.Fluent/PathIcon.xaml | 17 +++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/Avalonia.Controls/PathIcon.cs create mode 100644 src/Avalonia.Themes.Default/PathIcon.xaml create mode 100644 src/Avalonia.Themes.Fluent/PathIcon.xaml diff --git a/src/Avalonia.Controls/PathIcon.cs b/src/Avalonia.Controls/PathIcon.cs new file mode 100644 index 0000000000..e3a97b9544 --- /dev/null +++ b/src/Avalonia.Controls/PathIcon.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public class PathIcon : TemplatedControl + { + static PathIcon() + { + AffectsRender(SourceProperty); + } + + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + public Geometry Source + { + get { return GetValue(SourceProperty); } + set { SetValue(SourceProperty, value); } + } + } +} diff --git a/src/Avalonia.Themes.Default/PathIcon.xaml b/src/Avalonia.Themes.Default/PathIcon.xaml new file mode 100644 index 0000000000..bebebba90b --- /dev/null +++ b/src/Avalonia.Themes.Default/PathIcon.xaml @@ -0,0 +1,17 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/PathIcon.xaml b/src/Avalonia.Themes.Fluent/PathIcon.xaml new file mode 100644 index 0000000000..3fc25c50f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/PathIcon.xaml @@ -0,0 +1,17 @@ + + + From 71d02073275b5110ad473e2ad34fabe997ba2a32 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Sep 2020 15:54:17 +0100 Subject: [PATCH 27/32] add a preview. --- src/Avalonia.Themes.Fluent/PathIcon.xaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/PathIcon.xaml b/src/Avalonia.Themes.Fluent/PathIcon.xaml index 3fc25c50f8..6f7c3bcb52 100644 --- a/src/Avalonia.Themes.Fluent/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/PathIcon.xaml @@ -1,5 +1,13 @@ + + + + M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z + + + + diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 9ed3207235..30c6d39856 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -75,5 +75,8 @@ 18 8 + + 20 + 20 diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 46488c1c57..134e804c53 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -20,5 +20,7 @@ 1 2 10,6,6,5 + 20 + 20 From 376bb1ad49dc3aa629cb98d84b35c8b2078f4207 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 29 Sep 2020 13:27:00 +0300 Subject: [PATCH 32/32] fix issue #3113 compile avalonia.native.dylib with nuke --- nukebuild/Build.cs | 14 ++++++++++++-- src/Avalonia.Native/Avalonia.Native.csproj | 8 +++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 7e2bbc13bc..097815cc69 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -138,9 +138,19 @@ partial class Build : NukeBuild .SetWorkingDirectory(webappDir) .SetCommand("dist")); }); - - Target Compile => _ => _ + + Target CompileNative => _ => _ .DependsOn(Clean) + .OnlyWhenStatic(() => EnvironmentInfo.IsOsx) + .Executes(() => + { + var project = $"{RootDirectory}/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/"; + var args = $"-project {project} -configuration {Parameters.Configuration} CONFIGURATION_BUILD_DIR={RootDirectory}/Build/Products/Release"; + ProcessTasks.StartProcess("xcodebuild", args).AssertZeroExitCode(); + }); + + Target Compile => _ => _ + .DependsOn(Clean, CompileNative) .DependsOn(CompileHtmlPreviewer) .Executes(async () => { diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index f084411c2f..49bd578290 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -1,7 +1,8 @@  - false + $([MSBuild]::IsOSPlatform(OSX)) + $(PackAvaloniaNative) true netstandard2.0 /usr/bin/castxml @@ -10,8 +11,9 @@ false - + + libAvaloniaNative.dylib runtimes/osx/native/libAvaloniaNative.dylib true PreserveNewest @@ -26,4 +28,4 @@ - + \ No newline at end of file