1 // Permission is hereby granted, free of charge, to any person obtaining
2 // a copy of this software and associated documentation files (the
3 // "Software"), to deal in the Software without restriction, including
4 // without limitation the rights to use, copy, modify, merge, publish,
5 // distribute, sublicense, and/or sell copies of the Software, and to
6 // permit persons to whom the Software is furnished to do so, subject to
7 // the following conditions:
9 // The above copyright notice and this permission notice shall be
10 // included in all copies or substantial portions of the Software.
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 // Copyright (c) 2004 Novell, Inc. (http://www.novell.com)
23 // Peter Bartok pbartok@novell.com
29 // There's still plenty of things missing, I've got most of it planned, just hadn't had
30 // the time to write it all yet.
31 // Stuff missing (in no particular order):
32 // - Align text after RecalculateLine
33 // - Implement tag types to support wrapping (ie have a 'newline' tag), for images, etc.
34 // - Wrap and recalculate lines
35 // - Implement CaretPgUp/PgDown
36 // - Finish selection calculations (invalidate only changed, more ways to select)
43 using System.Collections;
45 using System.Drawing.Text;
48 namespace System.Windows.Forms {
49 public enum LineColor {
54 public enum CaretDirection {
55 CharForward, // Move a char to the right
56 CharBack, // Move a char to the left
57 LineUp, // Move a line up
58 LineDown, // Move a line down
59 Home, // Move to the beginning of the line
60 End, // Move to the end of the line
61 PgUp, // Move one page up
62 PgDn, // Move one page down
63 CtrlHome, // Move to the beginning of the document
64 CtrlEnd, // Move to the end of the document
65 WordBack, // Move to the beginning of the previous word (or beginning of line)
66 WordForward // Move to the beginning of the next word (or end of line)
69 // Being cloneable should allow for nice line and document copies...
70 public class Line : ICloneable, IComparable {
71 #region Local Variables
72 // Stuff that matters for our line
73 internal StringBuilder text; // Characters for the line
74 internal float[] widths; // Width of each character; always one larger than text.Length
75 internal int space; // Number of elements in text and widths
76 internal int line_no; // Line number
77 internal LineTag tags; // Tags describing the text
78 internal int Y; // Baseline
79 internal int height; // Height of the line (height of tallest tag)
80 internal int ascent; // Ascent of the line (ascent of the tallest tag)
82 // Stuff that's important for the tree
83 internal Line parent; // Our parent line
84 public Line left; // Line with smaller line number
85 public Line right; // Line with higher line number
86 internal LineColor color; // We're doing a black/red tree. this is the node color
87 internal int DEFAULT_TEXT_LEN; //
88 internal static StringFormat string_format; // For calculating widths/heights
89 internal bool recalc; // Line changed
90 #endregion // Local Variables
94 color = LineColor.Red;
101 if (string_format == null) {
102 string_format = new StringFormat(StringFormat.GenericTypographic);
103 string_format.Trimming = StringTrimming.None;
104 string_format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;
108 public Line(int LineNo, string Text, Font font, Brush color) : this() {
109 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
111 text = new StringBuilder(Text, space);
114 widths = new float[space + 1];
115 tags = new LineTag(this, 1, text.Length);
120 public Line(int LineNo, string Text, LineTag tag) : this() {
121 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
123 text = new StringBuilder(Text, space);
126 widths = new float[space + 1];
130 #endregion // Constructors
132 #region Public Properties
155 return text.ToString();
159 text = new StringBuilder(value, value.Length > DEFAULT_TEXT_LEN ? value.Length : DEFAULT_TEXT_LEN);
163 public StringBuilder Text {
173 #endregion // Public Properties
175 #region Public Methods
176 // Make sure we always have enoughs space in text and widths
177 public void Grow(int minimum) {
181 length = text.Length;
183 if ((length + minimum) > space) {
184 // We need to grow; double the size
186 if ((length + minimum) > (space * 2)) {
187 new_widths = new float[length + minimum * 2 + 1];
188 space = length + minimum * 2;
190 new_widths = new float[space * 2 + 1];
193 widths.CopyTo(new_widths, 0);
199 public void Streamline() {
206 // Catch what the loop below wont; eliminate 0 length
207 // tags, but only if there are other tags after us
208 while ((current.length == 0) && (next != null)) {
218 while (next != null) {
219 // Take out 0 length tags
220 if (next.length == 0) {
221 current.next = next.next;
222 if (current.next != null) {
223 current.next.previous = current;
229 if (current.Combine(next)) {
234 current = current.next;
239 // Find the tag on a line based on the character position
240 public LineTag FindTag(int pos) {
249 if (pos > text.Length) {
253 while (tag != null) {
254 if ((tag.start <= pos) && (pos < (tag.start + tag.length))) {
264 // Go through all tags on a line and recalculate all size-related values
265 // returns true if lineheight changed
267 public bool RecalculateLine(Graphics g) {
276 len = this.text.Length;
278 prev_height = this.height; // For drawing optimization calculations
279 this.height = 0; // Reset line height
280 this.ascent = 0; // Reset the ascent for the line
287 size = g.MeasureString(this.text.ToString(pos, 1), tag.font, 10000, string_format);
295 widths[pos] = widths[pos-1] + w;
297 if (pos == (tag.start-1 + tag.length)) {
298 // We just found the end of our current tag
299 tag.height = (int)tag.font.Height;
301 // Check if we're the tallest on the line (so far)
302 if (tag.height > this.height) {
303 this.height = tag.height; // Yep; make sure the line knows
306 if (tag.ascent == 0) {
309 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
312 if (tag.ascent > this.ascent) {
315 // We have a tag that has a taller ascent than the line;
319 t.shift = tag.ascent - t.ascent;
324 this.ascent = tag.ascent;
326 tag.shift = this.ascent - tag.ascent;
329 // Update our horizontal starting pixel position
330 if (tag.previous == null) {
333 tag.X = tag.previous.X + (int)tag.previous.width;
344 if (this.height == 0) {
345 this.height = tags.font.Height;
346 tag.height = this.height;
349 if (prev_height != this.height) {
354 #endregion // Public Methods
356 #region Administrative
357 public int CompareTo(object obj) {
362 if (! (obj is Line)) {
363 throw new ArgumentException("Object is not of type Line", "obj");
366 if (line_no < ((Line)obj).line_no) {
368 } else if (line_no > ((Line)obj).line_no) {
375 public object Clone() {
383 clone.left = (Line)left.Clone();
387 clone.left = (Line)left.Clone();
393 public object CloneLine() {
403 public override bool Equals(object obj) {
408 if (!(obj is Line)) {
416 if (line_no == ((Line)obj).line_no) {
424 public override string ToString() {
425 return "Line " + line_no;
428 #endregion // Administrative
431 public class Document : ICloneable, IEnumerable {
433 internal struct Marker {
435 internal LineTag tag;
439 #endregion Structures
441 #region Local Variables
442 private Line document;
444 private static Line sentinel;
445 private Line last_found;
446 private int document_id;
447 private Random random = new Random();
449 internal bool multiline;
452 internal Marker caret;
453 internal Marker selection_start;
454 internal Marker selection_end;
455 internal bool selection_visible;
457 internal int viewport_x;
458 internal int viewport_y; // The visible area of the document
460 internal int document_x; // Width of the document
461 internal int document_y; // Height of the document
463 internal Control owner; // Who's owning us?
464 #endregion // Local Variables
467 public Document(Control owner) {
474 // Tree related stuff
475 sentinel = new Line();
476 sentinel.color = LineColor.Black;
479 last_found = sentinel;
481 // We always have a blank line
482 Add(1, "", owner.Font, new SolidBrush(owner.ForeColor));
483 this.RecalculateDocument(owner.CreateGraphics());
487 selection_visible = false;
488 selection_start.line = this.document;
489 selection_start.pos = 0;
490 selection_end.line = this.document;
491 selection_end.pos = 0;
495 // Default selection is empty
497 document_id = random.Next();
501 #region Public Properties
518 public Line CaretLine {
524 public int CaretPosition {
530 public LineTag CaretTag {
536 public int ViewPortX {
546 public int ViewPortY {
558 return this.document_x;
564 return this.document_y;
568 #endregion // Public Properties
570 #region Private Methods
572 internal void DumpTree(Line line, bool with_tags) {
573 Console.Write("Line {0}, Y: {1} Text {2}", line.line_no, line.Y, line.text != null ? line.text.ToString() : "undefined");
575 if (line.left == sentinel) {
576 Console.Write(", left = sentinel");
577 } else if (line.left == null) {
578 Console.Write(", left = NULL");
581 if (line.right == sentinel) {
582 Console.Write(", right = sentinel");
583 } else if (line.right == null) {
584 Console.Write(", right = NULL");
587 Console.WriteLine("");
595 Console.Write(" Tags: ");
596 while (tag != null) {
597 Console.Write("{0} <{1}>-<{2}> ", count++, tag.start, tag.length);
598 if (tag.line != line) {
599 Console.Write("BAD line link");
600 throw new Exception("Bad line link in tree");
607 Console.WriteLine("");
609 if (line.left != null) {
610 if (line.left != sentinel) {
611 DumpTree(line.left, with_tags);
614 if (line != sentinel) {
615 throw new Exception("Left should not be NULL");
619 if (line.right != null) {
620 if (line.right != sentinel) {
621 DumpTree(line.right, with_tags);
624 if (line != sentinel) {
625 throw new Exception("Right should not be NULL");
630 private void DecrementLines(int line_no) {
634 while (current <= lines) {
635 GetLine(current).line_no--;
641 private void IncrementLines(int line_no) {
644 current = this.lines;
645 while (current >= line_no) {
646 GetLine(current).line_no++;
652 private void RebalanceAfterAdd(Line line1) {
655 while ((line1 != document) && (line1.parent.color == LineColor.Red)) {
656 if (line1.parent == line1.parent.parent.left) {
657 line2 = line1.parent.parent.right;
659 if ((line2 != null) && (line2.color == LineColor.Red)) {
660 line1.parent.color = LineColor.Black;
661 line2.color = LineColor.Black;
662 line1.parent.parent.color = LineColor.Red;
663 line1 = line1.parent.parent;
665 if (line1 == line1.parent.right) {
666 line1 = line1.parent;
670 line1.parent.color = LineColor.Black;
671 line1.parent.parent.color = LineColor.Red;
673 RotateRight(line1.parent.parent);
676 line2 = line1.parent.parent.left;
678 if ((line2 != null) && (line2.color == LineColor.Red)) {
679 line1.parent.color = LineColor.Black;
680 line2.color = LineColor.Black;
681 line1.parent.parent.color = LineColor.Red;
682 line1 = line1.parent.parent;
684 if (line1 == line1.parent.left) {
685 line1 = line1.parent;
689 line1.parent.color = LineColor.Black;
690 line1.parent.parent.color = LineColor.Red;
691 RotateLeft(line1.parent.parent);
695 document.color = LineColor.Black;
698 private void RebalanceAfterDelete(Line line1) {
701 while ((line1 != document) && (line1.color == LineColor.Black)) {
702 if (line1 == line1.parent.left) {
703 line2 = line1.parent.right;
704 if (line2.color == LineColor.Red) {
705 line2.color = LineColor.Black;
706 line1.parent.color = LineColor.Red;
707 RotateLeft(line1.parent);
708 line2 = line1.parent.right;
710 if ((line2.left.color == LineColor.Black) && (line2.right.color == LineColor.Black)) {
711 line2.color = LineColor.Red;
712 line1 = line1.parent;
714 if (line2.right.color == LineColor.Black) {
715 line2.left.color = LineColor.Black;
716 line2.color = LineColor.Red;
718 line2 = line1.parent.right;
720 line2.color = line1.parent.color;
721 line1.parent.color = LineColor.Black;
722 line2.right.color = LineColor.Black;
723 RotateLeft(line1.parent);
727 line2 = line1.parent.left;
728 if (line2.color == LineColor.Red) {
729 line2.color = LineColor.Black;
730 line1.parent.color = LineColor.Red;
731 RotateRight(line1.parent);
732 line2 = line1.parent.left;
734 if ((line2.right.color == LineColor.Black) && (line2.left.color == LineColor.Black)) {
735 line2.color = LineColor.Red;
736 line1 = line1.parent;
738 if (line2.left.color == LineColor.Black) {
739 line2.right.color = LineColor.Black;
740 line2.color = LineColor.Red;
742 line2 = line1.parent.left;
744 line2.color = line1.parent.color;
745 line1.parent.color = LineColor.Black;
746 line2.left.color = LineColor.Black;
747 RotateRight(line1.parent);
752 line1.color = LineColor.Black;
755 private void RotateLeft(Line line1) {
756 Line line2 = line1.right;
758 line1.right = line2.left;
760 if (line2.left != sentinel) {
761 line2.left.parent = line1;
764 if (line2 != sentinel) {
765 line2.parent = line1.parent;
768 if (line1.parent != null) {
769 if (line1 == line1.parent.left) {
770 line1.parent.left = line2;
772 line1.parent.right = line2;
779 if (line1 != sentinel) {
780 line1.parent = line2;
784 private void RotateRight(Line line1) {
785 Line line2 = line1.left;
787 line1.left = line2.right;
789 if (line2.right != sentinel) {
790 line2.right.parent = line1;
793 if (line2 != sentinel) {
794 line2.parent = line1.parent;
797 if (line1.parent != null) {
798 if (line1 == line1.parent.right) {
799 line1.parent.right = line2;
801 line1.parent.left = line2;
808 if (line1 != sentinel) {
809 line1.parent = line2;
814 public void UpdateView(Line line, int pos) {
817 // This is an optimization; we need to invalidate
818 prev_width = (int)line.widths[line.text.Length];
820 if (RecalculateDocument(owner.CreateGraphics(), line.line_no, line.line_no, true)) {
821 // Lineheight changed, invalidate the rest of the document
822 if ((line.Y - viewport_y) >=0 ) {
823 // We formatted something that's in view, only draw parts of the screen
824 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, owner.Width, owner.Height - line.Y - viewport_y));
826 // The tag was above the visible area, draw everything
830 owner.Invalidate(new Rectangle((int)line.widths[pos] - viewport_x - 1, line.Y - viewport_y, (int)owner.Width, line.height));
835 // Update display from line, down line_count lines; pos is unused, but required for the signature
836 public void UpdateView(Line line, int line_count, int pos) {
839 // This is an optimization; we need to invalidate
840 prev_width = (int)line.widths[line.text.Length];
842 if (RecalculateDocument(owner.CreateGraphics(), line.line_no, line.line_no + line_count - 1, true)) {
843 // Lineheight changed, invalidate the rest of the document
844 if ((line.Y - viewport_y) >=0 ) {
845 // We formatted something that's in view, only draw parts of the screen
846 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, owner.Width, owner.Height - line.Y - viewport_y));
848 // The tag was above the visible area, draw everything
854 end_line = GetLine(line.line_no + line_count -1);
855 if (end_line == null) {
859 owner.Invalidate(new Rectangle(0 - viewport_x, line.Y - viewport_y, (int)line.widths[line.text.Length], end_line.Y + end_line.height));
862 #endregion // Private Methods
864 #region Public Methods
865 // Clear the document and reset state
866 public void Empty() {
869 last_found = sentinel;
872 // We always have a blank line
873 Add(1, "", owner.Font, new SolidBrush(owner.ForeColor));
874 this.RecalculateDocument(owner.CreateGraphics());
877 selection_visible = false;
878 selection_start.line = this.document;
879 selection_start.pos = 0;
880 selection_end.line = this.document;
881 selection_end.pos = 0;
890 public void PositionCaret(Line line, int pos) {
891 caret.tag = line.FindTag(pos);
894 caret.height = caret.tag.height;
896 XplatUI.DestroyCaret(owner.Handle);
897 XplatUI.CreateCaret(owner.Handle, 2, caret.height);
898 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] - viewport_x, caret.tag.line.Y + caret.tag.shift - viewport_y);
901 public void PositionCaret(int x, int y) {
902 caret.tag = FindCursor(x + viewport_x, y + viewport_y, out caret.pos);
903 caret.line = caret.tag.line;
904 caret.height = caret.tag.height;
906 XplatUI.DestroyCaret(owner.Handle);
907 XplatUI.CreateCaret(owner.Handle, 2, caret.height);
908 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] - viewport_x, caret.tag.line.Y + caret.tag.shift - viewport_y);
911 public void CaretHasFocus() {
912 if (caret.tag != null) {
913 XplatUI.CreateCaret(owner.Handle, 2, caret.height);
914 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] - viewport_x, caret.tag.line.Y + caret.tag.shift - viewport_y);
915 XplatUI.CaretVisible(owner.Handle, true);
919 public void CaretLostFocus() {
920 XplatUI.DestroyCaret(owner.Handle);
923 public void AlignCaret() {
924 caret.tag = LineTag.FindTag(caret.line, caret.pos);
925 caret.height = caret.tag.height;
927 XplatUI.CreateCaret(owner.Handle, 2, caret.height);
928 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] - viewport_x, caret.tag.line.Y + caret.tag.shift - viewport_y);
929 XplatUI.CaretVisible(owner.Handle, true);
932 public void UpdateCaret() {
933 if (caret.tag.height != caret.height) {
934 caret.height = caret.tag.height;
935 XplatUI.CreateCaret(owner.Handle, 2, caret.height);
937 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] - viewport_x, caret.tag.line.Y + caret.tag.shift - viewport_y);
938 XplatUI.CaretVisible(owner.Handle, true);
941 public void DisplayCaret() {
942 XplatUI.CaretVisible(owner.Handle, true);
945 public void HideCaret() {
946 XplatUI.CaretVisible(owner.Handle, false);
949 public void MoveCaret(CaretDirection direction) {
951 case CaretDirection.CharForward: {
953 if (caret.pos > caret.line.text.Length) {
956 if (caret.line.line_no < this.lines) {
957 caret.line = GetLine(caret.line.line_no+1);
959 caret.tag = caret.line.tags;
964 // Single line; we stay where we are
968 if ((caret.tag.start - 1 + caret.tag.length) < caret.pos) {
969 caret.tag = caret.tag.next;
976 case CaretDirection.CharBack: {
978 // caret.pos--; // folded into the if below
979 if (--caret.pos > 0) {
980 if (caret.tag.start > caret.pos) {
981 caret.tag = caret.tag.previous;
985 if (caret.line.line_no > 1) {
986 caret.line = GetLine(caret.line.line_no - 1);
987 caret.pos = caret.line.text.Length;
988 caret.tag = LineTag.FindTag(caret.line, caret.pos);
995 case CaretDirection.WordForward: {
998 len = caret.line.text.Length;
999 if (caret.pos < len) {
1000 while ((caret.pos < len) && (caret.line.text.ToString(caret.pos, 1) != " ")) {
1003 if (caret.pos < len) {
1004 // Skip any whitespace
1005 while ((caret.pos < len) && (caret.line.text.ToString(caret.pos, 1) == " ")) {
1010 if (caret.line.line_no < this.lines) {
1011 caret.line = GetLine(caret.line.line_no+1);
1013 caret.tag = caret.line.tags;
1020 case CaretDirection.WordBack: {
1021 if (caret.pos > 0) {
1024 len = caret.line.text.Length;
1028 while ((caret.pos > 0) && (caret.line.text.ToString(caret.pos, 1) == " ")) {
1032 while ((caret.pos > 0) && (caret.line.text.ToString(caret.pos, 1) != " ")) {
1036 if (caret.line.text.ToString(caret.pos, 1) == " ") {
1037 if (caret.pos != 0) {
1040 caret.line = GetLine(caret.line.line_no - 1);
1041 caret.pos = caret.line.text.Length;
1042 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1046 if (caret.line.line_no > 1) {
1047 caret.line = GetLine(caret.line.line_no - 1);
1048 caret.pos = caret.line.text.Length;
1049 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1056 case CaretDirection.LineUp: {
1057 if (caret.line.line_no > 1) {
1060 pixel = (int)caret.line.widths[caret.pos];
1061 PositionCaret(pixel, GetLine(caret.line.line_no - 1).Y);
1062 XplatUI.CaretVisible(owner.Handle, true);
1067 case CaretDirection.LineDown: {
1068 if (caret.line.line_no < lines) {
1071 pixel = (int)caret.line.widths[caret.pos];
1072 PositionCaret(pixel, GetLine(caret.line.line_no + 1).Y);
1073 XplatUI.CaretVisible(owner.Handle, true);
1078 case CaretDirection.Home: {
1079 if (caret.pos > 0) {
1081 caret.tag = caret.line.tags;
1087 case CaretDirection.End: {
1088 if (caret.pos < caret.line.text.Length) {
1089 caret.pos = caret.line.text.Length;
1090 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1096 case CaretDirection.PgUp: {
1100 case CaretDirection.PgDn: {
1104 case CaretDirection.CtrlHome: {
1105 caret.line = GetLine(1);
1107 caret.tag = caret.line.tags;
1113 case CaretDirection.CtrlEnd: {
1114 caret.line = GetLine(lines);
1116 caret.tag = caret.line.tags;
1124 // Draw the document
1125 public void Draw(Graphics g, Rectangle clip) {
1126 Line line; // Current line being drawn
1127 LineTag tag; // Current tag being drawn
1128 int start; // First line to draw
1129 int end; // Last line to draw
1130 string s; // String representing the current line
1133 // First, figure out from what line to what line we need to draw
1134 start = GetLineByPixel(clip.Top - viewport_y, false).line_no;
1135 end = GetLineByPixel(clip.Bottom - viewport_y, false).line_no;
1137 // Now draw our elements; try to only draw those that are visible
1141 DateTime n = DateTime.Now;
1142 Console.WriteLine("Started drawing: {0}s {1}ms", n.Second, n.Millisecond);
1145 while (line_no <= end) {
1146 line = GetLine(line_no);
1148 s = line.text.ToString();
1149 while (tag != null) {
1150 if (((tag.X + tag.width) > (clip.Left - viewport_x)) || (tag.X < (clip.Right - viewport_x))) {
1151 // Check for selection
1152 if ((!selection_visible) || (line_no < selection_start.line.line_no) || (line_no > selection_end.line.line_no)) {
1154 g.DrawString(s.Substring(tag.start-1, tag.length), tag.font, tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1156 // we might have to draw our selection
1157 if ((line_no != selection_start.line.line_no) && (line_no != selection_end.line.line_no)) {
1158 g.FillRectangle(tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, line.widths[tag.start + tag.length - 1], tag.height);
1159 g.DrawString(s.Substring(tag.start-1, tag.length), tag.font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1168 // Check the partial drawings first
1169 if ((selection_start.tag == tag) && (selection_end.tag == tag)) {
1172 // First, the regular part
1173 g.DrawString(s.Substring(tag.start - 1, selection_start.pos - tag.start + 1), tag.font, tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1175 // Now the highlight
1176 g.FillRectangle(tag.color, line.widths[selection_start.pos], line.Y + tag.shift - viewport_y, line.widths[selection_end.pos] - line.widths[selection_start.pos], tag.height);
1177 g.DrawString(s.Substring(selection_start.pos, selection_end.pos - selection_start.pos), tag.font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), line.widths[selection_start.pos], line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1179 // And back to the regular
1180 g.DrawString(s.Substring(selection_end.pos, tag.length - selection_end.pos), tag.font, tag.color, line.widths[selection_end.pos], line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1181 } else if (selection_start.tag == tag) {
1184 // The highlighted part
1185 g.FillRectangle(tag.color, line.widths[selection_start.pos], line.Y + tag.shift - viewport_y, line.widths[tag.start + tag.length - 1], tag.height);
1186 g.DrawString(s.Substring(selection_start.pos, tag.length - selection_start.pos), tag.font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), line.widths[selection_start.pos], line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1189 g.DrawString(s.Substring(tag.start - 1, selection_start.pos - tag.start + 1), tag.font, tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1190 } else if (selection_end.tag == tag) {
1193 // The highlighted part
1194 g.FillRectangle(tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, line.widths[selection_end.pos], tag.height);
1195 g.DrawString(s.Substring(tag.start - 1, selection_end.pos - tag.start + 1), tag.font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1198 g.DrawString(s.Substring(selection_end.pos, tag.length - selection_end.pos), tag.font, tag.color, line.widths[selection_end.pos], line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1200 // no partially selected tags here, simple checks...
1201 if (selection_start.line == line) {
1202 if ((tag.start + tag.length - 1) > selection_start.pos) {
1206 if (selection_end.line == line) {
1207 if ((tag.start + tag.length - 1) < selection_start.pos) {
1215 g.FillRectangle(tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, line.widths[tag.start + tag.length - 1], tag.height);
1216 g.DrawString(s.Substring(tag.start-1, tag.length), tag.font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1218 g.DrawString(s.Substring(tag.start-1, tag.length), tag.font, tag.color, tag.X - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1233 Console.WriteLine("Finished drawing: {0}s {1}ms", n.Second, n.Millisecond);
1239 // Inserts a character at the given position
1240 public void InsertString(Line line, int pos, string s) {
1241 InsertString(line.FindTag(pos), pos, s);
1244 // Inserts a string at the given position
1245 public void InsertString(LineTag tag, int pos, string s) {
1252 line.text.Insert(pos, s);
1256 while (tag != null) {
1263 UpdateView(line, pos);
1266 // Inserts a string at the caret position
1267 public void InsertStringAtCaret(string s, bool move_caret) {
1273 caret.line.text.Insert(caret.pos, s);
1274 caret.tag.length += len;
1276 if (caret.tag.next != null) {
1277 tag = caret.tag.next;
1278 while (tag != null) {
1283 caret.line.Grow(len);
1284 caret.line.recalc = true;
1286 UpdateView(caret.line, caret.pos);
1295 // Inserts a character at the given position
1296 public void InsertChar(Line line, int pos, char ch) {
1297 InsertChar(line.FindTag(pos), pos, ch);
1300 // Inserts a character at the given position
1301 public void InsertChar(LineTag tag, int pos, char ch) {
1305 line.text.Insert(pos, ch);
1309 while (tag != null) {
1316 UpdateView(line, pos);
1319 // Inserts a character at the current caret position
1320 public void InsertCharAtCaret(char ch, bool move_caret) {
1323 caret.line.text.Insert(caret.pos, ch);
1326 if (caret.tag.next != null) {
1327 tag = caret.tag.next;
1328 while (tag != null) {
1334 caret.line.recalc = true;
1336 UpdateView(caret.line, caret.pos);
1343 // Inserts n characters at the given position; it will not delete past line limits
1344 public void DeleteChars(LineTag tag, int pos, int count) {
1352 if (pos == line.text.Length) {
1356 line.text.Remove(pos, count);
1358 // Check if we're crossing tag boundaries
1359 if ((pos + count) > (tag.start + tag.length)) {
1362 // We have to delete cross tag boundaries
1366 left -= pos - tag.start;
1367 tag.length -= pos - tag.start;
1370 while ((tag != null) && (left > 0)) {
1371 if (tag.length > left) {
1382 // We got off easy, same tag
1384 tag.length -= count;
1388 while (tag != null) {
1398 UpdateView(line, pos);
1402 // Deletes a character at or after the given position (depending on forward); it will not delete past line limits
1403 public void DeleteChar(LineTag tag, int pos, bool forward) {
1410 if ((pos == 0 && forward == false) || (pos == line.text.Length && forward == true)) {
1415 line.text.Remove(pos, 1);
1418 if (tag.length == 0) {
1423 line.text.Remove(pos, 1);
1424 if (pos >= (tag.start - 1)) {
1426 if (tag.length == 0) {
1429 } else if (tag.previous != null) {
1430 tag.previous.length--;
1431 if (tag.previous.length == 0) {
1438 while (tag != null) {
1447 UpdateView(line, pos);
1450 // Combine two lines
1451 public void Combine(int FirstLine, int SecondLine) {
1452 Combine(GetLine(FirstLine), GetLine(SecondLine));
1455 public void Combine(Line first, Line second) {
1459 // Combine the two tag chains into one
1462 while (last.next != null) {
1466 last.next = second.tags;
1467 last.next.previous = last;
1469 shift = last.start + last.length - 1;
1471 // Fix up references within the chain
1473 while (last != null) {
1475 last.start += shift;
1479 // Combine both lines' strings
1480 first.text.Insert(first.text.Length, second.text.ToString());
1481 first.Grow(first.text.Length);
1483 // Remove the reference to our (now combined) tags from the doomed line
1487 DecrementLines(first.line_no + 2); // first.line_no + 1 will be deleted, so we need to start renumbering one later
1490 first.recalc = true;
1491 first.height = 0; // This forces RecalcDocument/UpdateView to redraw from this line on
1498 check_first = GetLine(first.line_no);
1499 check_second = GetLine(check_first.line_no + 1);
1501 Console.WriteLine("Pre-delete: Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
1504 this.Delete(second);
1507 check_first = GetLine(first.line_no);
1508 check_second = GetLine(check_first.line_no + 1);
1510 Console.WriteLine("Post-delete Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
1515 // Split the line at the position into two
1516 public void Split(int LineNo, int pos) {
1520 line = GetLine(LineNo);
1521 tag = LineTag.FindTag(line, pos);
1522 Split(line, tag, pos);
1525 public void Split(Line line, int pos) {
1528 tag = LineTag.FindTag(line, pos);
1529 Split(line, tag, pos);
1532 public void Split(Line line, LineTag tag, int pos) {
1536 // cover the easy case first
1537 if (pos == line.text.Length) {
1538 Add(line.line_no + 1, "", tag.font, tag.color);
1542 // We need to move the rest of the text into the new line
1543 Add(line.line_no + 1, line.text.ToString(pos, line.text.Length - pos), tag.font, tag.color);
1545 // Now transfer our tags from this line to the next
1546 new_line = GetLine(line.line_no + 1);
1549 if ((tag.start - 1) == pos) {
1552 // We can simply break the chain and move the tag into the next line
1553 if (tag == line.tags) {
1554 new_tag = new LineTag(line, 1, 0);
1555 new_tag.font = tag.font;
1556 new_tag.color = tag.color;
1557 line.tags = new_tag;
1560 if (tag.previous != null) {
1561 tag.previous.next = null;
1563 new_line.tags = tag;
1564 tag.previous = null;
1565 tag.line = new_line;
1567 // Walk the list and correct the start location of the tags we just bumped into the next line
1568 shift = tag.start - 1;
1571 while (new_tag != null) {
1572 new_tag.start -= shift;
1573 new_tag.line = new_line;
1574 new_tag = new_tag.next;
1579 new_tag = new LineTag(new_line, 1, tag.start - 1 + tag.length - pos);
1580 new_tag.next = tag.next;
1581 new_tag.font = tag.font;
1582 new_tag.color = tag.color;
1583 new_line.tags = new_tag;
1584 if (new_tag.next != null) {
1585 new_tag.next.previous = new_tag;
1588 tag.length = pos - tag.start + 1;
1591 new_tag = new_tag.next;
1592 while (new_tag != null) {
1593 new_tag.start -= shift;
1594 new_tag.line = new_line;
1595 new_tag = new_tag.next;
1599 line.text.Remove(pos, line.text.Length - pos);
1602 // Adds a line of text, with given font.
1603 // Bumps any line at that line number that already exists down
1604 public void Add(int LineNo, string Text, Font font, Brush color) {
1609 if (LineNo<1 || Text == null) {
1611 throw new ArgumentNullException("LineNo", "Line numbers must be positive");
1613 throw new ArgumentNullException("Text", "Cannot insert NULL line");
1617 add = new Line(LineNo, Text, font, color);
1620 while (line != sentinel) {
1622 line_no = line.line_no;
1624 if (LineNo > line_no) {
1626 } else if (LineNo < line_no) {
1629 // Bump existing line numbers; walk all nodes to the right of this one and increment line_no
1630 IncrementLines(line.line_no);
1635 add.left = sentinel;
1636 add.right = sentinel;
1638 if (add.parent != null) {
1639 if (LineNo > add.parent.line_no) {
1640 add.parent.right = add;
1642 add.parent.left = add;
1649 RebalanceAfterAdd(add);
1654 public virtual void Clear() {
1656 document = sentinel;
1659 public virtual object Clone() {
1662 clone = new Document(null);
1664 clone.lines = this.lines;
1665 clone.document = (Line)document.Clone();
1670 public void Delete(int LineNo) {
1675 Delete(GetLine(LineNo));
1678 public void Delete(Line line1) {
1679 Line line2;// = new Line();
1682 if ((line1.left == sentinel) || (line1.right == sentinel)) {
1685 line3 = line1.right;
1686 while (line3.left != sentinel) {
1691 if (line3.left != sentinel) {
1694 line2 = line3.right;
1697 line2.parent = line3.parent;
1698 if (line3.parent != null) {
1699 if(line3 == line3.parent.left) {
1700 line3.parent.left = line2;
1702 line3.parent.right = line2;
1708 if (line3 != line1) {
1711 line1.ascent = line3.ascent;
1712 line1.height = line3.height;
1713 line1.line_no = line3.line_no;
1714 line1.recalc = line3.recalc;
1715 line1.space = line3.space;
1716 line1.tags = line3.tags;
1717 line1.text = line3.text;
1718 line1.widths = line3.widths;
1722 while (tag != null) {
1728 if (line3.color == LineColor.Black)
1729 RebalanceAfterDelete(line2);
1733 last_found = sentinel;
1736 // Set our selection markers
1737 public void Invalidate(Line start, int start_pos, Line end, int end_pos) {
1743 // figure out what's before what so the logic below is straightforward
1744 if (start.line_no < end.line_no) {
1750 } else if (start.line_no > end.line_no) {
1757 if (start_pos < end_pos) {
1771 owner.Invalidate(new Rectangle((int)l1.widths[p1] - viewport_x, l1.Y - viewport_y, (int)l2.widths[p2] - (int)l1.widths[p1], l1.height));
1775 // Three invalidates:
1776 // First line from start
1777 owner.Invalidate(new Rectangle((int)l1.widths[p1] - viewport_x, l1.Y - viewport_y, owner.Width, l1.height));
1780 if ((l1.line_no + 1) < l2.line_no) {
1783 y = GetLine(l1.line_no + 1).Y;
1784 owner.Invalidate(new Rectangle(0 - viewport_x, y - viewport_y, owner.Width, GetLine(l2.line_no - 1).Y - y - viewport_y));
1788 owner.Invalidate(new Rectangle((int)l1.widths[p1] - viewport_x, l1.Y - viewport_y, owner.Width, l1.height));
1793 // It's nothing short of pathetic to always invalidate the whole control
1794 // I will find time to finish the optimization and make it invalidate deltas only
1795 public void SetSelectionToCaret(bool start) {
1797 selection_start.line = caret.line;
1798 selection_start.tag = caret.tag;
1799 selection_start.pos = caret.pos;
1801 // start always also selects end
1802 selection_end.line = caret.line;
1803 selection_end.tag = caret.tag;
1804 selection_end.pos = caret.pos;
1806 selection_end.line = caret.line;
1807 selection_end.tag = caret.tag;
1808 selection_end.pos = caret.pos;
1812 if ((selection_start.line == selection_end.line) && (selection_start.pos == selection_end.pos)) {
1813 selection_visible = false;
1815 selection_visible = true;
1821 public void SetSelection(Line start, int start_pos, Line end, int end_pos) {
1822 // Ok, this is ugly, bad and slow, but I don't have time right now to optimize it.
1823 if (selection_visible) {
1824 // Try to only invalidate what's changed so we don't redraw the whole thing
1825 if ((start != selection_start.line) || (start_pos != selection_start.pos) && ((end == selection_end.line) && (end_pos == selection_end.pos))) {
1826 Invalidate(start, start_pos, selection_start.line, selection_start.pos);
1827 } else if ((end != selection_end.line) || (end_pos != selection_end.pos) && ((start == selection_start.line) && (start_pos == selection_start.pos))) {
1828 Invalidate(end, end_pos, selection_end.line, selection_end.pos);
1830 // both start and end changed
1831 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
1834 selection_start.line = start;
1835 selection_start.pos = start_pos;
1836 if (start != null) {
1837 selection_start.tag = LineTag.FindTag(start, start_pos);
1840 selection_end.line = end;
1841 selection_end.pos = end_pos;
1843 selection_end.tag = LineTag.FindTag(end, end_pos);
1846 if (((start == end) && (start_pos == end_pos)) || start == null || end == null) {
1847 selection_visible = false;
1849 selection_visible = true;
1854 // Make sure that start is always before end
1855 private void FixupSelection() {
1856 if (selection_start.line.line_no > selection_end.line.line_no) {
1861 line = selection_start.line;
1862 tag = selection_start.tag;
1863 pos = selection_start.pos;
1865 selection_start.line = selection_end.line;
1866 selection_start.tag = selection_end.tag;
1867 selection_start.pos = selection_end.pos;
1869 selection_end.line = line;
1870 selection_end.tag = tag;
1871 selection_end.pos = pos;
1876 if ((selection_start.line == selection_end.line) && (selection_start.pos > selection_end.pos)) {
1879 pos = selection_start.pos;
1880 selection_start.pos = selection_end.pos;
1881 selection_end.pos = pos;
1888 public void SetSelectionStart(Line start, int start_pos) {
1889 selection_start.line = start;
1890 selection_start.pos = start_pos;
1891 selection_start.tag = LineTag.FindTag(start, start_pos);
1895 if ((selection_end.line != selection_start.line) && (selection_end.pos != selection_start.pos)) {
1896 selection_visible = true;
1899 if (selection_visible) {
1900 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
1905 public void SetSelectionEnd(Line end, int end_pos) {
1906 selection_end.line = end;
1907 selection_end.pos = end_pos;
1908 selection_end.tag = LineTag.FindTag(end, end_pos);
1912 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
1913 selection_visible = true;
1916 if (selection_visible) {
1917 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
1921 public void SetSelection(Line start, int start_pos) {
1922 if (selection_visible) {
1923 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
1927 selection_start.line = start;
1928 selection_start.pos = start_pos;
1929 selection_start.tag = LineTag.FindTag(start, start_pos);
1931 selection_end.line = start;
1932 selection_end.tag = selection_start.tag;
1933 selection_end.pos = start_pos;
1935 selection_visible = false;
1938 public void InvalidateSelectionArea() {
1942 // Return the current selection, as string
1943 public string GetSelection() {
1944 // We return String.Empty if there is no selection
1945 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
1946 return string.Empty;
1949 if (!multiline || (selection_start.line == selection_end.line)) {
1950 return selection_start.line.text.ToString(selection_start.pos, selection_end.pos - selection_start.pos);
1957 sb = new StringBuilder();
1958 start = selection_start.line.line_no;
1959 end = selection_end.line.line_no;
1961 sb.Append(selection_start.line.text.ToString(selection_start.pos, selection_start.line.text.Length - selection_start.pos) + "\n");
1963 if ((start + 1) < end) {
1964 for (i = start + 1; i < end; i++) {
1965 sb.Append(GetLine(i).text.ToString() + "\n");
1969 sb.Append(selection_end.line.text.ToString(0, selection_end.pos));
1971 return sb.ToString();
1975 public void ReplaceSelection(string s) {
1976 // The easiest is to break the lines where the selection tags are and delete those lines
1977 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
1978 // Nothing to delete, simply insert
1979 InsertString(selection_start.tag, selection_start.pos, s);
1982 if (!multiline || (selection_start.line == selection_end.line)) {
1983 DeleteChars(selection_start.tag, selection_start.pos, selection_end.pos - selection_start.pos);
1985 // The tag might have been removed, we need to recalc it
1986 selection_start.tag = selection_start.line.FindTag(selection_start.pos);
1988 InsertString(selection_start.tag, selection_start.pos, s);
1997 start = selection_start.line.line_no;
1998 end = selection_end.line.line_no;
2000 // Delete first line
2001 DeleteChars(selection_start.tag, selection_start.pos, selection_end.pos - selection_start.pos);
2005 for (i = end - 1; i >= start; i++) {
2011 DeleteChars(selection_end.line.tags, 0, selection_end.pos);
2013 ins = s.Split(new char[] {'\n'});
2015 insert_lines = ins.Length;
2017 // Bump the text at insertion point a line down if we're inserting more than one line
2018 if (insert_lines > 1) {
2019 Split(selection_start.line, selection_start.pos);
2021 // Reminder of start line is now in startline+1
2023 // if the last line does not end with a \n we will insert the last line in front of the just moved text
2024 if (s.EndsWith("\n")) {
2025 insert_lines--; // We don't want to insert the last line as part of the loop anymore
2027 InsertString(GetLine(selection_start.line.line_no + 1), 0, ins[insert_lines - 1]);
2031 // Insert the first line
2032 InsertString(selection_start.line, selection_start.pos, ins[0]);
2035 if (insert_lines > 1) {
2036 base_line = selection_start.line.line_no + 1;
2038 for (i = 1; i < insert_lines; i++) {
2039 Add(base_line + i, ins[i], selection_start.tag.font, selection_start.tag.color);
2043 selection_end.line = selection_start.line;
2044 selection_end.pos = selection_start.pos;
2045 selection_end.tag = selection_start.tag;
2046 selection_visible = false;
2047 InvalidateSelectionArea();
2050 public int SelectionLength() {
2051 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
2055 if (!multiline || (selection_start.line == selection_end.line)) {
2056 return selection_end.pos - selection_start.pos;
2064 // Count first and last line
2065 length = selection_start.line.text.Length - selection_start.pos + selection_end.pos;
2067 // Count the lines in the middle
2068 start = selection_start.line.line_no + 1;
2069 end = selection_end.line.line_no;
2072 for (i = start; i < end; i++) {
2073 length += GetLine(i).text.Length;
2084 // Give it a Line number and it returns the Line object at with that line number
2085 public Line GetLine(int LineNo) {
2086 Line line = document;
2088 while (line != sentinel) {
2089 if (LineNo == line.line_no) {
2091 } else if (LineNo < line.line_no) {
2101 // Give it a Y pixel coordinate and it returns the Line covering that Y coordinate
2103 public Line GetLineByPixel(int y, bool exact) {
2104 Line line = document;
2107 while (line != sentinel) {
2109 if ((y >= line.Y) && (y < (line.Y+line.height))) {
2111 } else if (y < line.Y) {
2124 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
2125 public LineTag FindTag(int x, int y, out int index, bool exact) {
2129 line = GetLineByPixel(y, exact);
2137 if (x >= tag.X && x < (tag.X+tag.width)) {
2140 end = tag.start + tag.length - 1;
2142 for (int pos = tag.start; pos < end; pos++) {
2143 if (x < line.widths[pos]) {
2151 if (tag.next != null) {
2159 index = line.text.Length;
2165 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
2166 public LineTag FindCursor(int x, int y, out int index) {
2170 line = GetLineByPixel(y, false);
2174 if (x >= tag.X && x < (tag.X+tag.width)) {
2177 end = tag.start + tag.length - 1;
2179 for (int pos = tag.start-1; pos < end; pos++) {
2180 // When clicking on a character, we position the cursor to whatever edge
2181 // of the character the click was closer
2182 if (x < (line.widths[pos] + ((line.widths[pos+1]-line.widths[pos])/2))) {
2190 if (tag.next != null) {
2193 index = line.text.Length;
2199 // Calculate formatting for the whole document
2200 public bool RecalculateDocument(Graphics g) {
2201 return RecalculateDocument(g, 1, this.lines, false);
2204 // Calculate formatting starting at a certain line
2205 public bool RecalculateDocument(Graphics g, int start) {
2206 return RecalculateDocument(g, start, this.lines, false);
2209 // Calculate formatting within two given line numbers
2210 public bool RecalculateDocument(Graphics g, int start, int end) {
2211 return RecalculateDocument(g, start, end, false);
2214 // With optimize on, returns true if line heights changed
2215 public bool RecalculateDocument(Graphics g, int start, int end, bool optimize) {
2220 Y = GetLine(start).Y;
2227 while (line_no <= end) {
2228 line = GetLine(line_no++);
2231 if (line.RecalculateLine(g)) {
2233 // If the height changed, all subsequent lines change
2236 if (line.widths[line.text.Length] > this.document_x) {
2237 this.document_x = (int)line.widths[line.text.Length];
2246 while (line_no <= end) {
2247 line = GetLine(line_no++);
2249 line.RecalculateLine(g);
2250 if (line.widths[line.text.Length] > this.document_x) {
2251 this.document_x = (int)line.widths[line.text.Length];
2259 public bool SetCursor(int x, int y) {
2266 #endregion // Public Methods
2268 #region Administrative
2269 public IEnumerator GetEnumerator() {
2274 public override bool Equals(object obj) {
2279 if (!(obj is Document)) {
2287 if (ToString().Equals(((Document)obj).ToString())) {
2294 public override int GetHashCode() {
2298 public override string ToString() {
2299 return "document " + this.document_id;
2302 #endregion // Administrative
2305 public class LineTag {
2306 #region Local Variables;
2307 // Payload; formatting
2308 internal Font font; // System.Drawing.Font object for this tag
2309 internal Brush color; // System.Drawing.Brush object
2312 internal int start; // start, in chars; index into Line.text
2313 internal int length; // length, in chars
2314 internal bool r_to_l; // Which way is the font
2317 internal int height; // Height in pixels of the text this tag describes
2318 internal int X; // X location of the text this tag describes
2319 internal float width; // Width in pixels of the text this tag describes
2320 internal int ascent; // Ascent of the font for this tag
2321 internal int shift; // Shift down for this tag, to stay on baseline
2323 internal int soft_break; // Tag is 'broken soft' and continues in the next line
2326 internal Line line; // The line we're on
2327 internal LineTag next; // Next tag on the same line
2328 internal LineTag previous; // Previous tag on the same line
2331 #region Constructors
2332 public LineTag(Line line, int start, int length) {
2335 this.length = length;
2339 #endregion // Constructors
2341 #region Public Methods
2343 // Applies 'font' to characters starting at 'start' for 'length' chars
2344 // Removes any previous tags overlapping the same area
2345 // returns true if lineheight has changed
2347 public static bool FormatText(Line line, int start, int length, Font font, Brush color) {
2354 bool retval = false; // Assume line-height doesn't change
2357 if (font.Height != line.height) {
2360 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
2362 // A little sanity, not sure if it's needed, might be able to remove for speed
2363 if (length > line.text.Length) {
2364 length = line.text.Length;
2368 end = start + length;
2371 // Common special case
2372 if ((start == 1) && (length == tag.length)) {
2379 start_tag = FindTag(line, start);
2380 end_tag = FindTag(line, end);
2382 tag = new LineTag(line, start, length);
2390 if (start_tag.start == start) {
2391 tag.next = start_tag;
2392 tag.previous = start_tag.previous;
2393 if (start_tag.previous != null) {
2394 start_tag.previous.next = tag;
2396 start_tag.previous = tag;
2398 // Insert ourselves 'in the middle'
2399 if ((start_tag.next != null) && (start_tag.next.start < end)) {
2400 tag.next = start_tag.next;
2402 tag.next = new LineTag(line, start_tag.start, start_tag.length);
2403 tag.next.font = start_tag.font;
2404 tag.next.color = start_tag.color;
2406 if (start_tag.next != null) {
2407 tag.next.next = start_tag.next;
2408 tag.next.next.previous = tag.next;
2411 tag.next.previous = tag;
2413 start_tag.length = start - start_tag.start;
2415 tag.previous = start_tag;
2416 start_tag.next = tag;
2418 if (tag.next.start > (tag.start + tag.length)) {
2419 tag.next.length += tag.next.start - (tag.start + tag.length);
2420 tag.next.start = tag.start + tag.length;
2427 while ((tag != null) && (tag.start < end)) {
2428 if ((tag.start + tag.length) <= end) {
2430 tag.previous.next = tag.next;
2431 if (tag.next != null) {
2432 tag.next.previous = tag.previous;
2436 // Adjust the length of the tag
2437 tag.length = (tag.start + tag.length) - end;
2448 // Finds the tag that describes the character at position 'pos' on 'line'
2450 public static LineTag FindTag(Line line, int pos) {
2451 LineTag tag = line.tags;
2453 // Beginning of line is a bit special
2458 while (tag != null) {
2459 if ((tag.start <= pos) && (pos < (tag.start+tag.length))) {
2470 // Combines 'this' tag with 'other' tag.
2472 public bool Combine(LineTag other) {
2473 if (!this.Equals(other)) {
2477 this.width += other.width;
2478 this.length += other.length;
2479 this.next = other.next;
2480 if (this.next != null) {
2481 this.next.previous = this;
2489 // Remove 'this' tag ; to be called when formatting is to be removed
2491 public bool Remove() {
2492 if ((this.start == 1) && (this.next == null)) {
2493 // We cannot remove the only tag
2496 if (this.start != 1) {
2497 this.previous.length += this.length;
2498 this.previous.width = -1;
2499 this.previous.next = this.next;
2500 this.next.previous = this.previous;
2502 this.next.start = 1;
2503 this.next.length += this.length;
2504 this.next.width = -1;
2505 this.line.tags = this.next;
2506 this.next.previous = null;
2513 // Checks if 'this' tag describes the same formatting options as 'obj'
2515 public override bool Equals(object obj) {
2522 if (!(obj is LineTag)) {
2530 other = (LineTag)obj;
2532 if (this.font.Equals(other.font) && this.color.Equals(other.color)) { // FIXME add checking for things like link or type later
2539 public override string ToString() {
2540 return "Tag starts at index " + this.start + "length " + this.length + " text: " + this.line.Text.Substring(this.start-1, this.length) + "Font " + this.font.ToString();
2543 #endregion // Public Methods