Browse Source

Merge branch 'timeline-visuals' into avalonia-rewrite

flabbet 1 year ago
parent
commit
9466061e71
24 changed files with 482 additions and 219 deletions
  1. 23 0
      src/PixiEditor.AvaloniaUI/Helpers/Converters/FrameToTimeConverter.cs
  2. 13 6
      src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderValueToMarginConverter.cs
  3. 1 1
      src/PixiEditor.AvaloniaUI/Models/Dialogs/SizeUnit.cs
  4. 41 17
      src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs
  5. 27 3
      src/PixiEditor.AvaloniaUI/Styles/PortingWipStyles.axaml
  6. 16 3
      src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml
  7. 115 65
      src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml
  8. 31 5
      src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml
  9. 4 2
      src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml
  10. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/Dock/LayoutManager.cs
  11. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs
  12. 38 0
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs
  13. 10 3
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs
  14. 20 0
      src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs
  15. 23 1
      src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs
  16. 22 1
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineGroupHeader.cs
  17. 14 5
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs
  18. 67 52
      src/PixiEditor.AvaloniaUI/Views/Input/NumberInput.cs
  19. 4 3
      src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml
  20. 5 45
      src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml.cs
  21. 1 1
      src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml
  22. 2 2
      src/PixiEditor.AvaloniaUI/Views/MainView.axaml
  23. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrameData.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs

+ 23 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/FrameToTimeConverter.cs

@@ -0,0 +1,23 @@
+using System.Globalization;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class FrameToTimeConverter : SingleInstanceMultiValueConverter<FrameToTimeConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return Convert(new[] { value }, targetType, parameter, culture); 
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if(values.Count < 2) return null;
+        
+        if (values[0] is int frame && values[1] is int fps)
+        {
+            return TimeSpan.FromSeconds(frame / (double)fps).ToString("mm\\:ss\\.ff");
+        }
+        
+        return null;
+    }
+}

+ 13 - 6
src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderValueToMarginConverter.cs

@@ -3,7 +3,8 @@ using Avalonia;
 
 
 namespace PixiEditor.AvaloniaUI.Helpers.Converters;
 namespace PixiEditor.AvaloniaUI.Helpers.Converters;
 
 
-internal class TimelineSliderValueToMarginConverter : SingleInstanceMultiValueConverter<TimelineSliderValueToMarginConverter>
+internal class
+    TimelineSliderValueToMarginConverter : SingleInstanceMultiValueConverter<TimelineSliderValueToMarginConverter>
 {
 {
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
     {
@@ -12,14 +13,20 @@ internal class TimelineSliderValueToMarginConverter : SingleInstanceMultiValueCo
 
 
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
     {
-        if (values.Count != 4)
+        if (values.Count == 3)
         {
         {
-            throw new ArgumentException("TimelineSliderValueToMarginConverter requires 3 values");
+            if (values[0] is double minimum && values[1] is double scale && values[2] is Vector offset)
+            {
+                return new Thickness((-minimum) * scale - offset.X, 0, 0, 0);
+            }
         }
         }
-
-        if (values[0] is int frame && values[1] is double minimum && values[2] is double scale && values[3] is Vector offset)
+        else if (values.Count == 4)
         {
         {
-            return new Thickness((frame - minimum) * scale - offset.X, 0, 0, 0);
+            if (values[0] is int frame && values[1] is double minimum && values[2] is double scale &&
+                values[3] is Vector offset)
+            {
+                return new Thickness((frame - minimum) * scale - offset.X, 0, 0, 0);
+            }
         }
         }
 
 
         return new Thickness();
         return new Thickness();

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Dialogs/SizeUnit.cs

@@ -1,3 +1,3 @@
 namespace PixiEditor.AvaloniaUI.Models.Dialogs;
 namespace PixiEditor.AvaloniaUI.Models.Dialogs;
 
 
-public enum SizeUnit { Pixel, Percentage }
+public enum SizeUnit { Pixel, Percentage }

+ 41 - 17
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -1,6 +1,7 @@
 #nullable enable
 #nullable enable
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using ChunkyImageLib;
 using ChunkyImageLib;
@@ -505,12 +506,15 @@ internal class MemberPreviewUpdater
 
 
             if (memberVM is ILayerHandler)
             if (memberVM is ILayerHandler)
             {
             {
-                RenderLayerMainPreview((IReadOnlyLayerNode)member, memberVM, affArea.Value, position, scaling);
+                RenderLayerMainPreview((IReadOnlyLayerNode)member,
+                    memberVM.PreviewSurface, affArea.Value, position, scaling,
+                    doc.AnimationHandler.ActiveFrameBindable);
 
 
                 if (doc.AnimationHandler.FindKeyFrame(guid, out IKeyFrameHandler? keyFrame))
                 if (doc.AnimationHandler.FindKeyFrame(guid, out IKeyFrameHandler? keyFrame))
                 {
                 {
                     if (keyFrame is IKeyFrameGroupHandler group)
                     if (keyFrame is IKeyFrameGroupHandler group)
                     {
                     {
+                        RenderGroupPreview(keyFrame, memberVM, member, affArea, position, scaling);
                         foreach (var child in group.Children)
                         foreach (var child in group.Children)
                         {
                         {
                             if (member is IReadOnlyImageNode rasterLayer)
                             if (member is IReadOnlyImageNode rasterLayer)
@@ -535,6 +539,24 @@ internal class MemberPreviewUpdater
         }
         }
     }
     }
 
 
+    private void RenderGroupPreview(IKeyFrameHandler keyFrame, IStructureMemberHandler memberVM,
+        IReadOnlyStructureNode member, [DisallowNull] AffectedArea? affArea, VecI position, float scaling)
+    {
+        bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
+        if(!isEditingRootImage)
+            return;
+        
+        if (keyFrame.PreviewSurface == null ||
+            keyFrame.PreviewSurface.Size != memberVM.PreviewSurface.Size)
+        {
+            keyFrame.PreviewSurface?.Dispose();
+            keyFrame.PreviewSurface = new Surface(memberVM.PreviewSurface.Size);
+        }
+
+        RenderLayerMainPreview((IReadOnlyLayerNode)member, keyFrame.PreviewSurface, affArea.Value,
+            position, scaling, 0);
+    }
+
     /// <summary>
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> folder
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> folder
     /// </summary>
     /// </summary>
@@ -561,9 +583,10 @@ internal class MemberPreviewUpdater
             }
             }
             else
             else
             {
             {
-                rendered = doc.Renderer.RenderChunk(chunk, ChunkResolution.Full, contentNode, doc.AnimationHandler.ActiveFrameBindable);
+                rendered = doc.Renderer.RenderChunk(chunk, ChunkResolution.Full, contentNode,
+                    doc.AnimationHandler.ActiveFrameBindable);
             }
             }
-            
+
             if (rendered.IsT0)
             if (rendered.IsT0)
             {
             {
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos,
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos,
@@ -583,31 +606,31 @@ internal class MemberPreviewUpdater
     /// <summary>
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
     /// </summary>
     /// </summary>
-    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, IStructureMemberHandler memberVM, AffectedArea area,
-        VecI position, float scaling)
+    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Surface surface, AffectedArea area,
+        VecI position, float scaling, int frame)
     {
     {
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Save();
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Translate(-position);
-        memberVM.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
+        surface.DrawingSurface.Canvas.Save();
+        surface.DrawingSurface.Canvas.Scale(scaling);
+        surface.DrawingSurface.Canvas.Translate(-position);
+        surface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
 
 
         foreach (var chunk in area.Chunks)
         foreach (var chunk in area.Chunks)
         {
         {
             var pos = chunk * ChunkResolution.Full.PixelSize();
             var pos = chunk * ChunkResolution.Full.PixelSize();
             if (layer is not IReadOnlyImageNode raster) return;
             if (layer is not IReadOnlyImageNode raster) return;
-            IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(doc.AnimationHandler.ActiveFrameBindable);
+            IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(frame);
 
 
             if (!result.DrawCommittedChunkOn(
             if (!result.DrawCommittedChunkOn(
                     chunk,
                     chunk,
-                    ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos,
+                    ChunkResolution.Full, surface.DrawingSurface, pos,
                     scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
                     scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
             {
             {
-                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
+                surface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
                     ChunkyImage.FullChunkSize, ClearPaint);
                     ChunkyImage.FullChunkSize, ClearPaint);
             }
             }
         }
         }
 
 
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
+        surface.DrawingSurface.Canvas.Restore();
     }
     }
 
 
     private void RenderAnimationFramePreview(IReadOnlyImageNode node, IKeyFrameHandler keyFrameVM, AffectedArea area)
     private void RenderAnimationFramePreview(IReadOnlyImageNode node, IKeyFrameHandler keyFrameVM, AffectedArea area)
@@ -697,7 +720,7 @@ internal class MemberPreviewUpdater
 
 
     private void RenderNodePreviews(List<IRenderInfo> infos)
     private void RenderNodePreviews(List<IRenderInfo> infos)
     {
     {
-        foreach(var node in internals.Tracker.Document.NodeGraph.AllNodes)
+        foreach (var node in internals.Tracker.Document.NodeGraph.AllNodes)
         {
         {
             if (node is null)
             if (node is null)
                 return;
                 return;
@@ -712,7 +735,7 @@ internal class MemberPreviewUpdater
             {
             {
                 return;
                 return;
             }
             }
-            
+
             if (nodeVm.ResultPreview == null)
             if (nodeVm.ResultPreview == null)
             {
             {
                 nodeVm.ResultPreview =
                 nodeVm.ResultPreview =
@@ -726,8 +749,9 @@ internal class MemberPreviewUpdater
             nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
             nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
 
 
             RectI region = new RectI(0, 0, node.CachedResult.Size.X, node.CachedResult.Size.Y);
             RectI region = new RectI(0, 0, node.CachedResult.Size.X, node.CachedResult.Size.Y);
-           
-            nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0, ReplacingPaint);
+
+            nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0,
+                ReplacingPaint);
 
 
             nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
             nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
             infos.Add(new NodePreviewDirty_RenderInfo(node.Id));
             infos.Add(new NodePreviewDirty_RenderInfo(node.Id));

+ 27 - 3
src/PixiEditor.AvaloniaUI/Styles/PortingWipStyles.axaml

@@ -45,14 +45,15 @@
     <Style Selector="CheckBox.ImageCheckBox:checked">
     <Style Selector="CheckBox.ImageCheckBox:checked">
         <Setter Property="Content" Value="{DynamicResource icon-eye}" />
         <Setter Property="Content" Value="{DynamicResource icon-eye}" />
     </Style>
     </Style>
-    
+
     <Style Selector="ToggleButton.PlayButton">
     <Style Selector="ToggleButton.PlayButton">
         <Setter Property="Content" Value="{DynamicResource icon-play}" />
         <Setter Property="Content" Value="{DynamicResource icon-play}" />
         <Setter Property="Template">
         <Setter Property="Template">
             <Setter.Value>
             <Setter.Value>
                 <ControlTemplate>
                 <ControlTemplate>
                     <Border Cursor="Hand" Background="{TemplateBinding Background}">
                     <Border Cursor="Hand" Background="{TemplateBinding Background}">
-                        <TextBlock Text="{TemplateBinding Content}" FontSize="{TemplateBinding Width}" Classes="pixi-icon" />
+                        <TextBlock Text="{TemplateBinding Content}" FontSize="{TemplateBinding Width}"
+                                   Classes="pixi-icon" />
                     </Border>
                     </Border>
                 </ControlTemplate>
                 </ControlTemplate>
             </Setter.Value>
             </Setter.Value>
@@ -61,9 +62,32 @@
 
 
     <Style Selector="ToggleButton.PlayButton:checked">
     <Style Selector="ToggleButton.PlayButton:checked">
         <Setter Property="Content" Value="{DynamicResource icon-pause}" />
         <Setter Property="Content" Value="{DynamicResource icon-pause}" />
+        <Setter Property="Background" Value="Transparent" />
     </Style>
     </Style>
+    
+    <Style Selector="ToggleButton.PlayButton:pressed">
+        <Setter Property="Background" Value="Transparent" />
+   </Style> 
     <Style Selector="ToggleButton.ExpandCollapseToggleStyle">
     <Style Selector="ToggleButton.ExpandCollapseToggleStyle">
-
+        <Setter Property="Content" Value="{DynamicResource icon-chevron-down}" />
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate>
+                    <Border Cursor="Hand" Background="{TemplateBinding Background}">
+                        <TextBlock Text="{TemplateBinding Content}" FontSize="16" Classes="pixi-icon" />
+                    </Border>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+    
+    <Style Selector="ToggleButton.ExpandCollapseToggleStyle:pressed">
+        <Setter Property="Background" Value="Transparent" />
+    </Style>
+    
+    <Style Selector="ToggleButton.ExpandCollapseToggleStyle:checked">
+        <Setter Property="Content" Value="{DynamicResource icon-chevron-left}" />
+        <Setter Property="Background" Value="Transparent"/>
     </Style>
     </Style>
 
 
     <Style Selector="Button.SocialMediaButton">
     <Style Selector="Button.SocialMediaButton">

+ 16 - 3
src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml

@@ -6,19 +6,23 @@
                     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
                     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
         <Setter Property="ClipToBounds" Value="False"/>
         <Setter Property="ClipToBounds" Value="False"/>
+        <Setter Property="Height" Value="70"/>
+        <Setter Property="MinWidth" Value="35"/>
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
                 <Grid>
                 <Grid>
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
-                            Background="{DynamicResource ThemeBackgroundBrush1}" Height="20"
+                            Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 5"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                         <Grid>
                         <Grid>
                             <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
                             <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
-                            <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                            <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft"
+                                   Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
                         </Grid>
                         </Grid>
                     </Border>
                     </Border>
                     
                     
-                    <Border CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60" Margin="-30, 0, 0, 0"
+                    <Border IsVisible="{Binding !IsCollapsed, RelativeSource={RelativeSource TemplatedParent}}"
+                        CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60" Margin="-30, 0, 0, 0"
                             BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True" Name="PreviewBorder"
                             BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True" Name="PreviewBorder"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             RenderOptions.BitmapInterpolationMode="None">
                             RenderOptions.BitmapInterpolationMode="None">
@@ -45,6 +49,15 @@
             </ControlTemplate>
             </ControlTemplate>
         </Setter>
         </Setter>
         
         
+        <Style Selector="^:collapsed">
+            <Setter Property="Height" Value="30"/>
+            <Setter Property="MinWidth" Value="5"/>
+        </Style>
+        
+        <Style Selector="^:collapsed /template/ Panel#PART_ResizePanelLeft">
+            <Setter Property="Margin" Value="0"/>
+        </Style>
+        
         <Style Selector="^:selected /template/ Border#MainBorder">
         <Style Selector="^:selected /template/ Border#MainBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>
         </Style>

+ 115 - 65
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -19,28 +19,68 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="*" />
                         <RowDefinition Height="*" />
                     </Grid.RowDefinitions>
                     </Grid.RowDefinitions>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="200" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
 
 
-                    <DockPanel Grid.Column="0" Grid.Row="0" LastChildFill="True">
-                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
-                                Content="{DynamicResource icon-plus-square}"
-                                Command="{TemplateBinding NewKeyFrameCommand}" />
+                    <Border Grid.Row="0" Grid.Column="0" BorderThickness="0 0 1 0"
+                            BorderBrush="{DynamicResource ThemeBorderMidBrush}">
+                        <input:SizeInput Unit="FPS"
+                                         Width="80" Height="25" HorizontalAlignment="Left"
+                                         Size="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
+                    </Border>
+                    <Border Grid.Row="0" Grid.Column="1">
+                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
+                            <ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton"
+                                          Name="PART_PlayToggle" />
+                            <TextBlock VerticalAlignment="Center" FontSize="14">
+                                <Run>
+                                    <Run.Text>
+                                        <MultiBinding>
+                                            <MultiBinding.Converter>
+                                                <converters:FrameToTimeConverter />
+                                            </MultiBinding.Converter>
+                                            <Binding Path="ActiveFrame"
+                                                     RelativeSource="{RelativeSource TemplatedParent}" />
+                                            <Binding Path="Fps" RelativeSource="{RelativeSource TemplatedParent}" />
+                                        </MultiBinding>
+                                    </Run.Text>
+                                </Run>
+                                <Run Text="/" />
+                                <Run>
+                                    <Run.Text>
+                                        <MultiBinding>
+                                            <MultiBinding.Converter>
+                                                <converters:FrameToTimeConverter />
+                                            </MultiBinding.Converter>
+                                            <Binding Path="EndFrame" RelativeSource="{RelativeSource TemplatedParent}" />
+                                            <Binding Path="Fps" RelativeSource="{RelativeSource TemplatedParent}" />
+                                        </MultiBinding>
+                                    </Run.Text>
+                                </Run>
+                            </TextBlock>
+                        </StackPanel>
+                    </Border>
 
 
-                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
-                                Content="{DynamicResource icon-duplicate}"
-                                Command="{TemplateBinding DuplicateKeyFrameCommand}" />
-                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
-                                Content="{DynamicResource icon-trash}"
-                                Command="{TemplateBinding DeleteKeyFrameCommand}"
-                                IsEnabled="{Binding SelectedKeyFrames.Count, RelativeSource={RelativeSource TemplatedParent}}"
-                                CommandParameter="{Binding SelectedKeyFrames, RelativeSource={RelativeSource TemplatedParent}}" />
-                        <input:NumberInput Min="1"
-                                           Value="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
-                        <Panel>
-                            <ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton" Name="PART_PlayToggle" />
-                        </Panel>
-                    </DockPanel>
+                    <Border Grid.Column="0" Grid.Row="1" BorderThickness="0 1 1 1"
+                            BorderBrush="{DynamicResource ThemeBorderMidBrush}">
+                        <StackPanel Orientation="Horizontal" Height="40">
+                            <Button Classes="pixi-icon"
+                                    Content="{DynamicResource icon-plus-square}"
+                                    Command="{TemplateBinding NewKeyFrameCommand}" />
+                            <Button Classes="pixi-icon"
+                                    Content="{DynamicResource icon-duplicate}"
+                                    Command="{TemplateBinding DuplicateKeyFrameCommand}" />
+                            <Button Classes="pixi-icon"
+                                    Content="{DynamicResource icon-trash}"
+                                    Command="{TemplateBinding DeleteKeyFrameCommand}"
+                                    IsEnabled="{Binding SelectedKeyFrames.Count, RelativeSource={RelativeSource TemplatedParent}}"
+                                    CommandParameter="{Binding SelectedKeyFrames, RelativeSource={RelativeSource TemplatedParent}}" />
+                        </StackPanel>
+                    </Border>
 
 
-                    <Grid Grid.Row="2">
+                    <Grid Grid.Row="1" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="2">
                         <Grid.RowDefinitions>
                         <Grid.RowDefinitions>
                             <RowDefinition Height="Auto" /> <!-- For the timeline slider -->
                             <RowDefinition Height="Auto" /> <!-- For the timeline slider -->
                             <RowDefinition Height="*" />    <!-- For the keyframes and headers -->
                             <RowDefinition Height="*" />    <!-- For the keyframes and headers -->
@@ -49,50 +89,53 @@
                             <ColumnDefinition Width="200" /> <!-- For the headers -->
                             <ColumnDefinition Width="200" /> <!-- For the headers -->
                             <ColumnDefinition Width="*" />    <!-- For the timeline slider and keyframes -->
                             <ColumnDefinition Width="*" />    <!-- For the timeline slider and keyframes -->
                         </Grid.ColumnDefinitions>
                         </Grid.ColumnDefinitions>
-                        <animations:TimelineSlider
-                            Grid.Row="0" Grid.Column="1"
-                            TickFrequency="1" Height="35" ClipToBounds="False"
-                            TickPlacement="TopLeft" VerticalAlignment="Top"
-                            SmallChange="1" ZIndex="10"
-                            LargeChange="10"
-                            Scale="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}"
-                            Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}}"
-                            MinLeftOffset="{Binding MinLeftOffset, RelativeSource={RelativeSource TemplatedParent}}"
-                            IsSnapToTickEnabled="True"
-                            Name="PART_TimelineSlider"
-                            Minimum="1">
-                            <animations:TimelineSlider.Maximum>
-                                <MultiBinding>
-                                    <MultiBinding.Converter>
-                                        <converters:TimelineSliderWidthToMaximumConverter />
-                                    </MultiBinding.Converter>
-                                    <Binding Path="Bounds"
-                                             RelativeSource="{RelativeSource Self}" />
-                                    <Binding RelativeSource="{RelativeSource TemplatedParent}"
-                                             Path="Scale" />
-                                    <Binding RelativeSource="{RelativeSource TemplatedParent}"
-                                             Path="ScrollOffset" />
-                                </MultiBinding>
-                            </animations:TimelineSlider.Maximum>
-                            <Interaction.Behaviors>
-                                <behaviours:SliderUpdateBehavior
-                                    Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveFrame, Mode=OneWay}"
-                                    DragStarted="{xaml:Command PixiEditor.Document.StartChangeActiveFrame}"
-                                    DragValueChanged="{xaml:Command PixiEditor.Document.ChangeActiveFrame, UseProvided=True}"
-                                    DragEnded="{xaml:Command PixiEditor.Document.EndChangeActiveFrame}"
-                                    SetValueCommand="{xaml:Command PixiEditor.Animation.ActiveFrameSet, UseProvided=True}"
-                                    ValueFromSlider="{Binding ElementName=PART_TimelineSlider, Path=Value, Mode=TwoWay}" />
-                            </Interaction.Behaviors>
-                        </animations:TimelineSlider>
+                        <Border Grid.Row="0" Grid.Column="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                                BorderThickness="0 1">
+                            <animations:TimelineSlider
+                                TickFrequency="1" Height="40" ClipToBounds="False"
+                                TickPlacement="TopLeft" VerticalAlignment="Top"
+                                SmallChange="1" ZIndex="10"
+                                LargeChange="10"
+                                Scale="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}"
+                                Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}}"
+                                MinLeftOffset="{Binding MinLeftOffset, RelativeSource={RelativeSource TemplatedParent}}"
+                                IsSnapToTickEnabled="True"
+                                Name="PART_TimelineSlider"
+                                Minimum="1">
+                                <animations:TimelineSlider.Maximum>
+                                    <MultiBinding>
+                                        <MultiBinding.Converter>
+                                            <converters:TimelineSliderWidthToMaximumConverter />
+                                        </MultiBinding.Converter>
+                                        <Binding Path="Bounds"
+                                                 RelativeSource="{RelativeSource Self}" />
+                                        <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                                                 Path="Scale" />
+                                        <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                                                 Path="ScrollOffset" />
+                                    </MultiBinding>
+                                </animations:TimelineSlider.Maximum>
+                                <Interaction.Behaviors>
+                                    <behaviours:SliderUpdateBehavior
+                                        Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveFrame, Mode=OneWay}"
+                                        DragStarted="{xaml:Command PixiEditor.Document.StartChangeActiveFrame}"
+                                        DragValueChanged="{xaml:Command PixiEditor.Document.ChangeActiveFrame, UseProvided=True}"
+                                        DragEnded="{xaml:Command PixiEditor.Document.EndChangeActiveFrame}"
+                                        SetValueCommand="{xaml:Command PixiEditor.Animation.ActiveFrameSet, UseProvided=True}"
+                                        ValueFromSlider="{Binding ElementName=PART_TimelineSlider, Path=Value, Mode=TwoWay}" />
+                                </Interaction.Behaviors>
+                            </animations:TimelineSlider>
+                        </Border>
 
 
-                        <Panel ClipToBounds="True" Grid.Row="1" Grid.Column="1" Margin="29, -16, 0, 0" VerticalAlignment="Stretch"
+                        <Panel ClipToBounds="True" Grid.Row="1" Grid.Column="1" Margin="29, -22, 0, 0"
+                               VerticalAlignment="Stretch"
                                ZIndex="11" HorizontalAlignment="Left" IsHitTestVisible="False">
                                ZIndex="11" HorizontalAlignment="Left" IsHitTestVisible="False">
                             <Border Width="2" Background="{DynamicResource ThemeAccentBrush}">
                             <Border Width="2" Background="{DynamicResource ThemeAccentBrush}">
                                 <Border.Margin>
                                 <Border.Margin>
                                     <MultiBinding Converter="{converters:TimelineSliderValueToMarginConverter}">
                                     <MultiBinding Converter="{converters:TimelineSliderValueToMarginConverter}">
                                         <Binding Path="ActiveFrame"
                                         <Binding Path="ActiveFrame"
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
-                                        <Binding Path="Minimum" ElementName="PART_TimelineSlider"/>
+                                        <Binding Path="Minimum" ElementName="PART_TimelineSlider" />
                                         <Binding Path="Scale"
                                         <Binding Path="Scale"
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
                                         <Binding Path="ScrollOffset"
                                         <Binding Path="ScrollOffset"
@@ -106,19 +149,25 @@
                                       Name="PART_TimelineHeaderScroll"
                                       Name="PART_TimelineHeaderScroll"
                                       Grid.Row="1" Grid.Column="0">
                                       Grid.Row="1" Grid.Column="0">
                             <StackPanel Orientation="Vertical" Background="{DynamicResource ThemeBackgroundBrush1}">
                             <StackPanel Orientation="Vertical" Background="{DynamicResource ThemeBackgroundBrush1}">
-                                <ItemsControl Margin="0, 35"
-                                              ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
+                                <ItemsControl
+                                    ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                     <ItemsControl.DataTemplates>
                                     <ItemsControl.DataTemplates>
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
                                             <animations:TimelineGroupHeader Height="70"
                                             <animations:TimelineGroupHeader Height="70"
                                                                             Item="{Binding}" />
                                                                             Item="{Binding}" />
                                         </DataTemplate>
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>
                                     </ItemsControl.DataTemplates>
+                                    <ItemsControl.Styles>
+                                        <Style Selector="animations|TimelineGroupHeader:collapsed">
+                                            <Setter Property="Height" Value="30" />
+                                        </Style>
+                                    </ItemsControl.Styles>
                                 </ItemsControl>
                                 </ItemsControl>
                             </StackPanel>
                             </StackPanel>
                         </ScrollViewer>
                         </ScrollViewer>
                         <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
                         <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
-                                      Grid.Row="1" Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                      Grid.Row="1"
+                                      Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                       Name="PART_TimelineKeyFramesScroll" Grid.Column="1">
                                       Name="PART_TimelineKeyFramesScroll" Grid.Column="1">
                             <Grid Background="{DynamicResource ThemeBackgroundBrush}" Name="PART_ContentGrid">
                             <Grid Background="{DynamicResource ThemeBackgroundBrush}" Name="PART_ContentGrid">
                                 <Interaction.Behaviors>
                                 <Interaction.Behaviors>
@@ -130,12 +179,11 @@
                                     </EventTriggerBehavior>
                                     </EventTriggerBehavior>
                                 </Interaction.Behaviors>
                                 </Interaction.Behaviors>
                                 <ItemsControl ClipToBounds="False" Name="PART_KeyFramesHost"
                                 <ItemsControl ClipToBounds="False" Name="PART_KeyFramesHost"
-                                              Margin="0, 35, 0, 0"
                                               ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                               ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                     <ItemsControl.DataTemplates>
                                     <ItemsControl.DataTemplates>
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
-                                            <ItemsControl Padding="0 5" ClipToBounds="False" Height="70"
-                                                          BorderThickness="0, 0, 0, 1"
+                                            <ItemsControl ClipToBounds="False"
+                                                          BorderThickness="0, 0, 0, 1" 
                                                           BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                                                           BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                                                           ItemsSource="{Binding Children}">
                                                           ItemsSource="{Binding Children}">
                                                 <ItemsControl.ItemContainerTheme>
                                                 <ItemsControl.ItemContainerTheme>
@@ -160,6 +208,7 @@
                                                 Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
                                                 Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
                                                 IsEnabled="{Binding IsVisible}"
                                                 IsEnabled="{Binding IsVisible}"
                                                 IsSelected="{Binding IsSelected, Mode=TwoWay}"
                                                 IsSelected="{Binding IsSelected, Mode=TwoWay}"
+                                                IsCollapsed="{Binding IsCollapsed}"
                                                 Min="{Binding ElementName=PART_TimelineSlider, Path=Minimum}"
                                                 Min="{Binding ElementName=PART_TimelineSlider, Path=Minimum}"
                                                 Item="{Binding}">
                                                 Item="{Binding}">
                                                 <animations:KeyFrame.Width>
                                                 <animations:KeyFrame.Width>
@@ -194,10 +243,11 @@
                                         </DataTemplate>
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>
                                     </ItemsControl.DataTemplates>
                                 </ItemsControl>
                                 </ItemsControl>
-                                
-                                <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left" VerticalAlignment="Top"
+
+                                <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left"
+                                           VerticalAlignment="Top"
                                            IsVisible="False" ZIndex="100"
                                            IsVisible="False" ZIndex="100"
-                                           Fill="{DynamicResource SelectionFillBrush}" Opacity="1"/>
+                                           Fill="{DynamicResource SelectionFillBrush}" Opacity="1" />
                             </Grid>
                             </Grid>
                         </ScrollViewer>
                         </ScrollViewer>
                     </Grid>
                     </Grid>

+ 31 - 5
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml

@@ -7,11 +7,37 @@
     <ControlTheme TargetType="animations:TimelineGroupHeader" x:Key="{x:Type animations:TimelineGroupHeader}">
     <ControlTheme TargetType="animations:TimelineGroupHeader" x:Key="{x:Type animations:TimelineGroupHeader}">
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
-                <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
-                    <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
-                        <CheckBox Classes="ImageCheckBox" IsChecked="{Binding Item.IsVisible, RelativeSource={RelativeSource TemplatedParent}}"/>
-                        <TextBlock Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
-                    </StackPanel>
+                <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="0 0 1 1">
+                    <DockPanel LastChildFill="False" VerticalAlignment="Center">
+                        <CheckBox VerticalAlignment="Center" Classes="ImageCheckBox" DockPanel.Dock="Left"
+                                  IsChecked="{Binding Item.IsVisible, RelativeSource={RelativeSource TemplatedParent}}" />
+                        <Border IsVisible="{Binding ElementName=PART_CollapseButton, Path=!IsChecked}" CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60"
+                                BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True"
+                                Name="PreviewBorder" Margin="5 0"
+                                HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                                RenderOptions.BitmapInterpolationMode="None">
+                            <Border.Background>
+                                <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
+                                    <ImageBrush.Transform>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
+                                    </ImageBrush.Transform>
+                                </ImageBrush>
+                            </Border.Background>
+                            <visuals:SurfaceControl
+                                Surface="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
+                                Stretch="Uniform" Width="60" Height="60">
+                                <ui:RenderOptionsBindable.BitmapInterpolationMode>
+                                    <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
+                                        <Binding Path="Item.PreviewSurface.Size.X"
+                                                 RelativeSource="{RelativeSource TemplatedParent}" />
+                                        <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
+                                    </MultiBinding>
+                                </ui:RenderOptionsBindable.BitmapInterpolationMode>
+                            </visuals:SurfaceControl>
+                        </Border>
+                        <TextBlock Margin="5 0 0 0" VerticalAlignment="Center" Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
+                        <ToggleButton Name="PART_CollapseButton" Margin="0 0 5 0" DockPanel.Dock="Right" Classes="ExpandCollapseToggleStyle" HorizontalAlignment="Right" VerticalAlignment="Center" />
+                    </DockPanel>
                 </Border>
                 </Border>
             </ControlTemplate>
             </ControlTemplate>
         </Setter>
         </Setter>

+ 4 - 2
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml

@@ -24,7 +24,7 @@
                 <ControlTemplate>
                 <ControlTemplate>
                     <Grid Name="grid">
                     <Grid Name="grid">
                         <Border Background="{DynamicResource ThemeControlLowBrush}"
                         <Border Background="{DynamicResource ThemeControlLowBrush}"
-                                Height="35"
+                                Height="40"
                                 VerticalAlignment="Center">
                                 VerticalAlignment="Center">
                         </Border>
                         </Border>
                         <Canvas Margin="-6,-1">
                         <Canvas Margin="-6,-1">
@@ -38,7 +38,9 @@
                             Offset="{TemplateBinding Offset}"
                             Offset="{TemplateBinding Offset}"
                             MinLeftOffset="{TemplateBinding MinLeftOffset}"
                             MinLeftOffset="{TemplateBinding MinLeftOffset}"
                             MinValue="{TemplateBinding Minimum}"
                             MinValue="{TemplateBinding Minimum}"
-                            Fill="{DynamicResource ThemeForegroundBrush}" />
+                            Fill="{DynamicResource ThemeControlHighBrush}" 
+                            Foreground="{DynamicResource ThemeForegroundBrush}"
+                            Margin="0 0 0 5"/>
                         <animations:TimelineSliderTrack Name="PART_Track"
                         <animations:TimelineSliderTrack Name="PART_Track"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
                                Margin="15, 0, 0, 0"
                                Margin="15, 0, 0, 0"

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Dock/LayoutManager.cs

@@ -63,7 +63,7 @@ internal class LayoutManager
                         Id = "DocumentArea", FallbackContent = new CreateDocumentFallbackView(),
                         Id = "DocumentArea", FallbackContent = new CreateDocumentFallbackView(),
                         Dockables = [ DockContext.CreateDockable(nodeGraphDockViewModel) ]
                         Dockables = [ DockContext.CreateDockable(nodeGraphDockViewModel) ]
                     },
                     },
-                    FirstSize = 0.75,
+                    FirstSize = 0.85,
                     SplitDirection = DockingDirection.Bottom,
                     SplitDirection = DockingDirection.Bottom,
                     Second = new DockableArea
                     Second = new DockableArea
                     {
                     {
@@ -71,7 +71,7 @@ internal class LayoutManager
                         ActiveDockable = DockContext.CreateDockable(timelineDockViewModel)
                         ActiveDockable = DockContext.CreateDockable(timelineDockViewModel)
                     }
                     }
                 },
                 },
-                FirstSize = 0.8,
+                FirstSize = 0.85,
                 SplitDirection = DockingDirection.Right,
                 SplitDirection = DockingDirection.Right,
                 Second = new DockableTree
                 Second = new DockableTree
                 {
                 {

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -71,7 +71,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     {
     {
         if (!Document.UpdateableChangeActive)
         if (!Document.UpdateableChangeActive)
         {
         {
-            Internals.ActionAccumulator.AddFinishedActions(new CreateRasterKeyFrame_Action(targetLayerGuid, Guid.NewGuid(), frame,
+            Internals.ActionAccumulator.AddFinishedActions(new CreateRasterKeyFrame_Action(targetLayerGuid, Guid.NewGuid(), Math.Max(1, frame),
                 frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
                 frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
         }
         }
     }
     }

+ 38 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.Reactive.Linq;
 using System.Reactive.Linq;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
@@ -14,6 +15,24 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
 
 
     public string LayerName => Document.StructureHelper.Find(LayerGuid).NodeNameBindable;
     public string LayerName => Document.StructureHelper.Find(LayerGuid).NodeNameBindable;
 
 
+    public bool IsCollapsed
+    {
+        get => _isCollapsed;
+        set
+        {
+            SetProperty(ref _isCollapsed, value);
+            foreach (var child in Children)
+            {
+                if (child is KeyFrameViewModel keyFrame)
+                {
+                    keyFrame.IsCollapsed = value;
+                }
+            }
+        }
+    }
+
+    private bool _isCollapsed;
+
     public override void SetVisibility(bool isVisible)
     public override void SetVisibility(bool isVisible)
     {
     {
         foreach (var child in Children)
         foreach (var child in Children)
@@ -30,6 +49,7 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
     public KeyFrameGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
     public KeyFrameGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
         : base(startFrame, duration, layerGuid, id, doc, internalParts)
         : base(startFrame, duration, layerGuid, id, doc, internalParts)
     {
     {
+        Children.CollectionChanged += ChildrenOnCollectionChanged;
         Document.StructureHelper.Find(LayerGuid).PropertyChanged += (sender, args) =>
         Document.StructureHelper.Find(LayerGuid).PropertyChanged += (sender, args) =>
         {
         {
             if (args.PropertyName == nameof(StructureMemberViewModel.NodeNameBindable))
             if (args.PropertyName == nameof(StructureMemberViewModel.NodeNameBindable))
@@ -38,4 +58,22 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
             }
             }
         };
         };
     }
     }
+
+    private void ChildrenOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        OnPropertyChanged(nameof(StartFrameBindable));
+        OnPropertyChanged(nameof(DurationBindable));
+        
+        if (e.Action == NotifyCollectionChangedAction.Add)
+        {
+            foreach (var item in e.NewItems)
+            {
+                if (item is KeyFrameViewModel keyFrame)
+                {
+                    keyFrame.IsCollapsed = IsCollapsed;
+                    keyFrame.SetVisibility(IsVisible);
+                }
+            }
+        }
+    }
 }
 }

+ 10 - 3
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs

@@ -14,6 +14,13 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
     private int durationBindable;
     private int durationBindable;
     private bool isVisibleBindable = true;
     private bool isVisibleBindable = true;
     private bool isSelected;
     private bool isSelected;
+    private bool isCollapsed;
+    
+    public bool IsCollapsed
+    {
+        get => isCollapsed;
+        set => SetProperty(ref isCollapsed, value);
+    }
 
 
     public DocumentViewModel Document { get; }
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
     protected DocumentInternalParts Internals { get; }
@@ -69,7 +76,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         get => isVisibleBindable;
         get => isVisibleBindable;
         set
         set
         {
         {
-            if(!Document.UpdateableChangeActive)
+            if (!Document.UpdateableChangeActive)
             {
             {
                 Internals.ActionAccumulator.AddFinishedActions(new KeyFrameVisibility_Action(Id, value));
                 Internals.ActionAccumulator.AddFinishedActions(new KeyFrameVisibility_Action(Id, value));
             }
             }
@@ -107,7 +114,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         durationBindable = newDuration;
         durationBindable = newDuration;
         OnPropertyChanged(nameof(DurationBindable));
         OnPropertyChanged(nameof(DurationBindable));
     }
     }
-    
+
     public void ChangeFrameLength(int newStartFrame, int newDuration)
     public void ChangeFrameLength(int newStartFrame, int newDuration)
     {
     {
         newStartFrame = Math.Max(0, newStartFrame);
         newStartFrame = Math.Max(0, newStartFrame);
@@ -115,7 +122,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         Internals.ActionAccumulator.AddActions(
         Internals.ActionAccumulator.AddActions(
             new KeyFrameLength_Action(Id, newStartFrame, newDuration));
             new KeyFrameLength_Action(Id, newStartFrame, newDuration));
     }
     }
-    
+
     public void EndChangeFrameLength()
     public void EndChangeFrameLength()
     {
     {
         Internals.ActionAccumulator.AddFinishedActions(new EndKeyFrameLength_Action());
         Internals.ActionAccumulator.AddFinishedActions(new EndKeyFrameLength_Action());

+ 20 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -28,6 +28,15 @@ internal class KeyFrame : TemplatedControl
     public static readonly StyledProperty<double> MinProperty = AvaloniaProperty.Register<KeyFrame, double>(
     public static readonly StyledProperty<double> MinProperty = AvaloniaProperty.Register<KeyFrame, double>(
         nameof(Min), 1);
         nameof(Min), 1);
 
 
+    public static readonly StyledProperty<bool> IsCollapsedProperty = AvaloniaProperty.Register<KeyFrame, bool>(
+        nameof(IsCollapsed)); 
+
+    public bool IsCollapsed
+    {
+        get => GetValue(IsCollapsedProperty);
+        set => SetValue(IsCollapsedProperty, value);
+    }
+    
     public double Min
     public double Min
     {
     {
         get => GetValue(MinProperty);
         get => GetValue(MinProperty);
@@ -58,6 +67,7 @@ internal class KeyFrame : TemplatedControl
     static KeyFrame()
     static KeyFrame()
     {
     {
         IsSelectedProperty.Changed.Subscribe(IsSelectedChanged);
         IsSelectedProperty.Changed.Subscribe(IsSelectedChanged);
+        IsCollapsedProperty.Changed.Subscribe(IsCollapsedChanged);
     }
     }
 
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -178,4 +188,14 @@ internal class KeyFrame : TemplatedControl
 
 
         keyFrame.PseudoClasses.Set(":selected", keyFrame.IsSelected);
         keyFrame.PseudoClasses.Set(":selected", keyFrame.IsSelected);
     }
     }
+    
+    private static void IsCollapsedChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is not KeyFrame keyFrame)
+        {
+            return;
+        }
+
+        keyFrame.PseudoClasses.Set(":collapsed", keyFrame.IsCollapsed);
+    }
 }
 }

+ 23 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -139,6 +139,8 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         set { SetValue(FpsProperty, value); }
         set { SetValue(FpsProperty, value); }
     }
     }
 
 
+    public int EndFrame => KeyFrames?.FrameCount > 0 ? KeyFrames.FrameCount : DefaultEndFrame;
+
     public ICommand DraggedKeyFrameCommand { get; }
     public ICommand DraggedKeyFrameCommand { get; }
     public ICommand ReleasedKeyFrameCommand { get; }
     public ICommand ReleasedKeyFrameCommand { get; }
     public ICommand ClearSelectedKeyFramesCommand { get; }
     public ICommand ClearSelectedKeyFramesCommand { get; }
@@ -172,6 +174,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         IsPlayingProperty.Changed.Subscribe(IsPlayingChanged);
         IsPlayingProperty.Changed.Subscribe(IsPlayingChanged);
         FpsProperty.Changed.Subscribe(FpsChanged);
         FpsProperty.Changed.Subscribe(FpsChanged);
         KeyFramesProperty.Changed.Subscribe(OnKeyFramesChanged);
         KeyFramesProperty.Changed.Subscribe(OnKeyFramesChanged);
+        DefaultEndFrameProperty.Changed.Subscribe(OnDefaultEndFrameChanged);
     }
     }
 
 
     public Timeline()
     public Timeline()
@@ -332,7 +335,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
     private void PlayTimerOnTick(object? sender, EventArgs e)
     {
     {
-        if (ActiveFrame >= (KeyFrames.Count > 0 ? KeyFrames.FrameCount : DefaultEndFrame))
+        if (ActiveFrame >= EndFrame) 
         {
         {
             ActiveFrame = 1;
             ActiveFrame = 1;
         }
         }
@@ -570,12 +573,16 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             newCollection.KeyFrameAdded += timeline.KeyFrames_KeyFrameAdded;
             newCollection.KeyFrameAdded += timeline.KeyFrames_KeyFrameAdded;
             newCollection.KeyFrameRemoved += timeline.KeyFrames_KeyFrameRemoved;
             newCollection.KeyFrameRemoved += timeline.KeyFrames_KeyFrameRemoved;
         }
         }
+        
+        timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
+        timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
     }
 
 
     private void KeyFrames_KeyFrameAdded(KeyFrameViewModel keyFrame)
     private void KeyFrames_KeyFrameAdded(KeyFrameViewModel keyFrame)
     {
     {
         keyFrame.PropertyChanged += KeyFrameOnPropertyChanged;
         keyFrame.PropertyChanged += KeyFrameOnPropertyChanged;
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
+        PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
     }
 
 
     private void KeyFrames_KeyFrameRemoved(KeyFrameViewModel keyFrame)
     private void KeyFrames_KeyFrameRemoved(KeyFrameViewModel keyFrame)
@@ -587,6 +594,17 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         }
         }
         
         
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
+        PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
+    }
+    
+    private static void OnDefaultEndFrameChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is not Timeline timeline)
+        {
+            return;
+        }
+
+        timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
     }
     
     
     private void KeyFrameOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
     private void KeyFrameOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -597,6 +615,10 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             {
             {
                 PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
                 PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
             }
             }
+            else if (e.PropertyName == nameof(KeyFrameViewModel.StartFrameBindable) || e.PropertyName == nameof(KeyFrameViewModel.DurationBindable))
+            {
+                PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
+            }
         }
         }
     }
     }
 }
 }

+ 22 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineGroupHeader.cs

@@ -1,18 +1,39 @@
 using Avalonia;
 using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Markup.Xaml.Templates;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 
+[TemplatePart("PART_CollapseButton", typeof(ToggleButton))]
+[PseudoClasses(":collapsed")]
 internal class TimelineGroupHeader : TemplatedControl
 internal class TimelineGroupHeader : TemplatedControl
 {
 {
     public static readonly StyledProperty<KeyFrameGroupViewModel> ItemProperty = AvaloniaProperty.Register<TimelineGroupHeader, KeyFrameGroupViewModel>(
     public static readonly StyledProperty<KeyFrameGroupViewModel> ItemProperty = AvaloniaProperty.Register<TimelineGroupHeader, KeyFrameGroupViewModel>(
-        "Item");
+        nameof(Item));
 
 
     public KeyFrameGroupViewModel Item
     public KeyFrameGroupViewModel Item
     {
     {
         get => GetValue(ItemProperty);
         get => GetValue(ItemProperty);
         set => SetValue(ItemProperty, value);
         set => SetValue(ItemProperty, value);
     }
     }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        if (e.NameScope.Find("PART_CollapseButton") is { } collapseButton)
+        {
+            (collapseButton as ToggleButton).IsCheckedChanged += CollapseButtonOnIsCheckedChanged;
+        }
+    }
+
+    private void CollapseButtonOnIsCheckedChanged(object? sender, RoutedEventArgs e)
+    {
+        bool isCollapsed = (sender as ToggleButton).IsChecked == true;
+        PseudoClasses.Set(":collapsed", isCollapsed);
+        Item.IsCollapsed = isCollapsed;
+    }
 }
 }

+ 14 - 5
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs

@@ -20,6 +20,15 @@ public class TimelineTickBar : Control
     public static readonly StyledProperty<int> MinValueProperty = AvaloniaProperty.Register<TimelineTickBar, int>(
     public static readonly StyledProperty<int> MinValueProperty = AvaloniaProperty.Register<TimelineTickBar, int>(
         nameof(MinValue), 1);
         nameof(MinValue), 1);
 
 
+    public static readonly StyledProperty<IBrush> ForegroundProperty = AvaloniaProperty.Register<TimelineTickBar, IBrush>(
+        nameof(Foreground), Brushes.White);
+
+    public IBrush Foreground
+    {
+        get => GetValue(ForegroundProperty);
+        set => SetValue(ForegroundProperty, value);
+    }
+    
     public int MinValue
     public int MinValue
     {
     {
         get => GetValue(MinValueProperty);
         get => GetValue(MinValueProperty);
@@ -52,7 +61,7 @@ public class TimelineTickBar : Control
 
 
     static TimelineTickBar()
     static TimelineTickBar()
     {
     {
-        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty, OffsetProperty);
+        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty, OffsetProperty, MinValueProperty, ForegroundProperty, MinLeftOffsetProperty);
     }
     }
     
     
     private readonly int[] possibleLargeTickIntervals = { 1, 5, 10, 50, 100 };
     private readonly int[] possibleLargeTickIntervals = { 1, 5, 10, 50, 100 };
@@ -83,10 +92,10 @@ public class TimelineTickBar : Control
             smallTickInterval = 1;
             smallTickInterval = 1;
         }
         }
 
 
-        Pen largeTickPen = new Pen(Fill);
-        Pen smallTickPen = new Pen(Fill, 0.5);
+        Pen largeTickPen = new Pen(Fill, thickness: 2);
+        Pen smallTickPen = new Pen(Fill, 1.5);
         
         
-        int largeStart = visibleMin - (visibleMin % largeTickInterval);
+        int largeStart = visibleMin - (visibleMin % largeTickInterval) - MinValue;
         
         
         RenderBigTicks(context, largeStart, visibleMax, largeTickInterval, frameWidth, largeTickPen, height);
         RenderBigTicks(context, largeStart, visibleMax, largeTickInterval, frameWidth, largeTickPen, height);
         
         
@@ -117,7 +126,7 @@ public class TimelineTickBar : Control
             context.DrawLine(largeTickPen, new Point(x, height), new Point(x, height * 0.55f));
             context.DrawLine(largeTickPen, new Point(x, height), new Point(x, height * 0.55f));
             
             
             var text = new FormattedText((i + MinValue).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
             var text = new FormattedText((i + MinValue).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
-                Typeface.Default, 12, Fill);
+                Typeface.Default, 12, Foreground);
             
             
             double textCenter = text.WidthIncludingTrailingWhitespace / 2;
             double textCenter = text.WidthIncludingTrailingWhitespace / 2;
             Point textPosition = new Point(x - textCenter, height * 0.05);
             Point textPosition = new Point(x - textCenter, height * 0.05);

+ 67 - 52
src/PixiEditor.AvaloniaUI/Views/Input/NumberInput.cs

@@ -19,29 +19,33 @@ internal partial class NumberInput : TextBox
 {
 {
     public static readonly StyledProperty<double> ValueProperty =
     public static readonly StyledProperty<double> ValueProperty =
         AvaloniaProperty.Register<NumberInput, double>(
         AvaloniaProperty.Register<NumberInput, double>(
-            nameof(Value), 0);
+            nameof(Value), 0, coerce: CoerceValue);
 
 
     public static readonly StyledProperty<double> MinProperty =
     public static readonly StyledProperty<double> MinProperty =
         AvaloniaProperty.Register<NumberInput, double>(
         AvaloniaProperty.Register<NumberInput, double>(
-            nameof(Min), float.NegativeInfinity);
+            nameof(Min), float.NegativeInfinity, coerce: CoerceValue);
 
 
     public static readonly StyledProperty<double> MaxProperty =
     public static readonly StyledProperty<double> MaxProperty =
         AvaloniaProperty.Register<NumberInput, double>(
         AvaloniaProperty.Register<NumberInput, double>(
-            nameof(Max), double.PositiveInfinity);
+            nameof(Max), double.PositiveInfinity, coerce: CoerceValue);
 
 
-    public static readonly StyledProperty<string> FormattedValueProperty = AvaloniaProperty.Register<NumberInput, string>(
-        nameof(FormattedValue), "0");
+    public static readonly StyledProperty<string> FormattedValueProperty =
+        AvaloniaProperty.Register<NumberInput, string>(
+            nameof(FormattedValue), "0");
+
+    public static readonly StyledProperty<bool> EnableScrollChangeProperty =
+        AvaloniaProperty.Register<NumberInput, bool>(
+            "EnableScrollChange", true);
 
 
-    public static readonly StyledProperty<bool> EnableScrollChangeProperty = AvaloniaProperty.Register<NumberInput, bool>(
-        "EnableScrollChange", true);
     public string FormattedValue
     public string FormattedValue
     {
     {
         get => GetValue(FormattedValueProperty);
         get => GetValue(FormattedValueProperty);
         set => SetValue(FormattedValueProperty, value);
         set => SetValue(FormattedValueProperty, value);
     }
     }
 
 
-    public static readonly StyledProperty<bool> SelectOnMouseClickProperty = AvaloniaProperty.Register<NumberInput, bool>(
-        nameof(SelectOnMouseClick), true);
+    public static readonly StyledProperty<bool> SelectOnMouseClickProperty =
+        AvaloniaProperty.Register<NumberInput, bool>(
+            nameof(SelectOnMouseClick), true);
 
 
     public static readonly StyledProperty<bool> ConfirmOnEnterProperty = AvaloniaProperty.Register<NumberInput, bool>(
     public static readonly StyledProperty<bool> ConfirmOnEnterProperty = AvaloniaProperty.Register<NumberInput, bool>(
         nameof(ConfirmOnEnter), true);
         nameof(ConfirmOnEnter), true);
@@ -60,14 +64,8 @@ internal partial class NumberInput : TextBox
 
 
     private static Regex regex;
     private static Regex regex;
 
 
-    public int Decimals
-    {
-        get { return (int)GetValue(DecimalsProperty); }
-        set { SetValue(DecimalsProperty, value); }
-    }
-
     public static readonly StyledProperty<int> DecimalsProperty =
     public static readonly StyledProperty<int> DecimalsProperty =
-        AvaloniaProperty.Register<NumberInput, int>(nameof(Decimals), 2);
+        AvaloniaProperty.Register<NumberInput, int>(nameof(Decimals), 2, coerce: CoerceDecimals);
 
 
     public Action OnScrollAction
     public Action OnScrollAction
     {
     {
@@ -96,6 +94,13 @@ internal partial class NumberInput : TextBox
         set => SetValue(MaxProperty, value);
         set => SetValue(MaxProperty, value);
     }
     }
 
 
+
+    public int Decimals
+    {
+        get { return (int)GetValue(DecimalsProperty); }
+        set { SetValue(DecimalsProperty, value); }
+    }
+
     public static readonly StyledProperty<bool> FocusNextProperty =
     public static readonly StyledProperty<bool> FocusNextProperty =
         AvaloniaProperty.Register<NumberInput, bool>(
         AvaloniaProperty.Register<NumberInput, bool>(
             nameof(FocusNext));
             nameof(FocusNext));
@@ -107,10 +112,11 @@ internal partial class NumberInput : TextBox
     }
     }
 
 
     private static readonly DataTable DataTable = new DataTable();
     private static readonly DataTable DataTable = new DataTable();
+
     private static char[] allowedChars = new char[]
     private static char[] allowedChars = new char[]
     {
     {
-        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '(', ')', '.', ',', ' ',
-        'i', 'n', 'f', 't', 'y', 'e', 'I', 'N', 'F', 'T', 'Y', 'E'
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '(', ')', '.', ',', ' ', 'i', 'n',
+        'f', 't', 'y', 'e', 'I', 'N', 'F', 'T', 'Y', 'E'
     };
     };
 
 
 
 
@@ -124,10 +130,10 @@ internal partial class NumberInput : TextBox
 
 
     private Control? leftGrabber;
     private Control? leftGrabber;
     private Control? rightGrabber;
     private Control? rightGrabber;
-    
+
     private double _pressedValue;
     private double _pressedValue;
     private double _pressedRelativeX;
     private double _pressedRelativeX;
-    
+
     static NumberInput()
     static NumberInput()
     {
     {
         ValueProperty.Changed.Subscribe(OnValueChanged);
         ValueProperty.Changed.Subscribe(OnValueChanged);
@@ -143,11 +149,7 @@ internal partial class NumberInput : TextBox
         behaviors.Add(behavior);
         behaviors.Add(behavior);
         Interaction.SetBehaviors(this, behaviors);
         Interaction.SetBehaviors(this, behaviors);
 
 
-        Binding binding = new Binding(nameof(FormattedValue))
-        {
-            Source = this,
-            Mode = BindingMode.TwoWay
-        };
+        Binding binding = new Binding(nameof(FormattedValue)) { Source = this, Mode = BindingMode.TwoWay };
 
 
         this.Bind(TextProperty, binding);
         this.Bind(TextProperty, binding);
 
 
@@ -159,10 +161,10 @@ internal partial class NumberInput : TextBox
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     {
     {
         base.OnApplyTemplate(e);
         base.OnApplyTemplate(e);
-        
-        InnerLeftContent = leftGrabber = CreateMouseGrabber(); 
+
+        InnerLeftContent = leftGrabber = CreateMouseGrabber();
         leftGrabber.HorizontalAlignment = HorizontalAlignment.Left;
         leftGrabber.HorizontalAlignment = HorizontalAlignment.Left;
-        InnerRightContent = rightGrabber = CreateMouseGrabber(); 
+        InnerRightContent = rightGrabber = CreateMouseGrabber();
         rightGrabber.HorizontalAlignment = HorizontalAlignment.Right;
         rightGrabber.HorizontalAlignment = HorizontalAlignment.Right;
     }
     }
 
 
@@ -172,10 +174,10 @@ internal partial class NumberInput : TextBox
         {
         {
             rightGrabber.IsVisible = false;
             rightGrabber.IsVisible = false;
         }
         }
-        
+
         leftGrabber.Height = e.NewSize.Height - 10;
         leftGrabber.Height = e.NewSize.Height - 10;
         leftGrabber.Width = e.NewSize.Width / 4f;
         leftGrabber.Width = e.NewSize.Width / 4f;
-        
+
         rightGrabber.Height = e.NewSize.Height - 10;
         rightGrabber.Height = e.NewSize.Height - 10;
         rightGrabber.Width = e.NewSize.Width / 4f;
         rightGrabber.Width = e.NewSize.Width / 4f;
     }
     }
@@ -184,16 +186,15 @@ internal partial class NumberInput : TextBox
     {
     {
         var grabber = new Grid()
         var grabber = new Grid()
         {
         {
-            Cursor = new Cursor(StandardCursorType.SizeWestEast),
-            Background = Brushes.Transparent,
+            Cursor = new Cursor(StandardCursorType.SizeWestEast), Background = Brushes.Transparent,
         };
         };
 
 
         grabber.PointerPressed += GrabberPressed;
         grabber.PointerPressed += GrabberPressed;
         grabber.PointerMoved += GrabberMoved;
         grabber.PointerMoved += GrabberMoved;
-        
+
         return grabber;
         return grabber;
     }
     }
-    
+
     private void GrabberPressed(object sender, PointerPressedEventArgs e)
     private void GrabberPressed(object sender, PointerPressedEventArgs e)
     {
     {
         e.Pointer.Capture(leftGrabber);
         e.Pointer.Capture(leftGrabber);
@@ -201,53 +202,67 @@ internal partial class NumberInput : TextBox
         _pressedRelativeX = e.GetPosition(this).X;
         _pressedRelativeX = e.GetPosition(this).X;
         e.Handled = true;
         e.Handled = true;
     }
     }
-    
+
     private void GrabberMoved(object sender, PointerEventArgs e)
     private void GrabberMoved(object sender, PointerEventArgs e)
     {
     {
-        if(e.Pointer.Captured != null && (e.Pointer.Captured.Equals(leftGrabber) || e.Pointer.Captured.Equals(rightGrabber)))
+        if (e.Pointer.Captured != null &&
+            (e.Pointer.Captured.Equals(leftGrabber) || e.Pointer.Captured.Equals(rightGrabber)))
         {
         {
             double relativeX = e.GetPosition(this).X;
             double relativeX = e.GetPosition(this).X;
             double diff = relativeX - _pressedRelativeX;
             double diff = relativeX - _pressedRelativeX;
 
 
             double pixelsPerUnit = 5;
             double pixelsPerUnit = 5;
-            
+
             double newValue = _pressedValue + diff / pixelsPerUnit;
             double newValue = _pressedValue + diff / pixelsPerUnit;
-            Value = (float)Math.Round(Math.Clamp(newValue, Min, Max), Decimals);
-            e.Handled = true; 
+            Value = newValue;
+            e.Handled = true;
         }
         }
     }
     }
 
 
     private void BindTextBoxBehavior(TextBoxFocusBehavior behavior)
     private void BindTextBoxBehavior(TextBoxFocusBehavior behavior)
     {
     {
-        Binding focusNextBinding = new Binding(nameof(FocusNext))
-        {
-            Source = this,
-            Mode = BindingMode.OneWay
-        };
+        Binding focusNextBinding = new Binding(nameof(FocusNext)) { Source = this, Mode = BindingMode.OneWay };
 
 
         behavior.Bind(TextBoxFocusBehavior.FocusNextProperty, focusNextBinding);
         behavior.Bind(TextBoxFocusBehavior.FocusNextProperty, focusNextBinding);
 
 
         Binding selectOnMouseClickBinding = new Binding(nameof(SelectOnMouseClick))
         Binding selectOnMouseClickBinding = new Binding(nameof(SelectOnMouseClick))
         {
         {
-            Source = this,
-            Mode = BindingMode.OneWay
+            Source = this, Mode = BindingMode.OneWay
         };
         };
 
 
         behavior.Bind(TextBoxFocusBehavior.SelectOnMouseClickProperty, selectOnMouseClickBinding);
         behavior.Bind(TextBoxFocusBehavior.SelectOnMouseClickProperty, selectOnMouseClickBinding);
 
 
         Binding confirmOnEnterBinding = new Binding(nameof(ConfirmOnEnter))
         Binding confirmOnEnterBinding = new Binding(nameof(ConfirmOnEnter))
         {
         {
-            Source = this,
-            Mode = BindingMode.OneWay
+            Source = this, Mode = BindingMode.OneWay
         };
         };
 
 
         behavior.Bind(TextBoxFocusBehavior.ConfirmOnEnterProperty, confirmOnEnterBinding);
         behavior.Bind(TextBoxFocusBehavior.ConfirmOnEnterProperty, confirmOnEnterBinding);
     }
     }
 
 
+    private static double CoerceValue(AvaloniaObject o, double value)
+    {
+        double min = (double)o.GetValue(MinProperty);
+        double max = (double)o.GetValue(MaxProperty);
+        int decimals = (int)o.GetValue(DecimalsProperty);
+
+        return Math.Round(Math.Clamp(value, min, max), decimals);
+    }
+    
+    private static int CoerceDecimals(AvaloniaObject o, int value)
+    {
+        if (value < 0)
+        {
+            value = 0;
+        }
+        
+        return value;
+    }
+
     private static void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> e)
     private static void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> e)
     {
     {
         NumberInput input = (NumberInput)e.Sender;
         NumberInput input = (NumberInput)e.Sender;
-        input.Value = (float)Math.Round(Math.Clamp(e.NewValue.Value, input.Min, input.Max), input.Decimals);
+        //input.Value = (float)Math.Round(Math.Clamp(e.NewValue.Value, input.Min, input.Max), input.Decimals);
 
 
         var preFormatted = FormatValue(input.Value, input.Decimals);
         var preFormatted = FormatValue(input.Value, input.Decimals);
         input.FormattedValue = preFormatted;
         input.FormattedValue = preFormatted;
@@ -317,7 +332,7 @@ internal partial class NumberInput : TextBox
     private static void FormattedValueChanged(AvaloniaPropertyChangedEventArgs<string> e)
     private static void FormattedValueChanged(AvaloniaPropertyChangedEventArgs<string> e)
     {
     {
         NumberInput input = (NumberInput)e.Sender;
         NumberInput input = (NumberInput)e.Sender;
-        if(ContainsInvalidCharacter(e.NewValue.Value))
+        if (ContainsInvalidCharacter(e.NewValue.Value))
         {
         {
             input.FormattedValue = e.OldValue.Value;
             input.FormattedValue = e.OldValue.Value;
         }
         }
@@ -325,7 +340,7 @@ internal partial class NumberInput : TextBox
 
 
     private static bool ContainsInvalidCharacter(string text)
     private static bool ContainsInvalidCharacter(string text)
     {
     {
-        if(text == null)
+        if (text == null)
         {
         {
             return false;
             return false;
         }
         }
@@ -339,7 +354,7 @@ internal partial class NumberInput : TextBox
         {
         {
             return;
             return;
         }
         }
-        
+
         int step = (int)e.Delta.Y;
         int step = (int)e.Delta.Y;
 
 
         double newValue = Value;
         double newValue = Value;

+ 4 - 3
src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml

@@ -5,8 +5,6 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
-             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
-             xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
              xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
              xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
              mc:Ignorable="d" Focusable="True"
              mc:Ignorable="d" Focusable="True"
              d:DesignHeight="30" Name="uc"
              d:DesignHeight="30" Name="uc"
@@ -37,6 +35,8 @@
                      Decimals="0"
                      Decimals="0"
                      x:Name="input"
                      x:Name="input"
                      Value="{Binding Size, ElementName=uc, Mode=TwoWay}"
                      Value="{Binding Size, ElementName=uc, Mode=TwoWay}"
+                     Min="1"
+                     Max="{Binding MaxSize, ElementName=uc}"
                      d:Value="22"
                      d:Value="22"
                      FocusNext="{Binding FocusNext, ElementName=uc}"
                      FocusNext="{Binding FocusNext, ElementName=uc}"
                      SelectOnMouseClick="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}"
                      SelectOnMouseClick="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}"
@@ -44,7 +44,8 @@
                      Width="43"/>
                      Width="43"/>
             <Grid Grid.Column="1" Background="{Binding BorderBrush, ElementName=border}"
             <Grid Grid.Column="1" Background="{Binding BorderBrush, ElementName=border}"
                   d:Background="{DynamicResource ThemeAccentBrush}"/>
                   d:Background="{DynamicResource ThemeAccentBrush}"/>
-            <TextBlock ui:Translator.Key="{Binding Unit, ElementName=uc, Converter={converters:EnumToStringConverter}}" TextAlignment="Right"
+            <TextBlock ui:Translator.Key="{Binding Unit, ElementName=uc}" 
+                       TextAlignment="Right"
                        Grid.Column="2" Margin="5,0" VerticalAlignment="Center"
                        Grid.Column="2" Margin="5,0" VerticalAlignment="Center"
             />
             />
         </Grid>
         </Grid>

+ 5 - 45
src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml.cs

@@ -11,7 +11,7 @@ namespace PixiEditor.AvaloniaUI.Views.Input;
 internal partial class SizeInput : UserControl
 internal partial class SizeInput : UserControl
 {
 {
     public static readonly StyledProperty<int> SizeProperty =
     public static readonly StyledProperty<int> SizeProperty =
-        AvaloniaProperty.Register<SizeInput, int>(nameof(Size), defaultValue: 1, coerce: Coerce);
+        AvaloniaProperty.Register<SizeInput, int>(nameof(Size), defaultValue: 1);
 
 
     public static readonly StyledProperty<int> MaxSizeProperty =
     public static readonly StyledProperty<int> MaxSizeProperty =
         AvaloniaProperty.Register<SizeInput, int>(nameof(MaxSize), defaultValue: int.MaxValue);
         AvaloniaProperty.Register<SizeInput, int>(nameof(MaxSize), defaultValue: int.MaxValue);
@@ -19,8 +19,8 @@ internal partial class SizeInput : UserControl
     public static readonly StyledProperty<bool> BehaveLikeSmallEmbeddedFieldProperty =
     public static readonly StyledProperty<bool> BehaveLikeSmallEmbeddedFieldProperty =
         AvaloniaProperty.Register<SizeInput, bool>(nameof(BehaveLikeSmallEmbeddedField), defaultValue: true);
         AvaloniaProperty.Register<SizeInput, bool>(nameof(BehaveLikeSmallEmbeddedField), defaultValue: true);
 
 
-    public static readonly StyledProperty<SizeUnit> UnitProperty =
-        AvaloniaProperty.Register<SizeInput, SizeUnit>(nameof(Unit), defaultValue: SizeUnit.Pixel);
+    public static readonly StyledProperty<string> UnitProperty =
+        AvaloniaProperty.Register<SizeInput, string>(nameof(Unit), defaultValue: "PIXEL_UNIT");
 
 
     public static readonly StyledProperty<bool> FocusNextProperty = AvaloniaProperty.Register<SizeInput, bool>(
     public static readonly StyledProperty<bool> FocusNextProperty = AvaloniaProperty.Register<SizeInput, bool>(
         nameof(FocusNext), defaultValue: true);
         nameof(FocusNext), defaultValue: true);
@@ -58,11 +58,6 @@ internal partial class SizeInput : UserControl
         set => SetValue(BehaveLikeSmallEmbeddedFieldProperty, value);
         set => SetValue(BehaveLikeSmallEmbeddedFieldProperty, value);
     }
     }
 
 
-    static SizeInput()
-    {
-        SizeProperty.Changed.Subscribe(InputSizeChanged);
-    }
-
     public SizeInput()
     public SizeInput()
     {
     {
         InitializeComponent();
         InitializeComponent();
@@ -97,47 +92,12 @@ internal partial class SizeInput : UserControl
             input.Focus();
             input.Focus();
     }
     }
 
 
-    public SizeUnit Unit
+    public string Unit
     {
     {
-        get => (SizeUnit)GetValue(UnitProperty);
+        get => (string)GetValue(UnitProperty);
         set => SetValue(UnitProperty, value);
         set => SetValue(UnitProperty, value);
     }
     }
 
 
-
-    private static int Coerce(AvaloniaObject sender, int value)
-    {
-        if (value <= 0)
-        {
-            return 1;
-        }
-
-        int maxSize = sender.GetValue(MaxSizeProperty);
-        
-        if (value > maxSize)
-        {
-            return maxSize;
-        }
-        
-        return value;
-    }
-
-    private static void InputSizeChanged(AvaloniaPropertyChangedEventArgs<int> e)
-    {
-        int newValue = e.NewValue.Value;
-        int maxSize = (int)e.Sender.GetValue(MaxSizeProperty);
-
-        if (newValue > maxSize)
-        {
-            e.Sender.SetValue(SizeProperty, maxSize);
-
-            return;
-        }
-        else if (newValue <= 0)
-        {
-            e.Sender.SetValue(SizeProperty, 1);
-        }
-    }
-
     private void Border_MouseWheel(object? sender, PointerWheelEventArgs e)
     private void Border_MouseWheel(object? sender, PointerWheelEventArgs e)
     {
     {
         int step = (int)e.Delta.Y / 100;
         int step = (int)e.Delta.Y / 100;

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml

@@ -55,7 +55,7 @@
                                      x:Name="PercentageSizePicker"
                                      x:Name="PercentageSizePicker"
                                      IsEnabled="{Binding EditingEnabled, ElementName=uc}"
                                      IsEnabled="{Binding EditingEnabled, ElementName=uc}"
                                      Size="{Binding Path=ChosenPercentageSize, ElementName=uc, Mode=TwoWay}"
                                      Size="{Binding Path=ChosenPercentageSize, ElementName=uc, Mode=TwoWay}"
-                                     Unit="Percentage"
+                                     Unit="%"
                                      Margin="-10,0,0,0"
                                      Margin="-10,0,0,0"
                                      MaxSize="9999"
                                      MaxSize="9999"
                                      Width="{Binding Bounds.Width, ElementName=WidthPicker}">
                                      Width="{Binding Bounds.Width, ElementName=WidthPicker}">

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/MainView.axaml

@@ -32,9 +32,9 @@
 
 
                 <tools:Toolbar Grid.Row="0" DataContext="{Binding .}" />
                 <tools:Toolbar Grid.Row="0" DataContext="{Binding .}" />
                 <tools:ToolsPicker ZIndex="2" Grid.Row="1"
                 <tools:ToolsPicker ZIndex="2" Grid.Row="1"
-                                   Margin="10 0 0 0"
+                                   Margin="10 50 0 0"
                                    HorizontalAlignment="Left"
                                    HorizontalAlignment="Left"
-                                   VerticalAlignment="Center"
+                                   VerticalAlignment="Top"
                                    Tools="{Binding Path=ToolsSubViewModel.ToolSet}" />
                                    Tools="{Binding Path=ToolsSubViewModel.ToolSet}" />
                 <controls:DockableAreaRegion Grid.Row="1"
                 <controls:DockableAreaRegion Grid.Row="1"
                                              Root="{Binding LayoutSubViewModel.LayoutManager.ActiveLayout.Root}"
                                              Root="{Binding LayoutSubViewModel.LayoutManager.ActiveLayout.Root}"

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrameData.cs

@@ -8,4 +8,5 @@ public interface IReadOnlyKeyFrameData
     object Data { get; }
     object Data { get; }
     string AffectedElement { get; }
     string AffectedElement { get; }
     bool IsVisible { get; }
     bool IsVisible { get; }
+    bool IsInFrame(int frame);
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs

@@ -27,7 +27,7 @@ internal class CreateRasterKeyFrame_Change : Change
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
     {
     {
-        return target.TryFindMember(_targetLayerGuid, out _layer);
+        return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,