From 07b457d8a0021ada606699529c9ce7c42f234c1c Mon Sep 17 00:00:00 2001 From: Nelson Carrillo Date: Tue, 29 May 2018 22:34:32 -0400 Subject: [PATCH 01/16] Add support for locating custom renderers --- src/Avalonia.Visuals/Media/DrawingContext.cs | 2 +- .../Rendering/IRendererFactory.cs | 20 +++++++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 9 ++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/IRendererFactory.cs diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 1d25224b8d..962f2c1ba8 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -59,7 +59,7 @@ namespace Avalonia.Media //HACK: This is a temporary hack that is used in the render loop //to update TransformedBounds property [Obsolete("HACK for render loop, don't use")] - internal Matrix CurrentContainerTransform => _currentContainerTransform; + public Matrix CurrentContainerTransform => _currentContainerTransform; /// /// Draws a bitmap image. diff --git a/src/Avalonia.Visuals/Rendering/IRendererFactory.cs b/src/Avalonia.Visuals/Rendering/IRendererFactory.cs new file mode 100644 index 0000000000..0d7044e125 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/IRendererFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Rendering +{ + /// + /// Defines the interface for a renderer factory. + /// + public interface IRendererFactory + { + /// + /// Creates a renderer. + /// + /// The root visual. + /// The render loop. + IRenderer Create(IRenderRoot root, IRenderLoop renderLoop); + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7b9f8ee066..159c8386b6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -124,9 +124,12 @@ namespace Avalonia.Win32 public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetService(); - return Win32Platform.UseDeferredRendering ? - (IRenderer)new DeferredRenderer(root, loop) : - new ImmediateRenderer(root); + var customRendererFactory = AvaloniaLocator.Current.GetService(); + + if (customRendererFactory != null) + return customRendererFactory.Create(root, loop); + + return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root); } public void Resize(Size value) From f596cb13bb369dd27ffba3fea74d6f857748f2f7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 21:45:04 -0500 Subject: [PATCH 02/16] Don't set when value is equal to the last fetched value (invalidated on PropertyChanged event). --- .../Plugins/InpcPropertyAccessorPlugin.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index ba4e60eb74..c03a384eee 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -52,9 +52,11 @@ namespace Avalonia.Data.Core.Plugins private class Accessor : PropertyAccessorBase, IWeakSubscriber { + private static readonly object CacheInvalid = new object(); private readonly WeakReference _reference; private readonly PropertyInfo _property; private bool _eventRaised; + private object _lastValue = CacheInvalid; public Accessor(WeakReference reference, PropertyInfo property) { @@ -72,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins get { var o = _reference.Target; - return (o != null) ? _property.GetValue(o) : null; + return (_lastValue = (o != null) ? _property.GetValue(o) : null); } } @@ -80,6 +82,11 @@ namespace Avalonia.Data.Core.Plugins { if (_property.CanWrite) { + if (!ShouldSet(value)) + { + return true; + } + _eventRaised = false; _property.SetValue(_reference.Target, value); @@ -94,11 +101,24 @@ namespace Avalonia.Data.Core.Plugins return false; } + private bool ShouldSet(object value) + { + if (PropertyType.IsValueType) + { + return !_lastValue.Equals(value); + } + else + { + return !Object.ReferenceEquals(_lastValue, value); + } + } + void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { _eventRaised = true; + _lastValue = CacheInvalid; SendCurrentValue(); } } From 41b20f63f0db3b3a63a772e70835336ad3f29d67 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 22:18:44 -0500 Subject: [PATCH 03/16] Move fix up to PropertyAccessorNode from InpcPropertyAccessorPlugin. --- .../Plugins/InpcPropertyAccessorPlugin.cs | 22 +---------- .../Data/Core/PropertyAccessorNode.cs | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index c03a384eee..ba4e60eb74 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -52,11 +52,9 @@ namespace Avalonia.Data.Core.Plugins private class Accessor : PropertyAccessorBase, IWeakSubscriber { - private static readonly object CacheInvalid = new object(); private readonly WeakReference _reference; private readonly PropertyInfo _property; private bool _eventRaised; - private object _lastValue = CacheInvalid; public Accessor(WeakReference reference, PropertyInfo property) { @@ -74,7 +72,7 @@ namespace Avalonia.Data.Core.Plugins get { var o = _reference.Target; - return (_lastValue = (o != null) ? _property.GetValue(o) : null); + return (o != null) ? _property.GetValue(o) : null; } } @@ -82,11 +80,6 @@ namespace Avalonia.Data.Core.Plugins { if (_property.CanWrite) { - if (!ShouldSet(value)) - { - return true; - } - _eventRaised = false; _property.SetValue(_reference.Target, value); @@ -101,24 +94,11 @@ namespace Avalonia.Data.Core.Plugins return false; } - private bool ShouldSet(object value) - { - if (PropertyType.IsValueType) - { - return !_lastValue.Equals(value); - } - else - { - return !Object.ReferenceEquals(_lastValue, value); - } - } - void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { _eventRaised = true; - _lastValue = CacheInvalid; SendCurrentValue(); } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 4dbff4602f..efa5b88303 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -12,8 +12,10 @@ namespace Avalonia.Data.Core { internal class PropertyAccessorNode : ExpressionNode, ISettableNode { + private static readonly object CacheInvalid = new object(); private readonly bool _enableValidation; private IPropertyAccessor _accessor; + private object _lastValue = CacheInvalid; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -29,12 +31,32 @@ namespace Avalonia.Data.Core { if (_accessor != null) { - try { return _accessor.SetValue(value, priority); } catch { } + try + { + if (ShouldNotSet(value)) + { + return true; + } + else + { + return _accessor.SetValue(value, priority); + } + } + catch { } } return false; } + private bool ShouldNotSet(object value) + { + if (PropertyType.IsValueType) + { + return _lastValue.Equals(value); + } + return Object.ReferenceEquals(_lastValue, value); + } + protected override IObservable StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); @@ -58,7 +80,18 @@ namespace Avalonia.Data.Core _accessor = accessor; return Disposable.Create(() => _accessor = null); }, - _ => accessor); + _ => accessor).Select(value => + { + if (value is BindingNotification notification) + { + _lastValue = notification.HasValue ? notification.Value : CacheInvalid; + } + else + { + _lastValue = value; + } + return value; + }); } } } From a37e24dc441f471f8f9d1896b09b5be569fc5028 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 12:30:12 -0500 Subject: [PATCH 04/16] Add unit test. --- .../AvaloniaObjectTests_Binding.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 02fb1f11ad..65d37503b6 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -457,6 +457,17 @@ namespace Avalonia.Base.UnitTests Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + + Assert.False(source.SetterCalled); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -545,5 +556,22 @@ namespace Avalonia.Base.UnitTests } } } + + private class TestTwoWayBindingViewModel + { + private double _value; + + public double Value + { + get => _value; + set + { + _value = value; + SetterCalled = true; + } + } + + public bool SetterCalled { get; private set; } + } } } \ No newline at end of file From 5ea9677cb961bbe3a0a06443eb516daf8fd86aa2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 12:30:49 -0500 Subject: [PATCH 05/16] Use WeakReferences to make sure we aren't accidentally keeping objects alive in this new cache. --- src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index efa5b88303..c958cfdb25 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -15,7 +15,7 @@ namespace Avalonia.Data.Core private static readonly object CacheInvalid = new object(); private readonly bool _enableValidation; private IPropertyAccessor _accessor; - private object _lastValue = CacheInvalid; + private WeakReference _lastValue = null; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -52,9 +52,9 @@ namespace Avalonia.Data.Core { if (PropertyType.IsValueType) { - return _lastValue.Equals(value); + return _lastValue?.Target.Equals(value) ?? false; } - return Object.ReferenceEquals(_lastValue, value); + return Object.ReferenceEquals(_lastValue?.Target ?? CacheInvalid, value); } protected override IObservable StartListeningCore(WeakReference reference) @@ -84,11 +84,11 @@ namespace Avalonia.Data.Core { if (value is BindingNotification notification) { - _lastValue = notification.HasValue ? notification.Value : CacheInvalid; + _lastValue = notification.HasValue ? new WeakReference(notification.Value) : null; } else { - _lastValue = value; + _lastValue = new WeakReference(value); } return value; }); From 1c3b714a0ec25166a8f3b15291f432b09da9b883 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 13:02:13 -0500 Subject: [PATCH 06/16] Move fix up to SettableNode and ExpressionNode so all settable node types (i.e. PropertyAccessor and Indexer nodes) can get the fix. --- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 6 +++ .../Data/Core/ExpressionObserver.cs | 4 +- src/Avalonia.Base/Data/Core/ISettableNode.cs | 15 ------- src/Avalonia.Base/Data/Core/IndexerNode.cs | 6 +-- .../Data/Core/PropertyAccessorNode.cs | 44 ++++--------------- src/Avalonia.Base/Data/Core/SettableNode.cs | 38 ++++++++++++++++ .../AvaloniaObjectTests_Binding.cs | 21 +++++++++ 7 files changed, 79 insertions(+), 55 deletions(-) delete mode 100644 src/Avalonia.Base/Data/Core/ISettableNode.cs create mode 100644 src/Avalonia.Base/Data/Core/SettableNode.cs diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ae70cacdba..ac7e97a4b1 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -11,6 +11,7 @@ namespace Avalonia.Data.Core { internal abstract class ExpressionNode : ISubject { + private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); @@ -18,6 +19,8 @@ namespace Avalonia.Data.Core private IDisposable _valueSubscription; private IObserver _observer; + protected WeakReference LastValue { get; private set; } + public abstract string Description { get; } public ExpressionNode Next { get; set; } @@ -61,6 +64,7 @@ namespace Avalonia.Data.Core { _valueSubscription?.Dispose(); _valueSubscription = null; + LastValue = null; nextSubscription?.Dispose(); _observer = null; }); @@ -120,6 +124,7 @@ namespace Avalonia.Data.Core if (notification == null) { + LastValue = new WeakReference(value); if (Next != null) { Next.Target = new WeakReference(value); @@ -131,6 +136,7 @@ namespace Avalonia.Data.Core } else { + LastValue = new WeakReference(notification.Value); if (Next != null) { Next.Target = new WeakReference(notification.Value); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7719f93a02..14bc09f5b7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -154,7 +154,7 @@ namespace Avalonia.Data.Core /// public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { - if (Leaf is ISettableNode settable) + if (Leaf is SettableNode settable) { var node = _node; while (node != null) @@ -188,7 +188,7 @@ namespace Avalonia.Data.Core /// Gets the type of the expression result or null if the expression could not be /// evaluated. /// - public Type ResultType => (Leaf as ISettableNode)?.PropertyType; + public Type ResultType => (Leaf as SettableNode)?.PropertyType; /// /// Gets the leaf node. diff --git a/src/Avalonia.Base/Data/Core/ISettableNode.cs b/src/Avalonia.Base/Data/Core/ISettableNode.cs deleted file mode 100644 index 7788407833..0000000000 --- a/src/Avalonia.Base/Data/Core/ISettableNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Data; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Data.Core -{ - interface ISettableNode - { - bool SetTargetValue(object value, BindingPriority priority); - Type PropertyType { get; } - } -} diff --git a/src/Avalonia.Base/Data/Core/IndexerNode.cs b/src/Avalonia.Base/Data/Core/IndexerNode.cs index 47e82fa2d3..633d3558ee 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNode.cs @@ -15,7 +15,7 @@ using Avalonia.Data; namespace Avalonia.Data.Core { - internal class IndexerNode : ExpressionNode, ISettableNode + internal class IndexerNode : SettableNode { public IndexerNode(IList arguments) { @@ -52,7 +52,7 @@ namespace Avalonia.Data.Core return Observable.Merge(inputs).StartWith(GetValue(target)); } - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; @@ -154,7 +154,7 @@ namespace Avalonia.Data.Core public IList Arguments { get; } - public Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; private object GetValue(object target) { diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index c958cfdb25..9d657b3144 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -10,12 +10,10 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { - internal class PropertyAccessorNode : ExpressionNode, ISettableNode + internal class PropertyAccessorNode : SettableNode { - private static readonly object CacheInvalid = new object(); private readonly bool _enableValidation; private IPropertyAccessor _accessor; - private WeakReference _lastValue = null; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -25,22 +23,15 @@ namespace Avalonia.Data.Core public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _accessor?.PropertyType; + public override Type PropertyType => _accessor?.PropertyType; - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { if (_accessor != null) { try { - if (ShouldNotSet(value)) - { - return true; - } - else - { - return _accessor.SetValue(value, priority); - } + return _accessor.SetValue(value, priority); } catch { } } @@ -48,15 +39,6 @@ namespace Avalonia.Data.Core return false; } - private bool ShouldNotSet(object value) - { - if (PropertyType.IsValueType) - { - return _lastValue?.Target.Equals(value) ?? false; - } - return Object.ReferenceEquals(_lastValue?.Target ?? CacheInvalid, value); - } - protected override IObservable StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); @@ -78,20 +60,12 @@ namespace Avalonia.Data.Core () => { _accessor = accessor; - return Disposable.Create(() => _accessor = null); - }, - _ => accessor).Select(value => - { - if (value is BindingNotification notification) + return Disposable.Create(() => { - _lastValue = notification.HasValue ? new WeakReference(notification.Value) : null; - } - else - { - _lastValue = new WeakReference(value); - } - return value; - }); + _accessor = null; + }); + }, + _ => accessor); } } } diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs new file mode 100644 index 0000000000..1f6ec0bff4 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -0,0 +1,38 @@ +using Avalonia.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Data.Core +{ + internal abstract class SettableNode : ExpressionNode + { + public bool SetTargetValue(object value, BindingPriority priority) + { + if (ShouldNotSet(value)) + { + return true; + } + return SetTargetValueCore(value, priority); + } + + private bool ShouldNotSet(object value) + { + if (PropertyType == null) + { + return false; + } + if (PropertyType.IsValueType) + { + return LastValue?.Target.Equals(value) ?? false; + } + return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); + } + + protected abstract bool SetTargetValueCore(object value, BindingPriority priority); + + public abstract Type PropertyType { get; } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 65d37503b6..4638aa84a5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -468,6 +468,17 @@ namespace Avalonia.Base.UnitTests Assert.False(source.SetterCalled); } + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); + + Assert.False(source.SetterCalled); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -571,6 +582,16 @@ namespace Avalonia.Base.UnitTests } } + public double this[int index] + { + get => _value; + set + { + _value = value; + SetterCalled = true; + } + } + public bool SetterCalled { get; private set; } } } From f423dbbb1de22222abf01bdfdcd9f7fec5c20d8c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 15:45:05 -0500 Subject: [PATCH 07/16] Fix codepath where the property is a value type but the weakreference has been collected. --- src/Avalonia.Base/Data/Core/SettableNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 1f6ec0bff4..092cdbe48f 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -26,7 +26,7 @@ namespace Avalonia.Data.Core } if (PropertyType.IsValueType) { - return LastValue?.Target.Equals(value) ?? false; + return LastValue?.Target != null && LastValue.Target.Equals(value); } return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); } From 0af6dd2c0a0361012220bcccb9febddd37d4531a Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Mon, 4 Jun 2018 19:55:31 +0200 Subject: [PATCH 08/16] Implement topmost on window --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 5 +++++ src/Avalonia.Controls/Window.cs | 14 ++++++++++++++ .../Remote/PreviewerWindowImpl.cs | 4 ++++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 4 ++++ src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 6 +++++- src/Gtk/Avalonia.Gtk3/WindowImpl.cs | 4 +++- src/OSX/Avalonia.MonoMac/WindowImpl.cs | 5 +++++ .../Avalonia.Win32/Interop/UnmanagedMethods.cs | 8 ++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 17 +++++++++++++++++ 9 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index f1f3925133..093e9bc57c 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -55,6 +55,11 @@ namespace Avalonia.Platform /// void CanResize(bool value); + /// + /// Gets or sets whether this window appears on top of all other windows + /// + void SetTopmost(bool value); + /// /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..1943225b00 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -104,6 +104,9 @@ namespace Avalonia.Controls public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; @@ -125,6 +128,8 @@ namespace Avalonia.Controls CanResizeProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue)); + TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); + WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); } @@ -230,6 +235,15 @@ namespace Avalonia.Controls set { SetValue(CanResizeProperty, value); } } + /// + /// Gets or sets whether this window appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Gets or sets the icon of the window. /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index ef16d06b60..d9a30f0d29 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -102,5 +102,9 @@ namespace Avalonia.DesignerSupport.Remote public void CanResize(bool value) { } + + public void SetTopmost(bool value) + { + } } } \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index e749d10468..43f0e0a95e 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -104,6 +104,10 @@ namespace Avalonia.DesignerSupport.Remote public void CanResize(bool value) { } + + public void SetTopmost(bool value) + { + } } class ClipboardStub : IClipboard diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 1adaf9f4e1..9cc6ba6901 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -261,10 +261,13 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_unmaximize(GtkWindow window); - + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_close(GtkWindow window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_keep_above(GtkWindow gtkWindow, bool setting); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_geometry_hints(GtkWindow window, IntPtr geometry_widget, ref GdkGeometry geometry, GdkWindowHints geom_mask); @@ -472,6 +475,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_maximize GtkWindowMaximize; public static D.gtk_window_unmaximize GtkWindowUnmaximize; public static D.gtk_window_close GtkWindowClose; + public static D.gtk_window_set_keep_above GtkWindowSetKeepAbove; public static D.gdk_window_begin_move_drag GdkWindowBeginMoveDrag; public static D.gdk_window_begin_resize_drag GdkWindowBeginResizeDrag; public static D.gdk_event_request_motions GdkEventRequestMotions; diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index bae34db6f3..4756d33e0f 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -81,7 +81,9 @@ namespace Avalonia.Gtk3 public void ShowTaskbarIcon(bool value) => Native.GtkWindowSetSkipTaskbarHint(GtkWidget, !value); public void CanResize(bool value) => Native.GtkWindowSetResizable(GtkWidget, value); - + + public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); + class EmptyDisposable : IDisposable { diff --git a/src/OSX/Avalonia.MonoMac/WindowImpl.cs b/src/OSX/Avalonia.MonoMac/WindowImpl.cs index a2f8df6791..34a0702f8c 100644 --- a/src/OSX/Avalonia.MonoMac/WindowImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowImpl.cs @@ -116,6 +116,11 @@ namespace Avalonia.MonoMac UpdateStyle(); } + public void SetTopmost(bool value) + { + Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; + } + public void SetTitle(string title) => Window.Title = title; class ModalDisposable : IDisposable diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 86dcec410b..daaee9636e 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -78,6 +78,14 @@ namespace Avalonia.Win32.Interop SWP_RESIZE = SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER } + public static class WindowPosZOrder + { + public static readonly IntPtr HWND_BOTTOM = new IntPtr(1); + public static readonly IntPtr HWND_TOP = new IntPtr(0); + public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); + } + public enum SizeCommand { Restored, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7b9f8ee066..8c0af62c74 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -32,6 +32,7 @@ namespace Avalonia.Win32 private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; + private bool _topmost = false; private double _scaling = 1; private WindowState _showWindowState; private WindowState _lastWindowState; @@ -901,5 +902,21 @@ namespace Avalonia.Win32 _resizable = value; } + + public void SetTopmost(bool value) + { + if (value == _topmost) + { + return; + } + + IntPtr hWndInsertAfter = value ? WindowPosZOrder.HWND_TOPMOST : WindowPosZOrder.HWND_NOTOPMOST; + UnmanagedMethods.SetWindowPos(_hwnd, + hWndInsertAfter, + 0, 0, 0, 0, + SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + + _topmost = value; + } } } From c19971a44f49e0b2aa4d52b166d69aac956422c7 Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Mon, 4 Jun 2018 19:58:53 +0200 Subject: [PATCH 09/16] Replace relevant WindowPos magic number with variable --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8c0af62c74..97e5aa99ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -839,7 +839,7 @@ namespace Avalonia.Win32 var cx = Math.Abs(monitorInfo.rcWork.right - x); var cy = Math.Abs(monitorInfo.rcWork.bottom - y); - SetWindowPos(_hwnd, new IntPtr(-2), x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); + SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } } From 42121ec2c15c755f113edabc31da92e4307b5578 Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Fri, 8 Jun 2018 08:05:28 +0200 Subject: [PATCH 10/16] Move topmost to WindowBase --- src/Avalonia.Controls/Platform/IWindowBaseImpl.cs | 5 +++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 5 ----- src/Avalonia.Controls/Window.cs | 14 -------------- src/Avalonia.Controls/WindowBase.cs | 14 ++++++++++++++ src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 2 ++ src/Gtk/Avalonia.Gtk3/WindowImpl.cs | 2 -- src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs | 1 + src/OSX/Avalonia.MonoMac/WindowImpl.cs | 5 ----- 8 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 4f7ac82df7..9ba68f584e 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.Platform /// void SetMinMaxSize(Size minSize, Size maxSize); + /// + /// Sets whether this window appears on top of all other windows + /// + void SetTopmost(bool value); + /// /// Gets platform specific display information /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 093e9bc57c..f1f3925133 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -55,11 +55,6 @@ namespace Avalonia.Platform /// void CanResize(bool value); - /// - /// Gets or sets whether this window appears on top of all other windows - /// - void SetTopmost(bool value); - /// /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1943225b00..3cbfdbd657 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -104,9 +104,6 @@ namespace Avalonia.Controls public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); - public static readonly StyledProperty TopmostProperty = - AvaloniaProperty.Register(nameof(Topmost)); - private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; @@ -128,8 +125,6 @@ namespace Avalonia.Controls CanResizeProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue)); - TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); - WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); } @@ -235,15 +230,6 @@ namespace Avalonia.Controls set { SetValue(CanResizeProperty, value); } } - /// - /// Gets or sets whether this window appears on top of all other windows - /// - public bool Topmost - { - get { return GetValue(TopmostProperty); } - set { SetValue(TopmostProperty, value); } - } - /// /// Gets or sets the icon of the window. /// diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c427df1c26..5d66bee2f8 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -38,6 +38,9 @@ namespace Avalonia.Controls o => o.Owner, (o, v) => o.Owner = v); + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; @@ -52,6 +55,8 @@ namespace Avalonia.Controls MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); + + TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); } public WindowBase(IWindowBaseImpl impl) : this(impl, AvaloniaLocator.Current) @@ -124,6 +129,15 @@ namespace Avalonia.Controls set { SetAndRaise(OwnerProperty, ref _owner, value); } } + /// + /// Gets or sets whether this window appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Activates the window. /// diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 94537d3475..8a880fd306 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -416,6 +416,8 @@ namespace Avalonia.Gtk3 public void Hide() => Native.GtkWidgetHide(GtkWidget); + public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); + void GetGlobalPointer(out int x, out int y) { int mask; diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index 4756d33e0f..a0b754c229 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -82,8 +82,6 @@ namespace Avalonia.Gtk3 public void CanResize(bool value) => Native.GtkWindowSetResizable(GtkWidget, value); - public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); - class EmptyDisposable : IDisposable { diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index 8cbc6cbdd8..89cef59b53 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -123,6 +123,7 @@ namespace Avalonia.MonoMac public void Hide() => Window?.OrderOut(Window); + public void SetTopmost(bool value) => Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; public void BeginMoveDrag() { diff --git a/src/OSX/Avalonia.MonoMac/WindowImpl.cs b/src/OSX/Avalonia.MonoMac/WindowImpl.cs index 34a0702f8c..a2f8df6791 100644 --- a/src/OSX/Avalonia.MonoMac/WindowImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowImpl.cs @@ -116,11 +116,6 @@ namespace Avalonia.MonoMac UpdateStyle(); } - public void SetTopmost(bool value) - { - Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; - } - public void SetTitle(string title) => Window.Title = title; class ModalDisposable : IDisposable From 835fbe251c0ddf8cd9da75ed2d377e2575e8836a Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Fri, 8 Jun 2018 09:47:47 +0200 Subject: [PATCH 11/16] Implement Topmost property on Popup --- src/Avalonia.Controls/Primitives/Popup.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 656f3890cd..005717d681 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -70,6 +70,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty StaysOpenProperty = AvaloniaProperty.Register(nameof(StaysOpen), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private bool _isOpen; private PopupRoot _popupRoot; private TopLevel _topLevel; @@ -84,6 +90,7 @@ namespace Avalonia.Controls.Primitives IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); + TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue); } /// @@ -194,6 +201,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(StaysOpenProperty, value); } } + /// + /// Gets or sets whether this popup appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Gets the root of the popup window. /// From c29a21afc2eee3fbdb04cab379360f7ce08fd401 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 10 Jun 2018 11:39:52 +0100 Subject: [PATCH 12/16] ensure tooltips fit inside screen edges. --- src/Avalonia.Controls/ToolTip.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 464e4188d2..10e964d014 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -234,11 +234,12 @@ namespace Avalonia.Controls { Close(); - _popup = new PopupRoot { Content = this }; + _popup = new PopupRoot { Content = this, }; ((ISetLogicalParent)_popup).SetParent(control); _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, GetHorizontalOffset(control), GetVerticalOffset(control)); _popup.Show(); + _popup.SnapInsideScreenEdges(); } private void Close() From 1a4fd00f2b77d0a648b00ca680227bb164c53ddd Mon Sep 17 00:00:00 2001 From: Ivan Garcia Date: Tue, 12 Jun 2018 17:25:43 -0400 Subject: [PATCH 13/16] Fix trailing newlines in textboxes when using NetCore/Skia Fixes issue where if a textbox's text ended with a newline character the additional line at the end would not render. Did this by, in these cases, adding an additional blank AvaloniaFormattedTextLine. This also fixes an issue where line widths were being measured incorrectly because the Substring() call used for that measurement was using line.Start before it was initialized for the current line. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 31 +++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 00f0a48a7b..13dcd9669d 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -560,13 +560,11 @@ namespace Avalonia.Skia } measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); - AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); line.TextLength = measured; - + line.Start = curOff; subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); - line.Start = curOff; line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; @@ -575,10 +573,33 @@ namespace Avalonia.Skia _skiaLines.Add(line); curY += _lineHeight; - curY += mLeading; - curOff += measured; + + //if this is the last line and there are trailing newline characters then + //insert a additional line + if (curOff >= length) + { + var subStringMinusNewlines = subString.TrimEnd('\n', '\r'); + var lengthDiff = subString.Length - subStringMinusNewlines.Length; + if (lengthDiff > 0) + { + AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); + lastLine.TextLength = lengthDiff; + lastLine.Start = curOff - lengthDiff; + var lastLineSubString = Text.Substring(line.Start, line.TextLength); + var lastLineWidth = _paint.MeasureText(lastLineSubString); + lastLine.Length = 0; + lastLine.Width = lastLineWidth; + lastLine.Height = _lineHeight; + lastLine.Top = curY; + + _skiaLines.Add(lastLine); + + curY += _lineHeight; + curY += mLeading; + } + } } // Now convert to Avalonia data formats From 3d092b9fcf31bdb7e7132565b0bd9b2088018149 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Tue, 12 Jun 2018 23:41:13 -0400 Subject: [PATCH 14/16] Added a Scroll event to ScrollBar The event is fired when the ScrollBar's value is changed through interaction with the ScrollBar's Track --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 37 +++++++++++++ .../Primitives/ScrollEventType.cs | 53 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/Avalonia.Controls/Primitives/ScrollEventType.cs diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 0057b15150..3ddcb06303 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -6,9 +6,21 @@ using System.Reactive; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { + public class ScrollEventArgs : EventArgs + { + public ScrollEventArgs(ScrollEventType eventType, double newValue) + { + ScrollEventType = eventType; + NewValue = newValue; + } + public double NewValue { get; private set; } + public ScrollEventType ScrollEventType { get; private set; } + } + /// /// A scrollbar control. /// @@ -44,6 +56,9 @@ namespace Avalonia.Controls.Primitives { PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); + + Thumb.DragDeltaEvent.AddClassHandler(o => o.OnThumbDragDelta, RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler(o => o.OnThumbDragComplete, RoutingStrategies.Bubble); } /// @@ -88,6 +103,8 @@ namespace Avalonia.Controls.Primitives set { SetValue(OrientationProperty, value); } } + public event EventHandler Scroll; + /// /// Calculates whether the scrollbar should be visible. /// @@ -140,6 +157,8 @@ namespace Avalonia.Controls.Primitives _pageUpButton = e.NameScope.Find