csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
485 lines
18 KiB
485 lines
18 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Media;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Rendering.SceneGraph
|
|
{
|
|
/// <summary>
|
|
/// Builds a scene graph from a visual tree.
|
|
/// </summary>
|
|
public class SceneBuilder : ISceneBuilder
|
|
{
|
|
/// <inheritdoc/>
|
|
public void UpdateAll(Scene scene)
|
|
{
|
|
_ = scene ?? throw new ArgumentNullException(nameof(scene));
|
|
Dispatcher.UIThread.VerifyAccess();
|
|
|
|
UpdateSize(scene);
|
|
scene.Layers.GetOrAdd(scene.Root.Visual);
|
|
|
|
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
|
|
using (var context = new DrawingContext(impl))
|
|
{
|
|
var clip = new Rect(scene.Root.Visual.Bounds.Size);
|
|
Update(context, scene, (VisualNode)scene.Root, clip, true);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool Update(Scene scene, Visual visual)
|
|
{
|
|
_ = scene ?? throw new ArgumentNullException(nameof(scene));
|
|
_ = visual ?? throw new ArgumentNullException(nameof(visual));
|
|
|
|
Dispatcher.UIThread.VerifyAccess();
|
|
|
|
if (!scene.Root.Visual.IsVisible)
|
|
{
|
|
throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual.");
|
|
}
|
|
|
|
var node = (VisualNode?)scene.FindNode(visual);
|
|
|
|
if (visual == scene.Root.Visual)
|
|
{
|
|
UpdateSize(scene);
|
|
}
|
|
|
|
if (visual.VisualRoot == scene.Root.Visual)
|
|
{
|
|
if (node?.Parent != null &&
|
|
visual.VisualParent != null &&
|
|
node.Parent.Visual != visual.VisualParent)
|
|
{
|
|
// The control has changed parents. Remove the node and recurse into the new parent node.
|
|
((VisualNode)node.Parent).RemoveChild(node);
|
|
Deindex(scene, node);
|
|
node = (VisualNode?)scene.FindNode(visual.VisualParent);
|
|
}
|
|
|
|
if (visual.IsVisible)
|
|
{
|
|
// If the node isn't yet part of the scene, find the nearest ancestor that is.
|
|
node = node ?? FindExistingAncestor(scene, visual);
|
|
|
|
// We don't need to do anything if this part of the tree has already been fully
|
|
// updated.
|
|
if (node != null && !node.SubTreeUpdated)
|
|
{
|
|
// If the control we've been asked to update isn't part of the scene then
|
|
// we're carrying out an add operation, so recurse and add all the
|
|
// descendents too.
|
|
var recurse = node.Visual != visual;
|
|
|
|
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
|
|
using (var context = new DrawingContext(impl))
|
|
{
|
|
var clip = new Rect(scene.Root.Visual.Bounds.Size);
|
|
|
|
if (node.Parent != null)
|
|
{
|
|
context.PushPostTransform(node.Parent.Transform);
|
|
clip = node.Parent.ClipBounds;
|
|
}
|
|
|
|
using (context.PushTransformContainer())
|
|
{
|
|
Update(context, scene, node, clip, recurse);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (node != null)
|
|
{
|
|
// The control has been hidden so remove it from its parent and deindex the
|
|
// node and its descendents.
|
|
((VisualNode?)node.Parent)?.RemoveChild(node);
|
|
Deindex(scene, node);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else if (node != null)
|
|
{
|
|
// The control has been removed so remove it from its parent and deindex the
|
|
// node and its descendents.
|
|
var trim = FindFirstDeadAncestor(scene, node);
|
|
((VisualNode)trim.Parent!).RemoveChild(trim);
|
|
Deindex(scene, trim);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static VisualNode? FindExistingAncestor(Scene scene, Visual visual)
|
|
{
|
|
var node = scene.FindNode(visual);
|
|
|
|
while (node == null && visual.IsVisible)
|
|
{
|
|
var parent = visual.VisualParent;
|
|
|
|
if (parent is null)
|
|
return null;
|
|
|
|
visual = parent;
|
|
node = scene.FindNode(visual);
|
|
}
|
|
|
|
return visual.IsVisible ? (VisualNode?)node : null;
|
|
}
|
|
|
|
private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node)
|
|
{
|
|
var parent = node.Parent;
|
|
|
|
while (parent!.Visual.VisualRoot == null)
|
|
{
|
|
node = parent;
|
|
parent = node.Parent;
|
|
}
|
|
|
|
return (VisualNode)node;
|
|
}
|
|
|
|
private static object GetOrCreateChildNode(Scene scene, Visual child, VisualNode parent)
|
|
{
|
|
var result = (VisualNode?)scene.FindNode(child);
|
|
|
|
if (result != null && result.Parent != parent)
|
|
{
|
|
Deindex(scene, result);
|
|
((VisualNode?)result.Parent)?.RemoveChild(result);
|
|
result = null;
|
|
}
|
|
|
|
return result ?? CreateNode(scene, child, parent);
|
|
}
|
|
|
|
private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse)
|
|
{
|
|
var visual = node.Visual;
|
|
var opacity = visual.Opacity;
|
|
var clipToBounds = visual.ClipToBounds;
|
|
#pragma warning disable CS0618 // Type or member is obsolete
|
|
var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ?
|
|
roundRectClip.ClipToBoundsRadius :
|
|
default;
|
|
#pragma warning restore CS0618 // Type or member is obsolete
|
|
|
|
var bounds = new Rect(visual.Bounds.Size);
|
|
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
|
|
|
|
contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds);
|
|
|
|
if (visual.IsVisible)
|
|
{
|
|
var m = node != scene.Root ?
|
|
Matrix.CreateTranslation(visual.Bounds.Position) :
|
|
Matrix.Identity;
|
|
|
|
var renderTransform = Matrix.Identity;
|
|
|
|
// this should be calculated BEFORE renderTransform
|
|
if (visual.HasMirrorTransform)
|
|
{
|
|
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
|
|
renderTransform *= mirrorMatrix;
|
|
}
|
|
|
|
if (visual.RenderTransform != null)
|
|
{
|
|
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
|
|
var offset = Matrix.CreateTranslation(origin);
|
|
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset);
|
|
renderTransform *= finalTransform;
|
|
}
|
|
|
|
m = renderTransform * m;
|
|
|
|
using (contextImpl.BeginUpdate(node))
|
|
using (context.PushPostTransform(m))
|
|
using (context.PushTransformContainer())
|
|
{
|
|
var globalBounds = bounds.TransformToAABB(contextImpl.Transform);
|
|
var clipBounds = clipToBounds ?
|
|
globalBounds.Intersect(clip) :
|
|
clip;
|
|
|
|
forceRecurse = forceRecurse ||
|
|
node.ClipBounds != clipBounds ||
|
|
node.Opacity != opacity ||
|
|
node.Transform != contextImpl.Transform;
|
|
|
|
node.Transform = contextImpl.Transform;
|
|
node.ClipBounds = clipBounds;
|
|
node.ClipToBounds = clipToBounds;
|
|
node.LayoutBounds = globalBounds;
|
|
node.ClipToBoundsRadius = clipToBoundsRadius;
|
|
node.GeometryClip = visual.Clip?.PlatformImpl;
|
|
node.Opacity = opacity;
|
|
|
|
// TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning.
|
|
node.OpacityMask = visual.OpacityMask?.ToImmutable();
|
|
|
|
if (ShouldStartLayer(visual))
|
|
{
|
|
if (node.LayerRoot != visual)
|
|
{
|
|
MakeLayer(scene, node);
|
|
}
|
|
else
|
|
{
|
|
UpdateLayer(node, scene.Layers[node.LayerRoot]);
|
|
}
|
|
}
|
|
else if (node.LayerRoot == node.Visual && node.Parent != null)
|
|
{
|
|
ClearLayer(scene, node);
|
|
}
|
|
|
|
if (node.ClipToBounds)
|
|
{
|
|
clip = clip.Intersect(node.ClipBounds);
|
|
}
|
|
|
|
try
|
|
{
|
|
visual.Render(context);
|
|
}
|
|
catch { }
|
|
|
|
var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform);
|
|
visual.SetTransformedBounds(transformed);
|
|
|
|
if (forceRecurse)
|
|
{
|
|
var visualChildren = (IList<Visual>) visual.VisualChildren;
|
|
|
|
node.TryPreallocateChildren(visualChildren.Count);
|
|
|
|
if (visualChildren.Count == 1)
|
|
{
|
|
var childNode = GetOrCreateChildNode(scene, visualChildren[0], node);
|
|
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
|
|
}
|
|
else if (visualChildren.Count > 1)
|
|
{
|
|
var count = visualChildren.Count;
|
|
|
|
if (visual.HasNonUniformZIndexChildren)
|
|
{
|
|
var sortedChildren = new (Visual visual, int index)[count];
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
sortedChildren[i] = (visualChildren[i], i);
|
|
}
|
|
|
|
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
|
|
Array.Sort(sortedChildren, (lhs, rhs) =>
|
|
{
|
|
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual);
|
|
|
|
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
|
|
});
|
|
|
|
foreach (var child in sortedChildren)
|
|
{
|
|
var childNode = GetOrCreateChildNode(scene, child.Item1, node);
|
|
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
|
|
}
|
|
}
|
|
else
|
|
foreach (var child in visualChildren)
|
|
{
|
|
var childNode = GetOrCreateChildNode(scene, child, node);
|
|
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
|
|
}
|
|
}
|
|
|
|
node.SubTreeUpdated = true;
|
|
contextImpl.TrimChildren();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
contextImpl.BeginUpdate(node).Dispose();
|
|
}
|
|
}
|
|
|
|
private static void UpdateSize(Scene scene)
|
|
{
|
|
var renderRoot = scene.Root.Visual as IRenderRoot;
|
|
var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size;
|
|
|
|
scene.Scaling = renderRoot?.RenderScaling ?? 1;
|
|
|
|
if (scene.Size != newSize)
|
|
{
|
|
var oldSize = scene.Size;
|
|
|
|
scene.Size = newSize;
|
|
|
|
Rect horizontalDirtyRect = default;
|
|
Rect verticalDirtyRect = default;
|
|
|
|
if (newSize.Width > oldSize.Width)
|
|
{
|
|
horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height);
|
|
}
|
|
|
|
if (newSize.Height > oldSize.Height)
|
|
{
|
|
verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height);
|
|
}
|
|
|
|
foreach (var layer in scene.Layers)
|
|
{
|
|
layer.Dirty.Add(horizontalDirtyRect);
|
|
layer.Dirty.Add(verticalDirtyRect);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static VisualNode CreateNode(Scene scene, Visual visual, VisualNode parent)
|
|
{
|
|
var node = new VisualNode(visual, parent);
|
|
node.LayerRoot = parent.LayerRoot;
|
|
scene.Add(node);
|
|
return node;
|
|
}
|
|
|
|
private static void Deindex(Scene scene, VisualNode node)
|
|
{
|
|
var nodeChildren = node.Children;
|
|
var nodeChildrenCount = nodeChildren.Count;
|
|
|
|
for (var i = 0; i < nodeChildrenCount; i++)
|
|
{
|
|
if (nodeChildren[i] is VisualNode visual)
|
|
{
|
|
Deindex(scene, visual);
|
|
}
|
|
}
|
|
|
|
scene.Remove(node);
|
|
|
|
node.SubTreeUpdated = true;
|
|
|
|
scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds);
|
|
|
|
node.Visual.SetTransformedBounds(null);
|
|
|
|
if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual)
|
|
{
|
|
scene.Layers.Remove(node.LayerRoot);
|
|
}
|
|
}
|
|
|
|
private static void ClearLayer(Scene scene, VisualNode node)
|
|
{
|
|
var parent = (VisualNode)node.Parent!;
|
|
var oldLayerRoot = node.LayerRoot;
|
|
var newLayerRoot = parent.LayerRoot!;
|
|
var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty;
|
|
var newDirtyRects = scene.Layers[newLayerRoot].Dirty;
|
|
|
|
existingDirtyRects.Coalesce();
|
|
|
|
foreach (var r in existingDirtyRects)
|
|
{
|
|
newDirtyRects.Add(r);
|
|
}
|
|
|
|
var oldLayer = scene.Layers[oldLayerRoot!];
|
|
PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer);
|
|
scene.Layers.Remove(oldLayer);
|
|
}
|
|
|
|
private static void MakeLayer(Scene scene, VisualNode node)
|
|
{
|
|
var oldLayerRoot = node.LayerRoot!;
|
|
var layer = scene.Layers.Add(node.Visual);
|
|
var oldLayer = scene.Layers[oldLayerRoot!];
|
|
|
|
UpdateLayer(node, layer);
|
|
PropagateLayer(node, layer, scene.Layers[oldLayerRoot]);
|
|
}
|
|
|
|
private static void UpdateLayer(VisualNode node, SceneLayer layer)
|
|
{
|
|
layer.Opacity = node.Visual.Opacity;
|
|
|
|
if (node.Visual.OpacityMask != null)
|
|
{
|
|
layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable();
|
|
layer.OpacityMaskRect = node.ClipBounds;
|
|
}
|
|
else
|
|
{
|
|
layer.OpacityMask = null;
|
|
layer.OpacityMaskRect = default;
|
|
}
|
|
|
|
layer.GeometryClip = node.HasAncestorGeometryClip ?
|
|
CreateLayerGeometryClip(node) :
|
|
null;
|
|
}
|
|
|
|
private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer)
|
|
{
|
|
node.LayerRoot = layer.LayerRoot;
|
|
|
|
layer.Dirty.Add(node.Bounds);
|
|
oldLayer.Dirty.Add(node.Bounds);
|
|
|
|
foreach (VisualNode child in node.Children)
|
|
{
|
|
// If the child is not the start of a new layer, recurse.
|
|
if (child.LayerRoot != child.Visual)
|
|
{
|
|
PropagateLayer(child, layer, oldLayer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// HACK: Disabled layers because they're broken in current renderer. See #2244.
|
|
private static bool ShouldStartLayer(Visual visual) => false;
|
|
|
|
private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node)
|
|
{
|
|
IGeometryImpl? result = null;
|
|
VisualNode? n = node;
|
|
|
|
for (;;)
|
|
{
|
|
n = (VisualNode?)n!.Parent;
|
|
|
|
if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (n?.GeometryClip != null)
|
|
{
|
|
var transformed = n.GeometryClip.WithTransform(n.Transform);
|
|
|
|
result = result == null ? transformed : result.Intersect(transformed);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|