瀏覽代碼

Listeners aware preview generation

Krzysztof Krysiński 2 周之前
父節點
當前提交
2ffd16a790

+ 8 - 1
src/PixiEditor.ChangeableDocument/Rendering/PreviewRenderRequest.cs

@@ -6,10 +6,17 @@ public record struct PreviewRenderRequest
 {
     public Texture? Texture { get; set; }
     public string? ElementToRender { get; set; }
+    public Action TextureUpdatedAction { get; set; }
 
-    public PreviewRenderRequest(Texture? texture, string? elementToRender = null)
+    public PreviewRenderRequest(Texture? texture, Action textureUpdatedAction, string? elementToRender = null)
     {
         Texture = texture;
+        TextureUpdatedAction = textureUpdatedAction;
         ElementToRender = elementToRender;
     }
+
+    public void InvokeTextureUpdated()
+    {
+        TextureUpdatedAction?.Invoke();
+    }
 }

+ 12 - 1
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -149,7 +149,7 @@ internal class ActionAccumulator
                     if (undoBoundaryPassed || viewportRefreshRequest || changeFrameRequest ||
                         document.SizeBindable.LongestAxis <= LiveUpdatePerformanceThreshold)
                     {
-                        previewTextures = previewUpdater.UpdatePreviews(
+                        previewTextures = previewUpdater.GatherPreviewsToUpdate(
                             affectedAreas.ChangedMembers,
                             affectedAreas.ChangedMasks,
                             affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames,
@@ -158,8 +158,19 @@ internal class ActionAccumulator
                     }
                 }
 
+                List<Action>? updatePreviewActions = previewTextures?.Values.Select(x => x.Select(r => r.TextureUpdatedAction))
+                    .SelectMany(x => x).ToList();
+
                 await document.SceneRenderer.RenderAsync(internals.State.Viewports, affectedAreas.MainImageArea,
                     !previewsDisabled && updateDelayed, previewTextures);
+
+                if (updatePreviewActions != null)
+                {
+                    foreach (var action in updatePreviewActions)
+                    {
+                        action();
+                    }
+                }
             }
         }
         catch (Exception e)

+ 3 - 7
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -1,14 +1,10 @@
-using System.Collections.ObjectModel;
-using System.ComponentModel;
+using System.ComponentModel;
 using Avalonia;
 using Avalonia.Media;
-using ChunkyImageLib;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
-using Drawie.Backend.Core;
-using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.Handlers;
@@ -22,7 +18,7 @@ public interface INodeHandler : INotifyPropertyChanged, IDisposable
     public NodeMetadata Metadata { get; set; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
-    public Texture? Preview { get; set; }
+    public TexturePreview? Preview { get; set; }
     public VecD PositionBindable { get; set; }
     public Rect UiSize { get; set; }
     public bool IsNodeSelected { get; set; }

+ 3 - 2
src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs

@@ -7,6 +7,7 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Rendering;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Document;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 namespace PixiEditor.Models.Handlers;
@@ -14,8 +15,8 @@ namespace PixiEditor.Models.Handlers;
 internal interface IStructureMemberHandler : INodeHandler
 {
     public bool HasMaskBindable { get; }
-    public Texture? MaskPreview { get; set; }
-    public Texture? Preview { get; set; }
+    public TexturePreview? MaskPreview { get; set; }
+    public TexturePreview? Preview { get; set; }
     public bool MaskIsVisibleBindable { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public float OpacityBindable { get; set; }

+ 95 - 29
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -12,6 +12,7 @@ using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Models.Rendering;
 
@@ -30,7 +31,7 @@ internal class MemberPreviewUpdater
         AnimationKeyFramePreviewRenderer = new AnimationKeyFramePreviewRenderer(internals);
     }
 
-    public Dictionary<Guid, List<PreviewRenderRequest>> UpdatePreviews(HashSet<Guid> membersToUpdate,
+    public Dictionary<Guid, List<PreviewRenderRequest>> GatherPreviewsToUpdate(HashSet<Guid> membersToUpdate,
         HashSet<Guid> masksToUpdate, HashSet<Guid> nodesToUpdate, HashSet<Guid> keyFramesToUpdate,
         bool ignoreAnimationPreviews, bool renderMiniPreviews)
     {
@@ -66,7 +67,7 @@ internal class MemberPreviewUpdater
             //RenderAnimationPreviews(members, keyFramesToUpdate);
         }
 
-        //RenderNodePreviews(nodesToUpdate);
+        RenderNodePreviews(nodesToUpdate, previewTextures);
 
         return previewTextures;
     }
@@ -113,16 +114,38 @@ internal class MemberPreviewUpdater
                 if (!memberGuids.Contains(node.Id))
                     continue;
 
+                var member = internals.Tracker.Document.FindMember(node.Id);
                 if (structureMemberHandler.Preview == null)
                 {
-                    var member = internals.Tracker.Document.FindMember(node.Id);
-                    structureMemberHandler.Preview = Texture.ForDisplay(new VecI(30, 30));
+                    structureMemberHandler.Preview = new TexturePreview(node.Id);
+                    continue;
+                }
+
+                if (structureMemberHandler.Preview.Listeners.Count == 0)
+                {
+                    structureMemberHandler.Preview.Preview?.Dispose();
+                    continue;
                 }
 
                 if (!previewTextures.ContainsKey(node.Id))
                     previewTextures[node.Id] = new List<PreviewRenderRequest>();
 
-                previewTextures[node.Id].Add(new PreviewRenderRequest(structureMemberHandler.Preview));
+                VecI textureSize = structureMemberHandler.Preview.GetMaxListenerSize();
+                if (textureSize.X <= 0 || textureSize.Y <= 0)
+                    continue;
+
+                if (structureMemberHandler.Preview.Preview == null || structureMemberHandler.Preview.Preview.IsDisposed ||
+                    structureMemberHandler.Preview.Preview.Size != textureSize)
+                {
+                    structureMemberHandler.Preview.Preview?.Dispose();
+                    structureMemberHandler.Preview.Preview = Texture.ForDisplay(textureSize);
+                }
+                else
+                {
+                    structureMemberHandler.Preview.Preview?.DrawingSurface.Canvas.Clear();
+                }
+
+                previewTextures[node.Id].Add(new PreviewRenderRequest(structureMemberHandler.Preview.Preview, structureMemberHandler.Preview.InvokeTextureUpdated));
             }
         }
     }
@@ -224,18 +247,42 @@ internal class MemberPreviewUpdater
 
                 if (structureMemberHandler.MaskPreview == null)
                 {
-                    structureMemberHandler.MaskPreview = Texture.ForDisplay(new VecI(30, 30));
+                    structureMemberHandler.MaskPreview = new TexturePreview(node.Id);
+                    continue;
+                }
+
+                if (structureMemberHandler.MaskPreview.Listeners.Count == 0)
+                {
+                    structureMemberHandler.MaskPreview.Preview?.Dispose();
+                    continue;
                 }
+
                 if (!previewTextures.ContainsKey(node.Id))
                     previewTextures[node.Id] = new List<PreviewRenderRequest>();
 
-                previewTextures[node.Id].Add(new PreviewRenderRequest(structureMemberHandler.MaskPreview,
-                    nameof(StructureNode.EmbeddedMask)));
+                VecI textureSize = structureMemberHandler.MaskPreview.GetMaxListenerSize();
+                if (textureSize.X <= 0 || textureSize.Y <= 0)
+                    continue;
+
+                if (structureMemberHandler.MaskPreview.Preview == null || structureMemberHandler.MaskPreview.Preview.IsDisposed ||
+                    structureMemberHandler.MaskPreview.Preview.Size != textureSize)
+                {
+                    structureMemberHandler.MaskPreview.Preview?.Dispose();
+                    structureMemberHandler.MaskPreview.Preview = Texture.ForDisplay(textureSize);
+                }
+                else
+                {
+                    structureMemberHandler.MaskPreview.Preview?.DrawingSurface.Canvas.Clear();
+                }
+
+                previewTextures[node.Id].Add(new PreviewRenderRequest(structureMemberHandler.MaskPreview.Preview,
+                    structureMemberHandler.MaskPreview.InvokeTextureUpdated));
             }
         }
     }
 
-    /*private void RenderNodePreviews(HashSet<Guid> nodesGuids)
+    private void RenderNodePreviews(HashSet<Guid> nodesGuids,
+        Dictionary<Guid, List<PreviewRenderRequest>>? previews = null)
     {
         var outputNode = internals.Tracker.Document.NodeGraph.OutputNode;
 
@@ -252,12 +299,12 @@ internal class MemberPreviewUpdater
         List<Guid> actualRepaintedNodes = new();
         foreach (var guid in nodesGuids)
         {
-            QueueRepaintNode(actualRepaintedNodes, guid, allNodes);
+            QueueRepaintNode(actualRepaintedNodes, guid, allNodes, previews);
         }
-    }*/
+    }
 
-    /*private void QueueRepaintNode(List<Guid> actualRepaintedNodes, Guid guid,
-        IReadOnlyCollection<IReadOnlyNode> allNodes)
+    private void QueueRepaintNode(List<Guid> actualRepaintedNodes, Guid guid,
+        IReadOnlyCollection<IReadOnlyNode> allNodes, Dictionary<Guid, List<PreviewRenderRequest>>? previews)
     {
         if (actualRepaintedNodes.Contains(guid))
             return;
@@ -273,7 +320,7 @@ internal class MemberPreviewUpdater
         if (node is null)
             return;
 
-        RequestRepaintNode(node, nodeVm);
+        RequestRepaintNode(node, nodeVm, previews);
 
         nodeVm.TraverseForwards(next =>
         {
@@ -285,32 +332,51 @@ internal class MemberPreviewUpdater
             if (nextNode is null || actualRepaintedNodes.Contains(next.Id))
                 return Traverse.Further;
 
-            RequestRepaintNode(nextNode, nextVm);
+            RequestRepaintNode(nextNode, nextVm, previews);
             actualRepaintedNodes.Add(next.Id);
             return Traverse.Further;
         });
-    }*/
+    }
 
-    /*private void RequestRepaintNode(IReadOnlyNode node, INodeHandler nodeVm)
+    private void RequestRepaintNode(IReadOnlyNode node, INodeHandler nodeVm,
+        Dictionary<Guid, List<PreviewRenderRequest>>? previews)
     {
+        if (previews == null)
+            return;
+
         if (node is IPreviewRenderable renderable)
         {
-            if (nodeVm.ResultPainter == null)
+            nodeVm.Preview ??= new TexturePreview(node.Id);
+            if (nodeVm.Preview.Listeners.Count == 0)
             {
-                nodeVm.ResultPainter = new PreviewPainter(renderer, renderable,
-                    doc.AnimationHandler.ActiveFrameTime,
-                    doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
-                nodeVm.ResultPainter.AllowPartialResolutions = false;
-                nodeVm.ResultPainter.Repaint();
+                nodeVm.Preview.Preview?.Dispose();
+                return;
+            }
+
+            if (!previews.ContainsKey(node.Id))
+                previews[node.Id] = new List<PreviewRenderRequest>();
+
+            if (previews.TryGetValue(node.Id, out var existingPreviews) &&
+                existingPreviews.Any(x => string.IsNullOrEmpty(x.ElementToRender)))
+                return;
+
+            VecI textureSize = nodeVm.Preview.GetMaxListenerSize();
+            if (textureSize.X <= 0 || textureSize.Y <= 0)
+                return;
+
+            if (nodeVm.Preview.Preview == null || nodeVm.Preview.Preview.IsDisposed ||
+                nodeVm.Preview.Preview.Size != textureSize)
+            {
+                nodeVm.Preview.Preview?.Dispose();
+                nodeVm.Preview.Preview = Texture.ForDisplay(textureSize);
             }
             else
             {
-                nodeVm.ResultPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
-                nodeVm.ResultPainter.DocumentSize = doc.SizeBindable;
-                nodeVm.ResultPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
-
-                nodeVm.ResultPainter?.Repaint();
+                nodeVm.Preview.Preview?.DrawingSurface.Canvas.Clear();
             }
+
+            previews[node.Id]
+                .Add(new PreviewRenderRequest(nodeVm.Preview.Preview, nodeVm.Preview.InvokeTextureUpdated));
         }
-    }*/
+    }
 }

+ 1 - 1
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -59,7 +59,7 @@
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                     SocketDropCommand="{Binding SocketDropCommand,
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                    ResultPreview="{Binding ResultPainter}"
+                                    ResultPreview="{Binding Preview}"
                                     Bounds="{Binding UiSize, Mode=OneWayToSource}" />
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>

+ 4 - 4
src/PixiEditor/Styles/Templates/NodeView.axaml

@@ -56,17 +56,17 @@
                                 </ItemsControl>
                             </StackPanel>
                         </Border>
-                        <Border IsVisible="{Binding CanRenderPreview, RelativeSource={RelativeSource TemplatedParent}, FallbackValue=False}"
+                        <Border IsVisible="{Binding ResultPreview, RelativeSource={RelativeSource TemplatedParent}}"
                                 CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
                             <Panel RenderOptions.BitmapInterpolationMode="None" Width="200" Height="200">
                                 <Panel.Background>
                                     <ImageBrush Source="/Images/CheckerTile.png"
                                                 TileMode="Tile" DestinationRect="0, 0, 25, 25" />
                                 </Panel.Background>
-                                <visuals:TextureControl
-                                    Texture="{TemplateBinding ResultPreview}"
+                                <visuals:PreviewTextureControl
+                                    TexturePreview="{TemplateBinding ResultPreview}"
                                     RenderOptions.BitmapInterpolationMode="None">
-                                </visuals:TextureControl>
+                                </visuals:PreviewTextureControl>
                             </Panel>
                         </Border>
                     </Grid>

+ 6 - 11
src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs

@@ -19,6 +19,8 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
 {
     public StructureMemberViewModel()
     {
+        Preview = new TexturePreview(Id);
+        MaskPreview = new TexturePreview(Id);
     }
 
     private bool isVisible;
@@ -76,16 +78,16 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
         OnPropertyChanged(nameof(MaskIsVisibleBindable));
     }
 
-    private Texture? maskPreview;
-    private Texture? preview;
+    private TexturePreview? maskPreview;
+    private TexturePreview? preview;
 
-    public Texture? Preview
+    public TexturePreview? Preview
     {
         get => preview;
         set => SetProperty(ref preview, value);
     }
 
-    public Texture? MaskPreview
+    public TexturePreview? MaskPreview
     {
         get => maskPreview;
         set => SetProperty(ref maskPreview, value);
@@ -183,13 +185,6 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
     }
 
     IDocument IStructureMemberHandler.Document => Document;
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        Preview?.Dispose();
-        MaskPreview?.Dispose();
-    }
 }
 
 public static class StructureMemberViewModel

+ 62 - 0
src/PixiEditor/ViewModels/Document/TexturePreview.cs

@@ -0,0 +1,62 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Drawie.Backend.Core;
+using Drawie.Numerics;
+
+namespace PixiEditor.ViewModels.Document;
+
+public class TexturePreview : ObservableObject
+{
+    private Texture preview;
+
+    public Guid Id { get; }
+
+    public Texture Preview
+    {
+        get => preview;
+        set
+        {
+            bool updated = SetProperty(ref preview, value);
+            if(updated)
+                InvokeTextureUpdated();
+        }
+    }
+
+    public Dictionary<object, Func<VecI>> Listeners { get; } = new();
+
+    public event Action TextureUpdated;
+
+    public TexturePreview(Guid forId)
+    {
+        Id = forId;
+    }
+
+    public void Attach(object source, Func<VecI> getSize)
+    {
+        Listeners.TryAdd(source, getSize);
+    }
+
+    public void Detach(object source)
+    {
+        Listeners.Remove(source);
+    }
+
+    public void InvokeTextureUpdated()
+    {
+        TextureUpdated?.Invoke();
+    }
+
+    public VecI GetMaxListenerSize()
+    {
+        VecI maxSize = VecI.Zero;
+        foreach (var sizeFunc in Listeners.Values)
+        {
+            VecI size = sizeFunc();
+            if (size.Length > maxSize.Length)
+            {
+                maxSize = size;
+            }
+        }
+
+        return maxSize;
+    }
+}

+ 2 - 3
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -30,7 +30,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     private IBrush? categoryBrush;
     private string? nodeNameBindable;
     private VecD position;
-    private Texture? resultTexture;
+    private TexturePreview? resultTexture;
     private ObservableRangeCollection<INodePropertyHandler> inputs;
     private ObservableRangeCollection<INodePropertyHandler> outputs;
     private bool isSelected;
@@ -38,7 +38,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
     protected Guid id;
 
-    public Texture? Preview
+    public TexturePreview? Preview
     {
         get => resultTexture;
         set => SetProperty(ref resultTexture, value);
@@ -506,7 +506,6 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
     public virtual void Dispose()
     {
-        Preview?.Dispose();
     }
 
     public NodePropertyViewModel FindInputProperty(string propName)

+ 5 - 5
src/PixiEditor/Views/Layers/LayerControl.axaml

@@ -80,9 +80,9 @@
                                 <Binding ElementName="uc" Path="Layer.HasMaskBindable" />
                             </MultiBinding>
                         </Border.BorderBrush>
-                        <visuals:TextureControl
-                            ClipToBounds="True" 
-                            Texture="{Binding Layer.Preview, ElementName=uc}"
+                        <visuals:PreviewTextureControl
+                            ClipToBounds="True"
+                            TexturePreview="{Binding Layer.Preview, ElementName=uc}"
                             Width="30" Height="30"
                             RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
                     </Border>
@@ -106,8 +106,8 @@
                             </MultiBinding>
                         </Border.BorderBrush>
                         <Grid IsHitTestVisible="False">
-                             <visuals:TextureControl
-                                    Texture="{Binding Layer.MaskPreview, ElementName=uc}"
+                             <visuals:PreviewTextureControl
+                                    TexturePreview="{Binding Layer.MaskPreview, ElementName=uc}"
                                     Width="30" Height="30"
                                     ClipToBounds="True"
                                     RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>

+ 4 - 11
src/PixiEditor/Views/Nodes/NodeView.cs

@@ -17,6 +17,7 @@ using Drawie.Backend.Core;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
+using PixiEditor.ViewModels.Document;
 using PixiEditor.Views.Nodes.Properties;
 
 namespace PixiEditor.Views.Nodes;
@@ -41,8 +42,8 @@ public class NodeView : TemplatedControl
         AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
             nameof(Outputs));
 
-    public static readonly StyledProperty<Texture> ResultPreviewProperty =
-        AvaloniaProperty.Register<NodeView, Texture>(
+    public static readonly StyledProperty<TexturePreview> ResultPreviewProperty =
+        AvaloniaProperty.Register<NodeView, TexturePreview>(
             nameof(ResultPreview));
 
     public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<NodeView, bool>(
@@ -94,7 +95,7 @@ public class NodeView : TemplatedControl
         set => SetValue(IsSelectedProperty, value);
     }
 
-    public Texture ResultPreview
+    public TexturePreview ResultPreview
     {
         get => GetValue(ResultPreviewProperty);
         set => SetValue(ResultPreviewProperty, value);
@@ -310,12 +311,4 @@ public class NodeView : TemplatedControl
             nodeView.PseudoClasses.Set(":selected", e.NewValue.Value);
         }
     }
-
-    private void ResultPreview_CanRenderChanged(bool canRender)
-    {
-        if (CanRenderPreview != canRender)
-        {
-            CanRenderPreview = canRender;
-        }
-    }
 }

+ 70 - 0
src/PixiEditor/Views/Visuals/PreviewTextureControl.cs

@@ -0,0 +1,70 @@
+using Avalonia;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Interop.Avalonia.Core.Controls;
+using Drawie.Numerics;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Views.Visuals;
+
+public class PreviewTextureControl : DrawieControl
+{
+    public static readonly StyledProperty<TexturePreview?> TexturePreviewProperty =
+        AvaloniaProperty.Register<PreviewTextureControl, TexturePreview?>(
+            nameof(TexturePreview));
+
+    public TexturePreview? TexturePreview
+    {
+        get => GetValue(TexturePreviewProperty);
+        set => SetValue(TexturePreviewProperty, value);
+    }
+
+    static PreviewTextureControl()
+    {
+        AffectsRender<PreviewTextureControl>(TexturePreviewProperty);
+        TexturePreviewProperty.Changed.Subscribe(OnTexturePreviewChanged);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        if (TexturePreview != null)
+        {
+            TexturePreview.Attach(this, () => new VecI((int)Math.Ceiling(Bounds.Size.Width), (int)Math.Ceiling(Bounds.Size.Height)));
+            TexturePreview.TextureUpdated += QueueNextFrame;
+        }
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        TexturePreview?.Detach(this);
+        if (TexturePreview != null)
+            TexturePreview.TextureUpdated -= QueueNextFrame;
+    }
+
+    public override void Draw(DrawingSurface surface)
+    {
+        if (TexturePreview is { Preview: not null })
+        {
+            VecD scaling = new(Bounds.Size.Width / TexturePreview.Preview.Size.X, Bounds.Size.Height / TexturePreview.Preview.Size.Y);
+            surface.Canvas.Save();
+            surface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            surface.Canvas.DrawSurface(TexturePreview.Preview.DrawingSurface, 0, 0);
+            surface.Canvas.Restore();
+        }
+    }
+
+    private static void OnTexturePreviewChanged(AvaloniaPropertyChangedEventArgs<TexturePreview?> args)
+    {
+        if (args.Sender is PreviewTextureControl control)
+        {
+            args.OldValue.Value?.Detach(control);
+            if(args.OldValue.Value != null)
+                args.OldValue.Value.TextureUpdated -= control.QueueNextFrame;
+            args.NewValue.Value?.Attach(control, () =>
+                new VecI((int)Math.Ceiling(control.Bounds.Size.Width), (int)Math.Ceiling(control.Bounds.Size.Height)));
+            if(args.NewValue.Value != null)
+                args.NewValue.Value.TextureUpdated += control.QueueNextFrame;
+        }
+    }
+}