From 2ab44f600bd0ecab04d03ba08fdc17ea195babf2 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jean-Philippe=20Bruy=C3=A8re?= Date: Thu, 14 Jan 2021 08:07:43 +0100 Subject: [PATCH] label, cached line width in pixel in LineSpan struct, use stackalloc for a single utf8 conversion for size and print, perfs and samplebase --- Crow/Crow.csproj | 4 +- Crow/Default.style | 5 + Crow/src/DebugUtils/DebugLogger.cs | 22 +- Crow/src/Mono.Cairo/Context.cs | 5 + Crow/src/Widgets/FileDialog.cs | 7 +- Crow/src/Widgets/Label.cs | 132 +-- Crow/src/Widgets/OldLabel.cs | 842 ++++++++++++++++++ Samples/BasicTests/BasicTests.cs | 22 +- Samples/BasicTests/BasicTests.csproj | 3 + Samples/DebugLogAnalyzer/Program.cs | 2 +- Samples/PerfTests/Program.cs | 14 +- .../PerfTests/Properties/launchSettings.json | 2 +- Samples/ShowCase/ShowCase.cs | 9 - Samples/common/SampleBase.cs | 21 + .../ui/Interfaces/Divers/testVisibility.crow | 4 +- .../ui/Interfaces/Experimental/testDock2.crow | 26 - .../{0.crow => 0_single_Label.crow} | 0 .../PerfLabels/0_single_oldLabel.crow | 3 + .../common/ui/Interfaces/PerfLabels/1.crow | 3 - .../{2.crow => 1_single_lab_update.crow} | 0 .../PerfLabels/1_single_old_update.crow | 3 + .../{labels.crow => 2_labels_update.crow} | 24 - .../PerfLabels/2_old_labels_update.crow | 29 + 23 files changed, 1030 insertions(+), 152 deletions(-) create mode 100644 Crow/src/Widgets/OldLabel.cs delete mode 100644 Samples/common/ui/Interfaces/Experimental/testDock2.crow rename Samples/common/ui/Interfaces/PerfLabels/{0.crow => 0_single_Label.crow} (100%) create mode 100644 Samples/common/ui/Interfaces/PerfLabels/0_single_oldLabel.crow delete mode 100644 Samples/common/ui/Interfaces/PerfLabels/1.crow rename Samples/common/ui/Interfaces/PerfLabels/{2.crow => 1_single_lab_update.crow} (100%) create mode 100644 Samples/common/ui/Interfaces/PerfLabels/1_single_old_update.crow rename Samples/common/ui/Interfaces/PerfLabels/{labels.crow => 2_labels_update.crow} (52%) create mode 100644 Samples/common/ui/Interfaces/PerfLabels/2_old_labels_update.crow diff --git a/Crow/Crow.csproj b/Crow/Crow.csproj index 792794aa..ce2bd530 100644 --- a/Crow/Crow.csproj +++ b/Crow/Crow.csproj @@ -23,14 +23,14 @@ True true $(NoWarn);1591;1587;1570;1572;1573;1574 - DESIGN_MODE;_MEASURE_TIME + DESIGN_MODE;MEASURE_TIME false false App.config full - $(DefineConstants);_DEBUG_LOG;DEBUG;TRACE;_DEBUG_DISPOSE;_DEBUG_BINDING;_DEBUG_CLIP_RECTANGLE + $(DefineConstants);_DEBUG;_DEBUG_LOG;TRACE;_DEBUG_DISPOSE;_DEBUG_BINDING;_DEBUG_CLIP_RECTANGLE true diff --git a/Crow/Default.style b/Crow/Default.style index ae09ec1c..104b303c 100644 --- a/Crow/Default.style +++ b/Crow/Default.style @@ -74,6 +74,11 @@ Label { Width = "Fit"; Margin = "0"; } +OldLabel { + Height = "Fit"; + Width = "Fit"; + Margin = "0"; +} TextBox { Background = "White"; Foreground = "Black"; diff --git a/Crow/src/DebugUtils/DebugLogger.cs b/Crow/src/DebugUtils/DebugLogger.cs index 0c3f38dc..ce0fa214 100644 --- a/Crow/src/DebugUtils/DebugLogger.cs +++ b/Crow/src/DebugUtils/DebugLogger.cs @@ -232,22 +232,29 @@ namespace Crow parseTree (pc.getTemplateRoot, level + 1, y + 1); } } + +#endif /// /// Clear all recorded events from logger. /// - public static void Reset () - { + public static void Reset () { +#if DEBUG_LOG lock (logMutex) { - startedEvents.Clear (); + startedEvents.Clear (); events.Clear (); chrono.Restart (); } + Console.WriteLine ($"Crow Debug Log reseted"); +#else + Console.WriteLine ($"Logging disabled, compile Crow with DEBUG and DEBUG_LOG defined to enable logging."); +#endif } /// /// Save recorded events to disk /// - /// Iface. - public static void save(Interface iface, string dbgLogFilePath = "debug.log") { + /// Iface. + public static void Save(Interface iface, string dbgLogFilePath = "debug.log") { +#if DEBUG_LOG lock (logMutex) { foreach (Widget go in iface.GraphicTree) @@ -266,6 +273,10 @@ namespace Crow saveEventList (s, events); } } + Console.WriteLine ($"Crow Debug Log saved to: {dbgLogFilePath}"); +#else + Console.WriteLine ($"Compile Crow with DEBUG and DEBUG_LOG defined to enable logging. No log saved."); +#endif } static void saveEventList (StreamWriter s, List evts, int level = 0) @@ -278,7 +289,6 @@ namespace Crow saveEventList (s, e.Events, level + 1); } } -#endif } } diff --git a/Crow/src/Mono.Cairo/Context.cs b/Crow/src/Mono.Cairo/Context.cs index 57f6c517..9ac41f9f 100644 --- a/Crow/src/Mono.Cairo/Context.cs +++ b/Crow/src/Mono.Cairo/Context.cs @@ -907,6 +907,11 @@ namespace Crow.Cairo { NativeMethods.cairo_text_extents (handle, ref bytes.Slice(0, encodedBytes + 1).GetPinnableReference(), out extents); } + public void ShowText (Span bytes) => + NativeMethods.cairo_show_text (handle, ref bytes.GetPinnableReference ()); + public void TextExtents (Span bytes, out TextExtents extents) => + NativeMethods.cairo_text_extents (handle, ref bytes.GetPinnableReference (), out extents); + public TextExtents GlyphExtents (Glyph[] glyphs) { diff --git a/Crow/src/Widgets/FileDialog.cs b/Crow/src/Widgets/FileDialog.cs index 8c58e632..1a7ec842 100644 --- a/Crow/src/Widgets/FileDialog.cs +++ b/Crow/src/Widgets/FileDialog.cs @@ -106,10 +106,11 @@ namespace Crow SelectedDirectory = e.NewValue.ToString(); } public void goUpDirClick (object sender, MouseButtonEventArgs e){ - string root = Directory.GetDirectoryRoot(CurrentDirectory); - if (CurrentDirectory == root) + string root = Directory.GetDirectoryRoot(CurrentDirectory); + DirectoryInfo parentDir = Directory.GetParent (CurrentDirectory); + if (parentDir == null) return; - CurrentDirectory = Directory.GetParent(CurrentDirectory).FullName; + CurrentDirectory = parentDir.FullName; } void onFileSelect(object sender, MouseButtonEventArgs e){ if (string.IsNullOrEmpty (SelectedFile)) diff --git a/Crow/src/Widgets/Label.cs b/Crow/src/Widgets/Label.cs index 4d4f7f02..5724c7ff 100644 --- a/Crow/src/Widgets/Label.cs +++ b/Crow/src/Widgets/Label.cs @@ -16,13 +16,15 @@ namespace Crow { public readonly int Start; public readonly int End; public readonly int EndIncludingLineBreak; + public int LengthInPixel; public int Length => End - Start; public int LengthIncludingLineBreak => EndIncludingLineBreak - Start; public int LineBreakLength => EndIncludingLineBreak - End; public LineSpan (int start, int end, int endIncludingLineBreak) { Start = start; End = end; - EndIncludingLineBreak = endIncludingLineBreak; + EndIncludingLineBreak = endIncludingLineBreak; + LengthInPixel = -1; } public LineSpan WithStartOffset (int start) => new LineSpan (Start + start, End, EndIncludingLineBreak); /*public ReadOnlySpan GetSubString (string str) { @@ -150,7 +152,7 @@ namespace Crow { _text = value; - lines = getLines; + getLines (); OnTextChanged (this, new TextChangeEventArgs (Text)); RegisterForGraphicUpdate (); @@ -242,8 +244,8 @@ namespace Crow { set { if (value == _currentLine) return; - if (value >= lines.Count) - _currentLine = lines.Count-1; + if (value >= lines.Length) + _currentLine = lines.Length-1; else if (value < 0) _currentLine = 0; else @@ -329,30 +331,33 @@ namespace Crow { [XmlIgnore]public bool selectionIsEmpty { get { return SelRelease < 0; } } - List lines; - List getLines { - get { - if (string.IsNullOrEmpty(_text)) - return new List (new LineSpan[] { new LineSpan (0, 0, 0) }); - if (!_multiline) - return new List (new LineSpan[] { new LineSpan (0, _text.Length, _text.Length) }); - List lines = new List (); - bool lineBreak = false; - int start = 0, end = 0; - for (int i = 0; i < _text.Length; i++) { - if (_text[i].IsAnyLineBreakCharacter()) { - if (lineBreak) - continue; - lineBreak = true; - end = i; - }else if (lineBreak) { - lines.Add (new LineSpan (start, end, i)); - start = end = i; - lineBreak = false; - } - } - return lines; + LineSpan[] lines; + void getLines () { + if (string.IsNullOrEmpty (_text)) { + lines = new LineSpan[] { new LineSpan (0, 0, 0) }; + return; } + if (!_multiline) { + lines = new LineSpan[] { new LineSpan (0, _text.Length, _text.Length) }; + return; + } + + List _lines = new List (); + bool lineBreak = false; + int start = 0, end = 0; + for (int i = 0; i < _text.Length; i++) { + if (_text[i].IsAnyLineBreakCharacter()) { + if (lineBreak) + continue; + lineBreak = true; + end = i; + }else if (lineBreak) { + _lines.Add (new LineSpan (start, end, i)); + start = end = i; + lineBreak = false; + } + } + lines = _lines.ToArray(); } /// /// Moves cursor one char to the left. @@ -376,7 +381,7 @@ namespace Crow { public bool MoveRight(){ int tmp = _currentCol + 1; if (tmp > lines [_currentLine].Length){ - if (CurrentLine == lines.Count - 1) + if (CurrentLine == lines.Length - 1) return false; CurrentLine++; CurrentColumn = 0; @@ -409,7 +414,7 @@ namespace Crow { { if (selectionIsEmpty) { if (CurrentColumn == 0) { - if (CurrentLine == 0 && lines.Count == 1) + if (CurrentLine == 0 && lines.Length == 1) return; CurrentLine--; CurrentColumn = lines [CurrentLine].Length; @@ -485,7 +490,8 @@ namespace Crow { public override int measureRawSize(LayoutingType lt) { if (lines == null) - lines = getLines; + getLines (); + if (!textMeasureIsUpToDate) { using (Context gr = new Context (IFace.surf)) { //Cairo.FontFace cf = gr.GetContextFontFace (); @@ -498,12 +504,14 @@ namespace Crow { fe = gr.FontExtents; te = new TextExtents (); - cachedTextSize.Height = (int)Math.Ceiling ((fe.Ascent+fe.Descent) * Math.Max (1, lines.Count)); + cachedTextSize.Height = (int)Math.Ceiling ((fe.Ascent+fe.Descent) * Math.Max (1, lines.Length)); try { TextExtents tmp = default; - for (int i = 0; i < lines.Count; i++) { + for (int i = 0; i < lines.Length; i++) { gr.TextExtents (_text.GetLine (lines[i]), Interface.TAB_SIZE, out tmp); + if (lines[i].LengthInPixel < 0) + lines[i].LengthInPixel = (int)tmp.XAdvance; if (tmp.XAdvance > te.XAdvance) te = tmp; } @@ -659,33 +667,47 @@ namespace Crow { gr.Restore (); return; } - for (int i = 0; i < lines.Count; i++) { + + Foreground.SetAsSource (IFace, gr); + + TextExtents extents; + Span bytes = stackalloc byte[128]; + + for (int i = 0; i < lines.Length; i++) { if (lines[i].Length == 0) continue; - ReadOnlySpan chars = _text.GetLine (lines[i]); - + + int size = lines[i].Length * 4 + 1; + if (bytes.Length < size) + bytes = size > 512 ? new byte[size] : stackalloc byte[size]; + + int encodedBytes = Encoding.UTF8.GetBytes (_text.GetLine (lines[i]), bytes); + bytes[encodedBytes] = 0; + + if (lines[i].LengthInPixel < 0) { + gr.TextExtents (bytes.Slice (0, encodedBytes), out extents); + lines[i].LengthInPixel = (int)extents.XAdvance; + } //string l = lines [i].Replace ("\t", new String (' ', Interface.TAB_SIZE)); - int lineLength = (int)gr.TextExtents (chars, Interface.TAB_SIZE).XAdvance; + Rectangle lineRect = new Rectangle ( rText.X, rText.Y + i * (int)(fe.Ascent+fe.Descent), - lineLength, + lines[i].LengthInPixel, (int)(fe.Ascent+fe.Descent)); -// if (TextAlignment == Alignment.Center || -// TextAlignment == Alignment.Top || -// TextAlignment == Alignment.Bottom) -// lineRect.X += (rText.Width - lineLength) / 2; -// else if (TextAlignment == Alignment.Right || -// TextAlignment == Alignment.TopRight || -// TextAlignment == Alignment.BottomRight) -// lineRect.X += (rText.Width - lineLength); + // if (TextAlignment == Alignment.Center || + // TextAlignment == Alignment.Top || + // TextAlignment == Alignment.Bottom) + // lineRect.X += (rText.Width - lineLength) / 2; + // else if (TextAlignment == Alignment.Right || + // TextAlignment == Alignment.TopRight || + // TextAlignment == Alignment.BottomRight) + // lineRect.X += (rText.Width - lineLength); - Foreground.SetAsSource (IFace, gr); gr.MoveTo (lineRect.X,(double)rText.Y + fe.Ascent + (fe.Ascent+fe.Descent) * i) ; - - gr.ShowText (chars, Interface.TAB_SIZE); - gr.Fill (); + gr.ShowText (bytes.Slice (0, encodedBytes)); + //gr.Fill (); if (Selectable) { if (SelRelease >= 0 && i >= selectionStart.Y && i <= selectionEnd.Y) { @@ -706,7 +728,7 @@ namespace Crow { selRect.Left += cpStart; } if (i == selectionEnd.Y) - selRect.Width -= (lineLength - cpEnd); + selRect.Width -= (lines[i].LengthInPixel - cpEnd); gr.Rectangle (selRect); gr.FillPreserve (); @@ -714,9 +736,11 @@ namespace Crow { gr.Clip (); gr.SetSource (SelectionForeground); gr.MoveTo (lineRect.X, rText.Y + fe.Ascent + (fe.Ascent+fe.Descent) * i); - gr.ShowText (chars, Interface.TAB_SIZE); + gr.ShowText (bytes.Slice (0, encodedBytes)); gr.Fill (); gr.Restore (); + + Foreground.SetAsSource (IFace, gr); } } } @@ -741,7 +765,7 @@ namespace Crow { if (!_selectable) return; SelBegin = new Point(0,0); - SelRelease = new Point (lines.LastOrDefault ().Length, lines.Count-1); + SelRelease = new Point (lines.LastOrDefault ().Length, lines.Length-1); RegisterForRedraw (); } protected override void onUnfocused (object sender, EventArgs e) @@ -818,8 +842,8 @@ namespace Crow { CurrentLine = (int)(mouseLocalPos.Y / (fe.Ascent+fe.Descent)); //fix cu - if (CurrentLine >= lines.Count) - CurrentLine = lines.Count - 1; + if (CurrentLine >= lines.Length) + CurrentLine = lines.Length - 1; LineSpan ls = lines[CurrentLine]; ReadOnlySpan curLine = _text.GetLine (lines[CurrentLine]); diff --git a/Crow/src/Widgets/OldLabel.cs b/Crow/src/Widgets/OldLabel.cs new file mode 100644 index 00000000..080bc5a0 --- /dev/null +++ b/Crow/src/Widgets/OldLabel.cs @@ -0,0 +1,842 @@ +// Copyright (c) 2013-2019 Bruyère Jean-Philippe jp_bruyere@hotmail.com +// +// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) + +using System; +using System.Collections.Generic; +using System.Linq; +using Crow.Cairo; +using System.Text.RegularExpressions; +using System.ComponentModel; + +namespace Crow { + public class OldLabel : Widget + { + #region CTOR + protected OldLabel () {} + public OldLabel(Interface iface, string style = null) : base (iface, style) { } + #endregion + + public event EventHandler TextChanged; + + public virtual void OnTextChanged(Object sender, TextChangeEventArgs e) + { + textMeasureIsUpToDate = false; + NotifyValueChanged ("Text", Text); + TextChanged.Raise (this, e); + } + //TODO:change protected to private + + #region private and protected fields + string _text = "label"; + Alignment _textAlignment; + bool horizontalStretch; + bool verticalStretch; + bool _selectable; + bool _multiline; + Color selBackground; + Color selForeground; + Point mouseLocalPos = -1;//mouse coord in widget space, filled only when clicked + int _currentCol; //0 based cursor position in string + int _currentLine; + Point _selBegin = -1; //selection start (row,column) + Point _selRelease = -1; //selection end (row,column) + double textCursorPos; //cursor position in cairo units in widget client coord. + double SelStartCursorPos = -1; + double SelEndCursorPos = -1; + bool SelectionInProgress = false; + + protected Rectangle rText; + protected float widthRatio = 1f; + protected float heightRatio = 1f; + protected FontExtents fe; + protected TextExtents te; + #endregion + + [DefaultValue("SteelBlue")] + public virtual Color SelectionBackground { + get { return selBackground; } + set { + if (selBackground == value) + return; + selBackground = value; + NotifyValueChangedAuto (selBackground); + RegisterForRedraw (); + } + } + [DefaultValue("White")] + public virtual Color SelectionForeground { + get { return selForeground; } + set { + if (selForeground == value) + return; + selForeground = value; + NotifyValueChangedAuto (selForeground); + RegisterForRedraw (); + } + } + [DefaultValue(Alignment.Left)] + public Alignment TextAlignment + { + get { return _textAlignment; } + set { + if (value == _textAlignment) + return; + _textAlignment = value; + RegisterForRedraw (); + NotifyValueChangedAuto (_textAlignment); + } + } + [DefaultValue(false)] + public virtual bool HorizontalStretch { + get { return horizontalStretch; } + set { + if (horizontalStretch == value) + return; + horizontalStretch = value; + RegisterForRedraw (); + NotifyValueChangedAuto (horizontalStretch); + } + } + [DefaultValue(false)] + public virtual bool VerticalStretch { + get { return verticalStretch; } + set { + if (verticalStretch == value) + return; + verticalStretch = value; + RegisterForRedraw (); + NotifyValueChangedAuto (verticalStretch); + } + } + [DefaultValue("label")] + public string Text + { + get { + return lines == null ? + _text : lines.Aggregate((i, j) => i + Interface.LineBreak + j); + } + set + { + if (string.Equals (value, _text, StringComparison.Ordinal)) + return; + + _text = value; + + if (string.IsNullOrEmpty(_text)) + _text = ""; + + lines = getLines; + + OnTextChanged (this, new TextChangeEventArgs (Text)); + RegisterForGraphicUpdate (); + } + } + [DefaultValue(false)] + public bool Selectable + { + get { return _selectable; } + set + { + if (value == _selectable) + return; + _selectable = value; + NotifyValueChangedAuto (_selectable); + SelBegin = -1; + SelRelease = -1; + RegisterForRedraw (); + } + } + [DefaultValue(false)] + public bool Multiline + { + get { return _multiline; } + set + { + if (value == _multiline) + return; + _multiline = value; + NotifyValueChangedAuto (_multiline); + RegisterForGraphicUpdate(); + } + } + [DefaultValue(0)] + public int CurrentColumn{ + get { return _currentCol; } + set { + if (value == _currentCol) + return; + if (value < 0) + _currentCol = 0; + else if (value > lines [_currentLine].Length) + _currentCol = lines [_currentLine].Length; + else + _currentCol = value; + NotifyValueChangedAuto (_currentCol); + + Rectangle cb = ClientRectangle; + + if (Width == Measure.Fit || cb.Width >= cachedTextSize.Width) { + xTranslation = 0; + return; + } + int xpos = xposition; + if (xTranslation + xpos > cb.Width) + xTranslation = cb.Width - xpos; + else if (xpos < -xTranslation) + xTranslation = -xpos; + RegisterForRedraw (); + } + } + int xTranslation = 0; + int xposition { + get { + using (Context gr = new Context (IFace.surf)) { + //Cairo.FontFace cf = gr.GetContextFontFace (); + gr.SelectFontFace (Font.Name, Font.Slant, Font.Wheight); + gr.SetFontSize (Font.Size); + gr.FontOptions = Interface.FontRenderingOptions; + gr.Antialias = Interface.Antialias; + try { + string l = lines [_currentLine]; + if (_currentCol < l.Length) + l = l.Remove (Math.Min (_currentCol, l.Length)); + l = l.Replace ("\t", new String (' ', Interface.TAB_SIZE)); + return (int)Math.Ceiling (gr.TextExtents (l).XAdvance); + } catch { + System.Diagnostics.Debug.WriteLine ("xpos measuring fault in label"); + return 0; + } + } + } + } + + [DefaultValue(0)] + public int CurrentLine{ + get { return _currentLine; } + set { + if (value == _currentLine) + return; + if (value >= lines.Count) + _currentLine = lines.Count-1; + else if (value < 0) + _currentLine = 0; + else + _currentLine = value; + //force recheck of currentCol for bounding + int cc = _currentCol; + _currentCol = 0; + CurrentColumn = cc; + NotifyValueChangedAuto (_currentLine); + } + } + [XmlIgnore]public Point CurrentPosition { + get { return new Point(_currentCol, CurrentLine); } + } + //TODO:using HasFocus for drawing selection cause SelBegin and Release binding not to work + /// + /// Selection begin position in char units + /// + [DefaultValue("-1")] + public Point SelBegin { + get { + return _selBegin; + } + set { + if (value == _selBegin) + return; + _selBegin = value; + NotifyValueChangedAuto (_selBegin); + NotifyValueChanged ("SelectedText", SelectedText); + } + } + [DefaultValue("-1")] + public Point SelRelease { + get { + return _selRelease; + } + set { + if (value == _selRelease) + return; + _selRelease = value; + NotifyValueChangedAuto (_selRelease); + NotifyValueChanged ("SelectedText", SelectedText); + } + } + /// + /// return char at CurrentLine, CurrentColumn + /// + [XmlIgnore]protected Char CurrentChar + { + get { + return lines [CurrentLine][CurrentColumn]; + } + } + /// + /// ordered selection start and end positions in char units + /// + [XmlIgnore]protected Point selectionStart + { + get { + return SelRelease < 0 || SelBegin.Y < SelRelease.Y ? SelBegin : + SelBegin.Y > SelRelease.Y ? SelRelease : + SelBegin.X < SelRelease.X ? SelBegin : SelRelease; + } + } + [XmlIgnore]public Point selectionEnd + { + get { + return SelRelease < 0 || SelBegin.Y > SelRelease.Y ? SelBegin : + SelBegin.Y < SelRelease.Y ? SelRelease : + SelBegin.X > SelRelease.X ? SelBegin : SelRelease; + } + } + [XmlIgnore]public string SelectedText + { + get { + if (SelRelease < 0 || SelBegin < 0) + return ""; + if (selectionStart.Y == selectionEnd.Y) + return lines [selectionStart.Y].Substring (selectionStart.X, selectionEnd.X - selectionStart.X); + string tmp = ""; + tmp = lines [selectionStart.Y].Substring (selectionStart.X); + for (int l = selectionStart.Y + 1; l < selectionEnd.Y; l++) { + tmp += Interface.LineBreak + lines [l]; + } + tmp += Interface.LineBreak + lines [selectionEnd.Y].Substring (0, selectionEnd.X); + return tmp; + } + } + [XmlIgnore]public bool selectionIsEmpty + { get { return SelRelease < 0; } } + + List lines; + List getLines { + get { + return _multiline ? + Regex.Split (_text, "\r\n|\r|\n|\\\\n").ToList() : + new List(new string[] { _text }); + } + } + /// + /// Moves cursor one char to the left. + /// + /// true if move succeed + public bool MoveLeft(){ + int tmp = _currentCol - 1; + if (tmp < 0) { + if (_currentLine == 0) + return false; + CurrentLine--; + CurrentColumn = int.MaxValue; + } else + CurrentColumn = tmp; + return true; + } + /// + /// Moves cursor one char to the right. + /// + /// true if move succeed + public bool MoveRight(){ + int tmp = _currentCol + 1; + if (tmp > lines [_currentLine].Length){ + if (CurrentLine == lines.Count - 1) + return false; + CurrentLine++; + CurrentColumn = 0; + } else + CurrentColumn = tmp; + return true; + } + public void GotoWordStart(){ + CurrentColumn--; + //skip white spaces + while (!char.IsLetterOrDigit (this.CurrentChar) && CurrentColumn > 0) + CurrentColumn--; + while (char.IsLetterOrDigit (lines [CurrentLine] [CurrentColumn]) && CurrentColumn > 0) + CurrentColumn--; + if (!char.IsLetterOrDigit (this.CurrentChar)) + CurrentColumn++; + } + public void GotoWordEnd(){ + //skip white spaces + if (CurrentColumn >= lines [CurrentLine].Length - 1) + return; + while (!char.IsLetterOrDigit (this.CurrentChar) && CurrentColumn < lines [CurrentLine].Length-1) + CurrentColumn++; + while (char.IsLetterOrDigit (this.CurrentChar) && CurrentColumn < lines [CurrentLine].Length-1) + CurrentColumn++; + if (char.IsLetterOrDigit (this.CurrentChar)) + CurrentColumn++; + } + public void DeleteChar() + { + if (selectionIsEmpty) { + if (CurrentColumn == 0) { + if (CurrentLine == 0 && lines.Count == 1) + return; + CurrentLine--; + CurrentColumn = lines [CurrentLine].Length; + lines [CurrentLine] += lines [CurrentLine + 1]; + lines.RemoveAt (CurrentLine + 1); + + OnTextChanged (this, new TextChangeEventArgs (Text)); + return; + } + CurrentColumn--; + lines [CurrentLine] = lines [CurrentLine].Remove (CurrentColumn, 1); + } else { + int linesToRemove = selectionEnd.Y - selectionStart.Y + 1; + int l = selectionStart.Y; + + if (linesToRemove > 0) { + lines [l] = lines [l].Remove (selectionStart.X, lines [l].Length - selectionStart.X) + + lines [selectionEnd.Y].Substring (selectionEnd.X, lines [selectionEnd.Y].Length - selectionEnd.X); + l++; + for (int c = 0; c < linesToRemove-1; c++) + lines.RemoveAt (l); + CurrentLine = selectionStart.Y; + CurrentColumn = selectionStart.X; + } else + lines [l] = lines [l].Remove (selectionStart.X, selectionEnd.X - selectionStart.X); + CurrentColumn = selectionStart.X; + SelBegin = -1; + SelRelease = -1; + } + OnTextChanged (this, new TextChangeEventArgs (Text)); + } + /// + /// Insert new string at caret position, should be sure no line break is inside. + /// + /// String. + protected void Insert(string str) + { + if (!selectionIsEmpty) + this.DeleteChar (); + if (_multiline) { + string[] strLines = Regex.Split (str, "\r\n|\r|\n|" + @"\\n").ToArray(); + lines [CurrentLine] = lines [CurrentLine].Insert (CurrentColumn, strLines[0]); + CurrentColumn += strLines[0].Length; + for (int i = 1; i < strLines.Length; i++) { + InsertLineBreak (); + lines [CurrentLine] = lines [CurrentLine].Insert (CurrentColumn, strLines[i]); + CurrentColumn += strLines[i].Length; + } + } else { + lines [CurrentLine] = lines [CurrentLine].Insert (CurrentColumn, str); + CurrentColumn += str.Length; + } + OnTextChanged (this, new TextChangeEventArgs (Text)); + } + /// + /// Insert a line break. + /// + protected void InsertLineBreak() + { + lines.Insert(CurrentLine + 1, lines[CurrentLine].Substring(CurrentColumn)); + lines [CurrentLine] = lines [CurrentLine].Substring (0, CurrentColumn); + CurrentLine++; + CurrentColumn = 0; + OnTextChanged (this, new TextChangeEventArgs (Text)); + } + bool textMeasureIsUpToDate = false; + Size cachedTextSize = default(Size); + + #region GraphicObject overrides + public override int measureRawSize(LayoutingType lt) + { + if (lines == null) + lines = getLines; + if (!textMeasureIsUpToDate) { + using (Context gr = new Context (IFace.surf)) { + //Cairo.FontFace cf = gr.GetContextFontFace (); + + gr.SelectFontFace (Font.Name, Font.Slant, Font.Wheight); + gr.SetFontSize (Font.Size); + gr.FontOptions = Interface.FontRenderingOptions; + gr.Antialias = Interface.Antialias; + + fe = gr.FontExtents; + te = new TextExtents (); + + cachedTextSize.Height = (int)Math.Ceiling ((fe.Ascent+fe.Descent) * Math.Max (1, lines.Count)); + + try { + for (int i = 0; i < lines.Count; i++) { + string l = lines[i].Replace ("\t", new String (' ', Interface.TAB_SIZE)); + + TextExtents tmp = gr.TextExtents (l); + + if (tmp.XAdvance > te.XAdvance) + te = tmp; + } + cachedTextSize.Width = (int)Math.Ceiling (te.XAdvance); + textMeasureIsUpToDate = true; + } catch { + return -1; + } + } + } + return Margin * 2 + (lt == LayoutingType.Height ? cachedTextSize.Height : cachedTextSize.Width); + } + protected override void onDraw (Context gr) + { + base.onDraw (gr); + + gr.SelectFontFace (Font.Name, Font.Slant, Font.Wheight); + gr.SetFontSize (Font.Size); + gr.FontOptions = Interface.FontRenderingOptions; + gr.Antialias = Interface.Antialias; + + gr.Save (); + gr.Translate (xTranslation, 0); + + rText = new Rectangle(new Size( + measureRawSize(LayoutingType.Width), measureRawSize(LayoutingType.Height))); + rText.Width -= 2 * Margin; + rText.Height -= 2 * Margin; + + widthRatio = 1f; + heightRatio = 1f; + + Rectangle cb = ClientRectangle; + + rText.X = cb.X; + rText.Y = cb.Y; + + if (horizontalStretch) { + widthRatio = (float)cb.Width / (float)rText.Width; + if (!verticalStretch) + heightRatio = widthRatio; + } + + if (verticalStretch) { + heightRatio = (float)cb.Height / (float)rText.Height; + if (!horizontalStretch) + widthRatio = heightRatio; + } + + rText.Width = (int)(widthRatio * (float)rText.Width); + rText.Height = (int)(heightRatio * (float)rText.Height); + + switch (TextAlignment) + { + case Alignment.TopLeft: //ok + rText.X = cb.X; + rText.Y = cb.Y; + break; + case Alignment.Top: //ok + rText.Y = cb.Y; + rText.X = cb.X + cb.Width / 2 - rText.Width / 2; + break; + case Alignment.TopRight: //ok + rText.Y = cb.Y; + rText.X = cb.Right - rText.Width; + break; + case Alignment.Left://ok + rText.X = cb.X; + rText.Y = cb.Y + cb.Height / 2 - rText.Height / 2; + break; + case Alignment.Right://ok + rText.X = cb.X + cb.Width - rText.Width; + rText.Y = cb.Y + cb.Height / 2 - rText.Height / 2; + break; + case Alignment.Bottom://ok + rText.X = cb.Width / 2 - rText.Width / 2; + rText.Y = cb.Height - rText.Height; + break; + case Alignment.BottomLeft://ok + rText.X = cb.X; + rText.Y = cb.Bottom - rText.Height; + break; + case Alignment.BottomRight://ok + rText.Y = cb.Bottom - rText.Height; + rText.X = cb.Right - rText.Width; + break; + case Alignment.Center://ok + rText.X = cb.X + cb.Width / 2 - rText.Width / 2; + //rText.Y = cb.Y + cb.Height / 2 - rText.Height / 2; + rText.Y = cb.Y + (int)Math.Floor((double)cb.Height / 2.0 - (double)rText.Height / 2.0); + break; + } + + //gr.FontMatrix = new Matrix(widthRatio * (float)Font.Size, 0, 0, heightRatio * (float)Font.Size, 0, 0); + fe = gr.FontExtents; + + #region draw text cursor + if (HasFocus && Selectable) + { + if (mouseLocalPos >= 0) + { + computeTextCursor(gr); + + if (SelectionInProgress) + { + if (SelBegin < 0){ + SelBegin = new Point(CurrentColumn, CurrentLine); + SelStartCursorPos = textCursorPos; + SelRelease = -1; + }else{ + SelRelease = new Point(CurrentColumn, CurrentLine); + if (SelRelease == SelBegin) + SelRelease = -1; + else + SelEndCursorPos = textCursorPos; + } + }else + computeTextCursorPosition(gr); + }else + computeTextCursorPosition(gr); + + Foreground.SetAsSource (IFace, gr); + gr.LineWidth = 1.0; + gr.MoveTo (0.5 + textCursorPos + rText.X, rText.Y + CurrentLine * (fe.Ascent+fe.Descent)); + gr.LineTo (0.5 + textCursorPos + rText.X, rText.Y + (CurrentLine + 1) * (fe.Ascent+fe.Descent)); + gr.Stroke(); + } + #endregion + + //****** debug selection ************* +// if (SelRelease >= 0) { +// new SolidColor(Color.DarkGreen).SetAsSource(gr); +// Rectangle R = new Rectangle ( +// rText.X + (int)SelEndCursorPos - 3, +// rText.Y + (int)(SelRelease.Y * (fe.Ascent+fe.Descent)), +// 6, +// (int)(fe.Ascent+fe.Descent)); +// gr.Rectangle (R); +// gr.Fill (); +// } +// if (SelBegin >= 0) { +// new SolidColor(Color.DarkRed).SetAsSource(gr); +// Rectangle R = new Rectangle ( +// rText.X + (int)SelStartCursorPos - 3, +// rText.Y + (int)(SelBegin.Y * (fe.Ascent+fe.Descent)), +// 6, +// (int)(fe.Ascent+fe.Descent)); +// gr.Rectangle (R); +// gr.Fill (); +// } + //******************* + + for (int i = 0; i < lines.Count; i++) { + string l = lines [i].Replace ("\t", new String (' ', Interface.TAB_SIZE)); + int lineLength = (int)gr.TextExtents (l).XAdvance; + Rectangle lineRect = new Rectangle ( + rText.X, + rText.Y + i * (int)(fe.Ascent+fe.Descent), + lineLength, + (int)(fe.Ascent+fe.Descent)); + +// if (TextAlignment == Alignment.Center || +// TextAlignment == Alignment.Top || +// TextAlignment == Alignment.Bottom) +// lineRect.X += (rText.Width - lineLength) / 2; +// else if (TextAlignment == Alignment.Right || +// TextAlignment == Alignment.TopRight || +// TextAlignment == Alignment.BottomRight) +// lineRect.X += (rText.Width - lineLength); + if (string.IsNullOrWhiteSpace (l)) + continue; + + Foreground.SetAsSource (IFace, gr); + gr.MoveTo (lineRect.X,(double)rText.Y + fe.Ascent + (fe.Ascent+fe.Descent) * i) ; + + gr.ShowText (l); + gr.Fill (); + + if (Selectable) { + if (SelRelease >= 0 && i >= selectionStart.Y && i <= selectionEnd.Y) { + gr.SetSource (selBackground); + + Rectangle selRect = lineRect; + + int cpStart = (int)SelStartCursorPos, + cpEnd = (int)SelEndCursorPos; + + if (SelBegin.Y > SelRelease.Y) { + cpStart = cpEnd; + cpEnd = (int)SelStartCursorPos; + } + + if (i == selectionStart.Y) { + selRect.Width -= cpStart; + selRect.Left += cpStart; + } + if (i == selectionEnd.Y) + selRect.Width -= (lineLength - cpEnd); + + gr.Rectangle (selRect); + gr.FillPreserve (); + gr.Save (); + gr.Clip (); + gr.SetSource (SelectionForeground); + gr.MoveTo (lineRect.X, rText.Y + fe.Ascent + (fe.Ascent+fe.Descent) * i); + gr.ShowText (l); + gr.Fill (); + gr.Restore (); + } + } + } + + gr.Restore (); + } + #endregion + + #region Mouse handling + void updatemouseLocalPos(Point mpos){ + mouseLocalPos = mpos - ScreenCoordinates(Slot).TopLeft - ClientRectangle.TopLeft; + mouseLocalPos.X -= xTranslation; + if (mouseLocalPos.X < 0) + mouseLocalPos.X = 0; + if (mouseLocalPos.Y < 0) + mouseLocalPos.Y = 0; + } + protected override void onFocused (object sender, EventArgs e) + { + base.onFocused (sender, e); + + if (!_selectable) + return; + SelBegin = new Point(0,0); + SelRelease = new Point (lines.LastOrDefault ().Length, lines.Count-1); + RegisterForRedraw (); + } + protected override void onUnfocused (object sender, EventArgs e) + { + base.onUnfocused (sender, e); + + SelBegin = -1; + SelRelease = -1; + RegisterForRedraw (); + } + public override void onMouseMove (object sender, MouseMoveEventArgs e) + { + base.onMouseMove (sender, e); + + if (!(SelectionInProgress && HasFocus && _selectable)) + return; + + updatemouseLocalPos (e.Position); + + RegisterForRedraw(); + } + public override void onMouseDown (object sender, MouseButtonEventArgs e) + { + if (HasFocus) { + if (_selectable) { + updatemouseLocalPos (e.Position); + SelBegin = -1; + SelRelease = -1; + SelectionInProgress = true; + RegisterForRedraw ();//TODO:should put it in properties + } + } + + //done at the end to set 'hasFocus' value after testing it + base.onMouseDown (sender, e); + } + public override void onMouseUp (object sender, MouseButtonEventArgs e) + { + base.onMouseUp (sender, e); + if (!(HasFocus || _selectable)) + return; + if (!SelectionInProgress) + return; + + updatemouseLocalPos (e.Position); + SelectionInProgress = false; + RegisterForRedraw (); + } + public override void onMouseDoubleClick (object sender, MouseButtonEventArgs e) + { + base.onMouseDoubleClick (sender, e); + if (!(this.HasFocus || _selectable)) + return; + + GotoWordStart (); + SelBegin = CurrentPosition; + GotoWordEnd (); + SelRelease = CurrentPosition; + SelectionInProgress = false; + RegisterForRedraw (); + } + #endregion + + /// + /// Update Current Column, line and TextCursorPos + /// from mouseLocalPos + /// + void computeTextCursor(Context gr) + { + TextExtents te; + + double cPos = 0f; + + CurrentLine = (int)(mouseLocalPos.Y / (fe.Ascent+fe.Descent)); + + //fix cu + if (CurrentLine >= lines.Count) + CurrentLine = lines.Count - 1; + + switch (TextAlignment) { + case Alignment.Center: + case Alignment.Top: + case Alignment.Bottom: + cPos+= ClientRectangle.Width - gr.TextExtents(lines [CurrentLine]).Width/2.0; + break; + case Alignment.Right: + case Alignment.TopRight: + case Alignment.BottomRight: + cPos += ClientRectangle.Width - gr.TextExtents(lines [CurrentLine]).Width; + break; + } + + for (int i = 0; i < lines[CurrentLine].Length; i++) + { + string c = lines [CurrentLine].Substring (i, 1); + if (c == "\t") + c = new string (' ', Interface.TAB_SIZE); + + te = gr.TextExtents(c); + + double halfWidth = te.XAdvance / 2; + + if (mouseLocalPos.X <= cPos + halfWidth) + { + CurrentColumn = i; + textCursorPos = cPos; + mouseLocalPos = -1; + return; + } + + cPos += te.XAdvance; + } + CurrentColumn = lines[CurrentLine].Length; + textCursorPos = cPos; + + //reset mouseLocalPos + mouseLocalPos = -1; + } + /// Computes offsets in cairo units + void computeTextCursorPosition(Context gr) + { + if (SelBegin >= 0) + SelStartCursorPos = GetXFromTextPointer (gr, SelBegin); + if (SelRelease >= 0) + SelEndCursorPos = GetXFromTextPointer (gr, SelRelease); + textCursorPos = GetXFromTextPointer (gr, new Point(CurrentColumn, CurrentLine)); + } + /// Compute x offset in cairo unit from text position + double GetXFromTextPointer(Context gr, Point pos) + { + try { + string l = lines [pos.Y].Substring (0, pos.X). + Replace ("\t", new String (' ', Interface.TAB_SIZE)); + return gr.TextExtents (l).XAdvance; + } catch{ + return -1; + } + } + } +} diff --git a/Samples/BasicTests/BasicTests.cs b/Samples/BasicTests/BasicTests.cs index 755c4a0b..dcf38db4 100644 --- a/Samples/BasicTests/BasicTests.cs +++ b/Samples/BasicTests/BasicTests.cs @@ -28,14 +28,14 @@ namespace tests // += KeyboardKeyDown1; //testFiles = new string [] { @"Interfaces/Experimental/testDock.crow" }; - //testFiles = new string [] { @"Interfaces/Divers/welcome.crow" }; + testFiles = new string [] { @"Interfaces/Divers/welcome.crow" }; //testFiles = new string [] { @"Interfaces/TemplatedGroup/3.crow" }; //testFiles = new string [] { @"Interfaces/Divers/testShape.crow" }; //testFiles = new string [] { @"Interfaces/TemplatedControl/testEnumSelector.crow" }; //testFiles = new string [] { @"Interfaces/Divers/all.crow" }; //testFiles = new string [] { @"Interfaces/Divers/gauge.crow" }; //testFiles = new string [] { @"Interfaces/Stack/StretchedInFit4.crow" }; - testFiles = new string [] { @"Interfaces/TemplatedGroup/1.crow" }; + //testFiles = new string [] { @"Interfaces/TemplatedGroup/1.crow" }; //testFiles = new string [] { @"Interfaces/Divers/colorPicker2.crow" }; testFiles = testFiles.Concat (Directory.GetFiles (@"Interfaces/GraphicObject", "*.crow")).ToArray (); testFiles = testFiles.Concat (Directory.GetFiles (@"Interfaces/Container", "*.crow")).ToArray (); @@ -62,12 +62,7 @@ namespace tests switch (key) { case Key.Escape: Quit (); - break; - case Key.F2: - //if (IsKeyDown (Key.LeftShift)) - //DbgLogger.Reset (); - //DbgLogger.save (this); - return false; + break; case Key.F3: idx--; break; @@ -78,15 +73,6 @@ namespace tests //TestList.Add ("new string"); NotifyValueChanged ("TestList", TestList); break; - case Key.F5: - Load ("Interfaces/Divers/testFileDialog.crow").DataSource = this; - return false; - case Key.F6: - Load ("Interfaces/Divers/0.crow").DataSource = this; - return false; - case Key.F7: - Load ("Interfaces/Divers/perfMeasures.crow").DataSource = this; - return false; default: return base.OnKeyDown (key); } @@ -105,7 +91,7 @@ namespace tests #endif Load (testFiles [idx]).DataSource = this; } catch (Exception ex) { - (LoadIMLFragment ($"