Skip to content

Commit

Permalink
feat(effectiveviewport): Add support of the FrameworkElement.Effectiv…
Browse files Browse the repository at this point in the history
…eViewportChanged event
  • Loading branch information
dr1rrb committed Nov 16, 2020
1 parent cd5c57d commit 4fbdee5
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public double BringIntoViewDistanceY
}
}
#endif
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if false
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "NET461", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public global::Windows.Foundation.Rect EffectiveViewport
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ public bool AllowFocusOnInteraction
// Forced skipping of method Windows.UI.Xaml.FrameworkElement.IsLoaded.get
// Forced skipping of method Windows.UI.Xaml.FrameworkElement.EffectiveViewportChanged.add
// Forced skipping of method Windows.UI.Xaml.FrameworkElement.EffectiveViewportChanged.remove
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if false
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "NET461", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
protected void InvalidateViewport()
{
Expand Down Expand Up @@ -643,7 +643,7 @@ public static void DeferTree( global::Windows.UI.Xaml.DependencyObject element)
}
}
#endif
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if false
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "NET461", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public event global::Windows.Foundation.TypedEventHandler<global::Windows.UI.Xaml.FrameworkElement, global::Windows.UI.Xaml.EffectiveViewportChangedEventArgs> EffectiveViewportChanged
{
Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml/UIElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,7 @@ public void PopulatePropertyInfo( string propertyName, global::Windows.UI.Comp
// Forced skipping of method Windows.UI.Xaml.UIElement.KeyTipTargetProperty.get
// Forced skipping of method Windows.UI.Xaml.UIElement.KeyboardAcceleratorPlacementTargetProperty.get
// Forced skipping of method Windows.UI.Xaml.UIElement.KeyboardAcceleratorPlacementModeProperty.get
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if false
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "NET461", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public static void RegisterAsScrollPort( global::Windows.UI.Xaml.UIElement element)
{
Expand Down
20 changes: 20 additions & 0 deletions src/Uno.UI/UI/LayoutHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ internal static double FiniteOrDefault(this double value, double defaultValue)
#endif
}

[Pure]
internal static Point FiniteOrDefault(this Point value, Point defaultValue)
{
return new Point(
value.X.FiniteOrDefault(defaultValue.X),
value.Y.FiniteOrDefault(defaultValue.Y));
}

[Pure]
internal static Size FiniteOrDefault(this Size value, Size defaultValue)
{
Expand All @@ -284,6 +292,18 @@ internal static Size FiniteOrDefault(this Size value, Size defaultValue)
);
}

[Pure]
internal static Rect FiniteOrDefault(this Rect value, Rect defaultValue)
{
return new Rect(
value.X.FiniteOrDefault(defaultValue.X),
value.Y.FiniteOrDefault(defaultValue.Y),
value.Width.FiniteOrDefault(defaultValue.Width),
value.Height.FiniteOrDefault(defaultValue.Height));
}



[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static double AtMost(this double value, double most) => Math.Min(value, most);
Expand Down
6 changes: 6 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public ScrollViewer()
{
DefaultStyleKey = typeof(ScrollViewer);

UIElement.RegisterAsScrollPort(this);

UpdatesMode = Uno.UI.Xaml.Controls.ScrollViewer.GetUpdatesMode(this);
InitializePartial();

Expand Down Expand Up @@ -1140,6 +1142,10 @@ private void Update(bool isIntermediate)
HorizontalOffset = _pendingHorizontalOffset;
VerticalOffset = _pendingVerticalOffset;

// Effective viewport support
ScrollOffsets = new Point(_pendingHorizontalOffset, _pendingVerticalOffset);
InvalidateViewport();

ViewChanged?.Invoke(this, new ScrollViewerViewChangedEventArgs { IsIntermediate = isIntermediate });
}

Expand Down
15 changes: 15 additions & 0 deletions src/Uno.UI/UI/Xaml/EffectiveViewportChangedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

using Windows.Foundation;

namespace Windows.UI.Xaml
{
public partial class EffectiveViewportChangedEventArgs
{
internal EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}

public Rect EffectiveViewport { get; }
}
}
239 changes: 237 additions & 2 deletions src/Uno.UI/UI/Xaml/FrameworkElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
using Uno.Logging;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Uno;
using Uno.Disposables;
using Windows.UI.Core;
using System.ComponentModel;
using Uno.UI.DataBinding;
Expand All @@ -35,8 +38,7 @@ namespace Windows.UI.Xaml
{
public partial class FrameworkElement : UIElement, IFrameworkElement, IFrameworkElementInternal, ILayoutConstraints, IDependencyObjectParse
{
public
static class TraceProvider
public static class TraceProvider
{
public readonly static Guid Id = Guid.Parse("{DDDCCA61-5CB7-4585-95D7-58C5528AABE6}");

Expand Down Expand Up @@ -98,6 +100,236 @@ public object Tag

#endregion

#region EffectiveViewPort
private static RoutedEventHandler ReconfigureViewportPropagationOnLoad = (snd, e) => ((FrameworkElement)snd).ReconfigureViewportPropagation();
private event TypedEventHandler<FrameworkElement, EffectiveViewportChangedEventArgs> _effectiveViewportChanged;
private int _childrenInterestedInViewportUpdates;
private IDisposable _parentViewportUpdatesSubscription;
private Rect _parentViewport = Rect.Empty;
private Rect _localViewport = Rect.Empty; // i.e. the applied clipping, Empty if not clipped
private Rect _lastEffectiveSlot = new Rect();
private Rect _lastEffectiveViewport = new Rect();

public event TypedEventHandler<FrameworkElement, EffectiveViewportChangedEventArgs> EffectiveViewportChanged
{
add
{
_effectiveViewportChanged += value;
ReconfigureViewportPropagation();
}
remove
{
_effectiveViewportChanged -= value;
ReconfigureViewportPropagation();
}
}

/// <summary>
/// Indicates if the effective viewport should/will be propagated to/by this element
/// </summary>
private bool IsEffectiveViewportEnabled => _childrenInterestedInViewportUpdates > 0 || _effectiveViewportChanged != null;

/// <summary>
/// Make sure to request or disable effective viewport changes from the parent
/// </summary>
private void ReconfigureViewportPropagation(FrameworkElement child = null)
{
if (IsLoaded && IsEffectiveViewportEnabled)
{
if (_parentViewportUpdatesSubscription == null)
{
var parent = Parent;
// -- BEGIN -- WORKAROUND CASE FOR IFrameworkElement, can safely be removed when we strip out the IFrameworkElement
while (parent is IFrameworkElement pseudoFwElt && !(parent is FrameworkElement))
{
parent = pseudoFwElt.Parent;
}
// -- END -- WORKAROUND CASE FOR IFrameworkElement

if (parent is FrameworkElement parentFwElt)
{
_parentViewportUpdatesSubscription = parentFwElt.RequestViewportUpdates(this);
}
else
{
// We are the root of the visual tree (maybe just temporarily),
// we update the effective viewport in order to initialize the _parentViewport of children.
PropagateEffectiveViewportChange(isInitial: true);
}
}
else
{
// We are already subscribed, the parent won't send any update (and our _parentViewport is expected to be up-to-date).
// But if this "reconfigure" was made for a new child (child != null), we have to initialize its own _parentViewport.
child?.OnParentViewportChanged(this, GetEffectiveViewport(), isInitial: true);
}
}
else
{
if (_parentViewportUpdatesSubscription != null)
{
_parentViewportUpdatesSubscription.Dispose();
_parentViewportUpdatesSubscription = null;

_parentViewport = Rect.Empty;
}
}
}

/// <summary>
/// Used by a child of this element, in order to subscribe to viewport updates
/// (so the OnParentViewportChanged will be invoked on this given child)
/// </summary>
private IDisposable RequestViewportUpdates(FrameworkElement child)
{
//global::System.Diagnostics.Debug.Assert(Uno.UI.Extensions.DependencyObjectExtensions.GetChildren(this).Contains(child));

_childrenInterestedInViewportUpdates++;
ReconfigureViewportPropagation(child);

return Disposable.Create(() =>
{
_childrenInterestedInViewportUpdates--;
ReconfigureViewportPropagation();
});
}

/// <summary>
/// Used by a parent element to propagate down the viewport change
/// </summary>
private void OnParentViewportChanged(
UIElement parent, // We propagate the parent to avoid costly lookup and useless casting
Rect viewport, // Be aware tht it might be empty ([+∞,+∞,-∞,-∞]) if not clipped
bool isInitial = false) // Indicates that this update is only intended to initiate the _parentViewport
{
if (!IsEffectiveViewportEnabled)
{
// We do not keep the _parentViewport up-to-date if not needed.
// It's expected to the root parent to update its children when propagation activated.
return;
}

var viewportInLocalCoordinates = viewport.IsEmpty
? viewport
: GetTransform(this, parent).Transform(viewport);
if (viewportInLocalCoordinates == _parentViewport)
{
return;
}

_parentViewport = viewportInLocalCoordinates;
PropagateEffectiveViewportChange(isInitial);
}

private protected sealed override void OnViewportUpdated(Rect viewport) // a.k.a. OnLayoutUpdated
{
// Always keep it up-to-date, so if effective viewport is enable later, we will have a valid value.
_localViewport = viewport;

// Even if the viewport didn't changed, the LayoutSlot might have changed!
PropagateEffectiveViewportChange();
}

private Rect GetEffectiveViewport()
{
Rect viewport;
if (_localViewport.IsEmpty)
{
// The local element does not clips its children (the common case),
// so we only propagate the parent viewport (adjusted in the local coordinate space)
viewport = _parentViewport;
}
else
{
// The local element is clipping its children, so it defines the "effective" viewport for it and its children.
// We however still have to consider the offsets applied by the parent (i.e. _parentViewport X and Y)
// and constraint the viewport to the parent's viewport size (i.e. _parentViewport Width and Height).
// Note: At this point as the _parentViewport is defined in local coordinate space,
// _parentViewport.X and Y are usually negative.
// If there isn't any parent that clipped us, then it will be empty [+∞,+∞,-∞,-∞],
// in that case make sure to ignore it (we assume that it's possible to be clipped only on one direction).
viewport = new Rect(
x: _localViewport.X + _parentViewport.X.FiniteOrDefault(0),
y: _localViewport.Y + _parentViewport.Y.FiniteOrDefault(0),
width: Math.Min(_localViewport.Width, _parentViewport.Width.FiniteOrDefault(double.PositiveInfinity)),
height: Math.Min(_localViewport.Height, _parentViewport.Height.FiniteOrDefault(double.PositiveInfinity)));

// This element is also acting as scroller, so we also have to apply the local scroll offsets.
// Note: Those offsets should probably be part of the _localViewport (Frame vs. Bounds),
// but for now we supports only the internal controls that are able to set the internal ScrollOffsets property.
if (IsScrollPort)
{
viewport.X += ScrollOffsets.X;
viewport.Y += ScrollOffsets.Y;
}
}

return viewport;
}

private void PropagateEffectiveViewportChange(bool isInitial = false)
{
if (!IsEffectiveViewportEnabled)
{
return;
}

var viewport = GetEffectiveViewport();
var slot = LayoutSlot;

var isViewportUpdate = _lastEffectiveViewport != viewport;
var isSlotUpdate = _lastEffectiveSlot != LayoutSlot;

_lastEffectiveViewport = viewport;
_lastEffectiveSlot = slot;

if (!isInitial && (isViewportUpdate || isSlotUpdate))
{
// Note: Here the viewport might have some infinite values (notably if we don't have any parent that clipped us).
// In that case we fallback to the LayoutSlot as we should not raise the event with infinite values.
_effectiveViewportChanged?.Invoke(this, new EffectiveViewportChangedEventArgs(viewport.FiniteOrDefault(slot)));
}

if (_childrenInterestedInViewportUpdates > 0
&& (isViewportUpdate || isInitial)) // If isLayoutSlot update, then children element are also going to be arranged
{
var children = Uno.UI.Extensions.DependencyObjectExtensions.GetChildren(this);

// -- BEGIN -- WORKAROUND CASE FOR IFrameworkElement, can safely be removed when we strip out the IFrameworkElement
IEnumerable<DependencyObject> GetNestedChildren(DependencyObject c)
=> c is FrameworkElement
? new[] {c} :
c is IFrameworkElement pseudoFwElt
? Uno.UI.Extensions.DependencyObjectExtensions.GetChildren(pseudoFwElt).SelectMany(GetNestedChildren)
: Enumerable.Empty<DependencyObject>();
children = children.SelectMany(GetNestedChildren);
// -- END -- WORKAROUND CASE FOR IFrameworkElement

foreach (var child in children)
{
if (child is FrameworkElement childFwElt)
{
childFwElt.OnParentViewportChanged(this, viewport);
}
}
}
}

// This is the public API for the effective viewport invalidation
[NotImplemented] // Supported only for internal elements, cf. comment below
protected void InvalidateViewport()
{
if (!IsScrollPort)
{
throw new InvalidOperationException("InvalidateViewport can only be called on elements that have been registered as scroll ports.");
}

// Here we should use the clipping to determine the actual view port for external controls,
// but for now the clipping we support only internal controls that can set the ScrollOffsets property on UIElement.
PropagateEffectiveViewportChange();
}
#endregion

partial void Initialize()
{
#if !NETSTANDARD2_0
Expand All @@ -106,6 +338,9 @@ partial void Initialize()
Resources = new Windows.UI.Xaml.ResourceDictionary();

IFrameworkElementHelper.Initialize(this);

Loaded += ReconfigureViewportPropagationOnLoad;
Unloaded += ReconfigureViewportPropagationOnLoad;
}

public
Expand Down
Loading

0 comments on commit 4fbdee5

Please sign in to comment.