Forráskód Böngészése

Selecting text works

Krzysztof Krysiński 7 hónapja
szülő
commit
0f5c46b614

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 43ef993d73cb6a46667823ca5c80d6acf2607750
+Subproject commit 604eb062d12fa394b9fefb211eb62e7a78923598

+ 8 - 0
src/PixiEditor/Helpers/ThemeResources.cs

@@ -24,4 +24,12 @@ public static class ThemeResources
     public static Color BorderMidColor =>
         ResourceLoader.GetResource<SolidColorBrush>("ThemeBorderMidBrush", Application.Current.ActualThemeVariant).Color
             .ToColor();
+
+    public static Color ThemeControlHighlightColor =>
+        ResourceLoader.GetResource<Avalonia.Media.Color>("ThemeControlHighlightColor", Application.Current.ActualThemeVariant)
+            .ToColor();
+
+    public static Color SelectionFillColor =>
+        ResourceLoader.GetResource<Avalonia.Media.Color>("SelectionFillColor", Application.Current.ActualThemeVariant)
+            .ToColor();
 }

+ 9 - 9
src/PixiEditor/Views/Overlays/TextOverlay/Blinker.cs → src/PixiEditor/Views/Overlays/TextOverlay/Caret.cs

@@ -6,16 +6,16 @@ using Drawie.Numerics;
 
 namespace PixiEditor.Views.Overlays.TextOverlay;
 
-internal class Blinker : IDisposable
+internal class Caret : IDisposable
 {
-    public int BlinkerPosition
+    public int CaretPosition
     {
-        get => blinkerPosition;
+        get => _caretPosition;
         set
         {
-            if (blinkerPosition != value)
+            if (_caretPosition != value)
             {
-                blinkerPosition = value;
+                _caretPosition = value;
                 visible = true;
                 lastUpdate = DateTime.Now;
             }
@@ -26,13 +26,13 @@ internal class Blinker : IDisposable
     public VecF[] GlyphPositions { get; set; }
     public VecD Offset { get; set; }
     public float[] GlyphWidths { get; set; }
-    public float BlinkerWidth { get; set; } = 1;
+    public float CaretWidth { get; set; } = 1;
 
     private Paint paint = new Paint() { Color = Colors.White, Style = PaintStyle.StrokeAndFill, StrokeWidth = 3 };
 
     private bool visible;
     private DateTime lastUpdate = DateTime.Now;
-    private int blinkerPosition;
+    private int _caretPosition;
 
     public void Render(Canvas canvas)
     {
@@ -41,7 +41,7 @@ internal class Blinker : IDisposable
             return;
         }
 
-        int clampedBlinkerPosition = Math.Clamp(BlinkerPosition, 0, GlyphPositions.Length - 1);
+        int clampedBlinkerPosition = Math.Clamp(CaretPosition, 0, GlyphPositions.Length - 1);
 
         var glyphPosition = GlyphPositions[clampedBlinkerPosition];
 
@@ -50,7 +50,7 @@ internal class Blinker : IDisposable
         var x = glyphPosition.X + Offset.X;
         var y = glyphPosition.Y + Offset.Y;
 
-        paint.StrokeWidth = BlinkerWidth;
+        paint.StrokeWidth = CaretWidth;
 
         VecD from = new VecD(x, y + glyphHeight / 4f);
         VecD to = new VecD(x, y - glyphHeight);

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

@@ -1,10 +1,14 @@
 using Avalonia;
 using Avalonia.Input;
 using Avalonia.Threading;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Text;
 using Drawie.Numerics;
 using PixiEditor.Extensions.UI.Overlays;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Input;
@@ -52,9 +56,9 @@ internal class TextOverlay : Overlay
     }
 
     public static readonly StyledProperty<int> SelectionLengthProperty = AvaloniaProperty.Register<TextOverlay, int>(
-        nameof(SelectionLength));
+        nameof(SelectionEnd));
 
-    public int SelectionLength
+    public int SelectionEnd
     {
         get => GetValue(SelectionLengthProperty);
         set => SetValue(SelectionLengthProperty, value);
@@ -99,10 +103,16 @@ internal class TextOverlay : Overlay
 
     private Dictionary<KeyCombination, Action> shortcuts;
 
-    private Blinker blinker = new Blinker();
+    private Caret caret = new Caret();
     private VecF[] glyphPositions;
     private float[] glyphWidths;
     private RichText richText;
+    private VecD movedDistance;
+    private VecD initialPos;
+    private bool isLmbPressed;
+
+    private Paint selectionPaint;
+    private Paint opacityPaint;
 
     private int lastXMovementCursorIndex;
 
@@ -133,6 +143,12 @@ internal class TextOverlay : Overlay
             { new KeyCombination(Key.Down, KeyModifiers.None), () => MoveCursorBy(new VecI(0, 1)) },
             { new KeyCombination(Key.Escape, KeyModifiers.None), () => IsEditing = false }
         };
+
+        selectionPaint = new Paint()
+        {
+            Color = ThemeResources.SelectionFillColor.WithAlpha(255), Style = PaintStyle.Fill
+        };
+        opacityPaint = new Paint() { Color = Colors.White.WithAlpha(ThemeResources.SelectionFillColor.A) };
     }
 
 
@@ -140,41 +156,119 @@ internal class TextOverlay : Overlay
     {
         if (!IsEditing) return;
 
-        blinker.BlinkerPosition = CursorPosition;
-        blinker.FontSize = Font.Size;
-        blinker.GlyphPositions = glyphPositions;
-        blinker.GlyphWidths = glyphWidths;
-        blinker.Offset = Position;
-
         int saved = context.Save();
 
         context.SetMatrix(context.TotalMatrix.Concat(Matrix));
 
-        blinker.BlinkerWidth = 3f / (float)ZoomScale;
-        blinker.Render(context);
+        RenderCaret(context);
+        RenderSelection(context);
 
         context.RestoreToCount(saved);
-
         Refresh();
     }
 
+    private void RenderCaret(Canvas context)
+    {
+        caret.CaretPosition = CursorPosition;
+        caret.FontSize = Font.Size;
+        caret.GlyphPositions = glyphPositions;
+        caret.GlyphWidths = glyphWidths;
+        caret.Offset = Position;
+
+        caret.CaretWidth = 3f / (float)ZoomScale;
+        caret.Render(context);
+    }
+
+    private void RenderSelection(Canvas context)
+    {
+        if (CursorPosition == SelectionEnd) return;
+
+        int begin = Math.Min(CursorPosition, SelectionEnd);
+        int end = Math.Max(CursorPosition, SelectionEnd);
+
+        richText.IndexOnLine(CursorPosition, out int lineStart);
+
+        RectD? currentLineBounds = null;
+        int lastLine = lineStart;
+        int saved = context.SaveLayer(opacityPaint);
+
+        for (int i = begin; i <= end; i++)
+        {
+            richText.IndexOnLine(i, out int line);
+
+            if (line != lastLine || i == end)
+            {
+                if (currentLineBounds != null)
+                {
+                    context.DrawRect(currentLineBounds.Value, selectionPaint);
+                }
+
+                currentLineBounds = null;
+            }
+
+            lastLine = line;
+
+            double x = glyphPositions[i].X;
+            double width = glyphWidths[i];
+            VecD lineOffset = richText.GetLineOffset(line, Font);
+            RectD selectionBounds =
+                new RectD(new VecD(x, -Font.Size + lineOffset.Y), new VecD(width, Font.Size * 1.25f)).Offset(Position);
+            if (currentLineBounds == null)
+            {
+                currentLineBounds = selectionBounds;
+            }
+            else
+            {
+                currentLineBounds = currentLineBounds.Value.Union(selectionBounds);
+            }
+        }
+
+        context.RestoreToCount(saved);
+    }
+
     public override bool TestHit(VecD point)
     {
         VecD mapped = Matrix.Invert().MapPoint(point);
-        return richText != null && richText.MeasureBounds(Font).Offset(Position).ContainsInclusive(mapped);
+        return richText != null && richText.MeasureBounds(Font).Offset(Position).Inflate(2).ContainsInclusive(mapped);
     }
 
     protected override void OnOverlayPointerPressed(OverlayPointerArgs args)
     {
-        if (args.PointerButton == MouseButton.Left)
+        movedDistance = VecD.Zero;
+        initialPos = args.Point;
+        isLmbPressed = args.PointerButton == MouseButton.Left;
+        args.Pointer.Capture(this);
+    }
+
+    protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
+    {
+        movedDistance = args.Point - initialPos;
+        if (isLmbPressed)
+        {
+            if (movedDistance.Length > 2)
+            {
+                SetCursorPosToPosition(args.Point);
+                SetSelectionEndToPosition(initialPos);
+            }
+        }
+    }
+
+    protected override void OnOverlayPointerReleased(OverlayPointerArgs args)
+    {
+        if (movedDistance.Length < 2)
         {
-            if (!IsEditing)
+            if (args.InitialPressMouseButton == MouseButton.Left)
             {
-                IsEditing = true;
+                if (!IsEditing)
+                {
+                    IsEditing = true;
+                }
+
+                SetCursorPosToPosition(args.Point);
             }
-            
-            SetCursorPosToPosition(args.Point);
         }
+
+        isLmbPressed = false;
     }
 
     protected override void OnOverlayPointerEntered(OverlayPointerArgs args)
@@ -188,14 +282,27 @@ internal class TextOverlay : Overlay
     }
 
     private void SetCursorPosToPosition(VecD point)
+    {
+        var indexOfClosest = GetClosestCharacterIndex(point);
+
+        CursorPosition = indexOfClosest;
+        SelectionEnd = indexOfClosest;
+    }
+
+    private void SetSelectionEndToPosition(VecD point)
+    {
+        var indexOfClosest = GetClosestCharacterIndex(point);
+        SelectionEnd = indexOfClosest;
+    }
+
+    private int GetClosestCharacterIndex(VecD point)
     {
         VecD mapped = Matrix.Invert().MapPoint(point);
         var positions = richText.GetGlyphPositions(Font);
         int indexOfClosest = positions.Select((pos, index) => (pos, index))
             .OrderBy(pos => ((pos.pos + Position - new VecD(0, Font.Size / 2f)) - mapped).LengthSquared)
             .First().index;
-        
-        CursorPosition = indexOfClosest;
+        return indexOfClosest;
     }
 
     protected override void OnKeyPressed(Key key, KeyModifiers keyModifiers, string? keySymbol)
@@ -233,8 +340,21 @@ internal class TextOverlay : Overlay
 
     private void InsertTextAtCursor(string toAdd)
     {
-        Text = Text.Insert(CursorPosition, toAdd);
-        CursorPosition += toAdd.Length;
+        if (CursorPosition == SelectionEnd)
+        {
+            Text = Text.Insert(CursorPosition, toAdd);
+            CursorPosition += toAdd.Length;
+            SelectionEnd += toAdd.Length;
+        }
+        else
+        {
+            string newText = Text.Remove(Math.Min(CursorPosition, SelectionEnd),
+                Math.Abs(CursorPosition - SelectionEnd));
+            Text = newText.Insert(Math.Min(CursorPosition, SelectionEnd), toAdd);
+            CursorPosition = Math.Min(CursorPosition, SelectionEnd) + toAdd.Length;
+            SelectionEnd = CursorPosition;
+        }
+
         lastXMovementCursorIndex = CursorPosition;
     }
 
@@ -265,8 +385,20 @@ internal class TextOverlay : Overlay
     {
         if (Text.Length > 0 && CursorPosition + direction >= 0 && CursorPosition + direction < Text.Length)
         {
-            Text = Text.Remove(CursorPosition + direction, 1);
-            CursorPosition += direction;
+            if (SelectionEnd == CursorPosition)
+            {
+                Text = Text.Remove(CursorPosition + direction, 1);
+                CursorPosition += direction;
+                SelectionEnd = CursorPosition;
+            }
+            else
+            {
+                Text = Text.Remove(Math.Min(CursorPosition, SelectionEnd),
+                    Math.Abs(CursorPosition - SelectionEnd));
+                CursorPosition = Math.Min(CursorPosition, SelectionEnd);
+                SelectionEnd = CursorPosition;
+            }
+
             lastXMovementCursorIndex = CursorPosition;
         }
     }
@@ -292,6 +424,7 @@ internal class TextOverlay : Overlay
         }
 
         CursorPosition += moveBy;
+        SelectionEnd = CursorPosition;
     }
 
     private void RequestEditTextTriggered(object? sender, string e)

+ 1 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -492,7 +492,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
             InitialPressMouseButton = e is PointerReleasedEventArgs released
                 ? released.InitialPressMouseButton
                 : MouseButton.None,
-            ClickCount = e is PointerPressedEventArgs pressed ? pressed.ClickCount : 0
+            ClickCount = e is PointerPressedEventArgs pressed ? pressed.ClickCount : 0,
             
         };
     }