浏览代码

Zoombox conversion wip

Krzysztof Krysiński 2 年之前
父节点
当前提交
82b567b4d2
共有 23 个文件被更改,包括 1296 次插入118 次删除
  1. 1 1
      src/PixiEditor.Avalonia/Directory.Build.props
  2. 6 6
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Controllers/InputDevice/MouseUpdateController.cs
  3. 1 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj
  4. 36 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/MainVM.cs
  5. 29 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/MainVmEnum.cs
  6. 15 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/ToolVM.cs
  7. 421 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml
  8. 458 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml.cs
  9. 103 6
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/MainView.axaml
  10. 57 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Overlays/TogglableFlyout.axaml
  11. 34 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Overlays/TogglableFlyout.axaml.cs
  12. 1 1
      src/PixiEditor.Extensions/PixiEditor.Extensions.csproj
  13. 1 1
      src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj
  14. 0 10
      src/PixiEditor.Zoombox/AssemblyInfo.cs
  15. 3 2
      src/PixiEditor.Zoombox/Operations/IDragOperation.cs
  16. 3 2
      src/PixiEditor.Zoombox/Operations/ManipulationOperation.cs
  17. 8 4
      src/PixiEditor.Zoombox/Operations/MoveDragOperation.cs
  18. 10 5
      src/PixiEditor.Zoombox/Operations/RotateDragOperation.cs
  19. 9 4
      src/PixiEditor.Zoombox/Operations/ZoomDragOperation.cs
  20. 6 3
      src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj
  21. 1 0
      src/PixiEditor.Zoombox/ViewportRoutedEventArgs.cs
  22. 6 10
      src/PixiEditor.Zoombox/Zoombox.axaml
  23. 87 63
      src/PixiEditor.Zoombox/Zoombox.axaml.cs

+ 1 - 1
src/PixiEditor.Avalonia/Directory.Build.props

@@ -1,6 +1,6 @@
 <Project>
   <PropertyGroup>
     <Nullable>enable</Nullable>
-    <AvaloniaVersion>11.0.0</AvaloniaVersion>
+    <AvaloniaVersion>11.0.3</AvaloniaVersion>
   </PropertyGroup>
 </Project>

+ 6 - 6
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Controllers/InputDevice/MouseUpdateController.cs

@@ -11,9 +11,9 @@ public class MouseUpdateController : IDisposable
     
     private InputElement element;
     
-    private Action mouseMoveHandler;
+    private Action<PointerEventArgs> mouseMoveHandler;
     
-    public MouseUpdateController(InputElement uiElement, Action onMouseMove)
+    public MouseUpdateController(InputElement uiElement, Action<PointerEventArgs> onMouseMove)
     {
         mouseMoveHandler = onMouseMove;
         element = uiElement;
@@ -22,20 +22,20 @@ public class MouseUpdateController : IDisposable
         _timer.AutoReset = true;
         _timer.Elapsed += TimerOnElapsed;
         
-        element.AddHandler(InputElement.PointerMovedEvent, OnMouseMove);
+        element.PointerMoved += OnMouseMove;
     }
 
     private void TimerOnElapsed(object sender, ElapsedEventArgs e)
     {
         _timer.Stop();
-        element.AddHandler(InputElement.PointerMovedEvent, OnMouseMove);
+        element.PointerMoved += OnMouseMove;
     }
 
     private void OnMouseMove(object sender, PointerEventArgs e)
     {
-        element.RemoveHandler(InputElement.PointerMovedEvent, OnMouseMove);
+        element.PointerMoved -= OnMouseMove;
         _timer.Start();
-        mouseMoveHandler();
+        mouseMoveHandler(e);
     }
 
     public void Dispose()

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj

@@ -53,6 +53,7 @@
       <ProjectReference Include="..\..\PixiEditor.UI.Common\PixiEditor.UI.Common.csproj" />
       <ProjectReference Include="..\..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
       <ProjectReference Include="..\..\PixiEditor.Windows\PixiEditor.Windows.csproj" />
+      <ProjectReference Include="..\..\PixiEditor.Zoombox\PixiEditor.Zoombox.csproj" />
       <ProjectReference Include="..\..\PixiEditorGen\PixiEditorGen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
     </ItemGroup>
   

+ 36 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/MainVM.cs

@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using Avalonia.Markup.Xaml;
+using PixiEditor.AvaloniaUI.ViewModels;
+
+namespace PixiEditor.ViewModels;
+internal class MainVM : MarkupExtension
+{
+    private MainVmEnum? vm;
+    private static Dictionary<MainVmEnum, object> subVms = new();
+
+    public override object ProvideValue(IServiceProvider serviceProvider)
+    {
+        return vm != null ? subVms[vm.Value] : ViewModelMain.Current;
+    }
+
+    static MainVM()
+    {
+        var type = typeof(ViewModelMain);
+        var vm = ViewModelMain.Current;
+        
+        foreach (var value in Enum.GetValues<MainVmEnum>())
+        {
+            subVms.Add(value, type.GetProperty(value.ToString().Replace("SVM", "SubViewModel").Replace("VM", "ViewModel"))?.GetValue(vm));
+        }
+    }
+    
+    public MainVM()
+    {
+        vm = null;
+    }
+
+    public MainVM(MainVmEnum vm)
+    {
+        this.vm = vm;
+    }
+}

+ 29 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/MainVmEnum.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.ViewModels;
+
+enum MainVmEnum
+{
+    FileSVM,
+    UpdateSVM,
+    ToolsSVM,
+    IoSVM,
+    LayersSVM,
+    ClipboardSVM,
+    UndoSVM,
+    SelectionSVM,
+    ViewportSVM,
+    ColorsSVM,
+    MiscSVM,
+    DiscordVM,
+    DebugSVM,
+    DocumentManagerSVM,
+    StylusSVM,
+    WindowSVM,
+    SearchSVM,
+    RegistrySVM
+}

+ 15 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/ToolVM.cs

@@ -0,0 +1,15 @@
+using System.Windows.Markup;
+
+namespace PixiEditor.ViewModels;
+
+internal class ToolVM : MarkupExtension
+{
+    public string TypeName { get; set; }
+
+    public ToolVM(string typeName) => TypeName = typeName;
+
+    public override object ProvideValue(IServiceProvider serviceProvider)
+    {
+        return ViewModelMain.Current?.ToolsSubViewModel.ToolSet?.Where(tool => tool.GetType().Name == TypeName).FirstOrDefault();
+    }
+}

+ 421 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml

@@ -0,0 +1,421 @@
+<UserControl
+    x:Class="PixiEditor.Views.UserControls.Viewport"
+    x:ClassModifier="internal"
+    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:local="clr-namespace:PixiEditor.Views.UserControls"
+    xmlns:sys="clr-namespace:System;assembly=System.Runtime"
+    xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
+    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
+    xmlns:views="clr-namespace:PixiEditor.Views"
+    xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+    xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
+    xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
+    mc:Ignorable="d"
+    x:Name="vpUc"
+    d:DesignHeight="450"
+    d:DesignWidth="800">
+    <Grid 
+        x:Name="mainGrid"
+        PointerPressed="Image_MouseDown"
+        PointerReleased="Image_MouseUp">
+        <Interaction.Behaviors>
+            <EventTriggerBehavior EventName="StylusButtonDown">
+                <InvokeCommandAction Command="{Binding StylusButtonDownCommand, ElementName=vpUc}"
+                                        PassEventArgsToCommand="True"/>
+            </EventTriggerBehavior>
+            <EventTriggerBehavior EventName="StylusButtonUp">
+                <InvokeCommandAction Command="{Binding StylusButtonUpCommand, ElementName=vpUc}"
+                                        PassEventArgsToCommand="True"/>
+            </EventTriggerBehavior>
+            <EventTriggerBehavior EventName="StylusSystemGesture">
+                <InvokeCommandAction Command="{Binding StylusGestureCommand, ElementName=vpUc}"
+                                        PassEventArgsToCommand="True"/>
+            </EventTriggerBehavior>
+            <EventTriggerBehavior EventName="StylusOutOfRange">
+                <InvokeCommandAction Command="{Binding StylusOutOfRangeCommand, ElementName=vpUc}"
+                                        PassEventArgsToCommand="True"/>
+            </EventTriggerBehavior>
+        </Interaction.Behaviors>
+        <views:TogglableFlyout Margin="5" IconPath="/Images/Settings.png" ui:Translator.TooltipKey="VIEWPORT_SETTINGS"
+                               Panel.ZIndex="2" HorizontalAlignment="Right" VerticalAlignment="Top">
+            <views:TogglableFlyout.Child>
+                <Border BorderThickness="1" CornerRadius="5" Padding="5" Background="#C8202020" Panel.ZIndex="2">
+        <StackPanel Orientation="Vertical">
+            <StackPanel Orientation="Horizontal">
+            <TextBlock Margin="5 0" TextAlignment="Center"
+                       Text="{Binding Path=Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, 
+             Converter={converters:RadiansToDegreesConverter}, StringFormat={}{0}°}"
+                       Width="35" Foreground="White" VerticalAlignment="Center" FontSize="16"/>
+            <Button Width="32" Height="32" ui:Translator.TooltipKey="RESET_VIEWPORT"
+                    Style="{StaticResource OverlayButton}"
+                    Click="ResetViewportClicked"
+                    Cursor="Hand">
+            <Button.Content>
+                <Image Width="28" Height="28" Source="/Images/Layout.png"/>
+            </Button.Content>
+            </Button>
+        </StackPanel>
+            <Separator/>
+            <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
+                <ToggleButton Width="32" Height="32" ui:Translator.TooltipKey="TOGGLE_VERTICAL_SYMMETRY"
+                        Style="{StaticResource OverlayToggleButton}"
+                        IsChecked="{Binding Document.VerticalSymmetryAxisEnabledBindable, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+                        Cursor="Hand">
+                    <ToggleButton.Content>
+                        <Image Width="28" Height="28" Source="/Images/SymmetryVertical.png"/>
+                    </ToggleButton.Content>
+                </ToggleButton>
+                <ToggleButton Margin="10 0 0 0" Width="32" Height="32" ui:Translator.TooltipKey="TOGGLE_HORIZONTAL_SYMMETRY"
+                              Style="{StaticResource OverlayToggleButton}"
+                              IsChecked="{Binding Document.HorizontalSymmetryAxisEnabledBindable, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+                              Cursor="Hand">
+                    <ToggleButton.Content>
+                        <Image Width="28" Height="28" Source="/Images/SymmetryVertical.png">
+                            <Image.LayoutTransform>
+                                <RotateTransform Angle="90"/>
+                            </Image.LayoutTransform>
+                        </Image>
+                    </ToggleButton.Content>
+                </ToggleButton>
+            </StackPanel>
+            <Separator/>
+            <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
+                <ToggleButton Width="32" Height="32" ui:Translator.TooltipKey="FLIP_VIEWPORT_HORIZONTALLY"
+                              Style="{StaticResource OverlayToggleButton}"
+                              IsChecked="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+                              Cursor="Hand">
+                    <ToggleButton.Content>
+                        <Image Width="28" Height="28" Source="/Images/FlipHorizontal.png"/>
+                    </ToggleButton.Content>
+                </ToggleButton>
+                <ToggleButton Margin="10 0 0 0" Width="32" Height="32" ui:Translator.TooltipKey="FLIP_VIEWPORT_VERTICALLY"
+                              Style="{StaticResource OverlayToggleButton}"
+                              IsChecked="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+                              Cursor="Hand">
+                    <ToggleButton.Content>
+                        <Image Width="28" Height="28" Source="/Images/FlipHorizontal.png">
+                            <Image.LayoutTransform>
+                                <RotateTransform Angle="90"/>
+                            </Image.LayoutTransform>
+                        </Image>
+                    </ToggleButton.Content>
+                </ToggleButton>
+            </StackPanel>
+        </StackPanel>
+        </Border>
+            </views:TogglableFlyout.Child>
+        </views:TogglableFlyout>
+        <zoombox:Zoombox
+            Tag="{Binding ElementName=vpUc}"
+            x:Name="zoombox"
+            UseTouchGestures="{Binding UseTouchGestures, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWay}"
+            Scale="{Binding ZoomboxScale, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            Angle="{Binding Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            RealDimensions="{Binding RealDimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            Dimensions="{Binding Dimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            ZoomMode="{Binding ZoomMode, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+            ZoomOutOnClick="{Binding ZoomOutOnClick, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+            FlipX="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}"
+            FlipY="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}">
+            <Border
+                d:Width="64"
+                d:Height="64"
+                HorizontalAlignment="Center"
+                VerticalAlignment="Center"
+                DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}"
+                RenderOptions.BitmapScalingMode="NearestNeighbor">
+                <Border.Background>
+                    <ImageBrush ImageSource="/Images/CheckerTile.png" TileMode="Tile" ViewportUnits="Absolute">
+                        <ImageBrush.Viewport>
+                            <Binding Path="Scale" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type zoombox:Zoombox}}" Converter="{converters:ZoomToViewportConverter}">
+                                <Binding.ConverterParameter>
+                                    <sys:Double>16</sys:Double>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </ImageBrush.Viewport>
+                    </ImageBrush>
+                </Border.Background>
+                <Grid>
+                    <Canvas
+                        ZIndex="{Binding Document.ReferenceLayerViewModel.ShowHighest, Converter={converters:BoolToIntConverter}}"
+                        IsHitTestVisible="{Binding Document.ReferenceLayerViewModel.IsTransforming}">
+                        <Image
+                            Focusable="False"
+                            Width="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap.Width}"
+                            Height="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap.Height}"
+                            Source="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap, Mode=OneWay}"
+                            Visibility="{Binding Document.ReferenceLayerViewModel.IsVisibleBindable, Converter={converters:BoolToHiddenVisibilityConverter}}"
+                            SizeChanged="OnReferenceImageSizeChanged"
+                            RenderOptions.BitmapScalingMode="{Binding ReferenceLayerScale, Converter={converters:ScaleToBitmapScalingModeConverter}}"
+                            FlowDirection="LeftToRight">
+                            <Image.RenderTransform>
+                                <TransformGroup>
+                                    <MatrixTransform
+                                        Matrix="{Binding Document.ReferenceLayerViewModel.ReferenceTransformMatrix}" />
+                                </TransformGroup>
+                            </Image.RenderTransform>
+                            <Image.Style>
+                                <Style>
+                                    <Style.Triggers>
+                                        <DataTrigger Binding="{Binding Document.ReferenceLayerViewModel.ShowHighest, Mode=OneWay}" Value="True">
+                                            <DataTrigger.EnterActions>
+                                                <BeginStoryboard>
+                                                    <Storyboard>
+                                                        <DoubleAnimation 
+                                                            Storyboard.TargetProperty="(Button.Opacity)" 
+                                                            From="1" To="{x:Static subviews:ReferenceLayerViewModel.TopMostOpacity}" Duration="0:0:0.1" /> 
+                                                    </Storyboard>
+                                                </BeginStoryboard>
+                                            </DataTrigger.EnterActions>
+                                            <DataTrigger.ExitActions>
+                                                <BeginStoryboard>
+                                                    <Storyboard>
+                                                        <DoubleAnimation 
+                                                            Storyboard.TargetProperty="(Button.Opacity)" 
+                                                            From="{x:Static subviews:ReferenceLayerViewModel.TopMostOpacity}" To="1" Duration="0:0:0.1" /> 
+                                                    </Storyboard>
+                                                </BeginStoryboard>
+                                            </DataTrigger.ExitActions>
+                                        </DataTrigger>
+                                    </Style.Triggers>
+                                </Style>
+                            </Image.Style>
+                        </Image>
+                        <Canvas.Style>
+                            <Style>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding Source={vm:ToolVM ColorPickerToolViewModel}, Path=PickFromReferenceLayer, Mode=OneWay}" Value="False">
+                                        <DataTrigger.EnterActions>
+                                            <BeginStoryboard>
+                                                <Storyboard>
+                                                    <DoubleAnimation 
+                                                        Storyboard.TargetProperty="(Button.Opacity)" 
+                                                        From="1" To="0" Duration="0:0:0.1" /> 
+                                                </Storyboard>
+                                            </BeginStoryboard>
+                                        </DataTrigger.EnterActions>
+                                        <DataTrigger.ExitActions>
+                                            <BeginStoryboard>
+                                                <Storyboard>
+                                                    <DoubleAnimation 
+                                                        Storyboard.TargetProperty="(Button.Opacity)" 
+                                                        From="0" To="1" Duration="0:0:0.1" /> 
+                                                </Storyboard>
+                                            </BeginStoryboard>
+                                        </DataTrigger.ExitActions>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Canvas.Style>
+                    </Canvas>
+                    <Image
+                        Focusable="False"
+                        Width="{Binding Document.Width}"
+                        Height="{Binding Document.Height}"
+                        Source="{Binding TargetBitmap}"
+                        RenderOptions.BitmapScalingMode="{Binding Zoombox.Scale, Converter={converters:ScaleToBitmapScalingModeConverter}}"
+                        FlowDirection="LeftToRight">
+                        <Image.Style>
+                            <Style>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding Source={vm:ToolVM ColorPickerToolViewModel}, Path=PickOnlyFromReferenceLayer, Mode=OneWay}" Value="True">
+                                        <DataTrigger.EnterActions>
+                                            <BeginStoryboard>
+                                                <Storyboard>
+                                                    <DoubleAnimation 
+                                                        Storyboard.TargetProperty="(Button.Opacity)" 
+                                                        From="1" To="0" Duration="0:0:0.1" /> 
+                                                </Storyboard>
+                                            </BeginStoryboard>
+                                        </DataTrigger.EnterActions>
+                                        <DataTrigger.ExitActions>
+                                            <BeginStoryboard>
+                                                <Storyboard>
+                                                    <DoubleAnimation 
+                                                        Storyboard.TargetProperty="(Button.Opacity)" 
+                                                        From="0" To="1" Duration="0:0:0.1" /> 
+                                                </Storyboard>
+                                            </BeginStoryboard>
+                                        </DataTrigger.ExitActions>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Image.Style>
+                    </Image>
+                    <Grid ZIndex="5">
+                        <symOverlay:SymmetryOverlay
+                            Focusable="False"
+                            IsHitTestVisible="{Binding ZoomMode, Converter={converters:ZoomModeToHitTestVisibleConverter}}"
+                            ZoomboxScale="{Binding Zoombox.Scale}"
+                            HorizontalAxisVisible="{Binding Document.HorizontalSymmetryAxisEnabledBindable}"
+                            VerticalAxisVisible="{Binding Document.VerticalSymmetryAxisEnabledBindable}"
+                            HorizontalAxisY="{Binding Document.HorizontalSymmetryAxisYBindable, Mode=OneWay}"
+                            VerticalAxisX="{Binding Document.VerticalSymmetryAxisXBindable, Mode=OneWay}"
+                            DragCommand="{cmds:Command PixiEditor.Document.DragSymmetry, UseProvided=True}"
+                            DragEndCommand="{cmds:Command PixiEditor.Document.EndDragSymmetry, UseProvided=True}"
+                            DragStartCommand="{cmds:Command PixiEditor.Document.StartDragSymmetry, UseProvided=True}"
+                            FlowDirection="LeftToRight" />
+                        <overlays:SelectionOverlay
+                            Focusable="False"
+                            ShowFill="{Binding ToolsSubViewModel.ActiveTool, Source={vm:MainVM}, Converter={converters:IsSelectionToolConverter}}"
+                            Path="{Binding Document.SelectionPathBindable}"
+                            ZoomboxScale="{Binding Zoombox.Scale}"
+                            FlowDirection="LeftToRight" />
+                        <brushOverlay:BrushShapeOverlay
+                            Focusable="False"
+                            IsHitTestVisible="False"
+                            Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={converters:InverseBoolToVisibilityConverter}}"
+                            ZoomboxScale="{Binding Zoombox.Scale}"
+                            MouseEventSource="{Binding Zoombox.Tag.BackgroundGrid, Mode=OneTime}"
+                            MouseReference="{Binding Zoombox.Tag.MainImage, Mode=OneTime}"
+                            BrushSize="{Binding ToolsSubViewModel.ActiveBasicToolbar.ToolSize, Source={vm:MainVM}}"
+                            BrushShape="{Binding ToolsSubViewModel.ActiveTool.BrushShape, Source={vm:MainVM}, FallbackValue={x:Static brushOverlay:BrushShape.Hidden}}"
+                            FlowDirection="LeftToRight"/>
+                        <transformOverlay:TransformOverlay
+                            Focusable="False"
+                            Cursor="Arrow"
+                            IsHitTestVisible="{Binding ZoomMode, Converter={converters:ZoomModeToHitTestVisibleConverter}}"
+                            HorizontalAlignment="Stretch"
+                            VerticalAlignment="Stretch"
+                            Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={converters:BoolToVisibilityConverter}}"
+                            ActionCompleted="{Binding Document.TransformViewModel.ActionCompletedCommand}"
+                            Corners="{Binding Document.TransformViewModel.Corners, Mode=TwoWay}"
+                            RequestedCorners="{Binding Document.TransformViewModel.RequestedCorners, Mode=TwoWay}"
+                            CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
+                            SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
+                            LockRotation="{Binding Document.TransformViewModel.LockRotation}"
+                            CoverWholeScreen="{Binding Document.TransformViewModel.CoverWholeScreen}"
+                            SnapToAngles="{Binding Document.TransformViewModel.SnapToAngles}"
+                            InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
+                            ZoomboxScale="{Binding Zoombox.Scale}"
+                            ZoomboxAngle="{Binding Zoombox.Angle}" />
+                        <lineOverlay:LineToolOverlay
+                            Focusable="False"
+                            Visibility="{Binding Document.LineToolOverlayViewModel.IsEnabled, Converter={converters:BoolToVisibilityConverter}}"
+                            ActionCompleted="{Binding Document.LineToolOverlayViewModel.ActionCompletedCommand}"
+                            LineStart="{Binding Document.LineToolOverlayViewModel.LineStart, Mode=TwoWay}"
+                            LineEnd="{Binding Document.LineToolOverlayViewModel.LineEnd, Mode=TwoWay}"
+                            ZoomboxScale="{Binding Zoombox.Scale}"
+                            FlowDirection="LeftToRight"/>
+                    </Grid>
+                    <Grid IsHitTestVisible="False" 
+                        ShowGridLines="True" Width="{Binding Document.Width}" Height="{Binding Document.Height}" Panel.ZIndex="10" 
+                        Visibility="{Binding GridLinesVisible, Converter={converters:BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
+                        <Grid.Resources>
+                            <converters:ThresholdVisibilityConverter Threshold="10" x:Key="ThresholdVisibilityConverter"/>
+                        </Grid.Resources>
+                        <Rectangle Focusable="False" Visibility="{Binding Zoombox.Scale, Converter={StaticResource ThresholdVisibilityConverter}}">
+                            <Rectangle.Fill>
+                                <VisualBrush Viewport="{Binding Document.Width, Converter={converters:IntToViewportRectConverter}, ConverterParameter=vertical}" ViewboxUnits="Absolute" TileMode="Tile" >
+                                    <VisualBrush.Visual>
+                                        <Line X1="0" Y1="0" X2="0" Y2="1" Stroke="Black" 
+                                              StrokeThickness="{Binding Zoombox.Scale, Converter={converters:ReciprocalConverter}}"/>
+                                    </VisualBrush.Visual>
+                                </VisualBrush>
+                            </Rectangle.Fill>
+                        </Rectangle>
+                        <Rectangle Focusable="False" Visibility="{Binding Zoombox.Scale, Converter={StaticResource ThresholdVisibilityConverter}}">
+                            <Rectangle.Fill>
+                                <VisualBrush Viewport="{Binding Document.Height, Converter={converters:IntToViewportRectConverter}}" ViewboxUnits="Absolute" TileMode="Tile" >
+                                    <VisualBrush.Visual>
+                                        <Line X1="0" Y1="0" X2="1" Y2="0" Stroke="Black" StrokeThickness="{Binding Zoombox.Scale, Converter={converters:ReciprocalConverter}}"/>
+                                    </VisualBrush.Visual>
+                                </VisualBrush>
+                            </Rectangle.Fill>
+                        </Rectangle>
+                        <Rectangle Focusable="False" Visibility="{Binding Zoombox.Scale, Converter={StaticResource ThresholdVisibilityConverter}}">
+                            <Rectangle.Fill>
+                                <VisualBrush Viewport="{Binding Document.Width, Converter={converters:IntToViewportRectConverter}, ConverterParameter=vertical}" ViewboxUnits="Absolute" TileMode="Tile" >
+                                    <VisualBrush.Visual>
+                                        <Line X1="0" Y1="0" X2="0" Y2="1" Stroke="White">
+                                            <Line.StrokeThickness>
+                                                <Binding Converter="{converters:ReciprocalConverter}">
+                                                    <Binding.Path>Zoombox.Scale</Binding.Path>
+                                                    <Binding.ConverterParameter>
+                                                        <sys:Double>
+                                                            1.1
+                                                        </sys:Double>
+                                                    </Binding.ConverterParameter>
+                                                </Binding>
+                                            </Line.StrokeThickness>
+                                        </Line>
+                                    </VisualBrush.Visual>
+                                </VisualBrush>
+                            </Rectangle.Fill>
+                        </Rectangle>
+                        <Rectangle Focusable="False" Visibility="{Binding Zoombox.Scale, Converter={StaticResource ThresholdVisibilityConverter}}">
+                            <Rectangle.Fill>
+                                <VisualBrush Viewport="{Binding Document.Height, Converter={converters:IntToViewportRectConverter}}" ViewboxUnits="Absolute" TileMode="Tile" >
+                                    <VisualBrush.Visual>
+                                        <Line X1="0" Y1="0" X2="1" Y2="0" Stroke="White">
+                                            <Line.StrokeThickness>
+                                                <Binding Converter="{converters:ReciprocalConverter}">
+                                                    <Binding.Path>Zoombox.Scale</Binding.Path>
+                                                    <Binding.ConverterParameter>
+                                                        <sys:Double>
+                                                            1.1
+                                                        </sys:Double>
+                                                    </Binding.ConverterParameter>
+                                                </Binding>
+                                            </Line.StrokeThickness>
+                                        </Line>
+                                    </VisualBrush.Visual>
+                                </VisualBrush>
+                            </Rectangle.Fill>
+                        </Rectangle>
+                    </Grid>
+                    <Rectangle Stroke="{StaticResource AccentColor}" Opacity=".8" Panel.ZIndex="2" Visibility="{Binding Document.ReferenceLayerViewModel.IsVisibleBindable, Converter={converters:BoolToHiddenVisibilityConverter}}">
+                        <Rectangle.StrokeThickness>
+                            <Binding Converter="{converters:ReciprocalConverter}">
+                                <Binding.Path>Zoombox.Scale</Binding.Path>
+                                <Binding.ConverterParameter>
+                                    <sys:Double>
+                                        3
+                                    </sys:Double>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </Rectangle.StrokeThickness>
+                        <Rectangle.Margin>
+                            <Binding Converter="{converters:ReciprocalConverter}">
+                                <Binding.Path>Zoombox.Scale</Binding.Path>
+                                <Binding.ConverterParameter>
+                                    <sys:Double>
+                                        -3
+                                    </sys:Double>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </Rectangle.Margin>
+                    </Rectangle>
+                </Grid>
+            </Border>
+        </zoombox:Zoombox>
+        <Button 
+            Panel.ZIndex="99999"
+            DockPanel.Dock="Bottom"
+            Margin="5"
+            Padding="8,5,5,5"
+            VerticalAlignment="Bottom" 
+            HorizontalAlignment="Center"
+            Style="{StaticResource GrayRoundButton}"
+            Command="{xaml:Command PixiEditor.Tools.ApplyTransform}">
+            <Button.IsVisible>
+                <MultiBinding Converter="{converters:BoolOrToVisibilityConverter}">
+                    <MultiBinding.Bindings>
+                        <Binding ElementName="vpUc" Path="Document.TransformViewModel.ShowTransformControls"/>
+                        <Binding ElementName="vpUc" Path="Document.LineToolOverlayViewModel.IsEnabled"/>
+                    </MultiBinding.Bindings>
+                </MultiBinding>
+            </Button.IsVisible>
+            <StackPanel Orientation="Horizontal">
+                <TextBlock ui:Translator.Key="APPLY_TRANSFORM" VerticalAlignment="Center" Margin="0,0,5,0" />
+                <Border Padding="10,3" CornerRadius="5" Background="{StaticResource AccentColor}" Visibility="{cmds:ShortcutBinding PixiEditor.Tools.ApplyTransform, Converter={converters:NotNullToVisibilityConverter}}">
+                    <TextBlock Text="{xaml:ShortcutBinding PixiEditor.Tools.ApplyTransform}" />
+                </Border>
+            </StackPanel>
+        </Button>
+    </Grid>
+</UserControl>

+ 458 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml.cs

@@ -0,0 +1,458 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Data.Core;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media.Imaging;
+using ChunkyImageLib.DataHolders;
+using Hardware.Info;
+using PixiEditor.AvaloniaUI.Helpers.UI;
+using PixiEditor.AvaloniaUI.Models.Controllers.InputDevice;
+using PixiEditor.AvaloniaUI.Models.Position;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Zoombox;
+
+namespace PixiEditor.Views.UserControls;
+
+#nullable enable
+internal partial class Viewport : UserControl, INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    public static readonly StyledProperty<bool> FlipXProperty =
+        AvaloniaProperty.Register<Viewport, bool>(nameof(FlipX), false);
+
+    public static readonly StyledProperty<bool> FlipYProperty =
+        AvaloniaProperty.Register<Viewport, bool>(nameof(FlipY), false);
+
+    public static readonly StyledProperty<ZoomboxMode> ZoomModeProperty =
+        AvaloniaProperty.Register<Viewport, ZoomboxMode>(nameof(ZoomMode), ZoomboxMode.Normal);
+
+    public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
+        AvaloniaProperty.Register<Viewport, DocumentViewModel>(nameof(Document), null);
+
+    public static readonly StyledProperty<ICommand> MouseDownCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(MouseDownCommand), null);
+
+    public static readonly StyledProperty<ICommand> MouseMoveCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(MouseMoveCommand), null);
+
+    public static readonly StyledProperty<ICommand> MouseUpCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(MouseUpCommand), null);
+
+    private static readonly StyledProperty<Dictionary<ChunkResolution, WriteableBitmap>> BitmapsProperty =
+        AvaloniaProperty.Register<Viewport, Dictionary<ChunkResolution, WriteableBitmap>>(nameof(Bitmaps), null);
+
+    public static readonly StyledProperty<bool> DelayedProperty =
+        AvaloniaProperty.Register<Viewport, bool>(nameof(Delayed), false);
+
+    public static readonly StyledProperty<bool> GridLinesVisibleProperty =
+        AvaloniaProperty.Register<Viewport, bool>(nameof(GridLinesVisible), false);
+
+    public static readonly StyledProperty<bool> ZoomOutOnClickProperty =
+        AvaloniaProperty.Register<Viewport, bool>(nameof(ZoomOutOnClick), false);
+
+    public static readonly StyledProperty<ExecutionTrigger<double>> ZoomViewportTriggerProperty =
+        AvaloniaProperty.Register<Viewport, ExecutionTrigger<double>>(nameof(ZoomViewportTrigger), null);
+
+    public static readonly StyledProperty<ExecutionTrigger<VecI>> CenterViewportTriggerProperty =
+        AvaloniaProperty.Register<Viewport, ExecutionTrigger<VecI>>(nameof(CenterViewportTrigger), null);
+
+    public static readonly StyledProperty<bool> UseTouchGesturesProperty =
+        AvaloniaProperty.Register<Viewport, bool>(nameof(UseTouchGestures), false);
+
+    public static readonly StyledProperty<ICommand> StylusButtonDownCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(StylusButtonDownCommand), null);
+
+    public static readonly StyledProperty<ICommand> StylusButtonUpCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(StylusButtonUpCommand), null);
+
+    public static readonly StyledProperty<ICommand> StylusGestureCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(StylusGestureCommand), null);
+
+    public static readonly StyledProperty<ICommand> StylusOutOfRangeCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(StylusOutOfRangeCommand), null);
+
+    public static readonly StyledProperty<ICommand> MiddleMouseClickedCommandProperty =
+        AvaloniaProperty.Register<Viewport, ICommand>(nameof(MiddleMouseClickedCommand), null);
+
+    public ICommand? MiddleMouseClickedCommand
+    {
+        get => (ICommand?)GetValue(MiddleMouseClickedCommandProperty);
+        set => SetValue(MiddleMouseClickedCommandProperty, value);
+    }
+
+    public ICommand? StylusOutOfRangeCommand
+    {
+        get => (ICommand?)GetValue(StylusOutOfRangeCommandProperty);
+        set => SetValue(StylusOutOfRangeCommandProperty, value);
+    }
+
+    public ICommand? StylusGestureCommand
+    {
+        get => (ICommand?)GetValue(StylusGestureCommandProperty);
+        set => SetValue(StylusGestureCommandProperty, value);
+    }
+
+    public ICommand? StylusButtonUpCommand
+    {
+        get => (ICommand?)GetValue(StylusButtonUpCommandProperty);
+        set => SetValue(StylusButtonUpCommandProperty, value);
+    }
+
+    public ICommand? StylusButtonDownCommand
+    {
+        get => (ICommand?)GetValue(StylusButtonDownCommandProperty);
+        set => SetValue(StylusButtonDownCommandProperty, value);
+    }
+
+    public bool UseTouchGestures
+    {
+        get => (bool)GetValue(UseTouchGesturesProperty);
+        set => SetValue(UseTouchGesturesProperty, value);
+    }
+
+    public ExecutionTrigger<VecI>? CenterViewportTrigger
+    {
+        get => (ExecutionTrigger<VecI>)GetValue(CenterViewportTriggerProperty);
+        set => SetValue(CenterViewportTriggerProperty, value);
+    }
+
+    public ExecutionTrigger<double>? ZoomViewportTrigger
+    {
+        get => (ExecutionTrigger<double>)GetValue(ZoomViewportTriggerProperty);
+        set => SetValue(ZoomViewportTriggerProperty, value);
+    }
+
+    public bool ZoomOutOnClick
+    {
+        get => (bool)GetValue(ZoomOutOnClickProperty);
+        set => SetValue(ZoomOutOnClickProperty, value);
+    }
+
+    public bool GridLinesVisible
+    {
+        get => (bool)GetValue(GridLinesVisibleProperty);
+        set => SetValue(GridLinesVisibleProperty, value);
+    }
+
+    public bool Delayed
+    {
+        get => (bool)GetValue(DelayedProperty);
+        set => SetValue(DelayedProperty, value);
+    }
+
+    public Dictionary<ChunkResolution, WriteableBitmap>? Bitmaps
+    {
+        get => (Dictionary<ChunkResolution, WriteableBitmap>?)GetValue(BitmapsProperty);
+        set => SetValue(BitmapsProperty, value);
+    }
+
+    public ICommand? MouseDownCommand
+    {
+        get => (ICommand?)GetValue(MouseDownCommandProperty);
+        set => SetValue(MouseDownCommandProperty, value);
+    }
+
+    public ICommand? MouseMoveCommand
+    {
+        get => (ICommand?)GetValue(MouseMoveCommandProperty);
+        set => SetValue(MouseMoveCommandProperty, value);
+    }
+
+    public ICommand? MouseUpCommand
+    {
+        get => (ICommand?)GetValue(MouseUpCommandProperty);
+        set => SetValue(MouseUpCommandProperty, value);
+    }
+
+
+    public DocumentViewModel? Document
+    {
+        get => (DocumentViewModel)GetValue(DocumentProperty);
+        set => SetValue(DocumentProperty, value);
+    }
+
+    public ZoomboxMode ZoomMode
+    {
+        get => (ZoomboxMode)GetValue(ZoomModeProperty);
+        set => SetValue(ZoomModeProperty, value);
+    }
+
+    public double ZoomboxScale
+    {
+        get => zoombox.Scale;
+        // ReSharper disable once ValueParameterNotUsed
+        set
+        {
+            PropertyChanged?.Invoke(this, new(nameof(ReferenceLayerScale)));
+        }
+    }
+
+    public bool FlipX
+    {
+        get => (bool)GetValue(FlipXProperty);
+        set => SetValue(FlipXProperty, value);
+    }
+
+    public bool FlipY
+    {
+        get => (bool)GetValue(FlipYProperty);
+        set => SetValue(FlipYProperty, value);
+    }
+
+    private double angle = 0;
+
+    public double Angle
+    {
+        get => angle;
+        set
+        {
+            angle = value;
+            PropertyChanged?.Invoke(this, new(nameof(Angle)));
+            Document?.Operations.AddOrUpdateViewport(GetLocation());
+        }
+    }
+
+    private VecD center = new(32, 32);
+
+    public VecD Center
+    {
+        get => center;
+        set
+        {
+            center = value;
+            PropertyChanged?.Invoke(this, new(nameof(Center)));
+            Document?.Operations.AddOrUpdateViewport(GetLocation());
+        }
+    }
+
+    private VecD realDimensions = new(double.MaxValue, double.MaxValue);
+
+    public VecD RealDimensions
+    {
+        get => realDimensions;
+        set
+        {
+            ChunkResolution oldRes = CalculateResolution();
+            realDimensions = value;
+            ChunkResolution newRes = CalculateResolution();
+
+            PropertyChanged?.Invoke(this, new(nameof(RealDimensions)));
+            Document?.Operations.AddOrUpdateViewport(GetLocation());
+
+            if (oldRes != newRes)
+                PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+        }
+    }
+
+    private VecD dimensions = new(64, 64);
+
+    public VecD Dimensions
+    {
+        get => dimensions;
+        set
+        {
+            ChunkResolution oldRes = CalculateResolution();
+            dimensions = value;
+            ChunkResolution newRes = CalculateResolution();
+
+            PropertyChanged?.Invoke(this, new(nameof(Dimensions)));
+            Document?.Operations.AddOrUpdateViewport(GetLocation());
+
+            if (oldRes != newRes)
+                PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+        }
+    }
+
+    public WriteableBitmap? TargetBitmap
+    {
+        get
+        {
+            return Document?.LazyBitmaps.TryGetValue(CalculateResolution(), out WriteableBitmap? value) == true ? value : null;
+        }
+    }
+
+    public double ReferenceLayerScale =>
+        ZoomboxScale * ((Document?.ReferenceLayerViewModel.ReferenceBitmap != null && Document?.ReferenceLayerViewModel.ReferenceShapeBindable != null)
+            ? (Document.ReferenceLayerViewModel.ReferenceShapeBindable.RectSize.X / (double)Document.ReferenceLayerViewModel.ReferenceBitmap.PixelWidth)
+            : 1);
+
+    public PixiEditor.Zoombox.Zoombox Zoombox => zoombox;
+
+    public Guid GuidValue { get; } = Guid.NewGuid();
+
+    private MouseUpdateController mouseUpdateController;
+
+    static Viewport()
+    {
+        DocumentProperty.Changed.Subscribe(OnDocumentChange);
+        BitmapsProperty.Changed.Subscribe(OnBitmapsChange);
+        ZoomViewportTriggerProperty.Changed.Subscribe(ZoomViewportTriggerChanged);
+        CenterViewportTriggerProperty.Changed.Subscribe(CenterViewportTriggerChanged);
+    }
+
+    public Viewport()
+    {
+        InitializeComponent();
+
+        Binding binding = new Binding { Source = this, Path = new PropertyPath($"{nameof(Document)}.{nameof(Document.LazyBitmaps)}") };
+        Bind(BitmapsProperty, binding);
+
+        MainImage!.Loaded += OnImageLoaded;
+        Loaded += OnLoad;
+        Unloaded += OnUnload;
+        
+        mouseUpdateController = new MouseUpdateController(this, Image_MouseMove);
+    }
+
+    public Image? MainImage => (Image?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[1];
+    public Grid BackgroundGrid => mainGrid;
+
+    private void ForceRefreshFinalImage()
+    {
+        MainImage?.InvalidateVisual();
+    }
+
+    private void OnUnload(object sender, RoutedEventArgs e)
+    {
+        Document?.Operations.RemoveViewport(GuidValue);
+    }
+
+    private void OnLoad(object sender, RoutedEventArgs e)
+    {
+        Document?.Operations.AddOrUpdateViewport(GetLocation());
+    }
+
+    private static void OnDocumentChange(AvaloniaPropertyChangedEventArgs<DocumentViewModel> e)
+    {
+        DocumentViewModel? oldDoc = e.OldValue.Value;
+        DocumentViewModel? newDoc = e.NewValue.Value;
+        Viewport? viewport = (Viewport)e.Sender;
+        oldDoc?.Operations.RemoveViewport(viewport.GuidValue);
+        newDoc?.Operations.AddOrUpdateViewport(viewport.GetLocation());
+    }
+
+    private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, WriteableBitmap>?> e)
+    {
+        Viewport viewportObj = (Viewport)e.Sender;
+        ((Viewport)viewportObj).PropertyChanged?.Invoke(viewportObj, new(nameof(TargetBitmap)));
+    }
+
+    private ChunkResolution CalculateResolution()
+    {
+        VecD densityVec = Dimensions.Divide(RealDimensions);
+        double density = Math.Min(densityVec.X, densityVec.Y);
+        if (density > 8.01)
+            return ChunkResolution.Eighth;
+        else if (density > 4.01)
+            return ChunkResolution.Quarter;
+        else if (density > 2.01)
+            return ChunkResolution.Half;
+        return ChunkResolution.Full;
+    }
+
+    private ViewportInfo GetLocation()
+    {
+        return new(Angle, Center, RealDimensions, Dimensions, CalculateResolution(), GuidValue, Delayed, ForceRefreshFinalImage);
+    }
+
+    private void OnReferenceImageSizeChanged(object? sender, SizeChangedEventArgs e)
+    {
+        PropertyChanged?.Invoke(this, new(nameof(ReferenceLayerScale)));
+    }
+
+    private void Image_MouseDown(object sender, PointerEventArgs e)
+    {
+        bool isMiddle = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
+        HandleMiddleMouse(isMiddle);
+
+        if (MouseDownCommand is null)
+            return;
+
+        MouseButton mouseButton = e.GetCurrentPoint(this).Properties.PointerUpdateKind switch
+        {
+            PointerUpdateKind.LeftButtonPressed => MouseButton.Left,
+            PointerUpdateKind.RightButtonPressed => MouseButton.Right,
+            _ => MouseButton.Middle
+        };
+
+        Point pos = e.GetPosition(MainImage);
+        VecD conv = new VecD(pos.X, pos.Y);
+        MouseOnCanvasEventArgs? parameter = new MouseOnCanvasEventArgs(mouseButton, conv);
+
+        if (MouseDownCommand.CanExecute(parameter))
+            MouseDownCommand.Execute(parameter);
+    }
+
+    private void Image_MouseMove(PointerEventArgs pointerEventArgs)
+    {
+        if (MouseMoveCommand is null)
+            return;
+        Point pos = e.GetPosition(MainImage);
+        VecD conv = new VecD(pos.X, pos.Y);
+
+        if (MouseMoveCommand.CanExecute(conv))
+            MouseMoveCommand.Execute(conv);
+    }
+
+    private void Image_MouseUp(object? sender, PointerReleasedEventArgs e)
+    {
+        if (MouseUpCommand is null)
+            return;
+        if (MouseUpCommand.CanExecute(e.InitialPressMouseButton))
+            MouseUpCommand.Execute(e.InitialPressMouseButton);
+    }
+
+    private void CenterZoomboxContent(object? sender, VecI args)
+    {
+        zoombox.CenterContent(args);
+    }
+
+    private void ZoomZoomboxContent(object? sender, double delta)
+    {
+        zoombox.ZoomIntoCenter(delta);
+    }
+
+    private void OnImageLoaded(object sender, EventArgs e)
+    {
+        zoombox.CenterContent();
+    }
+    
+    private void ResetViewportClicked(object sender, RoutedEventArgs e)
+    {
+        zoombox.Angle = 0;
+        zoombox.CenterContent();
+    }
+
+    private static void CenterViewportTriggerChanged(AvaloniaPropertyChangedEventArgs<ExecutionTrigger<VecI>> e)
+    {
+        Viewport? viewport = (Viewport)e.Sender;
+        if (e.OldValue.Value != null)
+            e.OldValue.Value.Triggered -= viewport.CenterZoomboxContent;
+        if (e.NewValue.Value != null)
+            e.NewValue.Value.Triggered += viewport.CenterZoomboxContent;
+    }
+
+    private static void ZoomViewportTriggerChanged(AvaloniaPropertyChangedEventArgs<ExecutionTrigger<double>> e)
+    {
+        Viewport? viewport = (Viewport)e.Sender;
+        if (e.OldValue.Value != null)
+            e.OldValue.Value.Triggered -= viewport.ZoomZoomboxContent;
+        if (e.NewValue.Value != null)
+            e.NewValue.Value.Triggered += viewport.ZoomZoomboxContent;
+    }
+
+    private void HandleMiddleMouse(bool isMiddle)
+    {
+        if (MiddleMouseClickedCommand is null)
+            return;
+        if (isMiddle && MiddleMouseClickedCommand.CanExecute(null))
+            MiddleMouseClickedCommand.Execute(null);
+    }
+}

+ 103 - 6
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/MainView.axaml

@@ -4,6 +4,10 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:viewModels1="clr-namespace:PixiEditor.AvaloniaUI.ViewModels"
              xmlns:main1="clr-namespace:PixiEditor.AvaloniaUI.Views.Main"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
+             xmlns:userControls="clr-namespace:PixiEditor.Views.UserControls"
+             xmlns:viewModels="clr-namespace:PixiEditor.ViewModels"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.AvaloniaUI.Views.MainView"
              x:DataType="viewModels1:ViewModelMain" Background="{DynamicResource ThemeBackgroundBrush}">
@@ -52,12 +56,105 @@
               <DocumentDock x:Name="DocumentsPane" Id="DocumentsPane" CanCreateDocument="True" ActiveDockable="Document1">
                 <DocumentDock.DocumentTemplate>
                   <DocumentTemplate>
-                    <StackPanel x:DataType="Document">
-                      <TextBlock Text="Title"/>
-                      <TextBox Text="{Binding Title}"/>
-                      <TextBlock Text="Context"/>
-                      <TextBox Text="{Binding Context}"/>
-                    </StackPanel>
+                      <!--Tool cursor not present here, since Avalonia doesn't have Cursor property??-->
+                      <userControls:Viewport
+                                             CenterViewportTrigger="{Binding CenterViewportTrigger}"
+                                            ZoomViewportTrigger="{Binding ZoomViewportTrigger}"
+                                            MouseDownCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseDownCommand}"
+                                            MouseMoveCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseMoveCommand}"
+                                            MouseUpCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseUpCommand}"
+                                            MiddleMouseClickedCommand="{Binding IoSubViewModel.PreviewMouseMiddleButtonCommand, Source={viewModels:MainVM}}"
+                                            Cursor="{Binding ToolsSubViewModel.ToolCursor, Source={viewModels:MainVM}}"
+                                            GridLinesVisible="{Binding ViewportSubViewModel.GridLinesEnabled, Source={viewModels:MainVM}}"
+                                            ZoomMode="{Binding ToolsSubViewModel.ActiveTool, Source={viewModels:MainVM}, Converter={converters:ActiveToolToZoomModeConverter}}"
+                                            ZoomOutOnClick="{Binding ToolsSubViewModel.ZoomTool.ZoomOutOnClick, Source={viewModels:MainVM}}"
+                                            UseTouchGestures="{Binding StylusSubViewModel.UseTouchGestures, Source={viewModels:MainVM}}"
+                                            StylusButtonDownCommand="{xaml:Command PixiEditor.Stylus.StylusDown, UseProvided=True}"
+                                            StylusButtonUpCommand="{xaml:Command PixiEditor.Stylus.StylusUp, UseProvided=True}"
+                                            StylusGestureCommand="{xaml:Command PixiEditor.Stylus.StylusSystemGesture, UseProvided=True}"
+                                            StylusOutOfRangeCommand="{xaml:Command PixiEditor.Stylus.StylusOutOfRange, UseProvided=True}"
+                                            FlipX="{Binding FlipX, Mode=TwoWay}"
+                                            FlipY="{Binding FlipY, Mode=TwoWay}"
+
+                                            ContextMenuOpening="Viewport_OnContextMenuOpening"
+                                            Stylus.IsTapFeedbackEnabled="False"
+                                            Stylus.IsTouchFeedbackEnabled="False"
+                                            Document="{Binding Document}">
+                                            <userControls:Viewport.ContextMenu>
+                                                <ContextMenu DataContext="{Binding PlacementTarget.Document, RelativeSource={RelativeSource Self}}">
+                                                    <ContextMenu.Template>
+                                                        <ControlTemplate>
+                                                            <Border Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
+                                                                <Grid Height="235">
+                                                                    <Grid.ColumnDefinitions>
+                                                                        <ColumnDefinition Width="{Binding Palette, Converter={converters:PaletteItemsToWidthConverter}}"/>
+                                                                        <ColumnDefinition />
+                                                                    </Grid.ColumnDefinitions>
+                                                                    <Border Grid.Column="1" BorderThickness="0 0 1 0" BorderBrush="Black">
+                                                                        <StackPanel Orientation="Vertical" Grid.Column="0">
+                                                                            <MenuItem
+																		ui:Translator.Key="SELECT_ALL"
+																		xaml:ContextMenu.Command="PixiEditor.Selection.SelectAll" />
+                                                                            <MenuItem
+                                                                                ui:Translator.Key="DESELECT"
+                                                                                xaml:ContextMenu.Command="PixiEditor.Selection.Clear" />
+                                                                            <Separator />
+                                                                            <MenuItem
+                                                                                ui:Translator.Key="CUT"
+                                                                                xaml:ContextMenu.Command="PixiEditor.Clipboard.Cut" />
+                                                                            <MenuItem
+                                                                                ui:Translator.Key="COPY"
+                                                                                xaml:ContextMenu.Command="PixiEditor.Clipboard.Copy" />
+                                                                            <MenuItem
+                                                                                ui:Translator.Key="PASTE"
+                                                                                xaml:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
+                                                                            <Separator />
+                                                                            <MenuItem ui:Translator.Key="FLIP_LAYERS_HORIZONTALLY" xaml:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                                                                            <MenuItem ui:Translator.Key="FLIP_LAYERS_VERTICALLY" xaml:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                                                                            <Separator />
+                                                                            <MenuItem ui:Translator.Key="ROT_LAYERS_90_D" xaml:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
+                                                                            <MenuItem ui:Translator.Key="ROT_LAYERS_180_D" xaml:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
+                                                                            <MenuItem ui:Translator.Key="ROT_LAYERS_-90_D" xaml:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
+                                                                        </StackPanel>
+                                                                    </Border>
+                                                                    <ScrollViewer Margin="5" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+                                                                        <ItemsControl ItemsSource="{Binding Palette}">
+                                                                            <ItemsControl.ItemsPanel>
+                                                                                <ItemsPanelTemplate>
+                                                                                    <WrapPanel Orientation="Horizontal"
+                                  HorizontalAlignment="Left" VerticalAlignment="Top"/>
+                                                                                </ItemsPanelTemplate>
+                                                                            </ItemsControl.ItemsPanel>
+                                                                            <ItemsControl.ItemTemplate>
+                                                                                <DataTemplate>
+                                                                                    <palettes:PaletteColorControl Cursor="Hand" CornerRadius="0"
+                                                                                        ui:Translator.TooltipKey="CLICK_SELECT_PRIMARY"
+                                                                                        Width="22" Height="22" Color="{Binding}">
+                                                                                        <b:Interaction.Triggers>
+                                                                                            <b:EventTrigger EventName="MouseLeftButtonUp">
+                                                                                                <b:InvokeCommandAction
+                                                                                                    Command="{xaml:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
+                                                                                                    CommandParameter="{Binding}" />
+                                                                                            </b:EventTrigger>
+                                                                                            <b:EventTrigger EventName="MouseLeftButtonUp">
+                                                                                                <b:InvokeCommandAction
+                                                                                                    Command="{xaml:Command PixiEditor.CloseContextMenu, UseProvided=True}"
+                                                                                                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
+                                                                                                     AncestorType={x:Type ContextMenu}}}" />
+                                                                                            </b:EventTrigger>
+                                                                                        </b:Interaction.Triggers>
+                                                                                    </palettes:PaletteColorControl>
+                                                                                </DataTemplate>
+                                                                            </ItemsControl.ItemTemplate>
+                                                                        </ItemsControl>
+                                                                    </ScrollViewer>
+                                                                </Grid>
+                                                            </Border>
+                                                        </ControlTemplate>
+                                                    </ContextMenu.Template>
+                                                </ContextMenu>
+                                            </userControls:Viewport.ContextMenu>
+                                        </userControls:Viewport>
                   </DocumentTemplate>
                 </DocumentDock.DocumentTemplate>
                 <Document x:Name="Document1" Id="Document1" Title="SomeDrawing.pixi" x:DataType="Document">

+ 57 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Overlays/TogglableFlyout.axaml

@@ -0,0 +1,57 @@
+<UserControl x:Class="PixiEditor.Views.TogglableFlyout"
+             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"
+             mc:Ignorable="d"
+             d:DesignHeight="380" d:DesignWidth="200" Name="togglableFlyout">
+    <Border Background="Transparent">
+        <StackPanel Orientation="Vertical">
+            <Border HorizontalAlignment="Right" Background="#C8202020" CornerRadius="5" Padding="5" x:Name="btnBorder">
+                <ToggleButton Padding="0" Margin="0" ToolTip.Tip="{Binding ElementName=togglableFlyout, Path=ToolTip}"
+                              x:Name="toggleButton" BorderThickness="0" Width="24" Height="24" Background="Transparent">
+                    <ToggleButton.Template>
+                        <ControlTemplate TargetType="{x:Type ToggleButton}">
+                            <Grid>
+                                <Image Focusable="False" Width="24" Cursor="Hand" x:Name="btnBg" 
+                                       Source="{Binding ElementName=togglableFlyout, Path=IconPath}">
+                                    <Image.RenderTransform>
+                                        <RotateTransform Angle="0" CenterX="12" CenterY="12"/>
+                                    </Image.RenderTransform>
+                                </Image>
+                                <ContentPresenter/>
+                            </Grid>
+                            <!--<ControlTemplate.Triggers>
+                                <Trigger Property="IsChecked" Value="True">
+                                    <Trigger.EnterActions>
+                                        <BeginStoryboard x:Name="Rotate90Animation">
+                                            <Storyboard>
+                                                <DoubleAnimation From="0" To="180"
+                                                                 Storyboard.TargetName="btnBg"
+                                                                 Storyboard.TargetProperty="(ToggleButton.RenderTransform).(RotateTransform.Angle)"
+                                                                 Duration="0:0:0.15"/>
+                                            </Storyboard>
+                                        </BeginStoryboard>
+                                    </Trigger.EnterActions>
+                                    <Trigger.ExitActions>
+                                        <BeginStoryboard x:Name="RotateReverse90Animation">
+                                            <Storyboard>
+                                                <DoubleAnimation From="180" To="0"
+                                                                 Storyboard.TargetName="btnBg"
+                                                                 Storyboard.TargetProperty="(ToggleButton.RenderTransform).(RotateTransform.Angle)"
+                                                                 Duration="0:0:0.15"/>
+                                            </Storyboard>
+                                        </BeginStoryboard>
+                                    </Trigger.ExitActions>
+                                </Trigger>
+                            </ControlTemplate.Triggers>-->
+                        </ControlTemplate>
+                    </ToggleButton.Template>
+                </ToggleButton>
+            </Border>
+            <ContentControl x:Name="popup" DataContext="{Binding ElementName=togglableFlyout}"
+                              IsVisible="{Binding Path=IsChecked, ElementName=toggleButton}"
+                   Content="{Binding ElementName=togglableFlyout, Path=Child}" />
+        </StackPanel>
+    </Border>
+</UserControl>

+ 34 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Overlays/TogglableFlyout.axaml.cs

@@ -0,0 +1,34 @@
+using System.ComponentModel;
+using Avalonia;
+using Avalonia.Controls;
+
+namespace PixiEditor.Views;
+
+public partial class TogglableFlyout : UserControl
+{
+    public static readonly StyledProperty<AvaloniaObject> ChildProperty =
+        AvaloniaProperty.Register<TogglableFlyout, AvaloniaObject>(nameof(Child));
+
+    [Bindable(true)]
+    [Category("Content")]
+    public AvaloniaObject Child
+    {
+        get { return GetValue(ChildProperty); }
+        set { SetValue(ChildProperty, value); }
+    }
+
+    public static readonly StyledProperty<string> IconPathProperty =
+        AvaloniaProperty.Register<TogglableFlyout, string>(nameof(IconPath));
+
+    public string IconPath
+    {
+        get { return (string)GetValue(IconPathProperty); }
+        set { SetValue(IconPathProperty, value); }
+    }
+    
+    public TogglableFlyout()
+    {
+        InitializeComponent();
+    }
+}
+

+ 1 - 1
src/PixiEditor.Extensions/PixiEditor.Extensions.csproj

@@ -10,7 +10,7 @@
       <Authors>PixiEditor Organization</Authors>
       <Copyright>PixiEditor Organization</Copyright>
       <Description>Package for creating custom extensions for pixel art editor PixiEditor</Description>
-      <AvaloniaVersion>11.0.0</AvaloniaVersion>
+      <AvaloniaVersion>11.0.3</AvaloniaVersion>
     </PropertyGroup>
 
     <ItemGroup>

+ 1 - 1
src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj

@@ -4,7 +4,7 @@
         <TargetFramework>net7.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
-        <AvaloniaVersion>11.0.0</AvaloniaVersion>
+        <AvaloniaVersion>11.0.3</AvaloniaVersion>
     </PropertyGroup>
 
     <ItemGroup>

+ 0 - 10
src/PixiEditor.Zoombox/AssemblyInfo.cs

@@ -1,10 +0,0 @@
-using System.Windows;
-
-[assembly: ThemeInfo(
-    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
-                                     //(used if a resource is not found in the page,
-                                     // or application resource dictionaries)
-    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
-                                              //(used if a resource is not found in the page,
-                                              // app, or any theme specific resource dictionaries)
-)]

+ 3 - 2
src/PixiEditor.Zoombox/Operations/IDragOperation.cs

@@ -1,12 +1,13 @@
 using System.Windows.Input;
+using Avalonia.Input;
 
 namespace PixiEditor.Zoombox.Operations;
 
 internal interface IDragOperation
 {
-    void Start(MouseButtonEventArgs e);
+    void Start(PointerEventArgs e);
 
-    void Update(MouseEventArgs e);
+    void Update(PointerEventArgs e);
 
     void Terminate();
 }

+ 3 - 2
src/PixiEditor.Zoombox/Operations/ManipulationOperation.cs

@@ -27,7 +27,8 @@ internal class ManipulationOperation
         rotationProcess = new LockingRotationProcess(owner.Angle);
     }
 
-    public void Update(ManipulationDeltaEventArgs args)
+    //TODO: Implement this
+    /*public void Update(ManipulationDeltaEventArgs args)
     {
         args.Handled = true;
         double thresholdFactor = 1;
@@ -44,7 +45,7 @@ internal class ManipulationOperation
         if (owner.FlipX ^ owner.FlipY)
             deltaAngle = -deltaAngle;
         Manipulate(args.DeltaManipulation.Scale.X, screenTranslation, screenOrigin, deltaAngle, thresholdFactor);
-    }
+    }*/
 
     private void Manipulate(double deltaScale, VecD screenTranslation, VecD screenOrigin, double rotation, double thresholdFactor)
     {

+ 8 - 4
src/PixiEditor.Zoombox/Operations/MoveDragOperation.cs

@@ -1,4 +1,5 @@
 using System.Windows.Input;
+using Avalonia.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 
@@ -8,19 +9,21 @@ internal class MoveDragOperation : IDragOperation
 {
     private Zoombox parent;
     private VecD prevMousePos;
+    private IPointer? capturedPointer = null!;
 
     public MoveDragOperation(Zoombox zoomBox)
     {
         parent = zoomBox;
     }
 
-    public void Start(MouseButtonEventArgs e)
+    public void Start(PointerEventArgs e)
     {
         prevMousePos = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
-        parent.mainGrid.CaptureMouse();
+        e.Pointer.Capture(parent.mainGrid);
+        capturedPointer = e.Pointer;
     }
 
-    public void Update(MouseEventArgs e)
+    public void Update(PointerEventArgs e)
     {
         var curMousePos = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
         parent.Center += parent.ToZoomboxSpace(prevMousePos) - parent.ToZoomboxSpace(curMousePos);
@@ -29,6 +32,7 @@ internal class MoveDragOperation : IDragOperation
 
     public void Terminate()
     {
-        parent.mainGrid.ReleaseMouseCapture();
+        capturedPointer?.Capture(null);
+        capturedPointer = null!;
     }
 }

+ 10 - 5
src/PixiEditor.Zoombox/Operations/RotateDragOperation.cs

@@ -1,5 +1,7 @@
 using System.Windows;
 using System.Windows.Input;
+using Avalonia;
+using Avalonia.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 
@@ -11,31 +13,33 @@ internal class RotateDragOperation : IDragOperation
     private double initialZoomboxAngle;
     private double initialClickAngle;
     private LockingRotationProcess? rotationProcess;
+    private IPointer? capturedPointer = null!;
 
     public RotateDragOperation(Zoombox zoomBox)
     {
         owner = zoomBox;
     }
 
-    public void Start(MouseButtonEventArgs e)
+    public void Start(PointerEventArgs e)
     {
         Point pointCur = e.GetPosition(owner.mainCanvas);
         initialClickAngle = GetAngle(new(pointCur.X, pointCur.Y));
         initialZoomboxAngle = owner.Angle;
         rotationProcess = new LockingRotationProcess(initialZoomboxAngle);
-        owner.mainGrid.CaptureMouse();
+        e.Pointer.Capture(owner.mainGrid);
+        capturedPointer = e.Pointer;
     }
 
     private double GetAngle(VecD point)
     {
-        VecD center = new(owner.mainCanvas.ActualWidth / 2, owner.mainCanvas.ActualHeight / 2);
+        VecD center = new(owner.mainCanvas.Width / 2, owner.mainCanvas.Height / 2);
         double angle = (point - center).Angle;
         if (double.IsNaN(angle) || double.IsInfinity(angle))
             return 0;
         return angle;
     }
 
-    public void Update(MouseEventArgs e)
+    public void Update(PointerEventArgs e)
     {
         Point pointCur = e.GetPosition(owner.mainCanvas);
         double clickAngle = GetAngle(new(pointCur.X, pointCur.Y));
@@ -49,6 +53,7 @@ internal class RotateDragOperation : IDragOperation
 
     public void Terminate()
     {
-        owner.mainGrid.ReleaseMouseCapture();
+        capturedPointer?.Capture(null);
+        capturedPointer = null!;
     }
 }

+ 9 - 4
src/PixiEditor.Zoombox/Operations/ZoomDragOperation.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Windows;
 using System.Windows.Input;
+using Avalonia;
+using Avalonia.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 
@@ -13,21 +15,23 @@ internal class ZoomDragOperation : IDragOperation
     private VecD scaleOrigin;
     private VecD screenScaleOrigin;
     private double originalScale;
+    private IPointer? capturedPointer = null!;
 
     public ZoomDragOperation(Zoombox zoomBox)
     {
         parent = zoomBox;
     }
 
-    public void Start(MouseButtonEventArgs e)
+    public void Start(PointerEventArgs e)
     {
         screenScaleOrigin = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
         scaleOrigin = parent.ToZoomboxSpace(screenScaleOrigin);
         originalScale = parent.Scale;
-        parent.mainGrid.CaptureMouse();
+        capturedPointer = e.Pointer;
+        e.Pointer.Capture(parent.mainGrid);
     }
 
-    public void Update(MouseEventArgs e)
+    public void Update(PointerEventArgs e)
     {
         Point curScreenPos = e.GetPosition(parent.mainCanvas);
         double deltaX = curScreenPos.X - screenScaleOrigin.X;
@@ -41,6 +45,7 @@ internal class ZoomDragOperation : IDragOperation
 
     public void Terminate()
     {
-        parent.mainGrid.ReleaseMouseCapture();
+        capturedPointer?.Capture(null);
+        capturedPointer = null!;
     }
 }

+ 6 - 3
src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj

@@ -1,9 +1,8 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net7.0-windows</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <Nullable>enable</Nullable>
-    <UseWPF>true</UseWPF>
     <WarningsAsErrors>Nullable</WarningsAsErrors>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU</Platforms>
@@ -52,10 +51,14 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Update="StyleCop.Analyzers" Version="1.2.0-beta.435">
+    <PackageReference Update="StyleCop.Analyzers" Version="1.2.0-beta.507">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
+
+    <PackageReference Include="Avalonia" Version="11.0.3" />
+
+    <PackageReference Include="System.Reactive" Version="6.0.0" />
   </ItemGroup>
 
 </Project>

+ 1 - 0
src/PixiEditor.Zoombox/ViewportRoutedEventArgs.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using System.Windows;
+using Avalonia.Interactivity;
 using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace PixiEditor.Zoombox;

+ 6 - 10
src/PixiEditor.Zoombox/Zoombox.xaml → src/PixiEditor.Zoombox/Zoombox.axaml

@@ -1,24 +1,20 @@
 <ContentControl
     x:Class="PixiEditor.Zoombox.Zoombox"
-    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns="https://github.com/avaloniaui"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:local="clr-namespace:PixiEditor.Zoombox"
     mc:Ignorable="d"
     x:Name="uc"
     d:DesignHeight="450"
     d:DesignWidth="800">
     <Canvas
-        MouseDown="OnMouseDown"
-        MouseUp="OnMouseUp"
-        MouseMove="OnMouseMove"
-        MouseWheel="OnScroll"
+        PointerPressed="OnMouseDown"
+        PointerReleased="OnMouseUp"
+        PointerMoved="OnMouseMove"
+        PointerWheelChanged="OnScroll"
         ClipToBounds="True"
-        IsManipulationEnabled="{Binding UseTouchGestures, ElementName=uc}"
-        ManipulationDelta="OnManipulationDelta"
-        ManipulationStarted="OnManipulationStarted"
-        ManipulationCompleted="OnManipulationCompleted"
         x:Name="mainCanvas"
         Background="Transparent"
         SizeChanged="OnMainCanvasSizeChanged">

+ 87 - 63
src/PixiEditor.Zoombox/Zoombox.xaml.cs → src/PixiEditor.Zoombox/Zoombox.axaml.cs

@@ -1,59 +1,56 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Markup;
-using ChunkyImageLib.DataHolders;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Metadata;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Zoombox.Operations;
 
 namespace PixiEditor.Zoombox;
 
-[ContentProperty(nameof(AdditionalContent))]
 public partial class Zoombox : ContentControl, INotifyPropertyChanged
 {
-    public static readonly DependencyProperty AdditionalContentProperty =
-        DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(Zoombox),
-            new PropertyMetadata(null));
+ public static readonly StyledProperty<object> AdditionalContentProperty =
+            AvaloniaProperty.Register<Zoombox, object>(nameof(AdditionalContent));
 
-    public static readonly DependencyProperty ZoomModeProperty =
-        DependencyProperty.Register(nameof(ZoomMode), typeof(ZoomboxMode), typeof(Zoombox),
-            new PropertyMetadata(ZoomboxMode.Normal, ZoomModeChanged));
+        public static readonly StyledProperty<ZoomboxMode> ZoomModeProperty =
+            AvaloniaProperty.Register<Zoombox, ZoomboxMode>(nameof(ZoomMode), defaultValue: ZoomboxMode.Normal);
 
-    public static readonly DependencyProperty ZoomOutOnClickProperty =
-        DependencyProperty.Register(nameof(ZoomOutOnClick), typeof(bool), typeof(Zoombox),
-            new PropertyMetadata(false));
+        public static readonly StyledProperty<bool> ZoomOutOnClickProperty =
+            AvaloniaProperty.Register<Zoombox, bool>(nameof(ZoomOutOnClick), defaultValue: false);
 
-    public static readonly DependencyProperty UseTouchGesturesProperty =
-        DependencyProperty.Register(nameof(UseTouchGestures), typeof(bool), typeof(Zoombox));
+        public static readonly StyledProperty<bool> UseTouchGesturesProperty =
+            AvaloniaProperty.Register<Zoombox, bool>(nameof(UseTouchGestures));
 
-    public static readonly DependencyProperty ScaleProperty =
-        DependencyProperty.Register(nameof(Scale), typeof(double), typeof(Zoombox), new(1.0, OnPropertyChange));
+        public static readonly StyledProperty<double> ScaleProperty =
+            AvaloniaProperty.Register<Zoombox, double>(nameof(Scale), defaultValue: 1.0);
 
-    public static readonly DependencyProperty CenterProperty =
-        DependencyProperty.Register(nameof(Center), typeof(VecD), typeof(Zoombox), new(new VecD(0, 0), OnPropertyChange));
+        public static readonly StyledProperty<VecD> CenterProperty =
+            AvaloniaProperty.Register<Zoombox, VecD>(nameof(Center), defaultValue: new VecD(0, 0));
 
-    public static readonly DependencyProperty DimensionsProperty =
-        DependencyProperty.Register(nameof(Dimensions), typeof(VecD), typeof(Zoombox));
+        public static readonly StyledProperty<VecD> DimensionsProperty =
+            AvaloniaProperty.Register<Zoombox, VecD>(nameof(Dimensions));
 
-    public static readonly DependencyProperty RealDimensionsProperty =
-        DependencyProperty.Register(nameof(RealDimensions), typeof(VecD), typeof(Zoombox));
+        public static readonly StyledProperty<VecD> RealDimensionsProperty =
+            AvaloniaProperty.Register<Zoombox, VecD>(nameof(RealDimensions));
 
-    public static readonly DependencyProperty AngleProperty =
-        DependencyProperty.Register(nameof(Angle), typeof(double), typeof(Zoombox), new(0.0, OnPropertyChange));
+        public static readonly StyledProperty<double> AngleProperty =
+            AvaloniaProperty.Register<Zoombox, double>(nameof(Angle), defaultValue: 0.0);
 
-    public static readonly DependencyProperty FlipXProperty =
-        DependencyProperty.Register(nameof(FlipX), typeof(bool), typeof(Zoombox), new(false, OnPropertyChange));
+        public static readonly StyledProperty<bool> FlipXProperty =
+            AvaloniaProperty.Register<Zoombox, bool>(nameof(FlipX), defaultValue: false);
 
-    public static readonly DependencyProperty FlipYProperty =
-        DependencyProperty.Register(nameof(FlipY), typeof(bool), typeof(Zoombox), new(false, OnPropertyChange));
+        public static readonly StyledProperty<bool> FlipYProperty =
+            AvaloniaProperty.Register<Zoombox, bool>(nameof(FlipY), defaultValue: false);
 
-    public static readonly RoutedEvent ViewportMovedEvent = EventManager.RegisterRoutedEvent(
-        nameof(ViewportMoved), RoutingStrategy.Bubble, typeof(EventHandler<ViewportRoutedEventArgs>), typeof(Zoombox));
+    public static readonly RoutedEvent<ViewportRoutedEventArgs> ViewportMovedEvent = RoutedEvent.Register<Zoombox, ViewportRoutedEventArgs>(
+        nameof(ViewportMoved), RoutingStrategies.Bubble);
 
-    public object? AdditionalContent
+    [Content]
+    public object AdditionalContent
     {
         get => GetValue(AdditionalContentProperty);
         set => SetValue(AdditionalContentProperty, value);
@@ -139,8 +136,8 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         get
         {
             double fraction = Math.Max(
-                mainCanvas.ActualWidth / mainGrid.ActualWidth,
-                mainCanvas.ActualHeight / mainGrid.ActualHeight);
+                mainCanvas.Width / mainGrid.Width,
+                mainCanvas.Height / mainGrid.Height);
             return Math.Min(fraction / 8, 0.1);
         }
     }
@@ -155,13 +152,13 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
             delta.X = -delta.X;
         if (FlipY)
             delta.Y = -delta.Y;
-        delta += new VecD(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2);
+        delta += new VecD(mainCanvas.Width / 2, mainCanvas.Height / 2);
         return delta;
     }
 
     internal VecD ToZoomboxSpace(VecD mousePos)
     {
-        VecD delta = mousePos - new VecD(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2);
+        VecD delta = mousePos - new VecD(mainCanvas.Width / 2, mainCanvas.Height / 2);
         if (FlipX)
             delta.X = -delta.X;
         if (FlipY)
@@ -171,14 +168,14 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
     }
 
     private IDragOperation? activeDragOperation = null;
-    private MouseButtonEventArgs? activeMouseDownEventArgs = null;
+    private PointerEventArgs? activeMouseDownEventArgs = null;
     private VecD activeMouseDownPos;
 
     public event PropertyChangedEventHandler? PropertyChanged;
 
-    private static void ZoomModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    private static void ZoomModeChanged(AvaloniaPropertyChangedEventArgs<ZoomboxMode> e)
     {
-        Zoombox sender = (Zoombox)d;
+        Zoombox sender = (Zoombox)e.Sender;
         sender.activeDragOperation?.Terminate();
         sender.activeDragOperation = null;
         sender.activeMouseDownEventArgs = null;
@@ -186,11 +183,22 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     private double[]? zoomValues;
 
+    static Zoombox()
+    {
+        // Add here all with notifyingSetter
+        ZoomModeProperty.Changed.Subscribe(ZoomModeChanged);
+        ScaleProperty.Changed.Subscribe(OnPropertyChange);
+        AngleProperty.Changed.Subscribe(OnPropertyChange);
+        FlipXProperty.Changed.Subscribe(OnPropertyChange);
+        FlipYProperty.Changed.Subscribe(OnPropertyChange);
+        CenterProperty.Changed.Subscribe(OnPropertyChange);
+    }
+
     public Zoombox()
     {
         CalculateZoomValues();
         InitializeComponent();
-        Loaded += (_, _) => OnPropertyChange(this, new DependencyPropertyChangedEventArgs());
+        Loaded += (_, _) => OnPropertyChange(this);
     }
 
     private void CalculateZoomValues()
@@ -262,7 +270,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     private void RaiseViewportEvent()
     {
-        VecD realDim = new VecD(mainCanvas.ActualWidth, mainCanvas.ActualHeight);
+        VecD realDim = new VecD(mainCanvas.Width, mainCanvas.Height);
         RealDimensions = realDim;
         RaiseEvent(new ViewportRoutedEventArgs(
             ViewportMovedEvent,
@@ -272,14 +280,14 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
             Angle));
     }
 
-    public void CenterContent() => CenterContent(new(mainGrid.ActualWidth, mainGrid.ActualHeight));
+    public void CenterContent() => CenterContent(new(mainGrid.Width, mainGrid.Height));
 
     public void CenterContent(VecD newSize)
     {
         const double marginFactor = 1.1;
         double scaleFactor = Math.Max(
-            newSize.X * marginFactor / mainCanvas.ActualWidth,
-            newSize.Y * marginFactor / mainCanvas.ActualHeight);
+            newSize.X * marginFactor / mainCanvas.Width,
+            newSize.Y * marginFactor / mainCanvas.Height);
 
         Angle = 0;
         FlipX = false;
@@ -290,7 +298,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     public void ZoomIntoCenter(double delta)
     {
-        ZoomInto(new VecD(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2), delta);
+        ZoomInto(new VecD(mainCanvas.Width / 2, mainCanvas.Height / 2), delta);
     }
 
     public void ZoomInto(VecD mousePos, double delta)
@@ -330,16 +338,25 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         return index;
     }
 
-    private void OnMouseDown(object sender, MouseButtonEventArgs e)
+    private void OnMouseDown(object? sender, PointerPressedEventArgs e)
     {
-        if (e.ChangedButton == MouseButton.Right)
+        // TODO: idk if this is correct
+        MouseButton but = e.GetCurrentPoint(this).Properties.PointerUpdateKind switch
+        {
+            PointerUpdateKind.LeftButtonPressed => MouseButton.Left,
+            PointerUpdateKind.RightButtonPressed => MouseButton.Right,
+            PointerUpdateKind.MiddleButtonPressed => MouseButton.Middle,
+            _ => MouseButton.None,
+        };
+
+        if (but == MouseButton.Right)
             return;
         activeMouseDownEventArgs = e;
         activeMouseDownPos = ToVecD(e.GetPosition(mainCanvas));
-        Keyboard.Focus(this);
+        Focus(NavigationMethod.Unspecified);
     }
 
-    private void InitiateDrag(MouseButtonEventArgs e)
+    private void InitiateDrag(PointerEventArgs e)
     {
         if (ZoomMode == ZoomboxMode.Normal)
             return;
@@ -358,9 +375,9 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         activeDragOperation.Start(e);
     }
 
-    private void OnMouseUp(object sender, MouseButtonEventArgs e)
+    private void OnMouseUp(object? sender, PointerReleasedEventArgs e)
     {
-        if (e.ChangedButton == MouseButton.Right)
+        if (e.InitialPressMouseButton == MouseButton.Right)
             return;
         if (activeDragOperation is not null)
         {
@@ -369,13 +386,13 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         }
         else
         {
-            if (ZoomMode == ZoomboxMode.Zoom && e.ChangedButton == MouseButton.Left)
+            if (ZoomMode == ZoomboxMode.Zoom && e.InitialPressMouseButton == MouseButton.Left)
                 ZoomInto(ToVecD(e.GetPosition(mainCanvas)), ZoomOutOnClick ? -1 : 1);
         }
         activeMouseDownEventArgs = null;
     }
 
-    private void OnMouseMove(object sender, MouseEventArgs e)
+    private void OnMouseMove(object? sender, PointerEventArgs e)
     {
         if (activeDragOperation is null && activeMouseDownEventArgs is not null)
         {
@@ -387,18 +404,19 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         activeDragOperation?.Update(e);
     }
 
-    private void OnScroll(object sender, MouseWheelEventArgs e)
+    private void OnScroll(object sender, PointerWheelEventArgs e)
     {
-        double abs = Math.Abs(e.Delta / 100.0);
+        double abs = Math.Abs(e.Delta.Y / 100.0);
         for (int i = 0; i < abs; i++)
         {
-            ZoomInto(ToVecD(e.GetPosition(mainCanvas)), e.Delta / 100.0);
+            ZoomInto(ToVecD(e.GetPosition(mainCanvas)), e.Delta.Y / 100.0);
         }
     }
 
 
     private ManipulationOperation? activeManipulationOperation;
 
+    /* TODO: Avalonia uses Pointer events for both mouse and touch, so we can't use this, would be cool to Implement UseTouchGestures
     private void OnManipulationStarted(object? sender, ManipulationStartedEventArgs e)
     {
         if (!UseTouchGestures || activeManipulationOperation is not null)
@@ -420,15 +438,21 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
             return;
         activeManipulationOperation = null;
     }
+    */
 
     internal static VecD ToVecD(Point point) => new VecD(point.X, point.Y);
 
-    private static void OnPropertyChange(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+    private static void OnPropertyChange(AvaloniaPropertyChangedEventArgs e)
     {
-        Zoombox? zoombox = (Zoombox)obj;
+        Zoombox? zoombox = (Zoombox)e.Sender;
+
+       OnPropertyChange(zoombox);
+    }
 
+    private static void OnPropertyChange(Zoombox zoombox)
+    {
         VecD topLeft = zoombox.ToZoomboxSpace(VecD.Zero).Rotate(zoombox.Angle);
-        VecD bottomRight = zoombox.ToZoomboxSpace(new(zoombox.mainCanvas.ActualWidth, zoombox.mainCanvas.ActualHeight)).Rotate(zoombox.Angle);
+        VecD bottomRight = zoombox.ToZoomboxSpace(new(zoombox.mainCanvas.Width, zoombox.mainCanvas.Height)).Rotate(zoombox.Angle);
 
         zoombox.Dimensions = (bottomRight - topLeft).Abs();
         zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.ScaleTransformXY)));
@@ -442,13 +466,13 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     private void OnMainCanvasSizeChanged(object sender, SizeChangedEventArgs e)
     {
-        OnPropertyChange(this, new DependencyPropertyChangedEventArgs());
+        OnPropertyChange(this);
         RaiseViewportEvent();
     }
 
     private void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
     {
-        OnPropertyChange(this, new DependencyPropertyChangedEventArgs());
+        OnPropertyChange(this);
         RaiseViewportEvent();
     }
 }