From: Jean-Philippe Bruyère Date: Tue, 4 Mar 2025 12:52:08 +0000 (+0100) Subject: move textBuffer to drawing2d and several other text classes and measure X-Git-Url: https://git.osiis.dedyn.io/?a=commitdiff_plain;h=54687bd5dcf322096a8c8cf8dbc1374cb5ce49f9;p=jp%2Fcrow.git move textBuffer to drawing2d and several other text classes and measure --- diff --git a/Crow/src/2d/Measure.cs b/Crow/src/2d/Measure.cs deleted file mode 100644 index 4b66a975..00000000 --- a/Crow/src/2d/Measure.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) - -using System; - -namespace Crow -{ - /// - /// Measurement unit - /// - public enum Unit {Undefined, Pixel, Percent, Inherit } - /// - /// Measure class allow proportional sizes as well as stretched and fit on content. - /// - public struct Measure : IEquatable - { - /// - /// Integer value of the measure - /// - public int Value; - /// - /// Measurement unit - /// - public Unit Units; - - /// - /// Fit on content, this special measure is defined as a fixed integer set to -1 pixel - /// - public static Measure Fit = new Measure(-1,Unit.Percent); - /// - /// Stretched into parent client area. This special measure is defined as a proportional cote - /// set to 100 Percents - /// - 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 - - /// - /// True is size is fixed in pixels, this means not proportional, stretched nor fit. - /// - public bool IsFixed { get { return Units == Unit.Pixel; }} - public bool IsFit { get { return Value == -1 && Units == Unit.Percent; }} - /// - /// True if width is proportional to parent client rectangle - /// - 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/Crow/src/ExtensionsMethods.cs b/Crow/src/ExtensionsMethods.cs index ed226b8a..302caec9 100644 --- a/Crow/src/ExtensionsMethods.cs +++ b/Crow/src/ExtensionsMethods.cs @@ -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(this EventHandler 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 index 345f8fa3..00000000 --- a/Crow/src/Text/CharLocation.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// 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 - { - public readonly int Line; - /// - /// 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. - /// - public int Column; - /// - /// The column as presented to the user, counting spaces in tabulations. - /// Its value is set during computations. - /// - 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 index 77360ddd..00000000 --- a/Crow/src/Text/Extensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// 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 GetLine (this ReadOnlySpan str, TextLine ls) { - if (ls.Start >= str.Length) - return "".AsSpan (); - return str.Slice (ls.Start, ls.Length); - } - public static ReadOnlySpan GetLine (this ReadOnlySpan 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 GetLineIncludingLineBreak (this string str, TextLine ls) { - if (ls.Start >= str.Length) - return "".AsSpan (); - return str.AsSpan ().Slice (ls.Start, ls.LengthIncludingLineBreak); - } - public static ReadOnlySpan GetLineBreak (this ReadOnlySpan str, TextLine ls) { - if (ls.LineBreakLength == 0) - return "".AsSpan (); - return str.Slice (ls.End, ls.LineBreakLength); - } - public static ReadOnlySpan 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 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 index 1dc90de6..00000000 --- a/Crow/src/Text/LineSpan.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) -using System; - -namespace Crow.Text -{ - public struct LineSpan : IEquatable - { - 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 index 6533a9b4..00000000 --- a/Crow/src/Text/TextChange.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// 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 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 changedText) { - Start = position; - Length = length; - ChangedText = changedText.ToString(); - } - public TextChange Inverse (ReadOnlySpan 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 index d757ed77..00000000 --- a/Crow/src/Text/TextLine.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// 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 - } - /// - /// represent a single line span with end of line handling. - /// - [DebuggerDisplay ("{Start}, {Length}, {LengthInPixel}")] - public struct TextLine : IComparable - { - /// - /// Line start absolute character position. - /// - public int Start; - /// - /// Line's character count not including linebreak if any. - /// - public int Length; - /// - /// Total line's Character count including linebreak characters if any. - /// - public int LengthIncludingLineBreak; - /// - /// Cached line's length in pixel. If not yet computed by renderer, value is '-1' - /// - public int LengthInPixel; - /// - /// Absolute end character position just before linebreak if any. - /// - public int End => Start + Length; - public TextSpan Span => new TextSpan (Start, End); - public TextSpan SpanIncludingLineBreak => new TextSpan (Start, EndIncludingLineBreak); - /// - /// Absolute line's end position after linebreak if any. - /// - public int EndIncludingLineBreak => Start + LengthIncludingLineBreak; - /// - /// Character count of the linebreak, 0 if no linebreak. - /// - public int LineBreakLength => LengthIncludingLineBreak - Length; - /// - /// True line has a linebreak, false otherwise. - /// - public bool HasLineBreak => LineBreakLength > 0; - /// - /// Create a new TextLine span using the absolute start and end character positions. - /// - public TextLine (int start, int end, int endIncludingLineBreak) { - Start = start; - Length = end - start; - LengthIncludingLineBreak = endIncludingLineBreak - start; - LengthInPixel = -1; - } - /// - /// Create an empty line span without linebreak starting at absolute charater position given in argument. - /// - /// - public TextLine (int start) { - Start = start; - Length = 0; - LengthIncludingLineBreak = 0; - LengthInPixel = -1; - } - /// - /// Set a new line's length and reset computed length in pixel to '-1'. - /// - /// - public void SetLength (int newLength) { - LengthInPixel = -1; - Length = newLength; - } - /// - /// Create a new TextLine span with a start offset, length in pixel is reseted. - /// - /// - /// - 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 index 5031496b..00000000 --- a/Crow/src/Text/TextLineCollection.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// 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[] 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 _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 (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 (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 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[] 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 index 83fbc68d..00000000 --- a/Crow/src/Text/TextSpan.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2013-2021 Jean-Philippe Bruyère -// -// 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 - { - 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 index 00000000..345f8fa3 --- /dev/null +++ b/Drawing2D/src/CharLocation.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// 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 + { + public readonly int Line; + /// + /// 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. + /// + public int Column; + /// + /// The column as presented to the user, counting spaces in tabulations. + /// Its value is set during computations. + /// + 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 index 00000000..dba42420 --- /dev/null +++ b/Drawing2D/src/ExtensionsMethods.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2013-2022 Jean-Philippe Bruyère +// +// 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(this EventHandler 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 GetLine (this ReadOnlySpan str, TextLine ls) { + if (ls.Start >= str.Length) + return "".AsSpan (); + return str.Slice (ls.Start, ls.Length); + } + public static ReadOnlySpan GetLine (this ReadOnlySpan 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 GetLineIncludingLineBreak (this string str, TextLine ls) { + if (ls.Start >= str.Length) + return "".AsSpan (); + return str.AsSpan ().Slice (ls.Start, ls.LengthIncludingLineBreak); + } + public static ReadOnlySpan GetLineBreak (this ReadOnlySpan str, TextLine ls) { + if (ls.LineBreakLength == 0) + return "".AsSpan (); + return str.Slice (ls.End, ls.LineBreakLength); + } + public static ReadOnlySpan 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 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 index 00000000..1dc90de6 --- /dev/null +++ b/Drawing2D/src/LineSpan.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +using System; + +namespace Crow.Text +{ + public struct LineSpan : IEquatable + { + 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 index 00000000..4b66a975 --- /dev/null +++ b/Drawing2D/src/Measure.cs @@ -0,0 +1,103 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) + +using System; + +namespace Crow +{ + /// + /// Measurement unit + /// + public enum Unit {Undefined, Pixel, Percent, Inherit } + /// + /// Measure class allow proportional sizes as well as stretched and fit on content. + /// + public struct Measure : IEquatable + { + /// + /// Integer value of the measure + /// + public int Value; + /// + /// Measurement unit + /// + public Unit Units; + + /// + /// Fit on content, this special measure is defined as a fixed integer set to -1 pixel + /// + public static Measure Fit = new Measure(-1,Unit.Percent); + /// + /// Stretched into parent client area. This special measure is defined as a proportional cote + /// set to 100 Percents + /// + 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 + + /// + /// True is size is fixed in pixels, this means not proportional, stretched nor fit. + /// + public bool IsFixed { get { return Units == Unit.Pixel; }} + public bool IsFit { get { return Value == -1 && Units == Unit.Percent; }} + /// + /// True if width is proportional to parent client rectangle + /// + 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 index 00000000..ff85e2ff --- /dev/null +++ b/Drawing2D/src/TextBuffer.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2021-2025 Jean-Philippe Bruyère +// +// 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 buffer; + ReadOnlyMemory origBuffer; + LineCollection lines; + public bool mixedLineBreak = false; + public string lineBreak = null; + + internal LineCollection GetLineListCopy() => new LineCollection(lines.ToArray()); + public Span Span => buffer.Span.Slice(0, length); + public ReadOnlySpan 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 ReadOnlyCopy { + get { + return ReadOnlySpan.ToArray(); + } + } + public void ResetDirtyState () { + origBuffer = Span.ToArray(); + } + public TextBuffer(ReadOnlySpan 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 orig = buffer.Span; + char[] newBuff = null; + Span 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 GetText (TextLine line) => GetText(line.Span); + public ReadOnlySpan 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 index 00000000..6533a9b4 --- /dev/null +++ b/Drawing2D/src/TextChange.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// 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 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 changedText) { + Start = position; + Length = length; + ChangedText = changedText.ToString(); + } + public TextChange Inverse (ReadOnlySpan 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 index 00000000..d757ed77 --- /dev/null +++ b/Drawing2D/src/TextLine.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// 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 + } + /// + /// represent a single line span with end of line handling. + /// + [DebuggerDisplay ("{Start}, {Length}, {LengthInPixel}")] + public struct TextLine : IComparable + { + /// + /// Line start absolute character position. + /// + public int Start; + /// + /// Line's character count not including linebreak if any. + /// + public int Length; + /// + /// Total line's Character count including linebreak characters if any. + /// + public int LengthIncludingLineBreak; + /// + /// Cached line's length in pixel. If not yet computed by renderer, value is '-1' + /// + public int LengthInPixel; + /// + /// Absolute end character position just before linebreak if any. + /// + public int End => Start + Length; + public TextSpan Span => new TextSpan (Start, End); + public TextSpan SpanIncludingLineBreak => new TextSpan (Start, EndIncludingLineBreak); + /// + /// Absolute line's end position after linebreak if any. + /// + public int EndIncludingLineBreak => Start + LengthIncludingLineBreak; + /// + /// Character count of the linebreak, 0 if no linebreak. + /// + public int LineBreakLength => LengthIncludingLineBreak - Length; + /// + /// True line has a linebreak, false otherwise. + /// + public bool HasLineBreak => LineBreakLength > 0; + /// + /// Create a new TextLine span using the absolute start and end character positions. + /// + public TextLine (int start, int end, int endIncludingLineBreak) { + Start = start; + Length = end - start; + LengthIncludingLineBreak = endIncludingLineBreak - start; + LengthInPixel = -1; + } + /// + /// Create an empty line span without linebreak starting at absolute charater position given in argument. + /// + /// + public TextLine (int start) { + Start = start; + Length = 0; + LengthIncludingLineBreak = 0; + LengthInPixel = -1; + } + /// + /// Set a new line's length and reset computed length in pixel to '-1'. + /// + /// + public void SetLength (int newLength) { + LengthInPixel = -1; + Length = newLength; + } + /// + /// Create a new TextLine span with a start offset, length in pixel is reseted. + /// + /// + /// + 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 index 00000000..5031496b --- /dev/null +++ b/Drawing2D/src/TextLineCollection.cs @@ -0,0 +1,209 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// 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[] 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 _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 (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 (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 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[] 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 index 00000000..83fbc68d --- /dev/null +++ b/Drawing2D/src/TextSpan.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2013-2021 Jean-Philippe Bruyère +// +// 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 + { + 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; + } +}