Browse Source

Overwritten a lot of slider logic

flabbet 1 year ago
parent
commit
69e9cf14d2

+ 2 - 2
src/PixiEditor.AvaloniaUI/Helpers/Converters/DurationToMarginConverter.cs

@@ -18,9 +18,9 @@ internal class DurationToMarginConverter : SingleInstanceMultiValueConverter<Dur
             throw new ArgumentException("DurationToWidthConverter requires 2 values");
         }
         
-        if(values[0] is int duration && values[1] is double scale)
+        if(values[0] is int startFrame && values[1] is double scale)
         {
-            return new Thickness(duration * scale, 0, 0, 0);
+            return new Thickness(startFrame * scale, 0, 0, 0);
         }
         
         return 0;

+ 3 - 6
src/PixiEditor.AvaloniaUI/Helpers/Converters/DurationToWidthConverter.cs

@@ -12,18 +12,15 @@ internal class DurationToWidthConverter : SingleInstanceMultiValueConverter<Dura
 
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
-        if (values.Count != 3)
+        if (values.Count != 2)
         {
             return 0;
             throw new ArgumentException("DurationToWidthConverter requires 2 values");
         }
         
-        if(values[0] is int duration && values[1] is double width && values[2] is double scale && parameter is double margin)
+        if(values[0] is int duration && values[1] is double scale)
         {
-            int max = (int)Math.Floor((width) / scale);
-
-            double frameWidth = (width - margin) / max;
-            return frameWidth * duration;
+            return scale * duration;
         }
         
         return 0;

+ 45 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/MultiSubtractConverter.cs

@@ -0,0 +1,45 @@
+using System.Globalization;
+using Avalonia;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class MultiSubtractConverter : SingleInstanceMultiValueConverter<MultiSubtractConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return value;
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (values == null || values.Count == 0)
+        {
+            return 0;
+        }
+        
+        if (values.Count < 2)
+        {
+            return values[0];
+        }
+        
+        if (values[0] is not double)
+        {
+            return 0;
+        }
+
+        double result = (double)values[0];
+        for (int i = 1; i < values.Count; i++)
+        {
+            if (values[i] is double doubleVal)
+            {
+                result -= doubleVal;
+            }
+            else if (values[i] is Thickness thickness)
+            {
+                result -= thickness.Left + thickness.Right + thickness.Top + thickness.Bottom;
+            }
+        }
+
+        return result;
+    }
+}

+ 126 - 128
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -19,12 +19,8 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="*" />
                     </Grid.RowDefinitions>
-                    <Grid.ColumnDefinitions>
-                        <ColumnDefinition Width="200" />
-                        <ColumnDefinition Width="*" />
-                    </Grid.ColumnDefinitions>
 
-                    <DockPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" LastChildFill="True">
+                    <DockPanel Grid.Column="0" Grid.Row="0" LastChildFill="True">
                         <Button DockPanel.Dock="Left" Classes="pixi-icon"
                                 Content="{DynamicResource icon-plus-square}"
                                 Command="{TemplateBinding NewKeyFrameCommand}" />
@@ -44,136 +40,138 @@
                         </Panel>
                     </DockPanel>
 
-                    <ScrollViewer Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" Name="VerticalScroll"
-                                  HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
-                        <Grid>
-                            <Grid.ColumnDefinitions>
-                                <ColumnDefinition Width="200" />
-                                <ColumnDefinition Width="*" />
-                            </Grid.ColumnDefinitions>
+                    <Grid Grid.Row="2">
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="Auto" /> <!-- For the timeline slider -->
+                            <RowDefinition Height="*" />    <!-- For the keyframes and headers -->
+                        </Grid.RowDefinitions>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="200" /> <!-- For the headers -->
+                            <ColumnDefinition Width="*" />    <!-- For the timeline slider and keyframes -->
+                        </Grid.ColumnDefinitions>
+                        <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled"
+                                      Name="PART_TimelineSliderScroll"
+                                      Grid.Row="0" Grid.Column="1">
+                            <animations:TimelineSlider TickFrequency="1" Height="35" ClipToBounds="False"
+                                                           TickPlacement="TopLeft" VerticalAlignment="Top"
+                                                           SmallChange="1" ZIndex="10"
+                                                           LargeChange="10"
+                                                           Scale="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}"
+                                                           IsSnapToTickEnabled="True"
+                                                           Name="PART_TimelineSlider"
+                                                           Minimum="0">
+                                    <animations:TimelineSlider.Maximum>
+                                        <MultiBinding>
+                                            <MultiBinding.Converter>
+                                                <converters:TimelineSliderWidthToMaximumConverter />
+                                            </MultiBinding.Converter>
+                                            <Binding Path="Bounds"
+                                                     RelativeSource="{RelativeSource Self}" />
+                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                                                     Path="Scale" />
+                                        </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>
+                        </ScrollViewer>
 
-                            <ItemsControl Grid.Column="0" Margin="0, 35"
-                                          ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
-                                <ItemsControl.DataTemplates>
-                                    <DataTemplate DataType="document:KeyFrameGroupViewModel">
-                                        <animations:TimelineGroupHeader Height="70"
-                                                                        Item="{Binding}" />
-                                    </DataTemplate>
-                                </ItemsControl.DataTemplates>
-                            </ItemsControl>
-                            <ScrollViewer Background="{DynamicResource ThemeBackgroundBrush}"
-                                          Grid.Column="1" VerticalScrollBarVisibility="Disabled"
-                                          Name="HorizontalScroll"
-                                          HorizontalScrollBarVisibility="Auto">
-                                <Interaction.Behaviors>
-                                    <EventTriggerBehavior EventName="PointerPressed">
-                                        <InvokeCommandAction
-                                            Command="{Binding SelectKeyFrameCommand,
-                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                            CommandParameter="{x:Null}" />
-                                    </EventTriggerBehavior>
-                                </Interaction.Behaviors>
-                                <Grid VerticalAlignment="Top" HorizontalAlignment="Stretch">
-                                    <animations:TimelineSlider TickFrequency="1" Height="35" ClipToBounds="False"
-                                                               TickPlacement="TopLeft" VerticalAlignment="Top"
-                                                               SmallChange="1" ZIndex="10"
-                                                               LargeChange="10"
-                                                               Margin="{Binding #VerticalScroll.Offset, Converter={converters:OffsetToNegatingMarginConverter}}"
-                                                               Scale="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}"
-                                                               IsSnapToTickEnabled="True"
-                                                               Name="PART_TimelineSlider"
-                                                               Minimum="0">
-                                        <animations:TimelineSlider.Maximum>
-                                            <MultiBinding>
-                                                <MultiBinding.Converter>
-                                                    <converters:TimelineSliderWidthToMaximumConverter />
-                                                </MultiBinding.Converter>
-                                                <Binding Path="Bounds"
-                                                         RelativeSource="{RelativeSource Self}" />
-                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
-                                                         Path="Scale" />
-                                            </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>
-                                     <ItemsControl
-                                         Margin="0, 35, 0, 0"
-                                    ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
+                        <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden"
+                                        Name="PART_TimelineHeaderScroll"
+                                      Grid.Row="1" Grid.Column="0">
+                            <StackPanel Orientation="Vertical" Background="{DynamicResource ThemeBackgroundBrush1}">
+                                <ItemsControl Margin="0, 35"
+                                              ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                     <ItemsControl.DataTemplates>
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
-                                            <ItemsControl Padding="0 5" ClipToBounds="False" Height="70"
-                                                          BorderThickness="0, 0, 0, 1"
-                                                          BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-                                                          ItemsSource="{Binding Children}">
-                                                <ItemsControl.ItemContainerTheme>
-                                                    <ControlTheme TargetType="ContentPresenter">
-                                                        <Setter Property="HorizontalAlignment" Value="Left" />
-                                                        <Setter Property="ZIndex"
-                                                                Value="{Binding StartFrameBindable}" />
-                                                    </ControlTheme>
-                                                </ItemsControl.ItemContainerTheme>
-                                                <ItemsControl.ItemsPanel>
-                                                    <ItemsPanelTemplate>
-                                                        <Grid Margin="30, 0, 0, 0" />
-                                                    </ItemsPanelTemplate>
-                                                </ItemsControl.ItemsPanel>
-                                            </ItemsControl>
-                                        </DataTemplate>
-                                        <DataTemplate DataType="document:RasterKeyFrameViewModel">
-                                            <animations:KeyFrame
-                                                Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                                IsEnabled="{Binding IsVisible}"
-                                                Item="{Binding}">
-                                                <animations:KeyFrame.Width>
-                                                    <MultiBinding Converter="{converters:DurationToWidthConverter}">
-                                                        <MultiBinding.ConverterParameter>
-                                                            <!--TimelineSlider TimelineTickBar margin * 2-->
-                                                            <system:Double>60</system:Double>
-                                                        </MultiBinding.ConverterParameter>
-                                                        <Binding Path="DurationBindable" />
-                                                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=ItemsPresenter}"
-                                                                 Path="Bounds.Width"/>
-                                                        <Binding
-                                                            RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}"
-                                                            Path="Scale" />
-                                                    </MultiBinding>
-                                                </animations:KeyFrame.Width>
-                                                <animations:KeyFrame.IsSelected>
-                                                    <MultiBinding>
-                                                        <MultiBinding.Converter>
-                                                            <converters:AreEqualConverter />
-                                                        </MultiBinding.Converter>
-                                                        <Binding Path="SelectedKeyFrame"
-                                                                 RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" />
-                                                        <Binding
-                                                            RelativeSource="{RelativeSource Self}"
-                                                            Path="Item" />
-                                                    </MultiBinding>
-                                                </animations:KeyFrame.IsSelected>
-                                                <Interaction.Behaviors>
-                                                    <EventTriggerBehavior EventName="PointerPressed">
-                                                        <InvokeCommandAction
-                                                            Command="{Binding SelectKeyFrameCommand,
-                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                                            CommandParameter="{Binding}" />
-                                                    </EventTriggerBehavior>
-                                                </Interaction.Behaviors>
-                                            </animations:KeyFrame>
+                                            <animations:TimelineGroupHeader Height="70"
+                                                                            Item="{Binding}" />
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>
                                 </ItemsControl>
-                                </Grid>
-                            </ScrollViewer>
-                        </Grid>
+                            </StackPanel>
+                        </ScrollViewer>
+                        <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Row="1"
+                                        Name="PART_TimelineKeyFramesScroll" Grid.Column="1">
+                        <Border Background="{DynamicResource ThemeBackgroundBrush}">
+                            <Interaction.Behaviors>
+                                <EventTriggerBehavior EventName="PointerPressed">
+                                    <InvokeCommandAction
+                                        Command="{Binding SelectKeyFrameCommand,
+                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                        CommandParameter="{x:Null}" />
+                                </EventTriggerBehavior>
+                            </Interaction.Behaviors>
+                            <ItemsControl
+                                Margin="0, 35, 0, 0"
+                                ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
+                                <ItemsControl.DataTemplates>
+                                    <DataTemplate DataType="document:KeyFrameGroupViewModel">
+                                        <ItemsControl Padding="0 5" ClipToBounds="False" Height="70"
+                                                      BorderThickness="0, 0, 0, 1"
+                                                      BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                                                      ItemsSource="{Binding Children}">
+                                            <ItemsControl.ItemContainerTheme>
+                                                <ControlTheme TargetType="ContentPresenter">
+                                                    <Setter Property="HorizontalAlignment" Value="Left" />
+                                                    <Setter Property="ZIndex"
+                                                            Value="{Binding StartFrameBindable}" />
+                                                </ControlTheme>
+                                            </ItemsControl.ItemContainerTheme>
+                                            <ItemsControl.ItemsPanel>
+                                                <ItemsPanelTemplate>
+                                                    <Grid Margin="30, 0, 0, 0" />
+                                                </ItemsPanelTemplate>
+                                            </ItemsControl.ItemsPanel>
+                                        </ItemsControl>
+                                    </DataTemplate>
+                                    <DataTemplate DataType="document:RasterKeyFrameViewModel">
+                                        <animations:KeyFrame
+                                            Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                            IsEnabled="{Binding IsVisible}"
+                                            Item="{Binding}">
+                                            <animations:KeyFrame.Width>
+                                                <MultiBinding Converter="{converters:DurationToWidthConverter}">
+                                                    <Binding Path="DurationBindable" />
+                                                    <Binding
+                                                        RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}"
+                                                        Path="Scale" />
+                                                </MultiBinding>
+                                            </animations:KeyFrame.Width>
+                                            <animations:KeyFrame.IsSelected>
+                                                <MultiBinding>
+                                                    <MultiBinding.Converter>
+                                                        <converters:AreEqualConverter />
+                                                    </MultiBinding.Converter>
+                                                    <Binding Path="SelectedKeyFrame"
+                                                             RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" />
+                                                    <Binding
+                                                        RelativeSource="{RelativeSource Self}"
+                                                        Path="Item" />
+                                                </MultiBinding>
+                                            </animations:KeyFrame.IsSelected>
+                                            <Interaction.Behaviors>
+                                                <EventTriggerBehavior EventName="PointerPressed">
+                                                    <InvokeCommandAction
+                                                        Command="{Binding SelectKeyFrameCommand,
+                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                                        CommandParameter="{Binding}" />
+                                                </EventTriggerBehavior>
+                                            </Interaction.Behaviors>
+                                        </animations:KeyFrame>
+                                    </DataTemplate>
+                                </ItemsControl.DataTemplates>
+                            </ItemsControl>
+                        </Border>
                     </ScrollViewer>
+                    </Grid>
                 </Grid>
             </ControlTemplate>
         </Setter>

+ 6 - 5
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml

@@ -1,6 +1,7 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-                    xmlns:animations="clr-namespace:PixiEditor.AvaloniaUI.Views.Animations">
+                    xmlns:animations="clr-namespace:PixiEditor.AvaloniaUI.Views.Animations"
+                    xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
 
     <ControlTheme x:Key="SliderRepeatTrackTheme"
                   TargetType="RepeatButton">
@@ -34,10 +35,10 @@
                             Scale="{TemplateBinding Scale}"
                             Margin="30, 0"
                             Fill="White" />
-                        <Track Name="PART_Track"
-                               Grid.Row="1"
-                               Margin="15, 0"
+                        <animations:TimelineSliderTrack Name="PART_Track"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                               Margin="15, 0"
+                               ScaleFactor="{TemplateBinding Scale}"
                                Orientation="Horizontal">
                             <Track.DecreaseButton>
                                 <RepeatButton Name="PART_DecreaseButton"
@@ -63,7 +64,7 @@
                                     </ControlTemplate>
                                 </Thumb.Template>
                             </Thumb>
-                        </Track>
+                        </animations:TimelineSliderTrack>
                     </Grid>
                 </ControlTemplate>
             </Setter>

+ 6 - 4
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.VisualTree;
+using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 
@@ -78,8 +79,9 @@ internal class KeyFrame : TemplatedControl
                 Converter = new DurationToMarginConverter(),
                 Bindings =
                 {
-                    new Binding("StartFrameBindable") { Source = Item }, new Binding("Scale") { Source = this },
-                },
+                    new Binding("StartFrameBindable") { Source = Item }, 
+                    new Binding("Scale") { Source = this },
+                }
             };
 
             ContentPresenter contentPresenter = this.FindAncestorOfType<ContentPresenter>();
@@ -95,7 +97,7 @@ internal class KeyFrame : TemplatedControl
         }
         
         e.Pointer.Capture(sender as IInputElement);
-        clickFrameOffset = Item.StartFrameBindable - (int)Math.Floor(e.GetPosition(this.FindAncestorOfType<Grid>()).X / Scale);
+        clickFrameOffset = Item.StartFrameBindable - (int)Math.Floor(e.GetPosition(this.FindAncestorOfType<Border>()).X / Scale);
         e.Handled = true;
     }
 
@@ -153,7 +155,7 @@ internal class KeyFrame : TemplatedControl
 
     private int MousePosToFrame(PointerEventArgs e, bool round = true)
     {
-        double x = e.GetPosition(this.FindAncestorOfType<Grid>()).X;
+        double x = e.GetPosition(this.FindAncestorOfType<Border>()).X;
         int frame;
         if (round)
         {

+ 60 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -15,6 +15,10 @@ namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 [TemplatePart("PART_PlayToggle", typeof(ToggleButton))]
 [TemplatePart("PART_TimelineSlider", typeof(TimelineSlider))]
+[TemplatePart("PART_ContentGrid", typeof(Grid))]
+[TemplatePart("PART_TimelineSliderScroll", typeof(ScrollViewer))]
+[TemplatePart("PART_TimelineKeyFramesScroll", typeof(ScrollViewer))]
+[TemplatePart("PART_TimelineHeaderScroll", typeof(ScrollViewer))]
 internal class Timeline : TemplatedControl
 {
     public static readonly StyledProperty<KeyFrameCollection> KeyFramesProperty =
@@ -39,6 +43,15 @@ internal class Timeline : TemplatedControl
     public static readonly StyledProperty<KeyFrameViewModel> SelectedKeyFrameProperty = AvaloniaProperty.Register<Timeline, KeyFrameViewModel>(
         "SelectedKeyFrame");
 
+    public static readonly StyledProperty<Vector> ScrollOffsetProperty = AvaloniaProperty.Register<Timeline, Vector>(
+        "ScrollOffset");
+
+    public Vector ScrollOffset
+    {
+        get => GetValue(ScrollOffsetProperty);
+        set => SetValue(ScrollOffsetProperty, value);
+    }
+
     public KeyFrameViewModel SelectedKeyFrame
     {
         get => GetValue(SelectedKeyFrameProperty);
@@ -104,7 +117,11 @@ internal class Timeline : TemplatedControl
 
     private ToggleButton? _playToggle;
     private DispatcherTimer _playTimer;
+    private Grid? _contentGrid;
     private TimelineSlider? _timelineSlider;
+    private ScrollViewer? _timelineSliderScroll;
+    private ScrollViewer? _timelineKeyFramesScroll;
+    private ScrollViewer? _timelineHeaderScroll;
 
     static Timeline()
     {
@@ -143,8 +160,27 @@ internal class Timeline : TemplatedControl
             _playToggle.Click += PlayToggleOnClick;
         }
         
+        _contentGrid = e.NameScope.Find<Grid>("PART_ContentGrid");
+        
         _timelineSlider = e.NameScope.Find<TimelineSlider>("PART_TimelineSlider");
         _timelineSlider.PointerWheelChanged += TimelineSliderOnPointerWheelChanged;
+        
+        _timelineSliderScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineSliderScroll");
+        _timelineKeyFramesScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineKeyFramesScroll");
+        _timelineHeaderScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineHeaderScroll");
+        
+        _timelineKeyFramesScroll.ScrollChanged += TimelineKeyFramesScrollOnScrollChanged;
+    }
+
+    private void TimelineKeyFramesScrollOnScrollChanged(object? sender, ScrollChangedEventArgs e)
+    {
+        if (sender is not ScrollViewer scrollViewer)
+        {
+            return;
+        }
+
+        _timelineSliderScroll!.Offset = new Vector(scrollViewer.Offset.X, 0);
+        _timelineHeaderScroll!.Offset = new Vector(0, scrollViewer.Offset.Y);
     }
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
@@ -181,6 +217,8 @@ internal class Timeline : TemplatedControl
         double newScale = Scale;
         
         int ticks = e.KeyModifiers.HasFlag(KeyModifiers.Control) ? 1 : 10;
+        
+        int towardsFrame = MousePosToFrame(e);
 
         if (e.Delta.Y > 0)
         {
@@ -194,8 +232,30 @@ internal class Timeline : TemplatedControl
         newScale = Math.Clamp(newScale, 1, 900);
         Scale = newScale;
         
+        Dispatcher.UIThread.Post(() =>
+        {
+            ScrollOffset = new Vector(towardsFrame * Scale, 0);
+        });
+        
         e.Handled = true;
     }
+    
+    private int MousePosToFrame(PointerEventArgs e, bool round = true)
+    {
+        double x = e.GetPosition(_contentGrid).X;
+        int frame;
+        if (round)
+        {
+            frame = (int)Math.Round(x / Scale);
+        }
+        else
+        {
+            frame = (int)Math.Floor(x / Scale);
+        }
+        
+        frame = Math.Max(0, frame);
+        return frame;
+    }
 
     private static void IsPlayingChanged(AvaloniaPropertyChangedEventArgs e)
     {

+ 82 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSlider.cs

@@ -1,5 +1,9 @@
 using Avalonia;
+using Avalonia.Collections;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
 
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 
@@ -15,4 +19,82 @@ public class TimelineSlider : Slider
     }
     
     protected override Type StyleKeyOverride => typeof(TimelineSlider);
+    
+    private Button _increaseButton;
+    private Button _decreaseButton;
+    private Track _track;
+    
+    private bool _isDragging;
+    private IDisposable? _decreaseButtonPressDispose;
+    private IDisposable? _decreaseButtonReleaseDispose;
+    private IDisposable? _increaseButtonSubscription;
+    private IDisposable? _increaseButtonReleaseDispose;
+    private IDisposable? _pointerMovedDispose;
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        _decreaseButtonPressDispose?.Dispose();
+        _decreaseButtonReleaseDispose?.Dispose();
+        _increaseButtonSubscription?.Dispose();
+        _increaseButtonReleaseDispose?.Dispose();
+        _pointerMovedDispose?.Dispose();
+        
+        _increaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
+        _decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
+        _track = e.NameScope.Find<Track>("PART_Track");
+        
+        if (_track != null)
+        {
+            _track.IgnoreThumbDrag = true;
+        }
+
+        if (_decreaseButton != null)
+        {
+            _decreaseButtonPressDispose = _decreaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
+            _decreaseButtonReleaseDispose = _decreaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
+        }
+
+        if (_increaseButton != null)
+        {
+            _increaseButtonSubscription = _increaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
+            _increaseButtonReleaseDispose = _increaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
+        }
+
+        _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
+    }
+    
+    private void TrackPressed(object? sender, PointerPressedEventArgs e)
+    {
+        _isDragging = true;
+        MoveToPoint(e.GetCurrentPoint(_track));
+    }
+    
+    private void TrackReleased(object? sender, PointerReleasedEventArgs e)
+    {
+        _isDragging = false;
+    }
+    
+    private void TrackMoved(object? sender, PointerEventArgs e)
+    {
+        if (!IsEnabled)
+        {
+            _isDragging = false;
+            return;
+        }
+
+        if (_isDragging)
+        {
+            MoveToPoint(e.GetCurrentPoint(_track));
+        }
+    }
+    
+    private void MoveToPoint(PointerPoint point)
+    {
+        const double marginLeft = 15;
+        
+        double x = point.Position.X - marginLeft;
+        int value = (int)Math.Round(x / Scale);
+        
+        Value = value;
+    }
 }

+ 53 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSliderTrack.cs

@@ -0,0 +1,53 @@
+using Avalonia;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+
+namespace PixiEditor.AvaloniaUI.Views.Animations;
+
+internal class TimelineSliderTrack : Track
+{
+    // Define a dependency property for your scale factor
+    public static readonly StyledProperty<double> ScaleFactorProperty =
+        AvaloniaProperty.Register<TimelineSliderTrack, double>(nameof(ScaleFactor), defaultValue: 1.0);
+
+    public double ScaleFactor
+    {
+        get => GetValue(ScaleFactorProperty);
+        set => SetValue(ScaleFactorProperty, value);
+    }
+
+    protected override Type StyleKeyOverride => typeof(Track);
+
+    // Override the ArrangeOverride method
+    protected override Size ArrangeOverride(Size finalSize)
+    {
+        base.ArrangeOverride(finalSize);
+        if (Thumb != null)
+        {
+            double scaledValue = Value * ScaleFactor;
+            double thumbLength = Orientation == Orientation.Horizontal ? Thumb.DesiredSize.Width : Thumb.DesiredSize.Height;
+            
+            double thumbPosition = scaledValue;
+
+            Thumb.Arrange(new Rect(thumbPosition, 0, thumbLength, finalSize.Height));
+            
+            Rect decreaseButtonRect = new Rect(0, 0, thumbPosition, finalSize.Height);
+            Rect increaseButtonRect = new Rect(thumbPosition + thumbLength, 0, finalSize.Width - thumbPosition - thumbLength, finalSize.Height);
+            if (decreaseButtonRect.Width < 0)
+            {
+                decreaseButtonRect = new Rect(0, 0, 0, 0);
+            }
+            
+            if (increaseButtonRect.Width < 0)
+            {
+                increaseButtonRect = new Rect(0, 0, 0, 0);
+            }
+            
+            DecreaseButton?.Arrange(decreaseButtonRect);
+            IncreaseButton?.Arrange(increaseButtonRect);
+        }
+
+        return finalSize;
+    }
+
+}

+ 1 - 2
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs

@@ -36,12 +36,11 @@ public class TimelineTickBar : Control
 
     public override void Render(DrawingContext context)
     {
-        double width = Bounds.Width;
         double height = Bounds.Height;
         
         int max = (int)Math.Floor((Bounds.Width + Bounds.X * 2) / Scale);
 
-        double frameWidth = width / max;
+        double frameWidth = Scale;
         int largeTickInterval = possibleLargeTickIntervals[0];
         
         foreach (int interval in possibleLargeTickIntervals)