flabbet 7 hónapja
szülő
commit
d28254ad13

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 648ef9146c16bd272eb116729eff4d07b48b4f88
+Subproject commit 72558508e77873d12fa4c89289cac586564b0276

+ 4 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs

@@ -22,6 +22,7 @@ public class TextVectorData : ShapeVectorData, IDisposable
     }
 
     public VecD Position { get; set; }
+    public double MaxWidth { get; set; } = double.MaxValue;
     public Font Font { get; set; } = Font.CreateDefault();
 
     public override RectD GeometryAABB
@@ -38,7 +39,7 @@ public class TextVectorData : ShapeVectorData, IDisposable
 
     public override RectD VisualAABB => GeometryAABB;
     public VectorPath? Path { get; set; }
-    
+
     private RichText richText;
 
     public override VectorPath ToPath()
@@ -69,7 +70,7 @@ public class TextVectorData : ShapeVectorData, IDisposable
         }
 
         using Paint paint = new Paint() { IsAntiAliased = true };
-        
+
         richText.Fill = Fill;
         richText.FillColor = FillColor;
         richText.StrokeColor = StrokeColor;
@@ -85,7 +86,7 @@ public class TextVectorData : ShapeVectorData, IDisposable
 
     private void PaintText(Canvas canvas, Paint paint)
     {
-        richText.Paint(canvas, Position, Font, paint, Path);   
+        richText.Paint(canvas, Position, Font, paint, Path);
     }
 
     public override bool IsValid()

+ 8 - 14
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -48,14 +48,14 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
         var shape = layerHandler.GetShapeData(document.AnimationHandler.ActiveFrameBindable);
         if (shape is TextVectorData textData)
         {
-            document.TextOverlayHandler.Show(textData.Text, textData.Position, textData.Font.Size);
+            document.TextOverlayHandler.Show(textData.Text, textData.Position, textData.Font);
             lastText = textData.Text;
             position = textData.Position;
             lastMatrix = textData.TransformationMatrix;
         }
         else if (shape is null)
         {
-            document.TextOverlayHandler.Show("", controller.LastPrecisePosition, toolbar.FontSize);
+            document.TextOverlayHandler.Show("", controller.LastPrecisePosition, toolbar.ConstructFont());
             lastText = "";
             position = controller.LastPrecisePosition;
         }
@@ -81,6 +81,11 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
 
     public override void OnSettingsChanged(string name, object value)
     {
+        if (name is nameof(ITextToolbar.FontSize) or nameof(ITextToolbar.FontFamily))
+        {
+            document.TextOverlayHandler.Font = toolbar.ConstructFont();
+        }
+        
         internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(selectedMember.Id,
             ConstructTextData(lastText)));
     }
@@ -98,18 +103,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
 
     private TextVectorData ConstructTextData(string text)
     {
-        Font font = null;
-        if (toolbar.FontFamily != null)
-        {
-            font = Font.FromFontFamily(toolbar.FontFamily);
-        }
-
-        if (font is null)
-        {
-            font = Font.CreateDefault();
-        }
-
-        font.Size = (float)toolbar.FontSize;
+        Font font = toolbar.ConstructFont(); 
         return new TextVectorData()
         {
             Text = text,

+ 5 - 2
src/PixiEditor/Models/Handlers/ITextOverlayHandler.cs

@@ -1,9 +1,12 @@
-using Drawie.Numerics;
+using Drawie.Backend.Core.Text;
+using Drawie.Numerics;
 
 namespace PixiEditor.Models.Handlers;
 
 public interface ITextOverlayHandler : IHandler
 {
-    public void Show(string text, VecD position, double fontSize);
+    public void Show(string text, VecD position, Font font);
     public void Hide();
+    public Font Font { get; set; }
+    public VecD Position { get; set; }
 }

+ 2 - 0
src/PixiEditor/Models/Handlers/Toolbars/ITextToolbar.cs

@@ -6,4 +6,6 @@ internal interface ITextToolbar : IFillableShapeToolbar
 {
     public double FontSize { get; set; }
     public FontFamilyName? FontFamily { get; set; }
+    
+    public Font ConstructFont();
 }

+ 26 - 10
src/PixiEditor/ViewModels/Document/TransformOverlays/TextOverlayViewModel.cs

@@ -1,16 +1,19 @@
 using CommunityToolkit.Mvvm.ComponentModel;
+using Drawie.Backend.Core.Text;
 using Drawie.Numerics;
+using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.ViewModels.Document.TransformOverlays;
 
-public class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
+internal class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
 {
     private bool isActive;
     private string text;
     private VecD position;
-    private double fontSize;
-    
+    private Font font;
+    private ExecutionTrigger<string> requestEditTextTrigger;
+
     public event Action<string>? TextChanged;
 
     public bool IsActive
@@ -38,20 +41,33 @@ public class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
         set => SetProperty(ref position, value);
     }
 
-    public double FontSize
+    public Font Font
+    {
+        get => font;
+        set => SetProperty(ref font, value);
+    }
+
+    public ExecutionTrigger<string> RequestEditTextTrigger
     {
-        get => fontSize;
-        set => SetProperty(ref fontSize, value);
+        get => requestEditTextTrigger;
+        set => SetProperty(ref requestEditTextTrigger, value);
     }
 
-    public void Show(string text, VecD position, double fontSize)
+    public TextOverlayViewModel()
+    {
+        RequestEditTextTrigger = new ExecutionTrigger<string>();
+    }
+
+
+    public void Show(string text, VecD position, Font font)
     {
         IsActive = true;
-        Text = text;
+        Font = font;
         Position = position;
-        FontSize = fontSize;
+        Text = text;
+        RequestEditTextTrigger.Execute(this, text);
     }
-    
+
     public void Hide()
     {
         IsActive = false;

+ 20 - 2
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/TextToolbar.cs

@@ -17,7 +17,7 @@ internal class TextToolbar : FillableShapeToolbar, ITextToolbar
             GetSetting<FontFamilySettingViewModel>(nameof(FontFamily)).Value = value;
         }
     }
-    
+
     public double FontSize
     {
         get
@@ -29,11 +29,29 @@ internal class TextToolbar : FillableShapeToolbar, ITextToolbar
             GetSetting<SizeSettingViewModel>(nameof(FontSize)).Value = value;
         }
     }
-    
+
     public TextToolbar()
     {
         AddSetting(new FontFamilySettingViewModel(nameof(FontFamily), "FONT_LABEL"));
         var sizeSetting = new SizeSettingViewModel(nameof(FontSize), "FONT_SIZE_LABEL") { Value = 12 };
         AddSetting(sizeSetting);
     }
+
+    public Font ConstructFont()
+    {
+        Font font = null;
+        if (FontFamily != null)
+        {
+            font = Font.FromFontFamily(FontFamily);
+        }
+
+        if (font is null)
+        {
+            font = Font.CreateDefault();
+        }
+
+        font.Size = (float)FontSize;
+        
+        return font;
+    }
 }

+ 9 - 3
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -468,15 +468,21 @@ internal class ViewportOverlays
             Source = Viewport, Path = "Document.TextOverlayViewModel.Position", Mode = BindingMode.OneWay
         };
 
-        Binding fontSizeBinding = new()
+        Binding fontBinding = new()
         {
-            Source = Viewport, Path = "Document.TextOverlayViewModel.FontSize", Mode = BindingMode.OneWay
+            Source = Viewport, Path = "Document.TextOverlayViewModel.Font", Mode = BindingMode.OneWay
+        };
+        
+        Binding requestEditTextBinding = new()
+        {
+            Source = Viewport, Path = "Document.TextOverlayViewModel.RequestEditTextTrigger", Mode = BindingMode.OneWay
         };
 
         textOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         textOverlay.Bind(TextOverlay.TextProperty, textBinding);
         textOverlay.Bind(TextOverlay.PositionProperty, positionBinding);
-        textOverlay.Bind(TextOverlay.FontSizeProperty, fontSizeBinding);
+        textOverlay.Bind(TextOverlay.FontProperty, fontBinding);
+        textOverlay.Bind(TextOverlay.RequestEditTextProperty, requestEditTextBinding);
     }
 }
 

+ 53 - 0
src/PixiEditor/Views/Overlays/TextOverlay/Blinker.cs

@@ -0,0 +1,53 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Overlays.TextOverlay;
+
+internal class Blinker : IDisposable
+{
+    public int BlinkerPosition { get; set; }
+    public double FontSize { get; set; }
+    public VecF[] GlyphPositions { get; set; }
+    public VecD Offset { get; set; }
+    public float[] GlyphWidths { get; set; }
+
+    private Paint paint = new Paint() { Color = Colors.Black, Style = PaintStyle.StrokeAndFill, StrokeWidth = 1};
+    
+    public void Render(Canvas canvas)
+    {
+        if (GlyphPositions.Length == 0)
+        {
+            return;
+        }
+        
+        int clampedBlinkerPosition = Math.Clamp(BlinkerPosition, 0, GlyphPositions.Length);
+
+        VecF glyphPosition;
+        if (clampedBlinkerPosition == GlyphPositions.Length)
+        {
+            glyphPosition = GlyphPositions[^1];   
+            glyphPosition += new VecF(GlyphWidths[^1], 0);
+        }
+        else
+        {
+            glyphPosition = GlyphPositions[clampedBlinkerPosition];
+        }
+
+        var glyphHeight = FontSize;
+
+        var x = glyphPosition.X + Offset.X;
+        var y = glyphPosition.Y + Offset.Y;
+
+        VecD from = new VecD(x, y);
+        VecD to = new VecD(x, y - glyphHeight);
+        canvas.DrawLine(from, to, paint);
+    }
+
+    public void Dispose()
+    {
+        paint.Dispose();
+    }
+}

+ 150 - 24
src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs

@@ -1,7 +1,9 @@
 using Avalonia;
 using Avalonia.Input;
 using Avalonia.Threading;
+using Drawie.Backend.Core.Text;
 using Drawie.Numerics;
+using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Input;
 using PixiEditor.OperatingSystem;
@@ -9,18 +11,8 @@ using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 
 namespace PixiEditor.Views.Overlays.TextOverlay;
 
-public class TextOverlay : Overlay
+internal class TextOverlay : Overlay
 {
-    private Dictionary<KeyCombination, Action> shortcuts;
-
-    public TextOverlay()
-    {
-        shortcuts = new Dictionary<KeyCombination, Action>
-        {
-            { new KeyCombination(Key.V, KeyModifiers.Control), PasteText },
-        };
-    }
-
     public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<TextOverlay, string>(
         nameof(Text));
 
@@ -39,46 +31,115 @@ public class TextOverlay : Overlay
         set => SetValue(PositionProperty, value);
     }
 
-    public static readonly StyledProperty<double> FontSizeProperty = AvaloniaProperty.Register<TextOverlay, double>(
-        nameof(FontSize));
+    public static readonly StyledProperty<Font> FontProperty = AvaloniaProperty.Register<TextOverlay, Font>(
+        nameof(Font));
 
-    public double FontSize
+    public Font Font
     {
-        get => GetValue(FontSizeProperty);
-        set => SetValue(FontSizeProperty, value);
+        get => GetValue(FontProperty);
+        set => SetValue(FontProperty, value);
     }
 
+    public static readonly StyledProperty<int> CursorPositionProperty = AvaloniaProperty.Register<TextOverlay, int>(
+        nameof(CursorPosition));
+
+    public int CursorPosition
+    {
+        get => GetValue(CursorPositionProperty);
+        set => SetValue(CursorPositionProperty, value);
+    }
+
+    public static readonly StyledProperty<int> SelectionLengthProperty = AvaloniaProperty.Register<TextOverlay, int>(
+        nameof(SelectionLength));
+
+    public int SelectionLength
+    {
+        get => GetValue(SelectionLengthProperty);
+        set => SetValue(SelectionLengthProperty, value);
+    }
+
+    public static readonly StyledProperty<ExecutionTrigger<string>> RequestEditTextProperty =
+        AvaloniaProperty.Register<TextOverlay, ExecutionTrigger<string>>(
+            nameof(RequestEditText));
+
+    public ExecutionTrigger<string> RequestEditText
+    {
+        get => GetValue(RequestEditTextProperty);
+        set => SetValue(RequestEditTextProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> IsEditingProperty = AvaloniaProperty.Register<TextOverlay, bool>(
+        nameof(IsEditing));
+
+    public bool IsEditing
+    {
+        get => GetValue(IsEditingProperty);
+        set => SetValue(IsEditingProperty, value);
+    }
+
+    private Dictionary<KeyCombination, Action> shortcuts;
+
+    private Blinker blinker = new Blinker();
+    private VecF[] glyphPositions;
+    private float[] glyphWidths;
+
     static TextOverlay()
     {
         IsVisibleProperty.Changed.Subscribe(IsVisibleChanged);
+        RequestEditTextProperty.Changed.Subscribe(RequestEditTextChanged);
+        IsEditingProperty.Changed.Subscribe(IsEditingChanged);
+        TextProperty.Changed.Subscribe(TextChanged);
+        FontProperty.Changed.Subscribe(FontChanged);
+    }
+
+    public TextOverlay()
+    {
+        shortcuts = new Dictionary<KeyCombination, Action>
+        {
+            { new KeyCombination(Key.V, KeyModifiers.Control), PasteText },
+            { new KeyCombination(Key.Delete, KeyModifiers.None), () => DeleteChar(0) },
+            { new KeyCombination(Key.Back, KeyModifiers.None), () => DeleteChar(-1) },
+            { new KeyCombination(Key.Left, KeyModifiers.None), () => MoveCursorBy(-1) },
+            { new KeyCombination(Key.Right, KeyModifiers.None), () => MoveCursorBy(1) }
+        };
     }
 
+
     public override void RenderOverlay(Canvas context, RectD canvasBounds)
     {
+        if (!IsEditing) return;
+        
+        blinker.BlinkerPosition = CursorPosition;
+        blinker.FontSize = Font.Size;
+        blinker.GlyphPositions = glyphPositions;
+        blinker.GlyphWidths = glyphWidths;
+        blinker.Offset = Position;
+
+        blinker.Render(context);
     }
 
     protected override void OnKeyPressed(Key key, KeyModifiers keyModifiers)
     {
+        if (!IsEditing) return;
         if (IsShortcut(key, keyModifiers))
         {
             ExecuteShortcut(key, keyModifiers);
             return;
         }
 
-        if (key == Key.Back)
-        {
-            if (Text.Length > 0)
-            {
-                Text = Text[..^1];
-            }
-        }
-        else if (key == Key.Enter)
+        InsertChar(key, keyModifiers);
+    }
+
+    private void InsertChar(Key key, KeyModifiers keyModifiers)
+    {
+        if (key == Key.Enter)
         {
             Text += Environment.NewLine;
         }
         else if (key == Key.Space)
         {
             Text += " ";
+            CursorPosition++;
         }
         else
         {
@@ -91,6 +152,7 @@ public class TextOverlay : Overlay
             {
                 if (char.IsControl(keyChar.Value)) return;
                 Text += keyChar;
+                CursorPosition++;
             }
         }
     }
@@ -117,7 +179,51 @@ public class TextOverlay : Overlay
         }, TaskContinuationOptions.OnlyOnRanToCompletion);
     }
 
+    private void DeleteChar(int direction)
+    {
+        if (Text.Length > 0)
+        {
+            Text = Text.Remove(CursorPosition + direction, 1);
+            CursorPosition = Math.Clamp(CursorPosition + direction, 0, Text.Length);
+        }
+    }
+
+    private void MoveCursorBy(int direction)
+    {
+        CursorPosition = Math.Clamp(CursorPosition + direction, 0, Text.Length);
+    }
+
+    private void RequestEditTextTriggered(object? sender, string e)
+    {
+        IsEditing = true;
+    }
+
     private static void IsVisibleChanged(AvaloniaPropertyChangedEventArgs<bool> args)
+    {
+        TextOverlay sender = args.Sender as TextOverlay;
+        if (sender == null) return;
+
+        if (!args.NewValue.Value)
+        {
+            sender.IsEditing = false;
+        }
+    }
+
+    private static void RequestEditTextChanged(AvaloniaPropertyChangedEventArgs<ExecutionTrigger<string>> args)
+    {
+        var sender = args.Sender as TextOverlay;
+        if (args.OldValue.Value != null)
+        {
+            args.OldValue.Value.Triggered -= sender.RequestEditTextTriggered;
+        }
+
+        if (args.NewValue.Value != null)
+        {
+            args.NewValue.Value.Triggered += sender.RequestEditTextTriggered;
+        }
+    }
+
+    private static void IsEditingChanged(AvaloniaPropertyChangedEventArgs<bool> args)
     {
         if (args.NewValue.Value)
         {
@@ -128,4 +234,24 @@ public class TextOverlay : Overlay
             ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
         }
     }
+
+    private static void TextChanged(AvaloniaPropertyChangedEventArgs<string> args)
+    {
+        TextOverlay sender = args.Sender as TextOverlay;
+        sender.UpdateGlyphs();
+    }
+
+    private static void FontChanged(AvaloniaPropertyChangedEventArgs<Font> args)
+    {
+        TextOverlay sender = args.Sender as TextOverlay;
+        sender.UpdateGlyphs();
+    }
+
+    private void UpdateGlyphs()
+    {
+        if (Font == null) return;
+
+        glyphPositions = Font.GetGlyphPositions(Text);
+        glyphWidths = Font.GetGlyphWidths(Text);
+    }
 }