private void AlignCaretToNearestCluster(bool isTrailingHit, bool skipZeroWidth) { // Uses hit-testing to align the current caret position to a whole cluster, // rather than residing in the middle of a base character + diacritic, // surrogate pair, or character + UVS. // Align the caret to the nearest whole cluster. HitTestMetrics hitTestMetrics = TextLayout.HitTestTextPosition(m_caretPosition, false); // The caret position itself is always the leading edge. // An additional offset indicates a trailing edge when non-zero. // This offset comes from the number of code-units in the // selected cluster or surrogate pair. m_caretPosition = hitTestMetrics.TextPosition; m_caretPositionOffset = (isTrailingHit) ? hitTestMetrics.Length : 0; // For invisible, zero-width characters (like line breaks // and formatting characters), force leading edge of the // next position. if (skipZeroWidth && hitTestMetrics.Width == 0) { m_caretPosition += m_caretPositionOffset; m_caretPositionOffset = 0; } }
public void Should_Get_CharacterHit_From_Distance_RTL() { using (Start()) { var text = "أَبْجَدِيَّة عَرَبِيَّة"; var layout = new TextLayout( text, Typeface.Default, 12, Brushes.Black); var textLine = layout.TextLines[0]; var firstRun = (ShapedTextCharacters)textLine.TextRuns[0]; var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; var characterHit = textLine.GetCharacterHitFromDistance(0); Assert.Equal(firstCluster, characterHit.FirstCharacterIndex); Assert.Equal(text.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); var distance = textLine.GetDistanceFromCharacterHit(characterHit); Assert.Equal(0, distance); distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; Assert.Equal(firstAdvance, distance, 5); var rect = layout.HitTestTextPosition(22); Assert.Equal(firstAdvance, rect.Left, 5); rect = layout.HitTestTextPosition(23); Assert.Equal(0, rect.Left, 5); } }
public Point GetCaretPosition(FormattedText text, int caretIndex, Size constraint) { using (TextLayout layout = GetTextLayout(this.factory, text, constraint)) { float x; float y; layout.HitTestTextPosition(caretIndex, false, out x, out y); return(new Point(x, y)); } }
/// <summary> /// Renders the <see cref="AccessText"/> to a drawing context. /// </summary> /// <param name="context">The drawing context.</param> public override void Render(DrawingContext context) { base.Render(context); int underscore = Text?.IndexOf('_') ?? -1; if (underscore != -1 && ShowAccessKey) { var rect = TextLayout.HitTestTextPosition(underscore); var offset = new Vector(0, -1.5); context.DrawLine( new Pen(Foreground, 1), rect.BottomLeft + offset, rect.BottomRight + offset); } }
public void Evaluate(int SpreadMax) { if (!FLayout.IsConnected) { this.FPosition.SliceCount = 0; return; } if (this.FLayout.IsChanged || this.FIndex.IsChanged || this.FTrailing.IsChanged) { this.FPosition.SliceCount = SpreadMax; for (int i = 0; i < SpreadMax; i++) { TextLayout layout = this.FLayout[i]; float x, y; var result = layout.HitTestTextPosition(this.FIndex[i], this.FTrailing[i], out x, out y); this.FPosition[i] = new Vector2(x, y); } } }
/// <summary> /// Obtains the current caret position (in untransformed space)</summary> /// <returns>Current caret position rectangle (in untransformed space)</returns> public RectangleF GetCaretRect() { if (TextLayout == null) { return(new RectangleF()); } var caretMetrics = TextLayout.HitTestTextPosition(m_caretPosition, m_caretPositionOffset > 0); float caretX = caretMetrics.Point.X; float caretY = caretMetrics.Point.Y; UpdateSelectionRange(); // If a selection exists, draw the caret using the // line size rather than the font size. if (SelectionLength > 0) { var lineMetrics = TextLayout.HitTestTextRange(m_caretPosition, 0, 0, 0); caretY = lineMetrics[0].Top; } const int caretThickness = 1; return(new RectangleF(caretX - caretThickness / 2.0f, caretY, caretThickness, caretMetrics.Height)); }
/// <summary> /// Sets text selection. This may possibly only move the caret, not selecting characters.</summary> /// <param name="moveMode">Text selection mode</param> /// <param name="advance">Number of characters to advance or start selection</param> /// <param name="extendSelection">Whether to extend current selection to additional selection</param> /// <param name="updateCaretFormat">Whether to update caret format based on selection</param> /// <returns>True iff caret changed position as result of selection</returns> public bool SetSelection(SelectionMode moveMode, int advance, bool extendSelection, bool updateCaretFormat) { // Moves the caret relatively or absolutely, optionally extending the // selection range (for example, when shift is held). int line = int.MaxValue; // current line number, needed by a few modes int absolutePosition = m_caretPosition + m_caretPositionOffset; int oldAbsolutePosition = absolutePosition; int oldCaretAnchor = m_caretAnchor; switch (moveMode) { case SelectionMode.Left: m_caretPosition += m_caretPositionOffset; if (m_caretPosition > 0) { --m_caretPosition; AlignCaretToNearestCluster(false, true); // special check for CR/LF pair absolutePosition = m_caretPosition + m_caretPositionOffset; if (absolutePosition >= 1 && absolutePosition < TextLayout.Text.Length && TextLayout.Text[absolutePosition - 1] == '\r' && TextLayout.Text[absolutePosition] == '\n') { m_caretPosition = absolutePosition - 1; AlignCaretToNearestCluster(false, true); } } break; case SelectionMode.Right: m_caretPosition = absolutePosition; AlignCaretToNearestCluster(true, true); // special check for CR/LF pair absolutePosition = m_caretPosition + m_caretPositionOffset; if (absolutePosition >= 1 && absolutePosition < TextLayout.Text.Length && TextLayout.Text[absolutePosition - 1] == '\r' && TextLayout.Text[absolutePosition] == '\n') { m_caretPosition = absolutePosition + 1; AlignCaretToNearestCluster(false, true); } break; case SelectionMode.LeftChar: m_caretPosition = absolutePosition; m_caretPosition -= Math.Min(advance, absolutePosition); m_caretPositionOffset = 0; break; case SelectionMode.RightChar: m_caretPosition = absolutePosition + advance; m_caretPositionOffset = 0; { // Use hit-testing to limit text position. HitTestMetrics hitTestMetrics = TextLayout.HitTestTextPosition( m_caretPosition, false); m_caretPosition = Math.Min(m_caretPosition, hitTestMetrics.TextPosition + hitTestMetrics.Length); } break; case SelectionMode.Up: case SelectionMode.Down: { // Retrieve the line metrics to figure out what line we are on. var lineMetrics = TextLayout.GetLineMetrics(); int linePosition; GetLineFromPosition(lineMetrics, m_caretPosition, out line, out linePosition); // Move up a line or down if (moveMode == SelectionMode.Up) { if (line <= 0) { break; // already top line } line--; linePosition -= lineMetrics[line].Length; if (line <= TopLine) { TopLine = TopLine - 1 >= 0 ? TopLine - 1 : 0; // scroll down a line of text } } else { linePosition += lineMetrics[line].Length; line++; if (line >= lineMetrics.Length) { break; // already bottom line } // scroll up a line of text TopLine = TopLine + 1; } // To move up or down, we need three hit-testing calls to determine: // 1. The x of where we currently are. // 2. The y of the new line. // 3. New text position from the determined x and y. // This is because the characters are variable size. float caretX, caretY; // Get x of current text position var hitTestMetrics = TextLayout.HitTestTextPosition( m_caretPosition, m_caretPositionOffset > 0 // trailing if nonzero, else leading edge ); caretX = hitTestMetrics.Point.X; // Get y of new position hitTestMetrics = TextLayout.HitTestTextPosition( linePosition, false // leading edge ); caretY = hitTestMetrics.Point.Y; // Now get text position of new x,y hitTestMetrics = TextLayout.HitTestPoint(caretX, caretY); m_caretPosition = hitTestMetrics.TextPosition; m_caretPositionOffset = hitTestMetrics.IsTrailingHit ? (hitTestMetrics.Length > 0) ? 1 : 0 : 0; } break; case SelectionMode.LeftWord: case SelectionMode.RightWord: { // To navigate by whole words, we look for the canWrapLineAfter // flag in the cluster metrics. // Now we actually read them. var clusterMetrics = TextLayout.GetClusterMetrics(); if (clusterMetrics.Length == 0) { break; } m_caretPosition = absolutePosition; int clusterPosition = 0; int oldCaretPosition = m_caretPosition; if (moveMode == SelectionMode.LeftWord) { // Read through the clusters, keeping track of the farthest valid // stopping point just before the old position. m_caretPosition = 0; m_caretPositionOffset = 0; // leading edge for (int cluster = 0; cluster < clusterMetrics.Length; ++cluster) { clusterPosition += clusterMetrics[cluster].Length; if (clusterMetrics[cluster].CanWrapLineAfter) { if (clusterPosition >= oldCaretPosition) { break; } // Update in case we pass this point next loop. m_caretPosition = clusterPosition; } } } else // SetSelectionModeRightWord { // Read through the clusters, looking for the first stopping point // after the old position. for (int cluster = 0; cluster < clusterMetrics.Length; ++cluster) { int clusterLength = clusterMetrics[cluster].Length; m_caretPosition = clusterPosition; m_caretPositionOffset = clusterLength; // trailing edge if (clusterPosition >= oldCaretPosition && clusterMetrics[cluster].CanWrapLineAfter) { break; // first stopping point after old position. } clusterPosition += clusterLength; } } } break; case SelectionMode.SingleWord: { var clusterMetrics = TextLayout.GetClusterMetrics(); if (clusterMetrics.Length == 0) { break; } // Left of word m_caretPosition = absolutePosition; int clusterPosition = 0; int oldCaretPosition = m_caretPosition; // Read through the clusters, keeping track of the farthest valid // stopping point just before the old position. m_caretPosition = 0; m_caretPositionOffset = 0; // leading edge for (int cluster = 0; cluster < clusterMetrics.Length; ++cluster) { clusterPosition += clusterMetrics[cluster].Length; if (clusterMetrics[cluster].CanWrapLineAfter) { if (clusterPosition >= oldCaretPosition) { break; } // Update in case we pass this point next loop. m_caretPosition = clusterPosition; } } int leftOfWord = m_caretPosition; // Right of word // Read through the clusters, looking for the first stopping point // after the old position. for (int cluster = 0; cluster < clusterMetrics[cluster].Length; ++cluster) { int clusterLength = clusterMetrics[cluster].Length; m_caretPosition = clusterPosition; m_caretPositionOffset = clusterLength; // trailing edge if (clusterPosition >= oldCaretPosition && clusterMetrics[cluster].CanWrapLineAfter) { break; // first stopping point after old position. } clusterPosition += clusterLength; } int rightOfWord = m_caretPosition - 1; m_caretPositionOffset = 0; m_caretAnchor = leftOfWord; //while (rightOfWord > leftOfWord) //{ // char c = TextLayout.Text[rightOfWord]; // if (!(char.IsWhiteSpace(c) || char.IsPunctuation(c))) // break; // --rightOfWord; //} m_caretPosition = rightOfWord; } break; case SelectionMode.Home: case SelectionMode.End: { // Retrieve the line metrics to know first and last position // on the current line. var lineMetrics = TextLayout.GetLineMetrics(); int linePosition; GetLineFromPosition(lineMetrics, m_caretPosition, out line, out linePosition); m_caretPosition = linePosition; m_caretPositionOffset = 0; if (moveMode == SelectionMode.End) { // Place the caret at the last character on the line, // excluding line breaks. In the case of wrapped lines, // newlineLength will be 0. int lineLength = lineMetrics[line].Length - lineMetrics[line].NewlineLength; m_caretPositionOffset = Math.Min(lineLength, 1); m_caretPosition += lineLength - m_caretPositionOffset; AlignCaretToNearestCluster(true, false); } } break; case SelectionMode.First: m_caretPosition = 0; m_caretPositionOffset = 0; break; case SelectionMode.All: m_caretAnchor = 0; extendSelection = true; goto fallthrough; case SelectionMode.Last: fallthrough: m_caretPosition = int.MaxValue; m_caretPositionOffset = 0; AlignCaretToNearestCluster(true, false); break; case SelectionMode.AbsoluteLeading: m_caretPosition = advance; m_caretPositionOffset = 0; break; case SelectionMode.AbsoluteTrailing: m_caretPosition = advance; AlignCaretToNearestCluster(true, false); break; } absolutePosition = m_caretPosition + m_caretPositionOffset; if (!extendSelection) { m_caretAnchor = absolutePosition; } bool caretMoved = (absolutePosition != oldAbsolutePosition) || (m_caretAnchor != oldCaretAnchor); if (caretMoved) { // scroll the text automatically to avoid caret lost var lineMetrics = TextLayout.GetLineMetrics(); int linePosition; GetLineFromPosition(lineMetrics, m_caretPosition, out line, out linePosition); if (line < TopLine) { TopLine = line; } float visibleLines = TextLayout.Height / lineMetrics[0].Height; if (line > TopLine + visibleLines) { TopLine = TopLine + 1; } //RectF rect; GetCaretRect(); //UpdateSystemCaret(rect); //UpdateSelectionRange(); } //Trace.TraceInformation("caretMoved {0} caretPosition {1} caretPositionOffset {2} caretAnchor {3} ", // caretMoved, m_caretPosition, m_caretPositionOffset, m_caretAnchor); Validate(); return(caretMoved); }
public void Render() { RawVector2 nextPos; float cursorY; lock (ScreenElementsLock) { cursorY = Math.Min(TextElementsHeight, Screen.ScreenSize.Height - (ScrollMode ? 0 : Base.DefaultTextFormat.FontSize)); nextPos = new RawVector2(0, cursorY); float skipSz = 0; int x; for (x = ScreenElements.Count - 1; skipSz < TextElementsScroll; x--) { skipSz += ScreenElements[x].RenderedElementSize.Y; } var diff = TextElementsScroll - skipSz; nextPos.Y += diff; while (nextPos.Y > 0 && x >= 0) { nextPos.Y -= ScreenElements[x].RenderedElementSize.Y; ScreenElements[x].Render(nextPos); x--; } } TextLayout tl; lock (CommandLock) { tl = new TextLayout( Base.DWFactory, CommandPre + Command, Base.DefaultTextFormat, Screen.ScreenSize.Width, Screen.ScreenSize.Height ); Base.D2DRenderTarget.FillRectangle(new RawRectangleF(0, cursorY, tl.Metrics.Width, cursorY + tl.Metrics.Height), Base.Brushes.GetColor(Settings.Colors.Background)); Base.D2DRenderTarget.DrawTextLayout( new RawVector2(0, cursorY), tl, Base.Brushes.GetColor(Settings.Colors.NormalText), DrawTextOptions.Clip ); } if (DateTime.Now.Millisecond % 200 >= 100 && !Commands.Parser.CommandExecuting) { float x, y; lock (CursorLock) tl.HitTestTextPosition(Cursor + CommandPre.Length, new RawBool(false), out x, out y); //x = tl.Metrics.WidthIncludingTrailingWhitespace; Base.D2DRenderTarget.DrawLine( new RawVector2( x, cursorY ), new RawVector2( x, cursorY + Base.DefaultTextFormat.FontSize ), Base.Brushes.GetColor(Settings.Colors.NormalText) ); } tl.Dispose(); if (SpecialCommandPending || ScrollMode) { StrokeStyleProperties ssp = new StrokeStyleProperties(); ssp.DashStyle = DashStyle.Dash; ssp.DashOffset = 15.0f; using (var ss = new StrokeStyle(Base.D2DFactory, ssp)) Base.D2DRenderTarget.DrawRectangle(Screen.ScreenRect, Base.Brushes.GetColor(ScrollMode ? ConsoleColor.DarkBlue : ConsoleColor.DarkRed), 3.0f, ss); } }