From d40fed5ce2bc806a91245adb18039634eac13ed0 Mon Sep 17 00:00:00 2001 From: MichaelTheShifter Date: Wed, 20 Jul 2016 09:40:36 -0400 Subject: Move ShiftUI source code to ShiftOS This'll be a lot easier to work on. --- source/ShiftUI/Internal/Line.cs | 811 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 811 insertions(+) create mode 100644 source/ShiftUI/Internal/Line.cs (limited to 'source/ShiftUI/Internal/Line.cs') diff --git a/source/ShiftUI/Internal/Line.cs b/source/ShiftUI/Internal/Line.cs new file mode 100644 index 0000000..a523047 --- /dev/null +++ b/source/ShiftUI/Internal/Line.cs @@ -0,0 +1,811 @@ +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Copyright (c) 2004-2006 Novell, Inc. (http://www.novell.com) +// +// Authors: +// Peter Bartok pbartok@novell.com +// +// + +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Text; +using System.Text; + +namespace ShiftUI +{ + internal class Line : ICloneable, IComparable + { + #region Local Variables + + internal Document document; + // Stuff that matters for our line + internal StringBuilder text; // Characters for the line + internal float[] widths; // Width of each character; always one larger than text.Length + internal int space; // Number of elements in text and widths + internal int line_no; // Line number + internal LineTag tags; // Tags describing the text + internal int offset; // Baseline can be on the X or Y axis depending if we are in multiline mode or not + internal int height; // Height of the line (height of tallest tag) + internal int ascent; // Ascent of the line (ascent of the tallest tag) + internal HorizontalAlignment alignment; // Alignment of the line + internal int align_shift; // Pixel shift caused by the alignment + internal int indent; // Left indent for the first line + internal int hanging_indent; // Hanging indent (left indent for all but the first line) + internal int right_indent; // Right indent for all lines + internal LineEnding ending; + + // Stuff that's important for the tree + internal Line parent; // Our parent line + internal Line left; // Line with smaller line number + internal Line right; // Line with higher line number + internal LineColor color; // We're doing a black/red tree. this is the node color + static int DEFAULT_TEXT_LEN = 0; // + internal bool recalc; // Line changed + + private static Hashtable kerning_fonts = new Hashtable (); // record which fonts use kerning + #endregion // Local Variables + + #region Constructors + internal Line (Document document, LineEnding ending) + { + this.document = document; + color = LineColor.Red; + left = null; + right = null; + parent = null; + text = null; + recalc = true; + alignment = document.alignment; + + this.ending = ending; + } + + internal Line (Document document, int LineNo, string Text, Font font, Color color, LineEnding ending) : this (document, ending) + { + space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN; + + text = new StringBuilder (Text, space); + line_no = LineNo; + this.ending = ending; + + widths = new float[space + 1]; + + + tags = new LineTag(this, 1); + tags.Font = font; + tags.Color = color; + } + + internal Line (Document document, int LineNo, string Text, HorizontalAlignment align, Font font, Color color, LineEnding ending) : this(document, ending) + { + space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN; + + text = new StringBuilder (Text, space); + line_no = LineNo; + this.ending = ending; + alignment = align; + + widths = new float[space + 1]; + + + tags = new LineTag(this, 1); + tags.Font = font; + tags.Color = color; + } + + internal Line (Document document, int LineNo, string Text, LineTag tag, LineEnding ending) : this(document, ending) + { + space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN; + + text = new StringBuilder (Text, space); + this.ending = ending; + line_no = LineNo; + + widths = new float[space + 1]; + tags = tag; + } + + #endregion // Constructors + + #region Internal Properties + internal HorizontalAlignment Alignment { + get { return alignment; } + set { + if (alignment != value) { + alignment = value; + recalc = true; + } + } + } + + internal int HangingIndent { + get { return hanging_indent; } + set { + hanging_indent = value; + recalc = true; + } + } + + // UIA: Method used via reflection in TextRangeProvider + internal int Height { + get { return height; } + set { height = value; } + } + + internal int Indent { + get { return indent; } + set { + indent = value; + recalc = true; + } + } + + internal int LineNo { + get { return line_no; } + set { line_no = value; } + } + + internal int RightIndent { + get { return right_indent; } + set { + right_indent = value; + recalc = true; + } + } + + // UIA: Method used via reflection in TextRangeProvider + internal int Width { + get { + int res = (int) widths [text.Length]; + return res; + } + } + + internal string Text { + get { return text.ToString(); } + set { + int prev_length = text.Length; + text = new StringBuilder(value, value.Length > DEFAULT_TEXT_LEN ? value.Length + 1 : DEFAULT_TEXT_LEN); + + if (text.Length > prev_length) + Grow (text.Length - prev_length); + } + } + + // UIA: Method used via reflection in TextRangeProvider + internal int X { + get { + if (document.multiline) + return align_shift; + return offset + align_shift; + } + } + + // UIA: Method used via reflection in TextRangeProvider + internal int Y { + get { + if (!document.multiline) + return document.top_margin; + return document.top_margin + offset; + } + } + #endregion // Internal Properties + + #region Internal Methods + + /// + /// Builds a simple code to record which tags are links and how many tags + /// used to compare lines before and after to see if the scan for links + /// process has changed anything. + /// + internal void LinkRecord (StringBuilder linkRecord) + { + LineTag tag = tags; + + while (tag != null) { + if (tag.IsLink) + linkRecord.Append ("L"); + else + linkRecord.Append ("N"); + + tag = tag.Next; + } + } + + /// + /// Clears all link properties from tags + /// + internal void ClearLinks () + { + LineTag tag = tags; + + while (tag != null) { + tag.IsLink = false; + tag = tag.Next; + } + } + + public void DeleteCharacters(int pos, int count) + { + LineTag tag; + bool streamline = false; + + // Can't delete more than the line has + if (pos >= text.Length) + return; + + // Find the first tag that we are deleting from + tag = FindTag (pos + 1); + + // Remove the characters from the line + text.Remove (pos, count); + + if (tag == null) + return; + + // Check if we're crossing tag boundaries + if ((pos + count) > (tag.Start + tag.Length - 1)) { + int left; + + // We have to delete cross tag boundaries + streamline = true; + left = count; + + left -= tag.Start + tag.Length - pos - 1; + tag = tag.Next; + + // Update the start of each tag + while ((tag != null) && (left > 0)) { + // Cache tag.Length as is will be indireclty modified + // by changes to tag.Start + int tag_length = tag.Length; + tag.Start -= count - left; + + if (tag_length > left) { + left = 0; + } else { + left -= tag_length; + tag = tag.Next; + } + + } + } else { + // We got off easy, same tag + + if (tag.Length == 0) + streamline = true; + } + + // Delete empty orphaned tags at the end + LineTag walk = tag; + while (walk != null && walk.Next != null && walk.Next.Length == 0) { + LineTag t = walk; + walk.Next = walk.Next.Next; + if (walk.Next != null) + walk.Next.Previous = t; + walk = walk.Next; + } + + // Adjust the start point of any tags following + if (tag != null) { + tag = tag.Next; + while (tag != null) { + tag.Start -= count; + tag = tag.Next; + } + } + + recalc = true; + + if (streamline) + Streamline (document.Lines); + } + + // This doesn't do exactly what you would think, it just pulls off the \n part of the ending + internal void DrawEnding (Graphics dc, float y) + { + if (document.multiline) + return; + LineTag last = tags; + while (last.Next != null) + last = last.Next; + + string end_str = null; + switch (document.LineEndingLength (ending)) { + case 0: + return; + case 1: + end_str = "\u0013"; + break; + case 2: + end_str = "\u0013\u0013"; + break; + case 3: + end_str = "\u0013\u0013\u0013"; + break; + } + + TextBoxTextRenderer.DrawText (dc, end_str, last.Font, last.Color, X + widths [TextLengthWithoutEnding ()] - document.viewport_x + document.OffsetX, y, true); + } + + /// Find the tag on a line based on the character position, pos is 0-based + internal LineTag FindTag (int pos) + { + LineTag tag; + + if (pos == 0) + return tags; + + tag = this.tags; + + if (pos >= text.Length) + pos = text.Length - 1; + + while (tag != null) { + if (((tag.Start - 1) <= pos) && (pos <= (tag.Start + tag.Length - 1))) + return LineTag.GetFinalTag (tag); + + tag = tag.Next; + } + + return null; + } + + public override int GetHashCode () + { + return base.GetHashCode (); + } + + // Get the tag that contains this x coordinate + public LineTag GetTag (int x) + { + LineTag tag = tags; + + // Coord is to the left of the first character + if (x < tag.X) + return LineTag.GetFinalTag (tag); + + // All we have is a linked-list of tags, so we have + // to do a linear search. But there shouldn't be + // too many tags per line in general. + while (true) { + if (x >= tag.X && x < (tag.X + tag.Width)) + return tag; + + if (tag.Next != null) + tag = tag.Next; + else + return LineTag.GetFinalTag (tag); + } + } + + // Make sure we always have enoughs space in text and widths + internal void Grow (int minimum) + { + int length; + float[] new_widths; + + length = text.Length; + + if ((length + minimum) > space) { + // We need to grow; double the size + + if ((length + minimum) > (space * 2)) { + new_widths = new float[length + minimum * 2 + 1]; + space = length + minimum * 2; + } else { + new_widths = new float[space * 2 + 1]; + space *= 2; + } + widths.CopyTo (new_widths, 0); + + widths = new_widths; + } + } + public void InsertString (int pos, string s) + { + InsertString (pos, s, FindTag (pos)); + } + + // Inserts a string at the given position + public void InsertString (int pos, string s, LineTag tag) + { + int len = s.Length; + + // Insert the text into the StringBuilder + text.Insert (pos, s); + + // Update the start position of every tag after this one + tag = tag.Next; + + while (tag != null) { + tag.Start += len; + tag = tag.Next; + } + + // Make sure we have room in the widths array + Grow (len); + + // This line needs to be recalculated + recalc = true; + } + + /// + /// Go through all tags on a line and recalculate all size-related values; + /// returns true if lineheight changed + /// + internal bool RecalculateLine (Graphics g, Document doc) + { + return RecalculateLine (g, doc, kerning_fonts.ContainsKey (tags.Font.GetHashCode ())); + } + + private bool RecalculateLine (Graphics g, Document doc, bool handleKerning) + { + LineTag tag; + int pos; + int len; + SizeF size; + float w; + int prev_offset; + bool retval; + bool wrapped; + Line line; + int wrap_pos; + int prev_height; + int prev_ascent; + + pos = 0; + len = this.text.Length; + tag = this.tags; + prev_offset = this.offset; // For drawing optimization calculations + prev_height = this.height; + prev_ascent = this.ascent; + this.height = 0; // Reset line height + this.ascent = 0; // Reset the ascent for the line + tag.Shift = 0; // Reset shift (which should be stored as pixels, not as points) + + if (ending == LineEnding.Wrap) + widths[0] = document.left_margin + hanging_indent; + else + widths[0] = document.left_margin + indent; + + this.recalc = false; + retval = false; + wrapped = false; + + wrap_pos = 0; + + while (pos < len) { + + while (tag.Length == 0) { // We should always have tags after a tag.length==0 unless len==0 + //tag.Ascent = 0; + tag.Shift = (tag.Line.ascent - tag.Ascent) / 72; + tag = tag.Next; + } + + // kerning is a problem. The original code in this method assumed that the + // width of a string equals the sum of the widths of its characters. This is + // not true when kerning takes place during the display process. Since it's + // impossible to find out easily whether a font does kerning, and with which + // characters, we just detect that kerning must have happened and use a slower + // (but accurate) measurement for those fonts henceforth. Without handling + // kerning, many fonts for English become unreadable during typing for many + // input strings, and text in many other languages is even worse trying to + // type in TextBoxes. + // See https://bugzilla.xamarin.com/show_bug.cgi?id=26478 for details. + float newWidth; + if (handleKerning && !Char.IsWhiteSpace(text[pos])) + { + // MeasureText doesn't measure trailing spaces, so we do the best we can for those + // in the else branch. + size = TextBoxTextRenderer.MeasureText (g, text.ToString (0, pos + 1), tag.Font); + newWidth = widths[0] + size.Width; + } + else + { + size = tag.SizeOfPosition (g, pos); + w = size.Width; + newWidth = widths[pos] + w; + } + + if (Char.IsWhiteSpace (text[pos])) + wrap_pos = pos + 1; + + if (doc.wrap) { + if ((wrap_pos > 0) && (wrap_pos != len) && (newWidth + 5) > (doc.viewport_width - this.right_indent)) { + // Make sure to set the last width of the line before wrapping + widths[pos + 1] = newWidth; + + pos = wrap_pos; + len = text.Length; + doc.Split (this, tag, pos); + ending = LineEnding.Wrap; + len = this.text.Length; + + retval = true; + wrapped = true; + } else if (pos > 1 && newWidth > (doc.viewport_width - this.right_indent)) { + // No suitable wrap position was found so break right in the middle of a word + + // Make sure to set the last width of the line before wrapping + widths[pos + 1] = newWidth; + + doc.Split (this, tag, pos); + ending = LineEnding.Wrap; + len = this.text.Length; + retval = true; + wrapped = true; + } + } + + // Contract all wrapped lines that follow back into our line + if (!wrapped) { + pos++; + + widths[pos] = newWidth; + + if (pos == len) { + line = doc.GetLine (this.line_no + 1); + if ((line != null) && (ending == LineEnding.Wrap || ending == LineEnding.None)) { + // Pull the two lines together + doc.Combine (this.line_no, this.line_no + 1); + len = this.text.Length; + retval = true; + } + } + } + + if (pos == (tag.Start - 1 + tag.Length)) { + // We just found the end of our current tag + tag.Height = tag.MaxHeight (); + + // Check if we're the tallest on the line (so far) + if (tag.Height > this.height) + this.height = tag.Height; // Yep; make sure the line knows + + if (tag.Ascent > this.ascent) { + LineTag t; + + // We have a tag that has a taller ascent than the line; + t = tags; + while (t != null && t != tag) { + t.Shift = (tag.Ascent - t.Ascent) / 72; + t = t.Next; + } + + // Save on our line + this.ascent = tag.Ascent; + } else { + tag.Shift = (this.ascent - tag.Ascent) / 72; + } + + tag = tag.Next; + if (tag != null) { + tag.Shift = 0; + wrap_pos = pos; + } + } + } + + var fullText = text.ToString(); + if (!handleKerning && fullText.Length > 1 && !wrapped) + { + // Check whether kerning takes place for this string and font. + var realSize = TextBoxTextRenderer.MeasureText(g, fullText, tags.Font); + float realWidth = realSize.Width + widths[0]; + // MeasureText ignores trailing whitespace, so we will too at this point. + int length = fullText.TrimEnd().Length; + float sumWidth = widths[length]; + if (realWidth != sumWidth) + { + kerning_fonts.Add(tags.Font.GetHashCode (), true); + // Using a slightly incorrect width this time around isn't that bad. All that happens + // is that the cursor is a pixel or two off until the next character is typed. It's + // the accumulation of pixel after pixel that causes display problems. + } + } + + while (tag != null) { + tag.Shift = (tag.Line.ascent - tag.Ascent) / 72; + tag = tag.Next; + } + + if (this.height == 0) { + this.height = tags.Font.Height; + tags.Height = this.height; + tags.Shift = 0; + } + + if (prev_offset != offset || prev_height != this.height || prev_ascent != this.ascent) + retval = true; + + return retval; + } + + /// + /// Recalculate a single line using the same char for every character in the line + /// + internal bool RecalculatePasswordLine (Graphics g, Document doc) + { + LineTag tag; + int pos; + int len; + float w; + bool ret; + + pos = 0; + len = this.text.Length; + tag = this.tags; + ascent = 0; + tag.Shift = 0; + + this.recalc = false; + widths[0] = document.left_margin + indent; + + w = TextBoxTextRenderer.MeasureText (g, doc.password_char, tags.Font).Width; + + if (this.height != (int)tag.Font.Height) + ret = true; + else + ret = false; + + this.height = (int)tag.Font.Height; + tag.Height = this.height; + + this.ascent = tag.Ascent; + + while (pos < len) { + pos++; + widths[pos] = widths[pos - 1] + w; + } + + return ret; + } + + internal void Streamline (int lines) + { + LineTag current; + LineTag next; + + current = this.tags; + next = current.Next; + + // + // Catch what the loop below wont; eliminate 0 length + // tags, but only if there are other tags after us + // We only eliminate text tags if there is another text tag + // after it. Otherwise we wind up trying to type on picture tags + // + while ((current.Length == 0) && (next != null) && (next.IsTextTag)) { + tags = next; + tags.Previous = null; + current = next; + next = current.Next; + } + + + if (next == null) + return; + + while (next != null) { + // Take out 0 length tags unless it's the last tag in the document + if (current.IsTextTag && next.Length == 0 && next.IsTextTag) { + if ((next.Next != null) || (line_no != lines)) { + current.Next = next.Next; + if (current.Next != null) { + current.Next.Previous = current; + } + next = current.Next; + continue; + } + } + + if (current.Combine (next)) { + next = current.Next; + continue; + } + + current = current.Next; + next = current.Next; + } + } + + internal int TextLengthWithoutEnding () + { + return text.Length - document.LineEndingLength (ending); + } + + internal string TextWithoutEnding () + { + return text.ToString (0, text.Length - document.LineEndingLength (ending)); + } + #endregion // Internal Methods + + #region Administrative + public object Clone () + { + Line clone; + + clone = new Line (document, ending); + + clone.text = text; + + if (left != null) + clone.left = (Line)left.Clone(); + + if (left != null) + clone.left = (Line)left.Clone(); + + return clone; + } + + internal object CloneLine () + { + Line clone; + + clone = new Line (document, ending); + + clone.text = text; + + return clone; + } + + public int CompareTo (object obj) + { + if (obj == null) + return 1; + + if (! (obj is Line)) + throw new ArgumentException("Object is not of type Line", "obj"); + + if (line_no < ((Line)obj).line_no) + return -1; + else if (line_no > ((Line)obj).line_no) + return 1; + else + return 0; + } + + public override bool Equals (object obj) + { + if (obj == null) + return false; + + if (!(obj is Line)) + return false; + + if (obj == this) + return true; + + if (line_no == ((Line)obj).line_no) + return true; + + return false; + } + + public override string ToString() + { + return string.Format ("Line {0}", line_no); + } + #endregion // Administrative + } +} -- cgit v1.2.3