31 changed files with 823 additions and 65 deletions
@ -0,0 +1,204 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Utilities; |
|||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
internal class CompositionTreeSnapshot : IAsyncDisposable |
|||
{ |
|||
public Compositor Compositor { get; } |
|||
|
|||
public CompositionTreeSnapshotItem Root { get; } |
|||
|
|||
public Bitmap Bitmap { get; } |
|||
|
|||
private CompositionTreeSnapshot(Compositor compositor, ServerCompositionVisual root, Bitmap bitmap) |
|||
{ |
|||
Compositor = compositor; |
|||
Root = new CompositionTreeSnapshotItem(this, root); |
|||
Bitmap = bitmap; |
|||
} |
|||
|
|||
public bool IsDisposed { get; private set; } |
|||
|
|||
public ValueTask DisposeAsync() |
|||
{ |
|||
IsDisposed = true; |
|||
Bitmap.Dispose(); |
|||
return new ValueTask(Compositor.InvokeServerJobAsync(() => |
|||
{ |
|||
Root.Destroy(); |
|||
})); |
|||
} |
|||
|
|||
public static Task<CompositionTreeSnapshot?> TakeAsync(CompositionVisual visual) |
|||
{ |
|||
return visual.Compositor.InvokeServerJobAsync(() => |
|||
{ |
|||
if (visual.Root == null) |
|||
return null; |
|||
Bitmap bitmap; |
|||
var ri = visual.Compositor.Server.RenderInterface; |
|||
using (ri.EnsureCurrent()) |
|||
{ |
|||
using (var layer = |
|||
ri.Value.CreateLayer(new Size(Math.Ceiling(visual.Size.X), Math.Ceiling(visual.Size.Y)), 1)) |
|||
{ |
|||
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); |
|||
using (var context = layer.CreateDrawingContext(visualBrushHelper)) |
|||
{ |
|||
context.Clear(Colors.Transparent); |
|||
visual.Server.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), new Rect(0, |
|||
0, |
|||
visual.Server.Size.X, |
|||
visual.Server.Size.Y)); |
|||
} |
|||
|
|||
using (var ms = new MemoryStream()) |
|||
{ |
|||
layer.Save(ms); |
|||
ms.Position = 0; |
|||
bitmap = new Bitmap(ms); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new CompositionTreeSnapshot(visual.Compositor, visual.Server, bitmap); |
|||
}); |
|||
} |
|||
|
|||
private CompositionTreeSnapshotItem? HitTest(CompositionTreeSnapshotItem item, Point pt) |
|||
{ |
|||
if (!MatrixUtils.ToMatrix(item.Transform).TryInvert(out var inverted)) |
|||
return null; |
|||
pt = inverted.Transform(pt); |
|||
if (double.IsNaN(pt.X) || double.IsNaN(pt.Y) || double.IsInfinity(pt.X) || double.IsInfinity(pt.Y)) |
|||
return null; |
|||
|
|||
if (item.ClipToBounds && (item.Size.X < pt.X || item.Size.Y < pt.Y || pt.X < 0 || pt.Y < 0)) |
|||
return null; |
|||
|
|||
if (item.GeometryClip?.FillContains(pt) == false && item.GeometryClip.FillContains(pt) == false) |
|||
return null; |
|||
|
|||
for (var c = item.Children.Count - 1; c >= 0; c--) |
|||
{ |
|||
var ch = item.Children[c]; |
|||
var chResult = HitTest(ch, pt); |
|||
if (chResult != null) |
|||
return chResult; |
|||
} |
|||
|
|||
if (item.HitTest(pt)) |
|||
return item; |
|||
return null; |
|||
} |
|||
|
|||
public CompositionTreeSnapshotItem? HitTest(Point pt) => HitTest(Root, pt); |
|||
} |
|||
|
|||
internal class CompositionTreeSnapshotItem |
|||
{ |
|||
private readonly CompositionTreeSnapshot _snapshot; |
|||
public string? Name { get; } |
|||
private CompositionDrawList? _drawList; |
|||
|
|||
internal CompositionTreeSnapshotItem(CompositionTreeSnapshot snapshot, ServerCompositionVisual visual) |
|||
{ |
|||
_snapshot = snapshot; |
|||
Name = (visual as ICompositionVisualWithDiagnosticsInfo)?.Name ?? visual.GetType().Name; |
|||
_drawList = (visual as ICompositionVisualWithDrawList)?.DrawList?.Clone(); |
|||
DrawOperations = _drawList?.Select(x => x.Item.GetType().Name).ToList() ?? |
|||
(IReadOnlyList<string>)Array.Empty<string>(); |
|||
visual.PopulateDiagnosticProperties(Properties); |
|||
Transform = visual.CombinedTransformMatrix; |
|||
ClipToBounds = visual.ClipToBounds; |
|||
GeometryClip = visual.Clip; |
|||
Size = visual.Size; |
|||
if (visual is ServerCompositionContainerVisual container) |
|||
Children = container.Children.List.Select(x => new CompositionTreeSnapshotItem(snapshot, x)).ToList(); |
|||
else |
|||
Children = Array.Empty<CompositionTreeSnapshotItem>(); |
|||
} |
|||
|
|||
public bool HitTest(Point v) => _drawList?.HitTest(v) == true; |
|||
|
|||
public IGeometryImpl? GeometryClip { get; set; } |
|||
|
|||
public bool ClipToBounds { get; } |
|||
|
|||
public Matrix4x4 Transform { get; } |
|||
|
|||
public Vector2 Size { get; } |
|||
|
|||
public IReadOnlyList<string> DrawOperations { get; } |
|||
|
|||
public IReadOnlyList<CompositionTreeSnapshotItem> Children { get; } |
|||
|
|||
public Dictionary<string, object?> Properties { get; } = new(); |
|||
|
|||
public Task<Bitmap?> RenderToBitmapAsync(int? drawOperationIndex) |
|||
{ |
|||
if (_snapshot.IsDisposed || _drawList == null || _drawList.Count == 0) |
|||
return Task.FromResult<Bitmap?>(null); |
|||
return _snapshot.Compositor.InvokeServerJobAsync(() => |
|||
{ |
|||
using (_snapshot.Compositor.Server.RenderInterface.EnsureCurrent()) |
|||
{ |
|||
if (_snapshot.IsDisposed) |
|||
return null; |
|||
|
|||
var margin = 20; |
|||
var bounds = |
|||
drawOperationIndex == null |
|||
? _drawList.CalculateBounds() |
|||
: _drawList[drawOperationIndex.Value].Item.Bounds; |
|||
|
|||
using (var layer = _snapshot.Compositor.Server.RenderInterface.Value.CreateLayer(bounds.Size.Inflate(new Thickness(margin)), 1)) |
|||
{ |
|||
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); |
|||
using (var ctx = layer.CreateDrawingContext(visualBrushHelper)) |
|||
{ |
|||
var proxy = new CompositorDrawingContextProxy(ctx, visualBrushHelper); |
|||
proxy.PostTransform = Matrix.CreateTranslation(-bounds.Left + margin, -bounds.Top + margin); |
|||
proxy.Transform = Matrix.Identity; |
|||
if (drawOperationIndex != null) |
|||
_drawList[drawOperationIndex.Value].Item.Render(proxy); |
|||
else |
|||
foreach (var item in _drawList) |
|||
item.Item.Render(proxy); |
|||
} |
|||
|
|||
using (var ms = new MemoryStream()) |
|||
{ |
|||
layer.Save(ms); |
|||
ms.Position = 0; |
|||
return new Bitmap( |
|||
RefCountable.Create(AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>() |
|||
.LoadBitmap(ms))); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
internal void Destroy() |
|||
{ |
|||
_drawList?.Dispose(); |
|||
_drawList = null; |
|||
foreach (var ch in Children) |
|||
ch.Destroy(); |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,8 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
internal interface ICompositionVisualWithDiagnosticsInfo |
|||
{ |
|||
public string? Name { get; } |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
internal interface ICompositionVisualWithDrawList |
|||
{ |
|||
CompositionDrawList? DrawList { get; } |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels; |
|||
|
|||
internal class CompositionTreeSnapshotViewModel : ViewModelBase, IDisposable |
|||
{ |
|||
private CompositionTreeSnapshotItemViewModel? _selectedNode; |
|||
private bool _isPicking; |
|||
private CompositionTreeSnapshot _snapshot; |
|||
|
|||
public CompositionTreeSnapshot Snapshot |
|||
{ |
|||
get => _snapshot; |
|||
private set => RaiseAndSetIfChanged(ref _snapshot, value); |
|||
} |
|||
|
|||
public TopLevel TopLevel { get; } |
|||
|
|||
|
|||
public AvaloniaList<CompositionTreeSnapshotItemViewModel> RootItems { get; } |
|||
|
|||
public bool IsPicking |
|||
{ |
|||
get => _isPicking; |
|||
set => RaiseAndSetIfChanged(ref _isPicking, value); |
|||
} |
|||
|
|||
public void PickItem(Point pos) |
|||
{ |
|||
IsPicking = false; |
|||
var item = Snapshot.HitTest(pos); |
|||
if (item != null) |
|||
SelectedNode = ExpandToItem(RootItems![0], item); |
|||
} |
|||
|
|||
private CompositionTreeSnapshotItemViewModel? ExpandToItem(CompositionTreeSnapshotItemViewModel currentVm, CompositionTreeSnapshotItem item) |
|||
{ |
|||
if (currentVm.Item == item) |
|||
{ |
|||
SelectedNode = currentVm; |
|||
return currentVm; |
|||
} |
|||
|
|||
foreach (var ch in currentVm.Children) |
|||
{ |
|||
var chRes = ExpandToItem(ch, item); |
|||
if (chRes != null) |
|||
{ |
|||
currentVm.IsExpanded = true; |
|||
return chRes; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public CompositionTreeSnapshotItemViewModel? SelectedNode |
|||
{ |
|||
get => _selectedNode; |
|||
set => RaiseAndSetIfChanged(ref _selectedNode, value); |
|||
} |
|||
|
|||
public CompositionTreeSnapshotViewModel(TopLevel topLevel, CompositionTreeSnapshot snapshot) |
|||
{ |
|||
_snapshot = snapshot; |
|||
TopLevel = topLevel; |
|||
RootItems = new(new CompositionTreeSnapshotItemViewModel(snapshot.Root)); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
IsPicking = false; |
|||
Snapshot.DisposeAsync(); |
|||
} |
|||
} |
|||
|
|||
internal class CompositionTreeSnapshotItemViewModel : ViewModelBase |
|||
{ |
|||
private bool _isExpanded = false; |
|||
private CompositionTreeSnapshotItemPropertyViewModel? _selectedProperty; |
|||
private int _selectedDrawOperationIndex; |
|||
public CompositionTreeSnapshotItem Item { get; } |
|||
public IReadOnlyList<CompositionTreeSnapshotItemViewModel> Children { get; } |
|||
public IAvaloniaReadOnlyList<CompositionTreeSnapshotItemPropertyViewModel> Properties { get; } |
|||
|
|||
|
|||
public CompositionTreeSnapshotItemPropertyViewModel? SelectedProperty |
|||
{ |
|||
get => _selectedProperty; |
|||
set => RaiseAndSetIfChanged(ref _selectedProperty, value); |
|||
} |
|||
|
|||
public int SelectedDrawOperationIndex |
|||
{ |
|||
get => _selectedDrawOperationIndex; |
|||
set => RaiseAndSetIfChanged(ref _selectedDrawOperationIndex, value); |
|||
} |
|||
|
|||
public bool IsExpanded |
|||
{ |
|||
get => _isExpanded; |
|||
set => RaiseAndSetIfChanged(ref _isExpanded, value); |
|||
} |
|||
|
|||
public CompositionTreeSnapshotItemViewModel(CompositionTreeSnapshotItem item) |
|||
{ |
|||
Item = item; |
|||
Children = item.Children.Select(x => new CompositionTreeSnapshotItemViewModel(x)).ToList(); |
|||
Properties = |
|||
new AvaloniaList<CompositionTreeSnapshotItemPropertyViewModel>(item.Properties |
|||
.Select(x => new CompositionTreeSnapshotItemPropertyViewModel(x.Key, x.Value)) |
|||
.OrderBy(x => x.Name)); |
|||
} |
|||
} |
|||
|
|||
internal class CompositionTreeSnapshotItemPropertyViewModel : ViewModelBase |
|||
{ |
|||
public string Name { get; } |
|||
public object? Value { get; } |
|||
|
|||
public CompositionTreeSnapshotItemPropertyViewModel(string name, object? value) |
|||
{ |
|||
Name = name; |
|||
Value = value; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="Avalonia.Diagnostics.Views.CompositionTreeSnapshotItemView" |
|||
x:DataType="vm:CompositionTreeSnapshotItemViewModel"> |
|||
<Grid ColumnDefinitions="0.5*,4,0.5*"> |
|||
<DataGrid |
|||
Items="{Binding Properties}" |
|||
BorderThickness="0" |
|||
RowBackground="Transparent" |
|||
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}" |
|||
CanUserResizeColumns="true"> |
|||
<DataGrid.Columns> |
|||
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" |
|||
x:DataType="vm:CompositionTreeSnapshotItemPropertyViewModel" /> |
|||
<DataGridTextColumn Header="Value" Binding="{Binding Value}" |
|||
x:DataType="vm:CompositionTreeSnapshotItemPropertyViewModel" /> |
|||
</DataGrid.Columns> |
|||
|
|||
<DataGrid.Styles> |
|||
<Style Selector="DataGridRow TextBox"> |
|||
<Setter Property="SelectionBrush" Value="LightBlue" /> |
|||
</Style> |
|||
</DataGrid.Styles> |
|||
</DataGrid> |
|||
<GridSplitter Grid.Column="1"/> |
|||
<Grid Grid.Column="2" RowDefinitions="*,4,*"> |
|||
<DockPanel> |
|||
<Button DockPanel.Dock="Bottom" Click="ResetDrawOperationIndex">Show all</Button> |
|||
<ListBox Items="{Binding Item.DrawOperations}" SelectedIndex="{Binding SelectedDrawOperationIndex}"/> |
|||
</DockPanel> |
|||
<GridSplitter Grid.Row="1"/> |
|||
<Image Grid.Row="2" x:Name="Image" Stretch="Uniform"/> |
|||
</Grid> |
|||
</Grid> |
|||
|
|||
</UserControl> |
|||
@ -0,0 +1,65 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Threading.Tasks; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Avalonia.Diagnostics.Views; |
|||
|
|||
internal partial class CompositionTreeSnapshotItemView : TypedUserControl<CompositionTreeSnapshotItemViewModel> |
|||
{ |
|||
private readonly Image _image; |
|||
|
|||
public CompositionTreeSnapshotItemView() |
|||
{ |
|||
InitializeComponent(); |
|||
_image = this.FindControl<Image>("Image")!; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
protected override void Subscribe() |
|||
{ |
|||
UpdateImage(); |
|||
base.Subscribe(); |
|||
} |
|||
|
|||
protected override void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == nameof(Model.SelectedDrawOperationIndex)) |
|||
UpdateImage(); |
|||
base.OnModelPropertyChanged(sender, e); |
|||
} |
|||
|
|||
private void ResetDrawOperationIndex(object? sender, RoutedEventArgs e) |
|||
{ |
|||
if (Model != null) |
|||
Model.SelectedDrawOperationIndex = -1; |
|||
} |
|||
|
|||
async void UpdateImage() |
|||
{ |
|||
var drawForModel = Model; |
|||
var drawForIndex = Model!.SelectedDrawOperationIndex; |
|||
await Task.Delay(100); |
|||
if (drawForModel != Model || drawForIndex != drawForModel.SelectedDrawOperationIndex) |
|||
return; |
|||
var image = await Model!.Item.RenderToBitmapAsync(Model.SelectedDrawOperationIndex == -1 ? null : Model.SelectedDrawOperationIndex); |
|||
if (drawForModel != Model || drawForIndex != drawForModel.SelectedDrawOperationIndex) |
|||
{ |
|||
image?.Dispose(); |
|||
return; |
|||
} |
|||
|
|||
var oldSource = _image.Source; |
|||
_image.Source = image; |
|||
(oldSource as IDisposable)?.Dispose(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" |
|||
xmlns:views="clr-namespace:Avalonia.Diagnostics.Views" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="Avalonia.Diagnostics.Views.CompositionTreeSnapshotView" |
|||
x:DataType="vm:CompositionTreeSnapshotViewModel"> |
|||
<UserControl.Styles> |
|||
<SimpleTheme /> |
|||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" /> |
|||
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" /> |
|||
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" /> |
|||
</UserControl.Styles> |
|||
|
|||
<Grid> |
|||
<DockPanel> |
|||
<StackPanel Spacing="4" DockPanel.Dock="Top" Orientation="Horizontal"> |
|||
<ToggleButton IsChecked="{Binding IsPicking}">Pick element</ToggleButton> |
|||
<Button Click="FreezeMe">Freeze Me</Button> |
|||
</StackPanel> |
|||
<Grid ColumnDefinitions="0.35*,4,0.65*"> |
|||
<TreeView |
|||
x:DataType="vm:CompositionTreeSnapshotViewModel" |
|||
BorderThickness="0" |
|||
Items="{Binding Path=RootItems}" |
|||
SelectedItem="{Binding SelectedNode, Mode=TwoWay}" |
|||
SelectionMode="Single"> |
|||
<TreeView.DataTemplates> |
|||
<TreeDataTemplate DataType="vm:CompositionTreeSnapshotItemViewModel" |
|||
ItemsSource="{Binding Children}"> |
|||
<TextBlock Text="{Binding Item.Name}" /> |
|||
</TreeDataTemplate> |
|||
</TreeView.DataTemplates> |
|||
<TreeView.Styles> |
|||
<Style Selector="TreeViewItem" x:DataType="vm:CompositionTreeSnapshotItemViewModel"> |
|||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
</Style> |
|||
</TreeView.Styles> |
|||
</TreeView> |
|||
|
|||
<GridSplitter Background="{DynamicResource ThemeControlMidBrush}" Width="1" Grid.Column="1" /> |
|||
<views:CompositionTreeSnapshotItemView DataContext="{Binding SelectedNode}" Grid.Column="2" /> |
|||
</Grid> |
|||
</DockPanel> |
|||
<Border IsVisible="{Binding IsPicking}" Background="Black" PointerPressed="PickElement"> |
|||
<Image x:Name="ElementPicker" Source="{Binding Snapshot.Bitmap}" Stretch="Uniform"/> |
|||
</Border> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Media; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Diagnostics.Views; |
|||
|
|||
internal class CompositionTreeSnapshotView : TypedUserControl<CompositionTreeSnapshotViewModel> |
|||
{ |
|||
private readonly Image _elementPicker; |
|||
public CompositionTreeSnapshotView() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
_elementPicker = this.FindControl<Image>("ElementPicker")!; |
|||
} |
|||
|
|||
protected override void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == nameof(CompositionTreeSnapshotViewModel.SelectedNode)) |
|||
DispatcherTimer.RunOnce(() => |
|||
{ |
|||
var item = this.GetLogicalDescendants().OfType<TreeViewItem>() |
|||
.FirstOrDefault(i => i.DataContext == Model!.SelectedNode); |
|||
item?.BringIntoView(); |
|||
}, TimeSpan.FromMilliseconds(20)); |
|||
} |
|||
|
|||
protected override void Unsubscribe() |
|||
{ |
|||
Model!.IsPicking = false; |
|||
base.Unsubscribe(); |
|||
} |
|||
|
|||
private void PickElement(object sender, PointerPressedEventArgs e) |
|||
{ |
|||
var pos = e.GetPosition(_elementPicker); |
|||
var scale = Stretch.Uniform.CalculateScaling(Bounds.Size, _elementPicker.Bounds.Size); |
|||
pos = new Point(pos.X * scale.X, pos.Y * scale.Y); |
|||
Model?.PickItem(pos); |
|||
} |
|||
|
|||
private void FreezeMe(object? sender, RoutedEventArgs e) |
|||
{ |
|||
Model?.RootItems?.Clear(); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Styling; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Diagnostics.Views; |
|||
|
|||
internal class TypedUserControl<T> : UserControl, IStyleable where T : class, INotifyPropertyChanged |
|||
{ |
|||
Type IStyleable.StyleKey => typeof(UserControl); |
|||
|
|||
protected T? Model { get; private set; } |
|||
|
|||
protected virtual void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected virtual void Subscribe() |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected virtual void Unsubscribe() |
|||
{ |
|||
|
|||
} |
|||
|
|||
void UpdateSubscriptions() |
|||
{ |
|||
if (Model != null) |
|||
{ |
|||
Model.PropertyChanged -= OnModelPropertyChanged; |
|||
Unsubscribe(); |
|||
} |
|||
|
|||
Model = IsAttachedToVisualTree ? DataContext as T : null; |
|||
if (Model != null) |
|||
{ |
|||
Model.PropertyChanged += OnModelPropertyChanged; |
|||
Subscribe(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnDataContextChanged(EventArgs e) |
|||
{ |
|||
UpdateSubscriptions(); |
|||
base.OnDataContextChanged(e); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
UpdateSubscriptions(); |
|||
base.OnAttachedToVisualTree(e); |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
UpdateSubscriptions(); |
|||
base.OnDetachedFromVisualTree(e); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue