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; } }
/// <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); }