소스 검색

Fixed shelf not shrinking space

Krzysztof Krysiński 6 달 전
부모
커밋
2a80203ad8

+ 31 - 32
src/PixiEditor.UI.Common/Controls/Shelf.axaml

@@ -2,37 +2,40 @@
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls">
     <ControlTheme TargetType="controls:Shelf" x:Key="{x:Type controls:Shelf}">
+        <Setter Property="Transitions">
+            <Transitions>
+                <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform" Duration="0:0:0.3" />
+            </Transitions>
+        </Setter>
         <Setter Property="Template">
             <ControlTemplate>
                 <Border CornerRadius="{DynamicResource ControlCornerRadiusTop}"
                         Background="{DynamicResource ThemeBackgroundBrush1}">
-                    <Border.Transitions>
-                        <Transitions>
-                            <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform" Duration="0:0:0.3"/>
-                        </Transitions>
-                    </Border.Transitions>
                     <DockPanel LastChildFill="True">
-                        <CheckBox IsChecked="{TemplateBinding IsOpen, Mode=TwoWay}" Focusable="False" ZIndex="10" Name="PART_VisibilityCheckbox" Height="16" HorizontalAlignment="Right"
+                        <CheckBox IsChecked="{TemplateBinding IsOpen, Mode=TwoWay}" Focusable="False" ZIndex="10"
+                                  Name="PART_VisibilityCheckbox" Height="16" HorizontalAlignment="Right"
                                   DockPanel.Dock="Right" VerticalAlignment="Top" Margin="0,5,5,0">
-                        <CheckBox.Transitions>
-                            <Transitions>
-                                <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform" Duration="0:0:0.3"/>
-                            </Transitions>
-                        </CheckBox.Transitions>
-                    <CheckBox.Template>
-                        <ControlTemplate TargetType="{x:Type CheckBox}">
-                            <StackPanel Orientation="Horizontal" Focusable="False">
-                                <Image Focusable="False" Width="14" Cursor="Hand" Name="PART_CheckboxImage" Source="avares://PixiEditor.UI.Common/Assets/ChevronDown.png"/>
-                                <ContentPresenter Focusable="False"/>
-                            </StackPanel>
-                        </ControlTemplate>
-                    </CheckBox.Template>
-                </CheckBox>
+                            <CheckBox.Transitions>
+                                <Transitions>
+                                    <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform"
+                                                                   Duration="0:0:0.3" />
+                                </Transitions>
+                            </CheckBox.Transitions>
+                            <CheckBox.Template>
+                                <ControlTemplate TargetType="{x:Type CheckBox}">
+                                    <StackPanel Orientation="Horizontal" Focusable="False">
+                                        <Image Focusable="False" Width="14" Cursor="Hand" Name="PART_CheckboxImage"
+                                               Source="avares://PixiEditor.UI.Common/Assets/ChevronDown.png" />
+                                        <ContentPresenter Focusable="False" />
+                                    </StackPanel>
+                                </ControlTemplate>
+                            </CheckBox.Template>
+                        </CheckBox>
                         <ContentPresenter Content="{TemplateBinding Content}">
                             <ContentPresenter.Transitions>
                                 <Transitions>
-                                    <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.3"/>
-                                    <DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
+                                    <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.3" />
+                                    <DoubleTransition Property="Opacity" Duration="0:0:0.3" />
                                 </Transitions>
                             </ContentPresenter.Transitions>
                         </ContentPresenter>
@@ -42,21 +45,17 @@
         </Setter>
 
         <Style Selector="^ /template/ CheckBox">
-            <Setter Property="RenderTransformOrigin" Value="7, 7"/>
-            <Setter Property="RenderTransform" Value="rotate(0deg)"/>
+            <Setter Property="RenderTransformOrigin" Value="7, 7" />
+            <Setter Property="RenderTransform" Value="rotate(0deg)" />
         </Style>
 
         <Style Selector="^:not(:isOpen) /template/ CheckBox">
-            <Setter Property="RenderTransform" Value="rotate(180deg)"/>
-        </Style>
-
-        <Style Selector="^:not(:isOpen) /template/ Border">
-            <Setter Property="RenderTransform" Value="translateY(20px)"/>
+            <Setter Property="RenderTransform" Value="rotate(180deg)" />
         </Style>
 
         <Style Selector="^:not(:isOpen) /template/ ContentPresenter">
-            <Setter Property="Opacity" Value="0"/>
-            <Setter Property="IsHitTestVisible" Value="False"/>
+            <Setter Property="Opacity" Value="0" />
+            <Setter Property="IsHitTestVisible" Value="False" />
         </Style>
     </ControlTheme>
-</ResourceDictionary>
+</ResourceDictionary>

+ 78 - 0
src/PixiEditor.UI.Common/Controls/Shelf.cs

@@ -1,7 +1,11 @@
 using Avalonia;
+using Avalonia.Animation.Easings;
 using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
+using Avalonia.Interactivity;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
+using PixiEditor.UI.Common.Tweening;
 
 namespace PixiEditor.UI.Common.Controls;
 
@@ -11,12 +15,44 @@ public class Shelf : ContentControl
     public static readonly StyledProperty<bool> IsOpenProperty = AvaloniaProperty.Register<Shelf, bool>(
         nameof(IsOpen), true);
 
+    public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent =
+        RoutedEvent.Register<Shelf, RoutedEventArgs>(nameof(Opened), RoutingStrategies.Bubble);
+
+    public event EventHandler<RoutedEventArgs> Opened
+    {
+        add => AddHandler(OpenedEvent, value);
+        remove => RemoveHandler(OpenedEvent, value);
+    }
+
+    public static readonly RoutedEvent<RoutedEventArgs> ClosedEvent =
+        RoutedEvent.Register<Shelf, RoutedEventArgs>(nameof(Closed), RoutingStrategies.Bubble);
+
+    public event EventHandler<RoutedEventArgs> Closed
+    {
+        add => AddHandler(ClosedEvent, value);
+        remove => RemoveHandler(ClosedEvent, value);
+    }
+
     public bool IsOpen
     {
         get => GetValue(IsOpenProperty);
         set => SetValue(IsOpenProperty, value);
     }
 
+    public static readonly StyledProperty<Control?> ControlToCollapseProperty =
+        AvaloniaProperty.Register<Shelf, Control?>(
+            nameof(ControlToCollapse));
+
+    public Control? ControlToCollapse
+    {
+        get => GetValue(ControlToCollapseProperty);
+        set => SetValue(ControlToCollapseProperty, value);
+    }
+
+    private double originalControlHeight;
+
+    private Tweener<double> tween;
+
     static Shelf()
     {
         IsOpenProperty.Changed.Subscribe(OnIsOpenChanged);
@@ -31,5 +67,47 @@ public class Shelf : ContentControl
     {
         var shelf = (Shelf)e.Sender;
         shelf.PseudoClasses.Set(":isOpen", (bool)e.NewValue);
+        if (e.NewValue is bool isOpen)
+        {
+            if (isOpen)
+            {
+                shelf.RaiseEvent(new RoutedEventArgs(OpenedEvent));
+                shelf.ExpandControl();
+            }
+            else
+            {
+                shelf.RaiseEvent(new RoutedEventArgs(ClosedEvent));
+                shelf.CollapseControl();
+            }
+        }
+    }
+
+    private void CollapseControl()
+    {
+        if (ControlToCollapse == null) ControlToCollapse = this;
+        originalControlHeight = ControlToCollapse.Bounds.Height;
+        ControlToCollapse.Height = originalControlHeight;
+
+        tween?.Stop();
+        tween = Tween.Double(
+            Control.HeightProperty,
+            ControlToCollapse,
+            originalControlHeight - 20,
+            300,
+            new SineEaseInOut()
+        ).Run();
+    }
+
+    private void ExpandControl()
+    {
+        if (ControlToCollapse == null) ControlToCollapse = this;
+        tween?.Stop();
+        tween = Tween.Double(
+            Control.HeightProperty,
+            ControlToCollapse,
+            originalControlHeight,
+            300,
+            new SineEaseInOut()
+        ).Run();
     }
 }

+ 71 - 0
src/PixiEditor.UI.Common/Tweening/Tweener.cs

@@ -0,0 +1,71 @@
+using Avalonia;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Threading;
+
+namespace PixiEditor.UI.Common.Tweening;
+
+public static class Tween
+{
+    public static Tweener<double> Double(AvaloniaProperty<double> property, Control control,
+        double endValue, double duration, IEasing easing = null)
+    {
+        return new Tweener<double>(property, control, endValue, duration,
+            (start, end, t) => start + (end - start) * easing?.Ease(t) ?? t);
+    }
+}
+
+public class Tweener<T>
+{
+    public AvaloniaProperty<T> Property { get; }
+
+    public T StartValue { get; private set; }
+    public T EndValue { get; }
+    public double DurationMs { get; }
+
+    public Func<T, T, double, T> Interpolator { get; }
+
+    public Control Control { get; set; }
+
+    private DispatcherTimer timer;
+
+    public Tweener(AvaloniaProperty<T> property, Control control, T endValue, double durationMs,
+        Func<T, T, double, T> interpolator)
+    {
+        Property = property;
+        EndValue = endValue;
+        DurationMs = durationMs;
+        Interpolator = interpolator;
+        Control = control;
+    }
+
+    public Tweener<T> Run()
+    {
+        timer = new DispatcherTimer(DispatcherPriority.Default) { Interval = TimeSpan.FromMilliseconds(16) };
+        DateTime startTime = DateTime.Now;
+        StartValue = (T)Control.GetValue(Property);
+        timer.Tick += (sender, args) =>
+        {
+            double elapsed = (DateTime.Now - startTime).TotalMilliseconds;
+            if (elapsed >= DurationMs)
+            {
+                timer.Stop();
+                Control.SetValue(Property, EndValue);
+                return;
+            }
+
+            double t = elapsed / DurationMs;
+            T value = Interpolator(StartValue, EndValue, t);
+            Control.SetValue(Property, value);
+        };
+
+        timer.Start();
+        return this;
+    }
+
+    public void Stop()
+    {
+        timer.Stop();
+        Control.SetValue(Property, EndValue);
+    }
+}

+ 1 - 1
src/PixiEditor/Views/Layers/ReferenceLayer.axaml

@@ -29,7 +29,7 @@
             <behaviours:ClearFocusOnClickBehavior />
         </Interaction.Behaviors>
         <DockPanel Background="Transparent">
-            <controls:Shelf>
+            <controls:Shelf ControlToCollapse="{Binding RelativeSource={RelativeSource AncestorType=UserControl, Mode=FindAncestor}}">
                 <Grid Height="40" x:Name="mainDockPanel">
                     <Grid
                         IsVisible="{Binding Document.ReferenceLayerViewModel.ReferenceTexture, ElementName=uc, Converter={converters:NullToVisibilityConverter}}"

+ 10 - 6
src/PixiEditor/Views/Layers/ReferenceLayer.axaml.cs

@@ -2,6 +2,8 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.ViewModels;
@@ -13,16 +15,18 @@ namespace PixiEditor.Views.Layers;
 internal partial class ReferenceLayer : UserControl
 {
     private Command command;
+    private Rect originalBounds;
 
-    public static readonly StyledProperty<DocumentViewModel> DocumentProperty = AvaloniaProperty.Register<ReferenceLayer, DocumentViewModel>(
-        nameof(Document));
+    public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
+        AvaloniaProperty.Register<ReferenceLayer, DocumentViewModel>(
+            nameof(Document));
 
     public DocumentViewModel Document
     {
         get => GetValue(DocumentProperty);
         set => SetValue(DocumentProperty, value);
     }
-    
+
     static ReferenceLayer()
     {
         DocumentProperty.Changed.Subscribe(OnDocumentChanged);
@@ -32,12 +36,12 @@ internal partial class ReferenceLayer : UserControl
     {
         command = CommandController.Current.Commands["PixiEditor.Clipboard.PasteReferenceLayer"];
         InitializeComponent();
-        
+
         DragBorder.AddHandler(DragDrop.DragEnterEvent, ReferenceLayer_DragEnter);
         DragBorder.AddHandler(DragDrop.DragLeaveEvent, ReferenceLayer_DragLeave);
         DragBorder.AddHandler(DragDrop.DropEvent, ReferenceLayer_Drop);
     }
-    
+
     private static void OnDocumentChanged(AvaloniaPropertyChangedEventArgs<DocumentViewModel> e)
     {
         ReferenceLayer referenceLayer = (ReferenceLayer)e.Sender;
@@ -45,7 +49,7 @@ internal partial class ReferenceLayer : UserControl
         {
             e.OldValue.Value.ReferenceLayerViewModel.PropertyChanged -= referenceLayer.OnDocumentPropertyChanged;
         }
-        
+
         if (e.NewValue.HasValue && e.NewValue.Value != null)
         {
             e.NewValue.Value.ReferenceLayerViewModel.PropertyChanged += referenceLayer.OnDocumentPropertyChanged;