Browse Source

Category search wip

flabbet 11 months ago
parent
commit
c4955f9bc3
31 changed files with 184 additions and 52 deletions
  1. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs
  2. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  3. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  4. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  5. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs
  6. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs
  7. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  8. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs
  10. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs
  15. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs
  16. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs
  17. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  19. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs
  21. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  23. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/DistributePointsNode.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RasterizePointsNode.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RemoveClosePointsNode.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  29. 4 1
      src/PixiEditor/Models/Nodes/NodeTypeInfo.cs
  30. 32 12
      src/PixiEditor/Styles/Templates/NodePicker.axaml
  31. 119 12
      src/PixiEditor/Views/Nodes/NodePicker.cs

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs

@@ -8,6 +8,8 @@ public class NodeInfoAttribute : Attribute
     public string DisplayName { get; }
     
     public string? PickerName { get; set; }
+    
+    public string? Category { get; set; }
 
     public NodeInfoAttribute(string uniqueName, string displayName)
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs

@@ -3,7 +3,7 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
 
-[NodeInfo("Time", "TIME_NODE")]
+[NodeInfo("Time", "TIME_NODE", Category = "ANIMATION")]
 public class TimeNode : Node
 {
     public OutputProperty<int> ActiveFrame { get; set; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineChannels", "COMBINE_CHANNELS_NODE")]
+[NodeInfo("CombineChannels", "COMBINE_CHANNELS_NODE", Category = "IMAGE")]
 public class CombineChannelsNode : Node
 {
     private readonly Paint _screenPaint = new() { BlendMode = BlendMode.Screen };

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineColor", "COMBINE_COLOR_NODE")]
+[NodeInfo("CombineColor", "COMBINE_COLOR_NODE", Category = "COLOR")]
 public class CombineColorNode : Node
 {
     public FuncOutputProperty<Half4> Color { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecD", "COMBINE_VECD_NODE")]
+[NodeInfo("CombineVecD", "COMBINE_VECD_NODE", Category = "NUMBER")]
 public class CombineVecD : Node
 {
     public FuncOutputProperty<Float2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecI", "COMBINE_VECI_NODE")]
+[NodeInfo("CombineVecI", "COMBINE_VECI_NODE", Category = "NUMBER")]
 public class CombineVecI : Node
 {
     public FuncOutputProperty<Int2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateChannels", "SEPARATE_CHANNELS_NODE")]
+[NodeInfo("SeparateChannels", "SEPARATE_CHANNELS_NODE", Category = "COLOR")]
 public class SeparateChannelsNode : Node
 {
     private readonly Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateColor", "SEPARATE_COLOR_NODE")]
+[NodeInfo("SeparateColor", "SEPARATE_COLOR_NODE", Category = "COLOR")]
 public class SeparateColorNode : Node
 {
     public FuncInputProperty<Half4> Color { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecD", "SEPARATE_VECD_NODE")]
+[NodeInfo("SeparateVecD", "SEPARATE_VECD_NODE", Category = "NUMBER")]
 public class SeparateVecDNode : Node
 {
     public FuncInputProperty<Float2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecI", "SEPARATE_VECI_NODE")]
+[NodeInfo("SeparateVecI", "SEPARATE_VECI_NODE", Category = "NUMBER")]
 public class SeparateVecINode : Node
 {
     public FuncInputProperty<Int2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Ellipse", "ELLIPSE_NODE")]
+[NodeInfo("Ellipse", "ELLIPSE_NODE", Category = "SHAPE")]
 public class EllipseNode : Node
 {
     public InputProperty<VecI> Radius { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("CreateImage", "CREATE_IMAGE_NODE")]
+[NodeInfo("CreateImage", "CREATE_IMAGE_NODE", Category = "IMAGE")]
 public class CreateImageNode : Node
 {
     private Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ApplyFilter", "APPLY_FILTER_NODE")]
+[NodeInfo("ApplyFilter", "APPLY_FILTER_NODE", Category = "FILTERS")]
 public class ApplyFilterNode : Node
 {
     private Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ColorMatrixFilter", "COLOR_MATRIX_TRANSFORM_FILTER_NODE")]
+[NodeInfo("ColorMatrixFilter", "COLOR_MATRIX_TRANSFORM_FILTER_NODE", Category = "FILTERS")]
 public class ColorMatrixFilterNode : FilterNode
 {
     public InputProperty<ColorMatrix> Matrix { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("GrayscaleFilter", "GRAYSCALE_FILTER_NODE")]
+[NodeInfo("GrayscaleFilter", "GRAYSCALE_FILTER_NODE", Category = "FILTERS")]
 public class GrayscaleNode : FilterNode
 {
     private static readonly ColorMatrix WeightedMatrix = ColorMatrix.WeightedWavelengthGrayscale + ColorMatrix.UseAlpha;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs

@@ -4,7 +4,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("KernelFilter", "KERNEL_FILTER_NODE")]
+[NodeInfo("KernelFilter", "KERNEL_FILTER_NODE", Category = "FILTERS")]
 public class KernelFilterNode : FilterNode
 {
     private readonly Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Folder", "FOLDER_NODE")]
+[NodeInfo("Folder", "FOLDER_NODE", Category = "STRUCTURE")]
 public class FolderNode : StructureNode, IReadOnlyFolderNode
 {
     public InputProperty<Texture?> Content { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -10,7 +10,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ImageLayer", "IMAGE_LAYER_NODE")]
+[NodeInfo("ImageLayer", "IMAGE_LAYER_NODE", Category = "STRUCTURE")]
 public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 {
     public const string ImageFramesKey = "Frames";

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ImageSpace", "IMAGE_SPACE_NODE")]
+[NodeInfo("ImageSpace", "IMAGE_SPACE_NODE", Category = "IMAGE")]
 public class ImageSpaceNode : Node
 {
     public FuncOutputProperty<VecD> SpacePosition { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Lerp", "LERP_NODE")]
+[NodeInfo("Lerp", "LERP_NODE", Category = "NUMBERS")]
 public class LerpColorNode : Node // TODO: ILerpable as inputs? 
 {
     public FuncOutputProperty<Half4> Result { get; } 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Math", "MATH_NODE")]
+[NodeInfo("Math", "MATH_NODE", Category = "NUMBERS")]
 public class MathNode : Node
 {
     public FuncOutputProperty<Float1> Result { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Merge", "MERGE_NODE")]
+[NodeInfo("Merge", "MERGE_NODE", Category = "OPERATIONS")]
 public class MergeNode : Node, IBackgroundInput
 {
     private Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -13,7 +13,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageLeft", "MODIFY_IMAGE_LEFT_NODE", PickerName = "MODIFY_IMAGE_PAIR_NODE")]
+[NodeInfo("ModifyImageLeft", "MODIFY_IMAGE_LEFT_NODE", PickerName = "MODIFY_IMAGE_PAIR_NODE", Category = "IMAGE")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 public class ModifyImageLeftNode : Node, IPairNode
 {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -9,7 +9,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Noise", "NOISE_NODE")]
+[NodeInfo("Noise", "NOISE_NODE", Category = "GENERATION")]
 public class NoiseNode : Node
 {
     private double previousScale = double.NaN;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/DistributePointsNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
 
-[NodeInfo("DistributePoints", "DISTRIBUTE_POINTS")]
+[NodeInfo("DistributePoints", "DISTRIBUTE_POINTS", Category = "GENERATION")]
 public class DistributePointsNode : Node
 {
     public OutputProperty<PointList> Points { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RasterizePointsNode.cs

@@ -10,7 +10,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
 
-[NodeInfo("RasterizePoints", "RASTERIZE_POINTS")]
+[NodeInfo("RasterizePoints", "RASTERIZE_POINTS", Category = "IMAGE")]
 public class RasterizePointsNode : Node
 {
     private Paint _paint = new() { Color = Colors.White };

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RemoveClosePointsNode.cs

@@ -4,7 +4,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
 
-[NodeInfo("RemoveClosePoints", "REMOVE_CLOSE_POINTS")]
+[NodeInfo("RemoveClosePoints", "REMOVE_CLOSE_POINTS", Category = "OPERATIONS")]
 public class RemoveClosePointsNode : Node
 {
     public OutputProperty<PointList> Output { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("SampleImage", "SAMPLE_IMAGE")]
+[NodeInfo("SampleImage", "SAMPLE_IMAGE", Category = "IMAGE")]
 public class SampleImageNode : Node
 {
     public InputProperty<Texture?> Image { get; }

+ 4 - 1
src/PixiEditor/Models/Nodes/NodeTypeInfo.cs

@@ -12,6 +12,8 @@ public class NodeTypeInfo
     
     public string? PickerName { get; }
 
+    public string Category { get; }
+
     public LocalizedString FinalPickerName { get; }
 
     public bool Hidden => PickerName is { Length: 0 };
@@ -27,7 +29,8 @@ public class NodeTypeInfo
         UniqueName = attribute.UniqueName;
         DisplayName = attribute.DisplayName;
         PickerName = attribute.PickerName;
+        Category = attribute.Category ?? "";
 
-        FinalPickerName = PickerName ?? DisplayName;
+        FinalPickerName = (PickerName ?? DisplayName);
     }
 }

+ 32 - 12
src/PixiEditor/Styles/Templates/NodePicker.axaml

@@ -8,28 +8,48 @@
     <ControlTheme TargetType="nodes:NodePicker" x:Key="{x:Type nodes:NodePicker}">
         <Setter Property="Template">
             <ControlTemplate>
-                <Grid MinWidth="200">
+                <Grid MinWidth="200" MaxHeight="400">
                     <Grid.RowDefinitions>
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="*" />
                     </Grid.RowDefinitions>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="150" />
+                        <ColumnDefinition Width="300" />
+                    </Grid.ColumnDefinitions>
 
-                    <input:InputBox
+                    <input:InputBox Grid.ColumnSpan="2"
                         Text="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                         Name="PART_InputBox" />
-                    <ItemsControl MinHeight="200" Grid.Row="1" ItemsSource="{TemplateBinding FilteredNodeTypeInfos}">
-                        <ItemsControl.ItemTemplate>
-                            <DataTemplate DataType="nodeModels:NodeTypeInfo">
-                                <Button Command="{Binding SelectNodeCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodePicker}}"
+                    
+                    <ListBox Grid.Column="0" Grid.Row="1" ItemsSource="{TemplateBinding AllCategories}" 
+                             SelectedItem="{Binding SelectedCategory, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
+                        <ListBox.ItemTemplate>
+                            <DataTemplate>
+                                <TextBlock
+                                    VerticalAlignment="Center"
+                                    ui:Translator.Key="{Binding .}" />
+                            </DataTemplate>
+                        </ListBox.ItemTemplate>
+                    </ListBox>
+                    
+                    <ScrollViewer Grid.Row="1" Grid.Column="1" Name="PART_ScrollViewer"
+                                  Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
+                        <ItemsControl MinHeight="200" Name="PART_NodeList"
+                                      ItemsSource="{TemplateBinding FilteredNodeTypeInfos}">
+                            <ItemsControl.ItemTemplate>
+                                <DataTemplate DataType="nodeModels:NodeTypeInfo">
+                                    <Button
+                                        Command="{Binding SelectNodeCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodePicker}}"
                                         CommandParameter="{Binding}"
-                                        Margin="0,1"
                                         IsVisible="{Binding !Hidden}"
-                                        ui:Translator.LocalizedString="{Binding FinalPickerName}" />
-                            </DataTemplate>
-                        </ItemsControl.ItemTemplate>
-                    </ItemsControl>
+                                        ui:Translator.Key="{Binding FinalPickerName}" />
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                        </ItemsControl>
+                    </ScrollViewer>
                 </Grid>
             </ControlTemplate>
         </Setter>
     </ControlTheme>
-</ResourceDictionary>
+</ResourceDictionary>

+ 119 - 12
src/PixiEditor/Views/Nodes/NodePicker.cs

@@ -5,17 +5,15 @@ using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Helpers.Nodes;
 using PixiEditor.Models.Nodes;
-using PixiEditor.Numerics;
 using PixiEditor.Views.Input;
 
 namespace PixiEditor.Views.Nodes;
 
 [TemplatePart("PART_InputBox", typeof(InputBox))]
+[TemplatePart("PART_NodeList", typeof(ItemsControl))]
+[TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
 public partial class NodePicker : TemplatedControl
 {
     public static readonly StyledProperty<string> SearchQueryProperty = AvaloniaProperty.Register<NodePicker, string>(
@@ -29,11 +27,21 @@ public partial class NodePicker : TemplatedControl
 
     public static readonly StyledProperty<ObservableCollection<NodeTypeInfo>> AllNodeTypeInfosProperty =
         AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(
-            "AllNodeTypes");
+            nameof(AllNodeTypeInfos));
 
     public static readonly StyledProperty<ObservableCollection<NodeTypeInfo>> FilteredNodeTypeInfosProperty =
         AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(nameof(FilteredNodeTypeInfos));
 
+    public static readonly StyledProperty<string> SelectedCategoryProperty =
+        AvaloniaProperty.Register<NodePicker, string>(
+            nameof(SelectedCategory));
+
+    public string SelectedCategory
+    {
+        get => GetValue(SelectedCategoryProperty);
+        set => SetValue(SelectedCategoryProperty, value);
+    }
+
     public ObservableCollection<NodeTypeInfo> AllNodeTypeInfos
     {
         get => GetValue(AllNodeTypeInfosProperty);
@@ -46,8 +54,19 @@ public partial class NodePicker : TemplatedControl
         set => SetValue(FilteredNodeTypeInfosProperty, value);
     }
 
-    public static readonly StyledProperty<ICommand> SelectNodeCommandProperty = AvaloniaProperty.Register<NodePicker, ICommand>(
-        nameof(SelectNodeCommand));
+    public static readonly StyledProperty<ObservableCollection<string>> AllCategoriesProperty =
+        AvaloniaProperty.Register<NodePicker, ObservableCollection<string>>(
+            nameof(AllCategories));
+
+    public ObservableCollection<string> AllCategories
+    {
+        get => GetValue(AllCategoriesProperty);
+        set => SetValue(AllCategoriesProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand> SelectNodeCommandProperty =
+        AvaloniaProperty.Register<NodePicker, ICommand>(
+            nameof(SelectNodeCommand));
 
     public ICommand SelectNodeCommand
     {
@@ -55,17 +74,60 @@ public partial class NodePicker : TemplatedControl
         set => SetValue(SelectNodeCommandProperty, value);
     }
 
+    public Vector ScrollOffset
+    {
+        get { return (Vector)GetValue(ScrollOffsetProperty); }
+        set { SetValue(ScrollOffsetProperty, value); }
+    }
+
+    private Dictionary<string, int> _categoryIndexes = new();
+
+    public static readonly StyledProperty<Vector> ScrollOffsetProperty =
+        AvaloniaProperty.Register<NodePicker, Vector>(nameof(ScrollOffset));
+
+    private ItemsControl? _itemsControl;
+    private ScrollViewer? _scrollViewer;
+
+    private const string MiscCategory = "MISC";
+    
+    private bool SuppressCategoryChanged { get; set; }
+
     static NodePicker()
     {
         SearchQueryProperty.Changed.Subscribe(OnSearchQueryChanged);
         AllNodeTypeInfosProperty.Changed.Subscribe(OnAllNodeTypesChanged);
+        SelectedCategoryProperty.Changed.Subscribe(SelectedCategoryChanged);
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     {
         var inputBox = e.NameScope.Find<InputBox>("PART_InputBox");
-        
+
         inputBox.KeyDown += OnInputBoxKeyDown;
+
+        _itemsControl = e.NameScope.Find<ItemsControl>("PART_NodeList");
+        _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
+        _scrollViewer.ScrollChanged += Scrolled;
+    }
+
+    private void Scrolled(object? sender, ScrollChangedEventArgs e)
+    {
+        if (e.OffsetDelta.Y != 0)
+        {
+            double normalizedY = ScrollOffset.Y / _scrollViewer.ScrollBarMaximum.Y;
+
+            int index = (int)(normalizedY * _itemsControl.Items.Count);
+            index = Math.Clamp(index, 0, _itemsControl.Items.Count - 1);
+            string category = FilteredNodeTypeInfos[index].Category;
+            if (string.IsNullOrEmpty(category))
+            {
+                category = MiscCategory;
+            }
+
+            SuppressCategoryChanged = true;
+            SelectedCategory = category;
+            SuppressCategoryChanged = false;
+        }
     }
 
     private static void OnSearchQueryChanged(AvaloniaPropertyChangedEventArgs e)
@@ -82,8 +144,16 @@ public partial class NodePicker : TemplatedControl
         }
         else
         {
-            nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
-                .Where(x => SearchComparer(x, nodePicker.SearchQuery)));
+            if (string.IsNullOrEmpty(nodePicker.SearchQuery))
+            {
+                nodePicker.FilteredNodeTypeInfos =
+                    OrderByCategory(nodePicker);
+            }
+            else
+            {
+                nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
+                    .Where(x => SearchComparer(x, nodePicker.SearchQuery)));
+            }
         }
 
         return;
@@ -92,7 +162,15 @@ public partial class NodePicker : TemplatedControl
             x.FinalPickerName.Value.Replace(" ", "")
                 .Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
     }
-    
+
+    private static ObservableCollection<NodeTypeInfo> OrderByCategory(NodePicker nodePicker)
+    {
+        return new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
+            .Where(x => x.Category != null)
+            .OrderBy(
+                x => string.IsNullOrEmpty(x.Category) ? nodePicker._categoryIndexes[MiscCategory] : nodePicker._categoryIndexes[x.Category]));
+    }
+
     private void OnInputBoxKeyDown(object? sender, KeyEventArgs e)
     {
         if (e.Key != Key.Enter)
@@ -120,7 +198,36 @@ public partial class NodePicker : TemplatedControl
         if (e.Sender is NodePicker nodePicker)
         {
             nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos);
+            nodePicker.AllCategories = new ObservableCollection<string>(
+                nodePicker.AllNodeTypeInfos.Select(x => x.Category)
+                    .Where(x => !string.IsNullOrEmpty(x)).Distinct());
+
+            nodePicker.AllCategories.Add(MiscCategory);
+
+            nodePicker._categoryIndexes = nodePicker.AllCategories
+                .Select((x, i) => (x, i))
+                .ToDictionary(x => x.x, x => x.i);
+
+            nodePicker.FilteredNodeTypeInfos = OrderByCategory(nodePicker);
         }
     }
-}
 
+    private static void SelectedCategoryChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is NodePicker nodePicker)
+        {
+            if (nodePicker.SuppressCategoryChanged)
+            {
+                return;
+            }
+            
+            int indexOfFirstItemInCategory = nodePicker.FilteredNodeTypeInfos
+                .Select((x, i) => (x, i))
+                .FirstOrDefault(x => x.x.Category == nodePicker.SelectedCategory).i;
+
+            double normalizedY = indexOfFirstItemInCategory / (double)nodePicker.FilteredNodeTypeInfos.Count;
+            
+            nodePicker.ScrollOffset = new Vector(0, normalizedY * nodePicker._scrollViewer.ScrollBarMaximum.Y);
+        }
+    }
+}