提交 d24dfb77 编写于 作者: D David

chore: Improve hit testing tracing

上级 d010280f
......@@ -31,34 +31,120 @@ namespace Uno.UI.Extensions
internal enum IndentationFormat
{
/// <summary>
/// Use tabs to indent log, e.g.
/// ```
/// [VisualRoot]
/// \t[Grid]
/// \t\t[TextBlock]
/// ```
/// </summary>
Tabs,
/// <summary>
/// Use tabs to indent log, e.g.
/// ```
/// [VisualRoot]
/// [Grid]
/// [TextBlock]
/// ```
/// </summary>
Spaces,
/// <summary>
/// Draw ASCII columns to indent log, e.g.
/// ```
/// [VisualRoot]
/// | [Grid]
/// | | [TextBlock]
/// ```
/// </summary>
Columns,
/// <summary>
/// Use the depth numbers of each level to indent log, e.g.
/// ```
/// [VisualRoot]
/// -1> [Grid]
/// -1>-2> [TextBlock]
/// ```
/// </summary>
Numbered,
/// <summary>
/// Draw ASCII columns with depth number to indent log, e.g.
/// ```
/// 01> [VisualRoot]
/// | 02> [Grid]
/// | | 03> [TextBlock]
/// ```
/// </summary>
NumberedColumns,
/// <summary>
/// Use spaces with only the current level number to indent the log, e.g.
/// ```
/// 01> [VisualRoot]
/// 02> [Grid]
/// 03> [TextBlock]
/// ```
/// </summary>
NumberedSpaces,
}
private static readonly IndentationFormat _defaultIndentationFormat = IndentationFormat.NumberedSpaces;
/// <summary>
/// The name (<see cref="GetDebugName"/>) indented (<see cref="GetDebugIndent(object?)"/>) for log usage.
/// </summary>
internal static string GetDebugIdentifier(this object? elt)
=> GetDebugIdentifier(elt, _defaultIndentationFormat);
/// <summary>
/// The name (<see cref="GetDebugName"/>) indented (<see cref="GetDebugIndent(object?)"/>) for log usage.
/// </summary>
internal static string GetDebugIdentifier(this object? elt, IndentationFormat format)
=> $"{elt.GetDebugIndent(format)} [{elt.GetDebugName()}]";
/// <summary>
/// Indentation log prefix built from the depth (<see cref="GetDebugDepth"/>) of the element for tree-view like logs.
/// </summary>
/// <param name="subLine">
/// Indicates that the indent is for a sub-line log of the given element,
/// so it has to be indented one extra level but without numbering it.
/// </param>
internal static string GetDebugIndent(this object? elt, bool subLine = false)
=> GetDebugIndent(elt, _defaultIndentationFormat, subLine);
/// <summary>
/// Indentation log prefix built from the depth (<see cref="GetDebugDepth"/>) of the element for tree-view like logs.
/// </summary>
/// <param name="subLine">
/// Indicates that the indent is for a sub-line log of the given element,
/// so it has to be indented one extra level but without numbering it.
/// </param>
internal static string GetDebugIndent(this object? elt, IndentationFormat format, bool subLine = false)
{
var depth = elt.GetDebugDepth();
var indentation = _defaultIndentationFormat switch
return format switch
{
IndentationFormat.Numbered when depth < 0 => "-?>",
IndentationFormat.NumberedColumns when depth < 0 => "?>",
_ when depth < 0 => "",
IndentationFormat.Columns => GetColumnsIndentation(depth),
IndentationFormat.Numbered => GetNumberedIndentation(depth),
IndentationFormat.NumberedColumns => GetNumberedColumnIndentation(depth),
IndentationFormat.NumberedSpaces => $"{new string(' ', depth * 2)} {depth:D2}>",
IndentationFormat.Tabs => $"{new string('\t', depth)}",
_ => $"{new string(' ', depth * 2)} {depth:D2}>", // default to NumberedSpaces
IndentationFormat.Numbered => GetNumberedIndentation(depth, subLine),
IndentationFormat.NumberedColumns => GetNumberedColumnIndentation(depth, subLine),
IndentationFormat.Tabs when subLine => new string('\t', depth + 1),
IndentationFormat.Tabs => new string('\t', depth),
IndentationFormat.Spaces when subLine => new string(' ', (depth + 1) * 2),
IndentationFormat.Spaces => new string(' ', depth * 2),
// default to NumberedSpaces
_ when subLine => new string(' ', (depth + 1) * 2 + 1),
_ => $"{new string(' ', depth * 2)} {depth:D2}>",
};
return indentation + $"[{elt.GetDebugName()}]";
static string GetColumnsIndentation(int depth)
{
var sb = new StringBuilder(depth * 2);
......@@ -71,7 +157,7 @@ namespace Uno.UI.Extensions
return sb.ToString();
}
static string GetNumberedIndentation(int depth)
static string GetNumberedIndentation(int depth, bool subLine)
{
var sb = new StringBuilder(depth * 4);
for (var i = 0; i < depth; i++)
......@@ -81,25 +167,37 @@ namespace Uno.UI.Extensions
sb.Append('>');
}
if (subLine)
{
sb.Append(' ', depth.ToString(CultureInfo.InvariantCulture).Length + 2);
}
return sb.ToString();
}
static string GetNumberedColumnIndentation(int depth)
static string GetNumberedColumnIndentation(int depth, bool subLine)
{
var sb = new StringBuilder(depth * 4);
for (var i = 0; i < depth - 1; i++)
var count = subLine ? depth : depth - 1;
for (var i = 0; i < count; i++)
{
sb.Append(' ', i.ToString(CultureInfo.InvariantCulture).Length);
sb.Append('|');
}
sb.Append(depth);
sb.Append('>');
if (!subLine)
{
sb.Append(depth);
sb.Append('>');
}
return sb.ToString();
}
}
/// <summary>
/// Gets the depth of an element in the visual tree.
/// </summary>
internal static int GetDebugDepth(this object? elt) =>
elt switch
{
......
......@@ -131,7 +131,7 @@ namespace Windows.UI.Xaml
dragInfo.Position,
Window.Current.RootElement.XamlRoot, //TODO: Choose proper XamlRoot https://github.com/unoplatform/uno/issues/8978
getTestability: GetDropHitTestability,
isStale: elt => elt.IsDragOver(dragInfo.SourceId));
isStale: new (elt => elt.IsDragOver(dragInfo.SourceId), "IsDragOver"));
// First raise the drag leave event on stale branch if any.
if (target.stale is { } staleBranch)
......
......@@ -30,4 +30,6 @@ namespace Windows.UI.Xaml
/// </summary>
Visible,
}
internal record struct StalePredicate(Predicate<UIElement> Method, string Name);
}
......@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Uno.Foundation.Extensibility;
using Uno.Foundation.Logging;
using Uno.UI.Extensions;
......@@ -60,7 +61,7 @@ internal partial class InputManager
private static IPointerExtension? _pointerExtension;
// TODO: Use pointer ID for the predicates
private static readonly Predicate<UIElement> _isOver = e => e.IsPointerOver;
private static readonly StalePredicate _isOver = new(e => e.IsPointerOver, "IsPointerOver");
private readonly Dictionary<Pointer, UIElement> _pressedElements = new();
......@@ -107,7 +108,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerWheel [{originalSource.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerWheelChanged [{originalSource.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -116,16 +117,6 @@ internal partial class InputManager
RaiseUsingCaptures(Wheel, originalSource, routedArgs);
}
private (UIElement?, VisualTreeHelper.Branch?) HitTest(PointerEventArgs args, Predicate<UIElement>? isStale = null)
{
if (_inputManager._contentRoot?.XamlRoot is null)
{
throw new InvalidOperationException("The XamlRoot must be properly initialized for hit testng.");
}
return VisualTreeHelper.HitTest(args.CurrentPoint.Position, _inputManager._contentRoot.XamlRoot, isStale: isStale);
}
internal void OnPointerEntered(Windows.UI.Core.PointerEventArgs args)
{
var (originalSource, _) = HitTest(args);
......@@ -151,7 +142,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerEntered [{originalSource.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerEntered [{originalSource.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -163,7 +154,7 @@ internal partial class InputManager
{
// This is how UWP behaves: when out of the bounds of the Window, the root element is use.
var originalSource = Windows.UI.Xaml.Window.Current.Content;
if (originalSource == null)
if (originalSource is null)
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
......@@ -174,7 +165,6 @@ internal partial class InputManager
}
var overBranchLeaf = VisualTreeHelper.SearchDownForLeaf(originalSource, _isOver);
if (overBranchLeaf is null)
{
if (this.Log().IsEnabled(LogLevel.Trace))
......@@ -187,7 +177,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerExited [{overBranchLeaf.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerExited [{overBranchLeaf.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -222,7 +212,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerPressed [{originalSource.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerPressed [{originalSource.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -254,7 +244,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerReleased [{originalSource.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerReleased [{originalSource.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -289,7 +279,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerMoved [{originalSource.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerMoved [{originalSource.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -297,16 +287,22 @@ internal partial class InputManager
// First raise the PointerExited events on the stale branch
if (staleBranch.HasValue)
{
Raise(Leave, staleBranch.Value, routedArgs);
if (Raise(Leave, staleBranch.Value, routedArgs) is { VisualTreeAltered: true })
{
// The visual tree have been modified in a way that requires to perform a new hit test.
originalSource = HitTest(args).element ?? Windows.UI.Xaml.Window.Current.Content;
}
}
// Second (try to) raise the PointerEnter on the OriginalSource
// Note: This won't do anything if already over.
routedArgs.Handled = false;
Raise(Enter, originalSource, routedArgs);
if (Raise(Enter, originalSource, routedArgs) is { VisualTreeAltered: true })
{
// The visual tree have been modified in a way that requires to perform a new hit test.
originalSource = HitTest(args).element ?? Windows.UI.Xaml.Window.Current.Content;
}
// Finally raise the event, either on the OriginalSource or on the capture owners if any
routedArgs.Handled = false;
RaiseUsingCaptures(Move, originalSource, routedArgs);
}
......@@ -330,7 +326,7 @@ internal partial class InputManager
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"CoreWindow_PointerCancelled [{originalSource.GetDebugName()}");
this.Log().Trace($"CoreWindow_PointerCancelled [{originalSource.GetDebugName()}]");
}
var routedArgs = new PointerRoutedEventArgs(args, originalSource);
......@@ -344,7 +340,7 @@ internal partial class InputManager
{
if (_pointerExtension is not null)
{
_pointerExtension?.SetPointerCapture(uniqueId, _inputManager._contentRoot.XamlRoot);
_pointerExtension.SetPointerCapture(uniqueId, _inputManager._contentRoot.XamlRoot);
}
else
{
......@@ -356,7 +352,7 @@ internal partial class InputManager
{
if (_pointerExtension is not null)
{
_pointerExtension?.ReleasePointerCapture(uniqueId, _inputManager._contentRoot.XamlRoot);
_pointerExtension.ReleasePointerCapture(uniqueId, _inputManager._contentRoot.XamlRoot);
}
else
{
......@@ -368,7 +364,7 @@ internal partial class InputManager
{
if (PointerCapture.TryGet(routedArgs.Pointer, out var capture))
{
foreach (var target in capture.Targets)
foreach (var target in capture.Targets.ToList())
{
target.Element.ReleasePointerCapture(capture.Pointer.UniqueId, kinds: PointerCaptureKind.Any);
}
......@@ -392,11 +388,22 @@ internal partial class InputManager
}
#region Helpers
private (UIElement? element, VisualTreeHelper.Branch? stale) HitTest(PointerEventArgs args, StalePredicate? isStale = null)
{
if (_inputManager._contentRoot.XamlRoot is null)
{
throw new InvalidOperationException("The XamlRoot must be properly initialized for hit testing.");
}
return VisualTreeHelper.HitTest(args.CurrentPoint.Position, _inputManager._contentRoot.XamlRoot, isStale: isStale);
}
private delegate void RaisePointerEventArgs(UIElement element, PointerRoutedEventArgs args, BubblingContext ctx);
private readonly record struct PointerEvent(RaisePointerEventArgs Invoke, [CallerMemberName] string Name = "");
private static readonly RaisePointerEventArgs Wheel = (elt, args, ctx) => elt.OnPointerWheel(args, ctx);
private static readonly RaisePointerEventArgs Enter = (elt, args, ctx) => elt.OnPointerEnter(args, ctx);
private static readonly RaisePointerEventArgs Leave = (elt, args, ctx) =>
private static readonly PointerEvent Wheel = new((elt, args, ctx) => elt.OnPointerWheel(args, ctx));
private static readonly PointerEvent Enter = new((elt, args, ctx) => elt.OnPointerEnter(args, ctx));
private static readonly PointerEvent Leave = new((elt, args, ctx) =>
{
elt.OnPointerExited(args, ctx);
......@@ -407,38 +414,70 @@ internal partial class InputManager
ctx.IsInternal = true;
args.Handled = false;
elt.OnPointerUp(args, ctx);
};
private static readonly RaisePointerEventArgs Pressed = (elt, args, ctx) => elt.OnPointerDown(args, ctx);
private static readonly RaisePointerEventArgs Released = (elt, args, ctx) => elt.OnPointerUp(args, ctx);
private static readonly RaisePointerEventArgs Move = (elt, args, ctx) => elt.OnPointerMove(args, ctx);
private static readonly RaisePointerEventArgs Cancelled = (elt, args, ctx) => elt.OnPointerCancel(args, ctx);
});
private static readonly PointerEvent Pressed = new((elt, args, ctx) => elt.OnPointerDown(args, ctx));
private static readonly PointerEvent Released = new((elt, args, ctx) => elt.OnPointerUp(args, ctx));
private static readonly PointerEvent Move = new((elt, args, ctx) => elt.OnPointerMove(args, ctx));
private static readonly PointerEvent Cancelled = new((elt, args, ctx) => elt.OnPointerCancel(args, ctx));
private static PointerEventDispatchResult Raise(PointerEvent evt, UIElement originalSource, PointerRoutedEventArgs routedArgs)
{
routedArgs.Handled = false;
UIElement.UIElement.BeginPointerEventDispatch();
private static void Raise(RaisePointerEventArgs raise, UIElement originalSource, PointerRoutedEventArgs routedArgs)
=> raise(originalSource, routedArgs, BubblingContext.Bubble);
evt.Invoke(originalSource, routedArgs, BubblingContext.Bubble);
private static void Raise(RaisePointerEventArgs raise, VisualTreeHelper.Branch branch, PointerRoutedEventArgs routedArgs)
=> raise(branch.Leaf, routedArgs, BubblingContext.BubbleUpTo(branch.Root));
return EndPointerEventDispatch();
}
private static PointerEventDispatchResult Raise(PointerEvent evt, VisualTreeHelper.Branch branch, PointerRoutedEventArgs routedArgs)
{
routedArgs.Handled = false;
UIElement.BeginPointerEventDispatch();
evt.Invoke(branch.Leaf, routedArgs, BubblingContext.BubbleUpTo(branch.Root));
return UIElement.EndPointerEventDispatch();
}
private static void RaiseUsingCaptures(RaisePointerEventArgs raise, UIElement originalSource, PointerRoutedEventArgs routedArgs)
private PointerEventDispatchResult RaiseUsingCaptures(PointerEvent evt, UIElement originalSource, PointerRoutedEventArgs routedArgs)
{
routedArgs.Handled = false;
UIElement.BeginPointerEventDispatch();
if (PointerCapture.TryGet(routedArgs.Pointer, out var capture))
{
var targets = capture.Targets.ToList();
if (capture.IsImplicitOnly)
{
raise(originalSource, routedArgs, BubblingContext.Bubble);
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"[Implicit capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to original source first [{originalSource.GetDebugName()}]");
}
evt.Invoke(originalSource, routedArgs, BubblingContext.Bubble);
foreach (var target in targets)
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"[Implicit capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to capture target [{originalSource.GetDebugName()}] (-- no bubbling--)");
}
routedArgs.Handled = false;
raise(target.Element, routedArgs, BubblingContext.NoBubbling);
evt.Invoke(target.Element, routedArgs, BubblingContext.NoBubbling);
}
}
else
{
var explicitTarget = targets.Find(c => c.Kind.HasFlag(PointerCaptureKind.Explicit))!;
raise(explicitTarget.Element, routedArgs, BubblingContext.Bubble);
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"[Explicit capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to capture target [{explicitTarget.Element.GetDebugName()}]");
}
evt.Invoke(explicitTarget.Element, routedArgs, BubblingContext.Bubble);
foreach (var target in targets)
{
......@@ -447,15 +486,27 @@ internal partial class InputManager
continue;
}
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"[Explicit capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to alternative (implicit) target [{explicitTarget.Element.GetDebugName()}] (-- no bubbling--)");
}
routedArgs.Handled = false;
raise(target.Element, routedArgs, BubblingContext.NoBubbling);
evt.Invoke(target.Element, routedArgs, BubblingContext.NoBubbling);
}
}
}
else
{
raise(originalSource, routedArgs, BubblingContext.Bubble);
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"[No capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to original source [{originalSource.GetDebugName()}]");
}
evt.Invoke(originalSource, routedArgs, BubblingContext.Bubble);
}
return UIElement.EndPointerEventDispatch();
}
#endregion
}
......
#nullable disable // Not supported by WinUI yet
// #define TRACE_HIT_TESTING
//#define TRACE_HIT_TESTING
using System;
using System.Collections.Generic;
......@@ -353,12 +353,12 @@ namespace Windows.UI.Xaml.Media
Point position,
XamlRoot? xamlRoot,
GetHitTestability? getTestability = null,
Predicate<UIElement>? isStale = null
StalePredicate? isStale = null
#if TRACE_HIT_TESTING
, [CallerMemberName] string? caller = null)
, [CallerMemberName] string caller = "")
{
using var _ = BEGIN_TRACE();
TRACE($"[{caller!.Replace("CoreWindow_Pointer", "").ToUpperInvariant()}] @{position.ToDebugString()}");
TRACE($"[{caller!.ToUpperInvariant()}] @{position.ToDebugString()}");
#else
)
{
......@@ -375,7 +375,7 @@ namespace Windows.UI.Xaml.Media
Point posRelToParent,
UIElement element,
GetHitTestability getVisibility,
Predicate<UIElement>? isStale = null,
StalePredicate? isStale = null,
Func<IEnumerable<UIElement>, IEnumerable<UIElement>>? childrenFilter = null)
{
var stale = default(Branch?);
......@@ -391,9 +391,9 @@ namespace Windows.UI.Xaml.Media
if (elementHitTestVisibility == HitTestability.Collapsed)
{
// Even if collapsed, if the element is stale, we search down for the real stale leaf
if (isStale?.Invoke(element) ?? false)
if (isStale?.Method.Invoke(element) ?? false)
{
stale = SearchDownForStaleBranch(element, isStale);
stale = SearchDownForStaleBranch(element, isStale.Value);
}
TRACE($"> NOT FOUND (Element is HitTestability.Collapsed) | stale branch: {stale?.ToString() ?? "-- none --"}");
......@@ -467,9 +467,18 @@ namespace Windows.UI.Xaml.Media
if (!clippingBounds.Contains(posRelToElement))
{
// Even if out of bounds, if the element is stale, we search down for the real stale leaf
if (isStale?.Invoke(element) ?? false)
if (isStale is not null)
{
stale = SearchDownForStaleBranch(element, isStale);
if (isStale.Value.Method(element))
{
TRACE($"- Is {isStale.Value.Name}");
stale = SearchDownForStaleBranch(element, isStale.Value);
}
else
{
TRACE($"- Is NOT {isStale.Value.Name}");
}
}
TRACE($"> NOT FOUND (Out of the **clipped** bounds) | stale branch: {stale?.ToString() ?? "-- none --"}");
......@@ -485,29 +494,65 @@ namespace Windows.UI.Xaml.Media
var childResult = SearchDownForTopMostElementAt(posRelToElement, child.Current!, getVisibility, isChildStale);
// If we found a stale element in child sub-tree, keep it and stop looking for stale elements
if (childResult.stale is { })
if (childResult.stale is not null)
{
stale = childResult.stale;
isChildStale = null;
}
// If we found an acceptable element in the child's sub-tree, job is done!
if (childResult.element is { })
if (childResult.element is not null)
{
if (isChildStale is { }) // Also indicates that stale is null
if (isChildStale is not null) // Also indicates that stale is null
{
// If we didn't find any stale root in previous children or in the child's sub tree,
// we continue to enumerate sibling children to detect a potential stale root.
TRACE($"+ Searching for stale {isChildStale.Value.Name} branch.");
while (child.MoveNext())
{
if (isChildStale(child.Current))
#if TRACE_HIT_TESTING
using var __ = SET_TRACE_SUBJECT(child.Current);
#endif
if (isChildStale.Value.Method(child.Current))
{
stale = SearchDownForStaleBranch(child.Current!, isChildStale);
TRACE($"- Is {isChildStale.Value.Name}");
stale = SearchDownForStaleBranch(child.Current!, isChildStale.Value);
#if TRACE_HIT_TESTING
while (child.MoveNext())
{
using var ___ = SET_TRACE_SUBJECT(child.Current);
if (isChildStale.Value.Method(child.Current))
{
//Debug.Assert(false);
TRACE($"- Is {isChildStale.Value.Name} ***** INVALID: Only one branch can be considered as stale at once! ****");
}
TRACE($"> Ignored since leaf and stale branch has already been found.");
}
#endif
break;
}
else
{
TRACE($"- Is NOT {isChildStale.Value.Name}");
}
}
}
#if TRACE_HIT_TESTING
else
{
while (child.MoveNext())
{
using var __ = SET_TRACE_SUBJECT(child.Current);
TRACE($"> Ignored since leaf has already been found and no stale branch to find.");
}
}
#endif
TRACE($"> found child: {childResult.element.GetDebugName()} | stale branch: {stale?.ToString() ?? "-- none --"}");
return (childResult.element, stale);
......@@ -525,7 +570,7 @@ namespace Windows.UI.Xaml.Media
{
// If no stale element found yet, validate if the current is stale.
// Note: no needs to search down for stale child, we already did it!
if (isStale?.Invoke(element) ?? false)
if (isStale?.Method.Invoke(element) ?? false)
{
stale = new Branch(element, stale?.Leaf ?? element);
}
......@@ -535,16 +580,33 @@ namespace Windows.UI.Xaml.Media
}
}
private static Branch SearchDownForStaleBranch(UIElement staleRoot, Predicate<UIElement> isStale)
=> new Branch(staleRoot, SearchDownForLeaf(staleRoot, isStale));
private static Branch SearchDownForStaleBranch(UIElement staleRoot, StalePredicate isStale)
=> new Branch(staleRoot, SearchDownForLeafCore(staleRoot, isStale));
internal static UIElement SearchDownForLeaf(UIElement root, StalePredicate predicate)
{
#if TRACE_HIT_TESTING
using var trace = ENSURE_TRACE();
#endif
return SearchDownForLeafCore(root, predicate);
}
internal static UIElement SearchDownForLeaf(UIElement root, Predicate<UIElement> predicate)
private static UIElement SearchDownForLeafCore(UIElement root, StalePredicate predicate)
{
foreach (var child in GetManagedVisualChildren(root).Reverse())
{
if (predicate(child))
#if TRACE_HIT_TESTING
SET_TRACE_SUBJECT(child);
#endif
if (predicate.Method(child))
{
return SearchDownForLeaf(child, predicate);
TRACE($"- Is {predicate.Name}");
return SearchDownForLeafCore(child, predicate);
}
else
{
TRACE($"- Is NOT {predicate.Name}");
}
}
......@@ -560,7 +622,7 @@ namespace Windows.UI.Xaml.Media
}
}
#region Helpers
#region Helpers
private static Func<IEnumerable<UIElement>, IEnumerable<UIElement>> Except(UIElement element)
=> children => children.Except(element);
......@@ -585,12 +647,17 @@ namespace Windows.UI.Xaml.Media
}
}
internal static IEnumerable<UIElement> GetManagedVisualChildren(object view)
=> view is _ViewGroup elt
? GetManagedVisualChildren(elt)
: Enumerable.Empty<UIElement>();
#if __IOS__ || __MACOS__ || __ANDROID__
/// <summary>
/// Gets all immediate UIElement children of this <paramref name="view"/>. If any immediate subviews are native, it will descend into
/// them depth-first until it finds a UIElement, and return those UIElements.
/// </summary>
private static IEnumerable<UIElement> GetManagedVisualChildren(_ViewGroup view)
internal static IEnumerable<UIElement> GetManagedVisualChildren(_ViewGroup view)
{
foreach (var child in view.GetChildren())
{
......@@ -608,11 +675,12 @@ namespace Windows.UI.Xaml.Media
}
}
#else
private static IEnumerable<UIElement> GetManagedVisualChildren(_View view) => view.GetChildren().OfType<UIElement>();
internal static IEnumerable<UIElement> GetManagedVisualChildren(_View view)
=> view.GetChildren().OfType<UIElement>();
#endif
#endregion
#endregion
#region HitTest tracing
#region HitTest tracing
#if TRACE_HIT_TESTING
[ThreadStatic]
private static StringBuilder? _trace;
......@@ -631,6 +699,9 @@ namespace Windows.UI.Xaml.Media
});
}
private static IDisposable ENSURE_TRACE()
=> _trace is null ? BEGIN_TRACE() : Disposable.Empty;
private static IDisposable SET_TRACE_SUBJECT(UIElement element)
{
if (_trace is { })
......@@ -638,8 +709,7 @@ namespace Windows.UI.Xaml.Media
var previous = _traceSubject;
_traceSubject = element;
_trace.Append(new string('\t', _traceSubject.Depth - 1));
_trace.Append($"[{element.GetDebugName()}]\r\n");
_trace.AppendLine(_traceSubject.GetDebugIdentifier());
return Disposable.Create(() => _traceSubject = previous);
}
......@@ -656,7 +726,8 @@ namespace Windows.UI.Xaml.Media
#if TRACE_HIT_TESTING
if (_trace is { })
{
_trace.Append(new string('\t', _traceSubject?.Depth ?? 0));
_trace.Append(_traceSubject.GetDebugIndent(subLine: true));
_trace.Append(' ');
_trace.Append(msg.ToStringInvariant());
_trace.Append("\r\n");
}
......
......@@ -646,7 +646,7 @@ namespace Windows.UI.Xaml
internal bool RaiseEvent(RoutedEvent routedEvent, RoutedEventArgs args, BubblingContext ctx = default)
{
#if TRACE_ROUTED_EVENT_BUBBLING
Debug.Write($"{this.GetDebugIdentifier()} - [{routedEvent.Name.TrimEnd("Event")}] (ctx: {ctx})\r\n");
global::System.Diagnostics.Debug.Write($"{this.GetDebugIdentifier()} - [{routedEvent.Name.TrimEnd("Event")}-{args?.GetHashCode():X8}] (ctx: {ctx}){(this is Windows.UI.Xaml.Controls.ContentControl ctrl ? ctrl.DataContext : "")}\r\n");
#endif
if (routedEvent.Flag == RoutedEventFlag.None)
......@@ -845,12 +845,8 @@ namespace Windows.UI.Xaml
/// </remarks>
public bool IsInternal { get; set; }
public BubblingContext WithMode(BubblingMode mode) => new()
{
Mode = mode,
Root = Root,
IsInternal = IsInternal
};
public BubblingContext WithMode(BubblingMode mode)
=> this with { Mode = mode };
public override string ToString()
=> $"{Mode}{(IsInternal ? " *internal*" : "")}{(Root is { } r ? $" up to {Root.GetDebugName()}" : "")}";
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册