Browse Source

Slider Offset

flabbet 1 year ago
parent
commit
13f34f960c

+ 44 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/DoubleToThicknessConverter.cs

@@ -0,0 +1,44 @@
+using System.Globalization;
+using Avalonia;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class DoubleToThicknessConverter : SingleInstanceConverter<DoubleToThicknessConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is double doubleValue)
+        {
+            if (parameter is null)
+            {
+                return new Thickness(doubleValue);
+            }
+
+            if (parameter is string str)
+            {
+                double l = 0, t = 0, r = 0, b = 0;
+                if (str.Contains('L'))
+                {
+                    l = doubleValue;
+                }
+                if (str.Contains('T'))
+                {
+                    t = doubleValue;
+                }
+                if (str.Contains('R'))
+                {
+                    r = doubleValue;
+                }
+
+                if (str.Contains('B'))
+                {
+                    b = doubleValue;
+                }
+                
+                return new Thickness(l, t, r, b);
+            }
+        }
+
+        return new Thickness();
+    }
+}

+ 0 - 17
src/PixiEditor.AvaloniaUI/Helpers/Converters/OffsetToNegatingMarginConverter.cs

@@ -1,17 +0,0 @@
-using System.Globalization;
-using Avalonia;
-
-namespace PixiEditor.AvaloniaUI.Helpers.Converters;
-
-internal class OffsetToNegatingMarginConverter : SingleInstanceConverter<OffsetToNegatingMarginConverter>
-{
-    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-    {
-        if (value is Vector offset)
-        {
-            return new Thickness(offset.X, offset.Y, 0, 0);
-        }
-
-        return new Thickness();
-    }
-}

+ 3 - 3
src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderWidthToMaximumConverter.cs

@@ -12,14 +12,14 @@ internal class TimelineSliderWidthToMaximumConverter : SingleInstanceMultiValueC
 
 
     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 != 2)
+        if (values.Count != 3)
         {
         {
             throw new ArgumentException("TimelineSliderWidthToMaximumConverter requires 2 values");
             throw new ArgumentException("TimelineSliderWidthToMaximumConverter requires 2 values");
         }
         }
 
 
-        if (values[0] is Rect bounds && values[1] is double scale)
+        if (values[0] is Rect bounds && values[1] is double scale && values[2] is Vector offset)
         {
         {
-            return Math.Floor((bounds.Width) / scale);
+            return Math.Floor((bounds.Width + offset.X) / scale);
         }
         }
 
 
         return 0;
         return 0;

+ 110 - 105
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -49,42 +49,44 @@
                             <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>
-                        <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>
+                        <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="0">
+                            <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>
 
 
                         <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden"
                         <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden"
-                                        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"
                                 <ItemsControl Margin="0, 35"
@@ -98,79 +100,82 @@
                                 </ItemsControl>
                                 </ItemsControl>
                             </StackPanel>
                             </StackPanel>
                         </ScrollViewer>
                         </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,
+                        <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
+                                      Grid.Row="1"
+                                      Name="PART_TimelineKeyFramesScroll" Grid.Column="1">
+                            <Border Background="{DynamicResource ThemeBackgroundBrush}" Name="PART_ContentBorder">
+                                <Interaction.Behaviors>
+                                    <EventTriggerBehavior EventName="PointerPressed">
+                                        <InvokeCommandAction
+                                            Command="{Binding SelectKeyFrameCommand,
                                                         RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
                                                         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,
+                                            CommandParameter="{x:Null}" />
+                                    </EventTriggerBehavior>
+                                </Interaction.Behaviors>
+                                <ItemsControl ClipToBounds="False"
+                                              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="{Binding  Path=MinLeftOffset,
+                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline},
+                                                        Converter={converters:DoubleToThicknessConverter}, ConverterParameter=LR}"/>
+                                                    </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}}"
                                                         RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                                        CommandParameter="{Binding}" />
-                                                </EventTriggerBehavior>
-                                            </Interaction.Behaviors>
-                                        </animations:KeyFrame>
-                                    </DataTemplate>
-                                </ItemsControl.DataTemplates>
-                            </ItemsControl>
-                        </Border>
-                    </ScrollViewer>
+                                                            CommandParameter="{Binding}" />
+                                                    </EventTriggerBehavior>
+                                                </Interaction.Behaviors>
+                                            </animations:KeyFrame>
+                                        </DataTemplate>
+                                    </ItemsControl.DataTemplates>
+                                </ItemsControl>
+                            </Border>
+                        </ScrollViewer>
                     </Grid>
                     </Grid>
                 </Grid>
                 </Grid>
             </ControlTemplate>
             </ControlTemplate>

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

@@ -19,6 +19,7 @@
         <Style Selector="^:horizontal">
         <Style Selector="^:horizontal">
             <Setter Property="MinWidth" Value="40" />
             <Setter Property="MinWidth" Value="40" />
             <Setter Property="MinHeight" Value="20" />
             <Setter Property="MinHeight" Value="20" />
+            <Setter Property="ClipToBounds" Value="True"/>
             <Setter Property="Template">
             <Setter Property="Template">
                 <ControlTemplate>
                 <ControlTemplate>
                     <Grid Name="grid">
                     <Grid Name="grid">
@@ -32,13 +33,16 @@
                         </Canvas>
                         </Canvas>
                         <animations:TimelineTickBar
                         <animations:TimelineTickBar
                             Name="TopTickBar"
                             Name="TopTickBar"
+                            ClipToBounds="True"
                             Scale="{TemplateBinding Scale}"
                             Scale="{TemplateBinding Scale}"
-                            Margin="30, 0"
+                            Offset="{TemplateBinding Offset}"
+                            MinLeftOffset="{TemplateBinding MinLeftOffset}"
                             Fill="White" />
                             Fill="White" />
                         <animations:TimelineSliderTrack Name="PART_Track"
                         <animations:TimelineSliderTrack Name="PART_Track"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
-                               Margin="15, 0"
+                               Margin="15, 0, 0, 0"
                                ScaleFactor="{TemplateBinding Scale}"
                                ScaleFactor="{TemplateBinding Scale}"
+                               Offset="{TemplateBinding Offset}"
                                Orientation="Horizontal">
                                Orientation="Horizontal">
                             <Track.DecreaseButton>
                             <Track.DecreaseButton>
                                 <RepeatButton Name="PART_DecreaseButton"
                                 <RepeatButton Name="PART_DecreaseButton"

+ 16 - 8
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -16,7 +16,6 @@ namespace PixiEditor.AvaloniaUI.Views.Animations;
 [TemplatePart("PART_PlayToggle", typeof(ToggleButton))]
 [TemplatePart("PART_PlayToggle", typeof(ToggleButton))]
 [TemplatePart("PART_TimelineSlider", typeof(TimelineSlider))]
 [TemplatePart("PART_TimelineSlider", typeof(TimelineSlider))]
 [TemplatePart("PART_ContentGrid", typeof(Grid))]
 [TemplatePart("PART_ContentGrid", typeof(Grid))]
-[TemplatePart("PART_TimelineSliderScroll", typeof(ScrollViewer))]
 [TemplatePart("PART_TimelineKeyFramesScroll", typeof(ScrollViewer))]
 [TemplatePart("PART_TimelineKeyFramesScroll", typeof(ScrollViewer))]
 [TemplatePart("PART_TimelineHeaderScroll", typeof(ScrollViewer))]
 [TemplatePart("PART_TimelineHeaderScroll", typeof(ScrollViewer))]
 internal class Timeline : TemplatedControl
 internal class Timeline : TemplatedControl
@@ -77,6 +76,15 @@ internal class Timeline : TemplatedControl
     public static readonly StyledProperty<ICommand> DeleteKeyFrameCommandProperty = AvaloniaProperty.Register<Timeline, ICommand>(
     public static readonly StyledProperty<ICommand> DeleteKeyFrameCommandProperty = AvaloniaProperty.Register<Timeline, ICommand>(
         nameof(DeleteKeyFrameCommand));
         nameof(DeleteKeyFrameCommand));
 
 
+    public static readonly StyledProperty<double> MinLeftOffsetProperty = AvaloniaProperty.Register<Timeline, double>(
+        nameof(MinLeftOffset), 30);
+
+    public double MinLeftOffset
+    {
+        get => GetValue(MinLeftOffsetProperty);
+        set => SetValue(MinLeftOffsetProperty, value);
+    }
+
     public ICommand DeleteKeyFrameCommand
     public ICommand DeleteKeyFrameCommand
     {
     {
         get => GetValue(DeleteKeyFrameCommandProperty);
         get => GetValue(DeleteKeyFrameCommandProperty);
@@ -117,9 +125,8 @@ internal class Timeline : TemplatedControl
 
 
     private ToggleButton? _playToggle;
     private ToggleButton? _playToggle;
     private DispatcherTimer _playTimer;
     private DispatcherTimer _playTimer;
-    private Grid? _contentGrid;
+    private Border? _contentBorder;
     private TimelineSlider? _timelineSlider;
     private TimelineSlider? _timelineSlider;
-    private ScrollViewer? _timelineSliderScroll;
     private ScrollViewer? _timelineKeyFramesScroll;
     private ScrollViewer? _timelineKeyFramesScroll;
     private ScrollViewer? _timelineHeaderScroll;
     private ScrollViewer? _timelineHeaderScroll;
 
 
@@ -160,12 +167,11 @@ internal class Timeline : TemplatedControl
             _playToggle.Click += PlayToggleOnClick;
             _playToggle.Click += PlayToggleOnClick;
         }
         }
         
         
-        _contentGrid = e.NameScope.Find<Grid>("PART_ContentGrid");
+        _contentBorder = e.NameScope.Find<Border>("PART_ContentBorder");
         
         
         _timelineSlider = e.NameScope.Find<TimelineSlider>("PART_TimelineSlider");
         _timelineSlider = e.NameScope.Find<TimelineSlider>("PART_TimelineSlider");
         _timelineSlider.PointerWheelChanged += TimelineSliderOnPointerWheelChanged;
         _timelineSlider.PointerWheelChanged += TimelineSliderOnPointerWheelChanged;
         
         
-        _timelineSliderScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineSliderScroll");
         _timelineKeyFramesScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineKeyFramesScroll");
         _timelineKeyFramesScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineKeyFramesScroll");
         _timelineHeaderScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineHeaderScroll");
         _timelineHeaderScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineHeaderScroll");
         
         
@@ -179,7 +185,8 @@ internal class Timeline : TemplatedControl
             return;
             return;
         }
         }
 
 
-        _timelineSliderScroll!.Offset = new Vector(scrollViewer.Offset.X, 0);
+        ScrollOffset = new Vector(scrollViewer.Offset.X, 0);
+        _timelineSlider.Offset = new Vector(scrollViewer.Offset.X, 0);
         _timelineHeaderScroll!.Offset = new Vector(0, scrollViewer.Offset.Y);
         _timelineHeaderScroll!.Offset = new Vector(0, scrollViewer.Offset.Y);
     }
     }
 
 
@@ -234,7 +241,7 @@ internal class Timeline : TemplatedControl
         
         
         Dispatcher.UIThread.Post(() =>
         Dispatcher.UIThread.Post(() =>
         {
         {
-            ScrollOffset = new Vector(towardsFrame * Scale, 0);
+            //ScrollOffset = new Vector(towardsFrame * Scale, 0);
         });
         });
         
         
         e.Handled = true;
         e.Handled = true;
@@ -242,7 +249,8 @@ internal class Timeline : TemplatedControl
     
     
     private int MousePosToFrame(PointerEventArgs e, bool round = true)
     private int MousePosToFrame(PointerEventArgs e, bool round = true)
     {
     {
-        double x = e.GetPosition(_contentGrid).X;
+        double x = e.GetPosition(_contentBorder).X;
+        x -= MinLeftOffset;
         int frame;
         int frame;
         if (round)
         if (round)
         {
         {

+ 24 - 2
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSlider.cs

@@ -12,6 +12,17 @@ public class TimelineSlider : Slider
     public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<TimelineSlider, double>(
     public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<TimelineSlider, double>(
         nameof(Scale), 100);
         nameof(Scale), 100);
 
 
+    public static readonly StyledProperty<Vector> OffsetProperty = AvaloniaProperty.Register<TimelineSlider, Vector>(
+        nameof(Offset), new Vector(0, 0));
+    
+    public static readonly StyledProperty<double> MinLeftOffsetProperty = AvaloniaProperty.Register<TimelineSlider, double>("MinLeftOffset");
+
+    public Vector Offset
+    {
+        get => GetValue(OffsetProperty);
+        set => SetValue(OffsetProperty, value);
+    }
+
     public double Scale
     public double Scale
     {
     {
         get => GetValue(ScaleProperty);
         get => GetValue(ScaleProperty);
@@ -19,7 +30,13 @@ public class TimelineSlider : Slider
     }
     }
     
     
     protected override Type StyleKeyOverride => typeof(TimelineSlider);
     protected override Type StyleKeyOverride => typeof(TimelineSlider);
-    
+
+    public double MinLeftOffset
+    {
+        get { return (double)GetValue(MinLeftOffsetProperty); }
+        set { SetValue(MinLeftOffsetProperty, value); }
+    }
+
     private Button _increaseButton;
     private Button _increaseButton;
     private Button _decreaseButton;
     private Button _decreaseButton;
     private Track _track;
     private Track _track;
@@ -31,6 +48,11 @@ public class TimelineSlider : Slider
     private IDisposable? _increaseButtonReleaseDispose;
     private IDisposable? _increaseButtonReleaseDispose;
     private IDisposable? _pointerMovedDispose;
     private IDisposable? _pointerMovedDispose;
 
 
+    static TimelineSlider()
+    {
+        AffectsRender<TimelineSlider>(ScaleProperty, OffsetProperty, MinLeftOffsetProperty);
+    }
+
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     {
     {
         _decreaseButtonPressDispose?.Dispose();
         _decreaseButtonPressDispose?.Dispose();
@@ -92,7 +114,7 @@ public class TimelineSlider : Slider
     {
     {
         const double marginLeft = 15;
         const double marginLeft = 15;
         
         
-        double x = point.Position.X - marginLeft;
+        double x = point.Position.X - marginLeft + Offset.X;
         int value = (int)Math.Round(x / Scale);
         int value = (int)Math.Round(x / Scale);
         
         
         Value = value;
         Value = value;

+ 15 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSliderTrack.cs

@@ -10,6 +10,15 @@ internal class TimelineSliderTrack : Track
     public static readonly StyledProperty<double> ScaleFactorProperty =
     public static readonly StyledProperty<double> ScaleFactorProperty =
         AvaloniaProperty.Register<TimelineSliderTrack, double>(nameof(ScaleFactor), defaultValue: 1.0);
         AvaloniaProperty.Register<TimelineSliderTrack, double>(nameof(ScaleFactor), defaultValue: 1.0);
 
 
+    public static readonly StyledProperty<Vector> OffsetProperty = AvaloniaProperty.Register<TimelineSliderTrack, Vector>(
+        "Offset");
+
+    public Vector Offset
+    {
+        get => GetValue(OffsetProperty);
+        set => SetValue(OffsetProperty, value);
+    }
+
     public double ScaleFactor
     public double ScaleFactor
     {
     {
         get => GetValue(ScaleFactorProperty);
         get => GetValue(ScaleFactorProperty);
@@ -18,6 +27,11 @@ internal class TimelineSliderTrack : Track
 
 
     protected override Type StyleKeyOverride => typeof(Track);
     protected override Type StyleKeyOverride => typeof(Track);
 
 
+    static TimelineSliderTrack()
+    {
+        AffectsArrange<TimelineSliderTrack>(ScaleFactorProperty, OffsetProperty);
+    }
+
     // Override the ArrangeOverride method
     // Override the ArrangeOverride method
     protected override Size ArrangeOverride(Size finalSize)
     protected override Size ArrangeOverride(Size finalSize)
     {
     {
@@ -27,7 +41,7 @@ internal class TimelineSliderTrack : Track
             double scaledValue = Value * ScaleFactor;
             double scaledValue = Value * ScaleFactor;
             double thumbLength = Orientation == Orientation.Horizontal ? Thumb.DesiredSize.Width : Thumb.DesiredSize.Height;
             double thumbLength = Orientation == Orientation.Horizontal ? Thumb.DesiredSize.Width : Thumb.DesiredSize.Height;
             
             
-            double thumbPosition = scaledValue;
+            double thumbPosition = scaledValue - Offset.X;
 
 
             Thumb.Arrange(new Rect(thumbPosition, 0, thumbLength, finalSize.Height));
             Thumb.Arrange(new Rect(thumbPosition, 0, thumbLength, finalSize.Height));
             
             

+ 27 - 8
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs

@@ -14,6 +14,8 @@ public class TimelineTickBar : Control
 
 
     public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<TimelineTickBar, double>(
     public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<TimelineTickBar, double>(
         "Scale");
         "Scale");
+    
+    public static readonly StyledProperty<Vector> OffsetProperty = AvaloniaProperty.Register<TimelineTickBar, Vector>("Offset");
 
 
     public double Scale
     public double Scale
     {
     {
@@ -27,18 +29,32 @@ public class TimelineTickBar : Control
         set => SetValue(FillProperty, value);
         set => SetValue(FillProperty, value);
     }
     }
 
 
+    public Vector Offset
+    {
+        get { return (Vector)GetValue(OffsetProperty); }
+        set { SetValue(OffsetProperty, value); }
+    }
+
+    public double MinLeftOffset
+    {
+        get { return (double)GetValue(MinLeftOffsetProperty); }
+        set { SetValue(MinLeftOffsetProperty, value); }
+    }
+
     static TimelineTickBar()
     static TimelineTickBar()
     {
     {
-        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty);
+        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty, OffsetProperty);
     }
     }
     
     
     private readonly int[] possibleLargeTickIntervals = { 1, 5, 10, 50, 100 };
     private readonly int[] possibleLargeTickIntervals = { 1, 5, 10, 50, 100 };
+    public static readonly StyledProperty<double> MinLeftOffsetProperty = AvaloniaProperty.Register<TimelineTickBar, double>("MinOffset");
 
 
     public override void Render(DrawingContext context)
     public override void Render(DrawingContext context)
     {
     {
         double height = Bounds.Height;
         double height = Bounds.Height;
         
         
-        int max = (int)Math.Floor((Bounds.Width + Bounds.X * 2) / Scale);
+        int visibleMin = (int)Math.Floor(Offset.X / Scale);
+        int visibleMax = (int)Math.Ceiling((Offset.X + Bounds.Width) / Scale);
 
 
         double frameWidth = Scale;
         double frameWidth = Scale;
         int largeTickInterval = possibleLargeTickIntervals[0];
         int largeTickInterval = possibleLargeTickIntervals[0];
@@ -61,9 +77,11 @@ public class TimelineTickBar : Control
         Pen largeTickPen = new Pen(Fill);
         Pen largeTickPen = new Pen(Fill);
         Pen smallTickPen = new Pen(Fill, 0.5);
         Pen smallTickPen = new Pen(Fill, 0.5);
         
         
-        for (int i = 0; i <= max; i += largeTickInterval)
+        int largeStart = visibleMin - (visibleMin % largeTickInterval);
+        
+        for (int i = largeStart; i <= visibleMax; i += largeTickInterval)
         {
         {
-            double x = i * frameWidth;
+            double x = i * frameWidth - Offset.X + MinLeftOffset;
             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.ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
             var text = new FormattedText(i.ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
                 Typeface.Default, 12, Fill);
                 Typeface.Default, 12, Fill);
@@ -73,14 +91,15 @@ public class TimelineTickBar : Control
             
             
             context.DrawText(text, textPosition);
             context.DrawText(text, textPosition);
         }
         }
-
-        // Draw small ticks
-        for (int i = 0; i <= max; i += smallTickInterval)
+        
+        int smallStart = visibleMin - (visibleMin % smallTickInterval);
+        
+        for (int i = smallStart; i <= visibleMax; i += smallTickInterval)
         {
         {
             if (i % largeTickInterval == 0)
             if (i % largeTickInterval == 0)
                 continue;
                 continue;
 
 
-            double x = i * frameWidth;
+            double x = i * frameWidth - Offset.X + MinLeftOffset;
             context.DrawLine(smallTickPen, new Point(x, height), new Point(x, height * 0.7f));
             context.DrawLine(smallTickPen, new Point(x, height), new Point(x, height * 0.7f));
         }
         }
     }
     }