]> O.S.I.I.S - jp/crow.git/commitdiff
move textBuffer to drawing2d and several other text classes and measure
authorJean-Philippe Bruyère <jp_bruyere@hotmail.com>
Tue, 4 Mar 2025 12:52:08 +0000 (13:52 +0100)
committerJean-Philippe Bruyère <jp_bruyere@hotmail.com>
Tue, 4 Mar 2025 12:52:08 +0000 (13:52 +0100)
18 files changed:
Crow/src/2d/Measure.cs [deleted file]
Crow/src/ExtensionsMethods.cs
Crow/src/Text/CharLocation.cs [deleted file]
Crow/src/Text/Extensions.cs [deleted file]
Crow/src/Text/LineSpan.cs [deleted file]
Crow/src/Text/TextChange.cs [deleted file]
Crow/src/Text/TextLine.cs [deleted file]
Crow/src/Text/TextLineCollection.cs [deleted file]
Crow/src/Text/TextSpan.cs [deleted file]
Drawing2D/src/CharLocation.cs [new file with mode: 0644]
Drawing2D/src/ExtensionsMethods.cs [new file with mode: 0644]
Drawing2D/src/LineSpan.cs [new file with mode: 0644]
Drawing2D/src/Measure.cs [new file with mode: 0644]
Drawing2D/src/TextBuffer.cs [new file with mode: 0644]
Drawing2D/src/TextChange.cs [new file with mode: 0644]
Drawing2D/src/TextLine.cs [new file with mode: 0644]
Drawing2D/src/TextLineCollection.cs [new file with mode: 0644]
Drawing2D/src/TextSpan.cs [new file with mode: 0644]

diff --git a/Crow/src/2d/Measure.cs b/Crow/src/2d/Measure.cs
deleted file mode 100644 (file)
index 4b66a97..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-
-using System;
-
-namespace Crow
-{
-       /// <summary>
-       /// Measurement unit
-       /// </summary>
-       public enum Unit {Undefined, Pixel, Percent, Inherit }
-       /// <summary>
-       /// Measure class allow proportional sizes as well as stretched and fit on content.
-       /// </summary>
-       public struct Measure : IEquatable<Measure>
-       {
-               /// <summary>
-               /// Integer value of the measure
-               /// </summary>
-               public int Value;
-               /// <summary>
-               /// Measurement unit
-               /// </summary>
-               public Unit Units;
-
-               /// <summary>
-               /// Fit on content, this special measure is defined as a fixed integer set to -1 pixel
-               /// </summary>
-               public static Measure Fit = new Measure(-1,Unit.Percent);
-               /// <summary>
-               /// Stretched into parent client area. This special measure is defined as a proportional cote
-               /// set to 100 Percents
-               /// </summary>
-               public static Measure Stretched = new Measure(100, Unit.Percent);
-               public static Measure Inherit = new Measure (0, Unit.Inherit);
-               #region CTOR
-               public Measure (int _value, Unit _units = Unit.Pixel)
-               {
-                       Value = _value;
-                       Units = _units;
-               }
-               #endregion
-
-               /// <summary>
-               /// True is size is fixed in pixels, this means not proportional, stretched nor fit.
-               /// </summary>
-               public bool IsFixed { get { return Units == Unit.Pixel; }}
-               public bool IsFit { get { return Value == -1 && Units == Unit.Percent; }}
-               /// <summary>
-               /// True if width is proportional to parent client rectangle
-               /// </summary>
-               public bool IsRelativeToParent { get { return Value >= 0 && Units == Unit.Percent; }}
-               #region Operators
-               public static implicit operator int(Measure m) => m.Value;
-               public static implicit operator Measure(int i) => new Measure(i);
-               public static implicit operator string(Measure m) => m.ToString();
-               public static implicit operator Measure(string s) => Measure.Parse(s);
-
-               public static bool operator ==(Measure m1, Measure m2) => m1.Equals (m2);
-               public static bool operator !=(Measure m1, Measure m2) => !m1.Equals (m2);
-               #endregion
-
-               public bool Equals(Measure other) => Value == other.Value && Units == other.Units;
-
-               #region Object overrides
-               public override int GetHashCode () => HashCode.Combine (Value, Units);
-               public override bool Equals (object obj) => obj is Measure m ? Equals (m) : false;
-               public override string ToString ()
-               {
-                       return Units == Unit.Inherit ? "Inherit" :
-                               Value == -1 ? "Fit" :
-                               Units == Unit.Percent ? Value == 100 ? "Stretched" :
-                               Value.ToString () + "%" : Value.ToString ();
-               }
-               #endregion
-
-               public static Measure Parse(string s){
-                       if (string.IsNullOrEmpty (s))
-                               return default(Measure);
-
-                       string st = s.Trim ();
-                       int tmp = 0;
-
-                       if (string.Equals ("Inherit", st, StringComparison.Ordinal))
-                               return Measure.Inherit;
-                       else if (string.Equals ("Fit", st, StringComparison.Ordinal))
-                               return Measure.Fit;
-                       else if (string.Equals ("Stretched", s, StringComparison.Ordinal))
-                               return Measure.Stretched;
-                       else {
-                               if (st.EndsWith ("%", StringComparison.Ordinal)) {
-                                       if (int.TryParse (s.Substring(0, st.Length - 1), out tmp))
-                                               return new Measure (tmp, Unit.Percent);
-                               }else if (int.TryParse (s, out tmp))
-                                       return new Measure (tmp);
-                       }
-
-                       throw new Exception ("Error parsing Measure.");
-               }
-
-       }
-}
index ed226b8a972baba1ba3ce404d187268a846057f5..302caec9a4eb796e27ef8abc860ec4fe4fbaaa16 100644 (file)
@@ -179,32 +179,14 @@ namespace Crow
                        }
                        return Alignment.Center;
                }
-               public static void Raise(this EventHandler handler, object sender, EventArgs e)
-               {
-                       handler?.Invoke (sender, e);
-               }
-               public static void Raise<T>(this EventHandler<T> handler, object sender, T e)
-               {
-                       handler?.Invoke (sender, e);
-               }
-               public static byte[] GetBytes(this string str)
-               {
-                       byte[] bytes = new byte[str.Length * sizeof(char)];
-                       System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
-                       return bytes;
-               }
-               public static bool IsWhiteSpaceOrNewLine (this char c)
-               {
-                       return c == '\t' || c.IsAnyLineBreakCharacter() || char.IsWhiteSpace (c);
-               }
-               public static object GetDefaultValue(this object obj)
+               /*public static object GetDefaultValue(this object obj)
                {
                        Type t = obj.GetType ();
                        if (t.IsValueType)
                                return Activator.CreateInstance (t);
 
                        return null;
-               }
+               }*/
 
                public static FileSystemInfo [] GetFileSystemInfosOrdered (this DirectoryInfo di) {
                        try {
@@ -213,17 +195,6 @@ namespace Crow
                                return null;
                        }
                }
-
-               internal static bool IsAnyLineBreakCharacter (this char c)
-                       => c == '\n' || c == '\r' || c == '\u0085' || c == '\u2028' || c == '\u2029';
-
-               public static bool TryGetResource (this Assembly a, string resId, out Stream stream) {
-                       stream = null;
-                       if (a == null)
-                               return false;
-                       stream = a.GetManifestResourceStream (resId);
-                       return stream != null;
-               }
        }
 }
 
diff --git a/Crow/src/Text/CharLocation.cs b/Crow/src/Text/CharLocation.cs
deleted file mode 100644 (file)
index 345f8fa..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-using System.Diagnostics;
-
-namespace Crow.Text
-{
-       [DebuggerDisplay ("{Line}, {Column}, {VisualCharXPosition}")]
-       public struct CharLocation : IEquatable<CharLocation>
-       {
-               public readonly int Line;
-               /// <summary>
-               /// Character position in current line.
-               /// If Column value is '-1', the visualX must contains the on screen position, and column will be computed from it.
-               /// </summary>
-               public int Column;
-               /// <summary>
-               /// The column as presented to the user, counting spaces in tabulations.
-               /// Its value is set during computations.
-               /// </summary>
-               public int TabulatedColumn;
-               public double VisualCharXPosition;
-               public CharLocation (int line, int column, double visualX = -1) {
-                       Line = line;
-                       Column = column;
-                       TabulatedColumn = -1;
-                       VisualCharXPosition = visualX;
-               }
-               public bool HasVisualX => Column >= 0 && VisualCharXPosition >= 0;
-               public void ResetVisualX () => VisualCharXPosition = -1;
-               public static bool operator == (CharLocation a, CharLocation b)
-                       => a.Equals (b);
-               public static bool operator != (CharLocation a, CharLocation b)
-                       => !a.Equals (b);
-               public bool Equals (CharLocation other) {
-                       return Column < 0 ?
-                               Line == other.Line && VisualCharXPosition == other.VisualCharXPosition :
-                               Line == other.Line && Column == other.Column;
-               }
-               public override bool Equals (object obj) => obj is CharLocation loc ? Equals (loc) : false;
-               public override int GetHashCode () {
-                       return Column < 0 ?
-                               HashCode.Combine (Line, VisualCharXPosition) :
-                               HashCode.Combine (Line, Column);
-               }
-
-               public override string ToString () => $"{Line}, {Column}";
-       }
-}
diff --git a/Crow/src/Text/Extensions.cs b/Crow/src/Text/Extensions.cs
deleted file mode 100644 (file)
index 77360dd..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-
-namespace Crow.Text
-{
-    public static class Extensions
-    {
-               public static ReadOnlySpan<char> GetLine (this ReadOnlySpan<char> str, TextLine ls) {
-                       if (ls.Start >= str.Length)
-                               return "".AsSpan ();
-                       return str.Slice (ls.Start, ls.Length);
-               }
-               public static ReadOnlySpan<char> GetLine (this ReadOnlySpan<char> str, TextLine ls, int offset) {
-                       int start = ls.Start + offset;
-                       if (start >= str.Length)
-                               return "".AsSpan ();
-                       return str.Slice (start, ls.Length);
-               }
-               public static ReadOnlySpan<char> GetLineIncludingLineBreak (this string str, TextLine ls) {
-                       if (ls.Start >= str.Length)
-                               return "".AsSpan ();
-                       return str.AsSpan ().Slice (ls.Start, ls.LengthIncludingLineBreak);
-               }
-               public static ReadOnlySpan<char> GetLineBreak (this ReadOnlySpan<char> str, TextLine ls) {
-                       if (ls.LineBreakLength == 0)
-                               return "".AsSpan ();
-                       return str.Slice (ls.End, ls.LineBreakLength);
-               }
-               public static ReadOnlySpan<char> GetLineIncludingLineBreak (this string str, TextLine ls, int offset) {
-                       int start = ls.Start + offset;
-                       if (start >= str.Length)
-                               return "".AsSpan ();
-                       return str.AsSpan ().Slice (start, ls.LengthIncludingLineBreak);
-               }
-
-               public static ReadOnlySpan<char> ToCharSpan (this LineBreakKind lineBreak) {
-                       switch (lineBreak) {
-                       case LineBreakKind.Unix:
-                               return "\n".AsSpan ();
-                       case LineBreakKind.Windows:
-                               return "\r\n".AsSpan ();
-                       case LineBreakKind.Other:
-                               return "\r".AsSpan ();
-                       default:
-                               return "\r\n".AsSpan ();
-                       }
-               }
-
-       }
-}
diff --git a/Crow/src/Text/LineSpan.cs b/Crow/src/Text/LineSpan.cs
deleted file mode 100644 (file)
index 1dc90de..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-
-namespace Crow.Text
-{
-       public struct LineSpan : IEquatable<LineSpan>
-       {
-               public readonly CharLocation Start;
-               public readonly CharLocation End;
-               public LineSpan (CharLocation start, CharLocation end) {
-                       Start = start;
-                       End = end;
-               }
-               public bool IsEmpty => Start == End;
-
-               public bool Equals(LineSpan other)
-                       => Start == other.Start && End == other.End;
-               public override bool Equals(object obj)
-                       => obj is LineSpan ts ? Equals(ts) : false;
-
-               public override int GetHashCode()
-                       => HashCode.Combine(Start, End);
-               public static bool operator ==(LineSpan left, LineSpan right)
-                       => left.Equals (right);
-               public static bool operator !=(LineSpan left, LineSpan right)
-                       => !left.Equals (right);
-               public override string ToString() => $"{Start},{End}";
-       }
-}
diff --git a/Crow/src/Text/TextChange.cs b/Crow/src/Text/TextChange.cs
deleted file mode 100644 (file)
index 6533a9b..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Crow.Text
-{
-    public struct TextChange
-    {
-        public readonly int Start;
-        public readonly int Length;
-        public string ChangedText;
-
-        public int End => Start + Length;
-        public int End2 => End + CharDiff;
-
-               public int CharDiff => string.IsNullOrEmpty (ChangedText) ? - Length : ChangedText.Length - Length;
-        public bool IsEmpty => string.IsNullOrEmpty (ChangedText) && Length == 0;
-        public bool HasNewText => !string.IsNullOrEmpty (ChangedText);
-        
-        public bool HasNoEffect(ReadOnlySpan<char> src)
-            => src.Slice(Start, Length).Equals(ChangedText, StringComparison.Ordinal);
-
-        public TextChange (int position, int length, string changedText = null) {
-            Start = position;
-            Length = length;
-            ChangedText = changedText;
-        }
-        public TextChange (int position, int length, ReadOnlySpan<char> changedText) {
-            Start = position;
-            Length = length;
-            ChangedText = changedText.ToString();
-        }
-        public TextChange Inverse (ReadOnlySpan<char> src)
-            => new TextChange (Start, string.IsNullOrEmpty (ChangedText) ? 0 : ChangedText.Length,
-                Length == 0 ? "" : src.Slice (Start, Length).ToString ());
-        public override string ToString() => $"{Start},{ChangedText}";
-    }
-}
diff --git a/Crow/src/Text/TextLine.cs b/Crow/src/Text/TextLine.cs
deleted file mode 100644 (file)
index d757ed7..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Text;
-
-namespace Crow.Text
-{
-       public enum LineBreakKind
-    {
-               Undefined,
-               Unix,
-               Windows,
-               Other
-    }
-       /// <summary>
-       /// represent a single line span with end of line handling.
-       /// </summary>
-       [DebuggerDisplay ("{Start}, {Length}, {LengthInPixel}")]
-       public struct TextLine : IComparable<TextLine>
-       {
-               /// <summary>
-               /// Line start absolute character position.
-               /// </summary>
-               public int Start;
-               /// <summary>
-               /// Line's character count not including linebreak if any.
-               /// </summary>
-               public int Length;
-               /// <summary>
-               /// Total line's Character count including linebreak characters if any.
-               /// </summary>
-               public int LengthIncludingLineBreak;
-               /// <summary>
-               /// Cached line's length in pixel. If not yet computed by renderer, value is '-1'
-               /// </summary>
-               public int LengthInPixel;
-               /// <summary>
-               /// Absolute end character position just before linebreak if any.
-               /// </summary>
-               public int End => Start + Length;
-               public TextSpan Span => new TextSpan (Start, End);
-               public TextSpan SpanIncludingLineBreak => new TextSpan (Start, EndIncludingLineBreak);
-               /// <summary>
-               /// Absolute line's end position after linebreak if any.
-               /// </summary>
-               public int EndIncludingLineBreak => Start + LengthIncludingLineBreak;
-               /// <summary>
-               /// Character count of the linebreak, 0 if no linebreak.
-               /// </summary>
-               public int LineBreakLength => LengthIncludingLineBreak - Length;
-               /// <summary>
-               /// True line has a linebreak, false otherwise.
-               /// </summary>
-               public bool HasLineBreak => LineBreakLength > 0;
-               /// <summary>
-               /// Create a new TextLine span using the absolute start and end character positions.
-               /// </summary>
-               public TextLine (int start, int end, int endIncludingLineBreak) {
-                       Start = start;
-                       Length = end - start;
-                       LengthIncludingLineBreak = endIncludingLineBreak - start;
-                       LengthInPixel = -1;
-               }
-               /// <summary>
-               /// Create an empty line span without linebreak starting at absolute charater position given in argument.
-               /// </summary>
-               /// <param name="start"></param>
-               public TextLine (int start) {
-                       Start = start;
-                       Length = 0;
-                       LengthIncludingLineBreak = 0;
-                       LengthInPixel = -1;
-               }
-               /// <summary>
-               /// Set a new line's length and reset computed length in pixel to '-1'.
-               /// </summary>
-               /// <param name="newLength"></param>
-               public void SetLength (int newLength) {
-                       LengthInPixel = -1;
-                       Length = newLength;
-        }
-               /// <summary>
-               /// Create a new TextLine span with a start offset, length in pixel is reseted.
-               /// </summary>
-               /// <param name="start"></param>
-               /// <returns></returns>
-               public TextLine WithStartOffset (int start) => new TextLine (Start + start, End, EndIncludingLineBreak);
-               public int CompareTo (TextLine other) => Start - other.Start;
-    }
-}
diff --git a/Crow/src/Text/TextLineCollection.cs b/Crow/src/Text/TextLineCollection.cs
deleted file mode 100644 (file)
index 5031496..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Crow.Text
-{
-       public class LineCollection : IList<TextLine>
-       {
-               TextLine[] lines;
-               int length;
-
-               #region CTOR
-               public LineCollection (int capacity) {
-                       lines = new TextLine[capacity];
-                       length = 0;
-               }
-               public LineCollection (TextLine[] _lines, int capacity = -1) {
-                       if (capacity >= _lines.Length) {
-                               lines = new TextLine[capacity];
-                               _lines.AsSpan ().CopyTo (lines);
-                       } else
-                               lines =_lines;
-
-                       length = _lines.Length;
-               }
-
-               public LineCollection (string _text, int capacity = 4) : this (capacity) {
-                       Update (_text.AsSpan ());
-               }
-               #endregion
-
-               public void Update (ReadOnlySpan<char> _text) {
-                       length = 0;
-                       int start = 0, i = 0;
-                       while (i < _text.Length) {
-                               char c = _text[i];
-                               if (c == '\r') {
-                                       if (++i < _text.Length) {
-                                               if (_text[i] == '\n')
-                                                       Add (new TextLine (start, i - 1, ++i));
-                                               else
-                                                       Add (new TextLine (start, i - 1, i));
-                                       } else
-                                               Add (new TextLine (start, i - 1, i));
-                                       start = i;
-                               } else if (c == '\n') {
-                                       if (++i < _text.Length) {
-                                               if (_text[i] == '\r')
-                                                       Add (new TextLine (start, i - 1, ++i));
-                                               else
-                                                       Add (new TextLine (start, i - 1, i));
-                                       } else
-                                               Add (new TextLine (start, i - 1, i));
-                                       start = i;
-
-                               } else if (c == '\u0085' || c == '\u2028' || c == '\u2029')
-                                       Add (new TextLine (start, i - 1, i));
-                               else
-                                       i++;
-                       }
-
-                       if (start < i)
-                               Add (new TextLine (start, _text.Length, _text.Length));
-                       else
-                               Add (new TextLine (_text.Length, _text.Length, _text.Length));
-               }
-
-               public void Update (TextChange change) {
-                       CharLocation locStart = GetLocation (change.Start);
-                       int charsDiff = change.ChangedText.Length - change.Length;
-                       int lineEnd = locStart.Line;
-                       while (lineEnd < length - 1 && change.End >= lines[lineEnd + 1].Start)
-                               lineEnd++;
-                       int columnEnd = change.End - lines[lineEnd].Start;
-                       int lineEndLineBreakLength = lines[lineEnd].LineBreakLength;
-
-                       LineCollection newLines = new LineCollection (change.ChangedText);
-                       int linesDiff = newLines.length - 1 - (lineEnd - locStart.Line);
-                       TextLine endTl = lines[lineEnd];
-
-                       if (linesDiff < 0)
-                               RemoveAt (locStart.Line + 1, -linesDiff);
-                       else if (linesDiff > 0) {
-                               for (int i = 0; i < linesDiff; i++)
-                                       Insert (locStart.Line + 1, default);
-                       }
-
-                       int remainingColumns = endTl.Length - columnEnd;
-                       lineEnd += linesDiff;
-                       lines[lineEnd].SetLength (0);
-                       lines[locStart.Line].SetLength (locStart.Column + newLines[0].Length);
-                       lines[lineEnd].Length += remainingColumns;
-                       if (newLines.Count > 1) {
-                               lines[lineEnd].Length += newLines[newLines.Count - 1].Length;
-                               lines[locStart.Line].LengthIncludingLineBreak = lines[locStart.Line].Length + newLines[0].LineBreakLength;
-                       }
-                       lines[lineEnd].LengthIncludingLineBreak = lines[lineEnd].Length + endTl.LineBreakLength;
-
-                       for (int i = 1; i < newLines.Count - 1; i++) {
-                               int l = locStart.Line + i;
-                               lines[l] = newLines[i];
-                               lines[l].Start = lines[l - 1].EndIncludingLineBreak;
-                       }
-                       if (lineEnd > 0)
-                               lines[lineEnd].Start = lines[lineEnd - 1].EndIncludingLineBreak;
-
-                       //shift start for remaining lines
-                       for (int i = lineEnd + 1; i < length; i++)
-                               lines[i].Start += charsDiff;
-               }
-               public int GetAbsolutePosition (CharLocation loc) => lines[loc.Line].Start + loc.Column;
-               public CharLocation GetLocation (int absolutePosition) {
-                       TextLine tl = new TextLine (absolutePosition);
-                       int result = lines.AsSpan (0, length).BinarySearch (tl);
-                       if (result < 0) {
-                               result = ~result;
-                               return new CharLocation (result - 1, absolutePosition - lines[result - 1].Start);
-                       }
-                       return new CharLocation (result, absolutePosition - lines[result].Start);
-               }
-               [Obsolete]
-               public void UpdateLineLengthInPixel (int index, int lengthInPixel) {
-                       lines[index].LengthInPixel = lengthInPixel;
-               }
-               public int Count => length;
-               public bool IsReadOnly => false;
-               public bool IsEmpty => length == 0;
-
-               public TextLine this[int index] { get => lines[index]; set => lines[index] = value; }
-
-               public void Add (TextLine item) {
-                       if (lines.Length < length + 1) {
-                               TextLine[] tmp = new TextLine[length * 2];
-                               lines.AsSpan ().CopyTo (tmp);
-                               lines = tmp;
-                       }
-                       lines[length] = item;
-                       length++;
-               }
-
-               public void Clear () {
-                       length = 0;
-               }
-
-               public bool Contains (TextLine item) => Array.IndexOf<TextLine> (lines, item) >= 0;
-
-               public void CopyTo (TextLine[] array, int arrayIndex) {
-                       lines.AsSpan (0, length).CopyTo (array.AsSpan (arrayIndex));
-               }
-
-               public bool Remove (TextLine item) {
-                       int idx = Array.IndexOf<TextLine> (lines, item);
-                       if (idx < 0)
-                               return false;
-                       if (idx + 1 < length)
-                               lines.AsSpan (idx + 1, length - idx - 1).CopyTo (lines.AsSpan (idx));
-                       length--;
-                       return true;
-               }
-               public IEnumerator<TextLine> GetEnumerator () => new Enumerator (this);
-               IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
-
-               public int IndexOf (TextLine item) => Array.IndexOf (lines, item);
-
-               public void Insert (int index, TextLine item) {
-                       if (lines.Length < length + 1) {
-                               TextLine[] tmp = new TextLine[length * 2];
-                               lines.AsSpan (0, index).CopyTo (tmp);
-                               lines.AsSpan (index).CopyTo (tmp.AsSpan (index + 1));
-                               lines = tmp;
-                       }else
-                               lines.AsSpan (index, length - index).CopyTo (lines.AsSpan (index + 1));
-                       lines[index] = item;
-                       length++;
-               }
-
-               public void RemoveAt (int index) {
-                       if (index + 1 < length)
-                               lines.AsSpan (index + 1, length - index - 1).CopyTo (lines.AsSpan (index));
-                       length--;
-               }
-               public void RemoveAt (int index, int count) {
-                       if (index + count < length)
-                               lines.AsSpan (index + count, length - index - count).CopyTo (lines.AsSpan (index));
-                       length -= count;
-               }
-
-               public class Enumerator : IEnumerator<TextLine>
-               {
-                       TextLine[] lines;
-                       int length, position = -1;
-                       public Enumerator (LineCollection coll) {
-                               lines = coll.lines;
-                               length = coll.length;
-                       }
-                       public TextLine Current => lines[position];
-                       object IEnumerator.Current => Current;
-                       public void Dispose () { }
-                       public bool MoveNext () => ++position < length;
-                       public void Reset () {
-                               position = -1;
-                       }
-               }
-       }
-}
diff --git a/Crow/src/Text/TextSpan.cs b/Crow/src/Text/TextSpan.cs
deleted file mode 100644 (file)
index 83fbc68..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
-//
-// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Crow.Text
-{
-       public struct TextSpan : IEquatable<TextSpan>
-       {
-               public readonly int Start;
-               public readonly int End;
-               public TextSpan (int start, int end) {
-                       Start = start;
-                       End = end;
-               }
-               public static TextSpan FromStartAndLength (int start, int length = 0) => new TextSpan (start, start + length);
-               public bool IsEmpty => Start == End;
-               public int Length => End - Start;
-
-               public bool Equals(TextSpan other)
-                       => Start == other.Start && End == other.End;
-               public override bool Equals(object obj)
-                       => obj is TextSpan ts ? Equals(ts) : false;
-
-               public override int GetHashCode()
-                       => HashCode.Combine(Start, End);
-               public static bool operator ==(TextSpan left, TextSpan right)
-                       => left.Equals (right);
-               public static bool operator !=(TextSpan left, TextSpan right)
-                       => !left.Equals (right);
-               public override string ToString() => $"{Start},{End}";
-               public bool Contains (int absolutePosition)
-                       => absolutePosition >= Start && absolutePosition < End;
-               public bool Contains (TextSpan span)
-                       => Start <= span.Start && End >= span.End;
-       }
-}
diff --git a/Drawing2D/src/CharLocation.cs b/Drawing2D/src/CharLocation.cs
new file mode 100644 (file)
index 0000000..345f8fa
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
+using System.Diagnostics;
+
+namespace Crow.Text
+{
+       [DebuggerDisplay ("{Line}, {Column}, {VisualCharXPosition}")]
+       public struct CharLocation : IEquatable<CharLocation>
+       {
+               public readonly int Line;
+               /// <summary>
+               /// Character position in current line.
+               /// If Column value is '-1', the visualX must contains the on screen position, and column will be computed from it.
+               /// </summary>
+               public int Column;
+               /// <summary>
+               /// The column as presented to the user, counting spaces in tabulations.
+               /// Its value is set during computations.
+               /// </summary>
+               public int TabulatedColumn;
+               public double VisualCharXPosition;
+               public CharLocation (int line, int column, double visualX = -1) {
+                       Line = line;
+                       Column = column;
+                       TabulatedColumn = -1;
+                       VisualCharXPosition = visualX;
+               }
+               public bool HasVisualX => Column >= 0 && VisualCharXPosition >= 0;
+               public void ResetVisualX () => VisualCharXPosition = -1;
+               public static bool operator == (CharLocation a, CharLocation b)
+                       => a.Equals (b);
+               public static bool operator != (CharLocation a, CharLocation b)
+                       => !a.Equals (b);
+               public bool Equals (CharLocation other) {
+                       return Column < 0 ?
+                               Line == other.Line && VisualCharXPosition == other.VisualCharXPosition :
+                               Line == other.Line && Column == other.Column;
+               }
+               public override bool Equals (object obj) => obj is CharLocation loc ? Equals (loc) : false;
+               public override int GetHashCode () {
+                       return Column < 0 ?
+                               HashCode.Combine (Line, VisualCharXPosition) :
+                               HashCode.Combine (Line, Column);
+               }
+
+               public override string ToString () => $"{Line}, {Column}";
+       }
+}
diff --git a/Drawing2D/src/ExtensionsMethods.cs b/Drawing2D/src/ExtensionsMethods.cs
new file mode 100644 (file)
index 0000000..dba4242
--- /dev/null
@@ -0,0 +1,80 @@
+// Copyright (c) 2013-2022  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+
+using Crow.Text;
+using System;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+
+using Drawing2D;
+
+namespace Crow
+{
+       public static class ExtensionsMethods
+       {
+               public static void Raise(this EventHandler handler, object sender, EventArgs e)
+               {
+                       handler?.Invoke (sender, e);
+               }
+               public static void Raise<T>(this EventHandler<T> handler, object sender, T e)
+               {
+                       handler?.Invoke (sender, e);
+               }
+               public static byte[] GetBytes(this string str)
+               {
+                       byte[] bytes = new byte[str.Length * sizeof(char)];
+                       System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
+                       return bytes;
+               }
+               public static bool IsWhiteSpaceOrNewLine (this char c)
+               {
+                       return c == '\t' || c.IsAnyLineBreakCharacter() || char.IsWhiteSpace (c);
+               }
+               internal static bool IsAnyLineBreakCharacter (this char c)
+                       => c == '\n' || c == '\r' || c == '\u0085' || c == '\u2028' || c == '\u2029';
+               public static ReadOnlySpan<char> GetLine (this ReadOnlySpan<char> str, TextLine ls) {
+                       if (ls.Start >= str.Length)
+                               return "".AsSpan ();
+                       return str.Slice (ls.Start, ls.Length);
+               }
+               public static ReadOnlySpan<char> GetLine (this ReadOnlySpan<char> str, TextLine ls, int offset) {
+                       int start = ls.Start + offset;
+                       if (start >= str.Length)
+                               return "".AsSpan ();
+                       return str.Slice (start, ls.Length);
+               }
+               public static ReadOnlySpan<char> GetLineIncludingLineBreak (this string str, TextLine ls) {
+                       if (ls.Start >= str.Length)
+                               return "".AsSpan ();
+                       return str.AsSpan ().Slice (ls.Start, ls.LengthIncludingLineBreak);
+               }
+               public static ReadOnlySpan<char> GetLineBreak (this ReadOnlySpan<char> str, TextLine ls) {
+                       if (ls.LineBreakLength == 0)
+                               return "".AsSpan ();
+                       return str.Slice (ls.End, ls.LineBreakLength);
+               }
+               public static ReadOnlySpan<char> GetLineIncludingLineBreak (this string str, TextLine ls, int offset) {
+                       int start = ls.Start + offset;
+                       if (start >= str.Length)
+                               return "".AsSpan ();
+                       return str.AsSpan ().Slice (start, ls.LengthIncludingLineBreak);
+               }
+
+               public static ReadOnlySpan<char> ToCharSpan (this LineBreakKind lineBreak) {
+                       switch (lineBreak) {
+                       case LineBreakKind.Unix:
+                               return "\n".AsSpan ();
+                       case LineBreakKind.Windows:
+                               return "\r\n".AsSpan ();
+                       case LineBreakKind.Other:
+                               return "\r".AsSpan ();
+                       default:
+                               return "\r\n".AsSpan ();
+                       }
+               }
+       }
+}
+
diff --git a/Drawing2D/src/LineSpan.cs b/Drawing2D/src/LineSpan.cs
new file mode 100644 (file)
index 0000000..1dc90de
--- /dev/null
@@ -0,0 +1,31 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
+
+namespace Crow.Text
+{
+       public struct LineSpan : IEquatable<LineSpan>
+       {
+               public readonly CharLocation Start;
+               public readonly CharLocation End;
+               public LineSpan (CharLocation start, CharLocation end) {
+                       Start = start;
+                       End = end;
+               }
+               public bool IsEmpty => Start == End;
+
+               public bool Equals(LineSpan other)
+                       => Start == other.Start && End == other.End;
+               public override bool Equals(object obj)
+                       => obj is LineSpan ts ? Equals(ts) : false;
+
+               public override int GetHashCode()
+                       => HashCode.Combine(Start, End);
+               public static bool operator ==(LineSpan left, LineSpan right)
+                       => left.Equals (right);
+               public static bool operator !=(LineSpan left, LineSpan right)
+                       => !left.Equals (right);
+               public override string ToString() => $"{Start},{End}";
+       }
+}
diff --git a/Drawing2D/src/Measure.cs b/Drawing2D/src/Measure.cs
new file mode 100644 (file)
index 0000000..4b66a97
--- /dev/null
@@ -0,0 +1,103 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+
+using System;
+
+namespace Crow
+{
+       /// <summary>
+       /// Measurement unit
+       /// </summary>
+       public enum Unit {Undefined, Pixel, Percent, Inherit }
+       /// <summary>
+       /// Measure class allow proportional sizes as well as stretched and fit on content.
+       /// </summary>
+       public struct Measure : IEquatable<Measure>
+       {
+               /// <summary>
+               /// Integer value of the measure
+               /// </summary>
+               public int Value;
+               /// <summary>
+               /// Measurement unit
+               /// </summary>
+               public Unit Units;
+
+               /// <summary>
+               /// Fit on content, this special measure is defined as a fixed integer set to -1 pixel
+               /// </summary>
+               public static Measure Fit = new Measure(-1,Unit.Percent);
+               /// <summary>
+               /// Stretched into parent client area. This special measure is defined as a proportional cote
+               /// set to 100 Percents
+               /// </summary>
+               public static Measure Stretched = new Measure(100, Unit.Percent);
+               public static Measure Inherit = new Measure (0, Unit.Inherit);
+               #region CTOR
+               public Measure (int _value, Unit _units = Unit.Pixel)
+               {
+                       Value = _value;
+                       Units = _units;
+               }
+               #endregion
+
+               /// <summary>
+               /// True is size is fixed in pixels, this means not proportional, stretched nor fit.
+               /// </summary>
+               public bool IsFixed { get { return Units == Unit.Pixel; }}
+               public bool IsFit { get { return Value == -1 && Units == Unit.Percent; }}
+               /// <summary>
+               /// True if width is proportional to parent client rectangle
+               /// </summary>
+               public bool IsRelativeToParent { get { return Value >= 0 && Units == Unit.Percent; }}
+               #region Operators
+               public static implicit operator int(Measure m) => m.Value;
+               public static implicit operator Measure(int i) => new Measure(i);
+               public static implicit operator string(Measure m) => m.ToString();
+               public static implicit operator Measure(string s) => Measure.Parse(s);
+
+               public static bool operator ==(Measure m1, Measure m2) => m1.Equals (m2);
+               public static bool operator !=(Measure m1, Measure m2) => !m1.Equals (m2);
+               #endregion
+
+               public bool Equals(Measure other) => Value == other.Value && Units == other.Units;
+
+               #region Object overrides
+               public override int GetHashCode () => HashCode.Combine (Value, Units);
+               public override bool Equals (object obj) => obj is Measure m ? Equals (m) : false;
+               public override string ToString ()
+               {
+                       return Units == Unit.Inherit ? "Inherit" :
+                               Value == -1 ? "Fit" :
+                               Units == Unit.Percent ? Value == 100 ? "Stretched" :
+                               Value.ToString () + "%" : Value.ToString ();
+               }
+               #endregion
+
+               public static Measure Parse(string s){
+                       if (string.IsNullOrEmpty (s))
+                               return default(Measure);
+
+                       string st = s.Trim ();
+                       int tmp = 0;
+
+                       if (string.Equals ("Inherit", st, StringComparison.Ordinal))
+                               return Measure.Inherit;
+                       else if (string.Equals ("Fit", st, StringComparison.Ordinal))
+                               return Measure.Fit;
+                       else if (string.Equals ("Stretched", s, StringComparison.Ordinal))
+                               return Measure.Stretched;
+                       else {
+                               if (st.EndsWith ("%", StringComparison.Ordinal)) {
+                                       if (int.TryParse (s.Substring(0, st.Length - 1), out tmp))
+                                               return new Measure (tmp, Unit.Percent);
+                               }else if (int.TryParse (s, out tmp))
+                                       return new Measure (tmp);
+                       }
+
+                       throw new Exception ("Error parsing Measure.");
+               }
+
+       }
+}
diff --git a/Drawing2D/src/TextBuffer.cs b/Drawing2D/src/TextBuffer.cs
new file mode 100644 (file)
index 0000000..ff85e2f
--- /dev/null
@@ -0,0 +1,99 @@
+// Copyright (c) 2021-2025  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using Crow.Text;
+using Crow;
+
+namespace CrowEditBase
+{
+       public class TextBuffer {
+               static int bufferExpension = 100;
+               int length;
+               Memory<char> buffer;
+               ReadOnlyMemory<char> origBuffer;
+               LineCollection lines;
+               public bool mixedLineBreak = false;
+               public string lineBreak = null;
+
+               internal LineCollection GetLineListCopy() => new LineCollection(lines.ToArray());
+               public Span<char> Span => buffer.Span.Slice(0, length);
+               public ReadOnlySpan<char> ReadOnlySpan => buffer.Span.Slice(0, length);
+               public bool IsEmpty => length == 0;
+               public bool IsDirty => !origBuffer.Span.Equals(Span, StringComparison.Ordinal);
+               public int LinesCount => lines.Count;
+               public int Length => length;
+               public ReadOnlyMemory<char> ReadOnlyCopy {
+                       get {
+                               return ReadOnlySpan.ToArray();
+                       }
+               } 
+               public void ResetDirtyState () {
+                       origBuffer = Span.ToArray();
+               }
+               public TextBuffer(ReadOnlySpan<char> origText) {
+                       length = origText.Length;
+                       buffer = new char[length + bufferExpension];
+                       origText.CopyTo(buffer.Span);
+                       lines = new LineCollection (10);
+                       if (IsEmpty)
+                               lines.Add (new TextLine (0, 0, 0));
+                       else
+                               lines.Update (Span);
+                       ResetDirtyState ();
+               }
+               public void Update (TextChange change) {
+                       ReadOnlySpan<char> orig = buffer.Span;
+                       char[] newBuff = null;
+                       Span<char> tmp;
+                       if (buffer.Length < length + change.CharDiff) {
+                               newBuff = new char[length + change.CharDiff + bufferExpension];
+                               tmp = newBuff;
+                               orig.Slice(0, change.Start).CopyTo(tmp);
+                               if (change.CharDiff == 0)
+                                       orig.Slice(change.End, length - change.End).CopyTo(tmp.Slice(change.End));
+                       } else
+                               tmp = buffer.Span;
+                       
+                       if (change.CharDiff != 0)
+                               orig.Slice(change.End, length - change.End).CopyTo(tmp.Slice(change.End2));
+                       if (!string.IsNullOrEmpty (change.ChangedText))
+                               change.ChangedText.AsSpan ().CopyTo (tmp.Slice (change.Start));
+                       
+                       if (newBuff != null)
+                               buffer = newBuff;
+                       length += change.CharDiff;
+
+                       lines.Update (change);
+               }
+               public string GetLineBreak () {
+                       if (string.IsNullOrEmpty (lineBreak)) {
+                               mixedLineBreak = false;
+
+                               if (lines.Count == 0 || lines[0].LineBreakLength == 0)
+                                       lineBreak = Environment.NewLine;
+                               else {
+                                       lineBreak = ReadOnlySpan.GetLineBreak (lines[0]).ToString ();
+                                       for (int i = 1; i < lines.Count; i++) {
+                                               if (!ReadOnlySpan.GetLineBreak (lines[i]).SequenceEqual (lineBreak)) {
+                                                       mixedLineBreak = true;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+                       return lineBreak;
+               }
+               public CharLocation GetLocation (int absolutePosition) => lines.GetLocation (absolutePosition);
+               public TextLine GetLine (int index) => lines[index];
+               public void SetLine (int index, TextLine line) => lines[index] = line;
+               public ReadOnlySpan<char> GetText (TextLine line) => GetText(line.Span);
+               public ReadOnlySpan<char> GetText (TextSpan textSpan) => buffer.Span.Slice(textSpan.Start, textSpan.Length);
+               public int GetAbsolutePosition (CharLocation loc) => lines.GetAbsolutePosition (loc);
+               public CharLocation EndLocation => new CharLocation (lines.Count - 1, lines[lines.Count - 1].Length);
+        public override string ToString() => ReadOnlySpan.ToString();
+    }
+}
\ No newline at end of file
diff --git a/Drawing2D/src/TextChange.cs b/Drawing2D/src/TextChange.cs
new file mode 100644 (file)
index 0000000..6533a9b
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Crow.Text
+{
+    public struct TextChange
+    {
+        public readonly int Start;
+        public readonly int Length;
+        public string ChangedText;
+
+        public int End => Start + Length;
+        public int End2 => End + CharDiff;
+
+               public int CharDiff => string.IsNullOrEmpty (ChangedText) ? - Length : ChangedText.Length - Length;
+        public bool IsEmpty => string.IsNullOrEmpty (ChangedText) && Length == 0;
+        public bool HasNewText => !string.IsNullOrEmpty (ChangedText);
+        
+        public bool HasNoEffect(ReadOnlySpan<char> src)
+            => src.Slice(Start, Length).Equals(ChangedText, StringComparison.Ordinal);
+
+        public TextChange (int position, int length, string changedText = null) {
+            Start = position;
+            Length = length;
+            ChangedText = changedText;
+        }
+        public TextChange (int position, int length, ReadOnlySpan<char> changedText) {
+            Start = position;
+            Length = length;
+            ChangedText = changedText.ToString();
+        }
+        public TextChange Inverse (ReadOnlySpan<char> src)
+            => new TextChange (Start, string.IsNullOrEmpty (ChangedText) ? 0 : ChangedText.Length,
+                Length == 0 ? "" : src.Slice (Start, Length).ToString ());
+        public override string ToString() => $"{Start},{ChangedText}";
+    }
+}
diff --git a/Drawing2D/src/TextLine.cs b/Drawing2D/src/TextLine.cs
new file mode 100644 (file)
index 0000000..d757ed7
--- /dev/null
@@ -0,0 +1,93 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace Crow.Text
+{
+       public enum LineBreakKind
+    {
+               Undefined,
+               Unix,
+               Windows,
+               Other
+    }
+       /// <summary>
+       /// represent a single line span with end of line handling.
+       /// </summary>
+       [DebuggerDisplay ("{Start}, {Length}, {LengthInPixel}")]
+       public struct TextLine : IComparable<TextLine>
+       {
+               /// <summary>
+               /// Line start absolute character position.
+               /// </summary>
+               public int Start;
+               /// <summary>
+               /// Line's character count not including linebreak if any.
+               /// </summary>
+               public int Length;
+               /// <summary>
+               /// Total line's Character count including linebreak characters if any.
+               /// </summary>
+               public int LengthIncludingLineBreak;
+               /// <summary>
+               /// Cached line's length in pixel. If not yet computed by renderer, value is '-1'
+               /// </summary>
+               public int LengthInPixel;
+               /// <summary>
+               /// Absolute end character position just before linebreak if any.
+               /// </summary>
+               public int End => Start + Length;
+               public TextSpan Span => new TextSpan (Start, End);
+               public TextSpan SpanIncludingLineBreak => new TextSpan (Start, EndIncludingLineBreak);
+               /// <summary>
+               /// Absolute line's end position after linebreak if any.
+               /// </summary>
+               public int EndIncludingLineBreak => Start + LengthIncludingLineBreak;
+               /// <summary>
+               /// Character count of the linebreak, 0 if no linebreak.
+               /// </summary>
+               public int LineBreakLength => LengthIncludingLineBreak - Length;
+               /// <summary>
+               /// True line has a linebreak, false otherwise.
+               /// </summary>
+               public bool HasLineBreak => LineBreakLength > 0;
+               /// <summary>
+               /// Create a new TextLine span using the absolute start and end character positions.
+               /// </summary>
+               public TextLine (int start, int end, int endIncludingLineBreak) {
+                       Start = start;
+                       Length = end - start;
+                       LengthIncludingLineBreak = endIncludingLineBreak - start;
+                       LengthInPixel = -1;
+               }
+               /// <summary>
+               /// Create an empty line span without linebreak starting at absolute charater position given in argument.
+               /// </summary>
+               /// <param name="start"></param>
+               public TextLine (int start) {
+                       Start = start;
+                       Length = 0;
+                       LengthIncludingLineBreak = 0;
+                       LengthInPixel = -1;
+               }
+               /// <summary>
+               /// Set a new line's length and reset computed length in pixel to '-1'.
+               /// </summary>
+               /// <param name="newLength"></param>
+               public void SetLength (int newLength) {
+                       LengthInPixel = -1;
+                       Length = newLength;
+        }
+               /// <summary>
+               /// Create a new TextLine span with a start offset, length in pixel is reseted.
+               /// </summary>
+               /// <param name="start"></param>
+               /// <returns></returns>
+               public TextLine WithStartOffset (int start) => new TextLine (Start + start, End, EndIncludingLineBreak);
+               public int CompareTo (TextLine other) => Start - other.Start;
+    }
+}
diff --git a/Drawing2D/src/TextLineCollection.cs b/Drawing2D/src/TextLineCollection.cs
new file mode 100644 (file)
index 0000000..5031496
--- /dev/null
@@ -0,0 +1,209 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Crow.Text
+{
+       public class LineCollection : IList<TextLine>
+       {
+               TextLine[] lines;
+               int length;
+
+               #region CTOR
+               public LineCollection (int capacity) {
+                       lines = new TextLine[capacity];
+                       length = 0;
+               }
+               public LineCollection (TextLine[] _lines, int capacity = -1) {
+                       if (capacity >= _lines.Length) {
+                               lines = new TextLine[capacity];
+                               _lines.AsSpan ().CopyTo (lines);
+                       } else
+                               lines =_lines;
+
+                       length = _lines.Length;
+               }
+
+               public LineCollection (string _text, int capacity = 4) : this (capacity) {
+                       Update (_text.AsSpan ());
+               }
+               #endregion
+
+               public void Update (ReadOnlySpan<char> _text) {
+                       length = 0;
+                       int start = 0, i = 0;
+                       while (i < _text.Length) {
+                               char c = _text[i];
+                               if (c == '\r') {
+                                       if (++i < _text.Length) {
+                                               if (_text[i] == '\n')
+                                                       Add (new TextLine (start, i - 1, ++i));
+                                               else
+                                                       Add (new TextLine (start, i - 1, i));
+                                       } else
+                                               Add (new TextLine (start, i - 1, i));
+                                       start = i;
+                               } else if (c == '\n') {
+                                       if (++i < _text.Length) {
+                                               if (_text[i] == '\r')
+                                                       Add (new TextLine (start, i - 1, ++i));
+                                               else
+                                                       Add (new TextLine (start, i - 1, i));
+                                       } else
+                                               Add (new TextLine (start, i - 1, i));
+                                       start = i;
+
+                               } else if (c == '\u0085' || c == '\u2028' || c == '\u2029')
+                                       Add (new TextLine (start, i - 1, i));
+                               else
+                                       i++;
+                       }
+
+                       if (start < i)
+                               Add (new TextLine (start, _text.Length, _text.Length));
+                       else
+                               Add (new TextLine (_text.Length, _text.Length, _text.Length));
+               }
+
+               public void Update (TextChange change) {
+                       CharLocation locStart = GetLocation (change.Start);
+                       int charsDiff = change.ChangedText.Length - change.Length;
+                       int lineEnd = locStart.Line;
+                       while (lineEnd < length - 1 && change.End >= lines[lineEnd + 1].Start)
+                               lineEnd++;
+                       int columnEnd = change.End - lines[lineEnd].Start;
+                       int lineEndLineBreakLength = lines[lineEnd].LineBreakLength;
+
+                       LineCollection newLines = new LineCollection (change.ChangedText);
+                       int linesDiff = newLines.length - 1 - (lineEnd - locStart.Line);
+                       TextLine endTl = lines[lineEnd];
+
+                       if (linesDiff < 0)
+                               RemoveAt (locStart.Line + 1, -linesDiff);
+                       else if (linesDiff > 0) {
+                               for (int i = 0; i < linesDiff; i++)
+                                       Insert (locStart.Line + 1, default);
+                       }
+
+                       int remainingColumns = endTl.Length - columnEnd;
+                       lineEnd += linesDiff;
+                       lines[lineEnd].SetLength (0);
+                       lines[locStart.Line].SetLength (locStart.Column + newLines[0].Length);
+                       lines[lineEnd].Length += remainingColumns;
+                       if (newLines.Count > 1) {
+                               lines[lineEnd].Length += newLines[newLines.Count - 1].Length;
+                               lines[locStart.Line].LengthIncludingLineBreak = lines[locStart.Line].Length + newLines[0].LineBreakLength;
+                       }
+                       lines[lineEnd].LengthIncludingLineBreak = lines[lineEnd].Length + endTl.LineBreakLength;
+
+                       for (int i = 1; i < newLines.Count - 1; i++) {
+                               int l = locStart.Line + i;
+                               lines[l] = newLines[i];
+                               lines[l].Start = lines[l - 1].EndIncludingLineBreak;
+                       }
+                       if (lineEnd > 0)
+                               lines[lineEnd].Start = lines[lineEnd - 1].EndIncludingLineBreak;
+
+                       //shift start for remaining lines
+                       for (int i = lineEnd + 1; i < length; i++)
+                               lines[i].Start += charsDiff;
+               }
+               public int GetAbsolutePosition (CharLocation loc) => lines[loc.Line].Start + loc.Column;
+               public CharLocation GetLocation (int absolutePosition) {
+                       TextLine tl = new TextLine (absolutePosition);
+                       int result = lines.AsSpan (0, length).BinarySearch (tl);
+                       if (result < 0) {
+                               result = ~result;
+                               return new CharLocation (result - 1, absolutePosition - lines[result - 1].Start);
+                       }
+                       return new CharLocation (result, absolutePosition - lines[result].Start);
+               }
+               [Obsolete]
+               public void UpdateLineLengthInPixel (int index, int lengthInPixel) {
+                       lines[index].LengthInPixel = lengthInPixel;
+               }
+               public int Count => length;
+               public bool IsReadOnly => false;
+               public bool IsEmpty => length == 0;
+
+               public TextLine this[int index] { get => lines[index]; set => lines[index] = value; }
+
+               public void Add (TextLine item) {
+                       if (lines.Length < length + 1) {
+                               TextLine[] tmp = new TextLine[length * 2];
+                               lines.AsSpan ().CopyTo (tmp);
+                               lines = tmp;
+                       }
+                       lines[length] = item;
+                       length++;
+               }
+
+               public void Clear () {
+                       length = 0;
+               }
+
+               public bool Contains (TextLine item) => Array.IndexOf<TextLine> (lines, item) >= 0;
+
+               public void CopyTo (TextLine[] array, int arrayIndex) {
+                       lines.AsSpan (0, length).CopyTo (array.AsSpan (arrayIndex));
+               }
+
+               public bool Remove (TextLine item) {
+                       int idx = Array.IndexOf<TextLine> (lines, item);
+                       if (idx < 0)
+                               return false;
+                       if (idx + 1 < length)
+                               lines.AsSpan (idx + 1, length - idx - 1).CopyTo (lines.AsSpan (idx));
+                       length--;
+                       return true;
+               }
+               public IEnumerator<TextLine> GetEnumerator () => new Enumerator (this);
+               IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
+
+               public int IndexOf (TextLine item) => Array.IndexOf (lines, item);
+
+               public void Insert (int index, TextLine item) {
+                       if (lines.Length < length + 1) {
+                               TextLine[] tmp = new TextLine[length * 2];
+                               lines.AsSpan (0, index).CopyTo (tmp);
+                               lines.AsSpan (index).CopyTo (tmp.AsSpan (index + 1));
+                               lines = tmp;
+                       }else
+                               lines.AsSpan (index, length - index).CopyTo (lines.AsSpan (index + 1));
+                       lines[index] = item;
+                       length++;
+               }
+
+               public void RemoveAt (int index) {
+                       if (index + 1 < length)
+                               lines.AsSpan (index + 1, length - index - 1).CopyTo (lines.AsSpan (index));
+                       length--;
+               }
+               public void RemoveAt (int index, int count) {
+                       if (index + count < length)
+                               lines.AsSpan (index + count, length - index - count).CopyTo (lines.AsSpan (index));
+                       length -= count;
+               }
+
+               public class Enumerator : IEnumerator<TextLine>
+               {
+                       TextLine[] lines;
+                       int length, position = -1;
+                       public Enumerator (LineCollection coll) {
+                               lines = coll.lines;
+                               length = coll.length;
+                       }
+                       public TextLine Current => lines[position];
+                       object IEnumerator.Current => Current;
+                       public void Dispose () { }
+                       public bool MoveNext () => ++position < length;
+                       public void Reset () {
+                               position = -1;
+                       }
+               }
+       }
+}
diff --git a/Drawing2D/src/TextSpan.cs b/Drawing2D/src/TextSpan.cs
new file mode 100644 (file)
index 0000000..83fbc68
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright (c) 2013-2021  Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Crow.Text
+{
+       public struct TextSpan : IEquatable<TextSpan>
+       {
+               public readonly int Start;
+               public readonly int End;
+               public TextSpan (int start, int end) {
+                       Start = start;
+                       End = end;
+               }
+               public static TextSpan FromStartAndLength (int start, int length = 0) => new TextSpan (start, start + length);
+               public bool IsEmpty => Start == End;
+               public int Length => End - Start;
+
+               public bool Equals(TextSpan other)
+                       => Start == other.Start && End == other.End;
+               public override bool Equals(object obj)
+                       => obj is TextSpan ts ? Equals(ts) : false;
+
+               public override int GetHashCode()
+                       => HashCode.Combine(Start, End);
+               public static bool operator ==(TextSpan left, TextSpan right)
+                       => left.Equals (right);
+               public static bool operator !=(TextSpan left, TextSpan right)
+                       => !left.Equals (right);
+               public override string ToString() => $"{Start},{End}";
+               public bool Contains (int absolutePosition)
+                       => absolutePosition >= Start && absolutePosition < End;
+               public bool Contains (TextSpan span)
+                       => Start <= span.Start && End >= span.End;
+       }
+}