Browse Source

custom TickBar

flabbet 1 year ago
parent
commit
c5120a6a7e

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

@@ -0,0 +1,17 @@
+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();
+    }
+}

+ 54 - 42
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -33,9 +33,9 @@
                                 Command="{TemplateBinding DuplicateKeyFrameCommand}" />
                         <Button DockPanel.Dock="Left" Classes="pixi-icon"
                                 Content="{DynamicResource icon-trash}"
-                                Command="{TemplateBinding DeleteKeyFrameCommand}" 
+                                Command="{TemplateBinding DeleteKeyFrameCommand}"
                                 IsEnabled="{Binding !!SelectedKeyFrame, RelativeSource={RelativeSource TemplatedParent}}"
-                                CommandParameter="{Binding SelectedKeyFrame.Id, RelativeSource={RelativeSource TemplatedParent}}"/>
+                                CommandParameter="{Binding SelectedKeyFrame.Id, RelativeSource={RelativeSource TemplatedParent}}" />
                         <input:NumberInput Min="1"
                                            Value="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
                         <Panel>
@@ -43,71 +43,80 @@
                         </Panel>
                     </DockPanel>
 
-                    <DockPanel Grid.Column="1" Grid.Row="1"
-                               LastChildFill="True">
-                        <animations:TimelineSlider Margin="20, 0" TickFrequency="1" TickPlacement="TopLeft"
-                                                   SmallChange="1"
-                                                   LargeChange="10"
-                                                   IsSnapToTickEnabled="True"
-                                                   Name="ActiveFrameSlider"
-                                                   Minimum="0">
-                            <animations:TimelineSlider.Maximum>
-                                <MultiBinding>
-                                    <MultiBinding.Converter>
-                                        <converters:TimelineSliderWidthToMaximumConverter />
-                                    </MultiBinding.Converter>
-                                    <Binding Path="VisualParent.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=ActiveFrameSlider, Path=Value, Mode=TwoWay}" />
-                            </Interaction.Behaviors>
-                        </animations:TimelineSlider>
-                    </DockPanel>
-
-                    <ScrollViewer Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"
+                    <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>
-                            <ItemsControl
-                                ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
+
+                            <ItemsControl Grid.Column="0"
+                                          ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                 <ItemsControl.DataTemplates>
                                     <DataTemplate DataType="document:KeyFrameGroupViewModel">
-                                        <animations:TimelineGroupHeader Height="70" 
+                                        <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,
+                                        <InvokeCommandAction
+                                            Command="{Binding SelectKeyFrameCommand,
                                                         RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                                             CommandParameter="{x:Null}"/>
+                                            CommandParameter="{x:Null}" />
                                     </EventTriggerBehavior>
                                 </Interaction.Behaviors>
-                                <ItemsControl
+                                <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="VisualParent.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}}">
                                     <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}" />
+                                                        <Setter Property="ZIndex"
+                                                                Value="{Binding StartFrameBindable}" />
                                                     </ControlTheme>
                                                 </ItemsControl.ItemContainerTheme>
                                                 <ItemsControl.ItemsPanel>
@@ -135,7 +144,8 @@
                                                         <MultiBinding.Converter>
                                                             <converters:AreEqualConverter />
                                                         </MultiBinding.Converter>
-                                                        <Binding Path="SelectedKeyFrame" RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" />
+                                                        <Binding Path="SelectedKeyFrame"
+                                                                 RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" />
                                                         <Binding
                                                             RelativeSource="{RelativeSource Self}"
                                                             Path="Item" />
@@ -143,15 +153,17 @@
                                                 </animations:KeyFrame.IsSelected>
                                                 <Interaction.Behaviors>
                                                     <EventTriggerBehavior EventName="PointerPressed">
-                                                        <InvokeCommandAction Command="{Binding SelectKeyFrameCommand,
+                                                        <InvokeCommandAction
+                                                            Command="{Binding SelectKeyFrameCommand,
                                                         RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                                                             CommandParameter="{Binding}"/>
+                                                            CommandParameter="{Binding}" />
                                                     </EventTriggerBehavior>
                                                 </Interaction.Behaviors>
                                             </animations:KeyFrame>
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>
                                 </ItemsControl>
+                                </Grid>
                             </ScrollViewer>
                         </Grid>
                     </ScrollViewer>

+ 10 - 17
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml

@@ -18,32 +18,25 @@
         <Style Selector="^:horizontal">
             <Setter Property="MinWidth" Value="40" />
             <Setter Property="MinHeight" Value="20" />
-            <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
             <Setter Property="Template">
                 <ControlTemplate>
                     <Grid Name="grid">
-                        <Border Margin="6, 0" CornerRadius="4" Background="{DynamicResource ThemeControlLowBrush}"
-                                Height="6"
+                        <Border Background="{DynamicResource ThemeControlLowBrush}"
+                                Height="35"
                                 VerticalAlignment="Center">
                         </Border>
                         <Canvas Margin="-6,-1">
                             <Rectangle IsVisible="false" x:Name="PART_SelectionRange" Height="4.0"
                                        StrokeThickness="1.0" />
                         </Canvas>
-                        <TickBar
+                        <animations:TimelineTickBar
                             Name="TopTickBar"
-                            Ticks="{TemplateBinding Ticks}"
-                            TickFrequency="{TemplateBinding Slider.TickFrequency}"
-                            Orientation="{TemplateBinding Slider.Orientation}"
-                            Minimum="{TemplateBinding Slider.Minimum}"
-                            Maximum="{TemplateBinding Slider.Maximum}"
-                            Height="15"
-                            Margin="0,0,0,4"
-                            VerticalAlignment="Center"
-                            Placement="Top"
-                            Fill="{DynamicResource ThemeBackgroundBrush2}" />
+                            Scale="{TemplateBinding Scale}"
+                            Margin="30, 0"
+                            Fill="White" />
                         <Track Name="PART_Track"
                                Grid.Row="1"
+                               Margin="15, 0"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
                                Orientation="Horizontal">
                             <Track.DecreaseButton>
@@ -54,12 +47,12 @@
                                 <RepeatButton Name="PART_IncreaseButton"
                                               Theme="{StaticResource SliderRepeatTrackTheme}" />
                             </Track.IncreaseButton>
-                            <Thumb Name="thumb"
-                                   MinWidth="20"
+                            <Thumb Name="thumb" VerticalAlignment="Top"
+                                   MinWidth="30"
                                    MinHeight="20">
                                 <Thumb.Template>
                                     <ControlTemplate>
-                                        <Border Background="{DynamicResource ThemeAccentBrush}" Width="20"
+                                        <Border Background="{DynamicResource ThemeAccentBrush}" Width="30"
                                                 Height="18"
                                                 CornerRadius="4">
                                             <TextBlock

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

@@ -14,6 +14,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Document;
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 [TemplatePart("PART_PlayToggle", typeof(ToggleButton))]
+[TemplatePart("PART_TimelineSlider", typeof(TimelineSlider))]
 internal class Timeline : TemplatedControl
 {
     public static readonly StyledProperty<KeyFrameCollection> KeyFramesProperty =
@@ -103,6 +104,7 @@ internal class Timeline : TemplatedControl
 
     private ToggleButton? _playToggle;
     private DispatcherTimer _playTimer;
+    private TimelineSlider? _timelineSlider;
 
     static Timeline()
     {
@@ -140,6 +142,9 @@ internal class Timeline : TemplatedControl
         {
             _playToggle.Click += PlayToggleOnClick;
         }
+        
+        _timelineSlider = e.NameScope.Find<TimelineSlider>("PART_TimelineSlider");
+        _timelineSlider.PointerWheelChanged += TimelineSliderOnPointerWheelChanged;
     }
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
@@ -170,11 +175,9 @@ internal class Timeline : TemplatedControl
             IsPlaying = false;
         }
     }
-
-    protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+    
+    private void TimelineSliderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
     {
-        base.OnPointerWheelChanged(e);
-        
         double newScale = Scale;
         
         int ticks = e.KeyModifiers.HasFlag(KeyModifiers.Control) ? 1 : 10;
@@ -183,7 +186,7 @@ internal class Timeline : TemplatedControl
         {
             newScale += ticks;
         }
-        else
+        else if (e.Delta.Y < 0)
         {
             newScale -= ticks;
         }

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

@@ -13,5 +13,6 @@ public class TimelineSlider : Slider
         get => GetValue(ScaleProperty);
         set => SetValue(ScaleProperty, value);
     }
+    
     protected override Type StyleKeyOverride => typeof(TimelineSlider);
 }

+ 88 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs

@@ -0,0 +1,88 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace PixiEditor.AvaloniaUI.Views.Animations;
+
+public class TimelineTickBar : Control
+{
+    private const int MinLargeTickDistance = 25;
+
+    public static readonly StyledProperty<IBrush> FillProperty = AvaloniaProperty.Register<TimelineTickBar, IBrush>(
+        nameof(Fill), Brushes.Black);
+
+    public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<TimelineTickBar, double>(
+        "Scale");
+
+    public double Scale
+    {
+        get => GetValue(ScaleProperty);
+        set => SetValue(ScaleProperty, value);
+    }
+
+    public IBrush Fill
+    {
+        get => GetValue(FillProperty);
+        set => SetValue(FillProperty, value);
+    }
+
+    static TimelineTickBar()
+    {
+        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty);
+    }
+    
+    private readonly int[] possibleLargeTickIntervals = { 1, 5, 10, 50, 100 };
+
+    public override void Render(DrawingContext context)
+    {
+        double width = Bounds.Width;
+        double height = Bounds.Height;
+
+        double frameWidth = Scale;
+        int largeTickInterval = possibleLargeTickIntervals[0];
+        
+        foreach (int interval in possibleLargeTickIntervals)
+        {
+            if (interval * frameWidth >= MinLargeTickDistance)
+            {
+                largeTickInterval = interval;
+                break;
+            }
+        }
+        
+        int smallTickInterval = largeTickInterval / 5;
+        if (smallTickInterval < 1)
+        {
+            smallTickInterval = 1;
+        }
+
+        Pen largeTickPen = new Pen(Fill);
+        Pen smallTickPen = new Pen(Fill, 0.5);
+        
+        int max = (int)Math.Ceiling(width / frameWidth);
+        
+        for (int i = 0; i <= max; i += largeTickInterval)
+        {
+            double x = i * frameWidth;
+            context.DrawLine(largeTickPen, new Point(x, height), new Point(x, height * 0.55f));
+            var text = new FormattedText(i.ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+                Typeface.Default, 12, Fill);
+            
+            double textCenter = text.WidthIncludingTrailingWhitespace / 2;
+            Point textPosition = new Point(x - textCenter, height * 0.05);
+            
+            context.DrawText(text, textPosition);
+        }
+
+        // Draw small ticks
+        for (int i = 0; i <= max; i += smallTickInterval)
+        {
+            if (i % largeTickInterval == 0)
+                continue;
+
+            double x = i * frameWidth;
+            context.DrawLine(smallTickPen, new Point(x, height), new Point(x, height * 0.7f));
+        }
+    }
+}