public static void SplitKey(Slice key, out string docId, out string name) { var bytes = key.AsSpan(); var separatorIndex = key.Content.IndexOf(SpecialChars.RecordSeparator); docId = Encoding.UTF8.GetString(bytes.Slice(0, separatorIndex)); var index = separatorIndex + 1; name = Encoding.UTF8.GetString(bytes.Slice(index, bytes.Length - index)); }
public static IEnumerable <Run> GetFontRuns(Slice <int> codePoints, SKTypeface typeface) { // Get the font manager - we'll use this to select font fallbacks var fontManager = SKFontManager.Default; // Get glyphs using the top-level typeface var glyphs = new ushort[codePoints.Length]; var font = new SKFont(typeface); font.GetGlyphs(codePoints.AsSpan(), glyphs); // Look for subspans that need font fallback (where glyphs are zero) int runStart = 0; for (int i = 0; i < codePoints.Length; i++) { // Do we need fallback for this character? if (glyphs[i] == 0) { // Check if there's a fallback available, if not, might as well continue with the current top-level typeface var subSpanTypeface = fontManager.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[i]); if (subSpanTypeface == null) { continue; } // We can do font fallback... // Flush the current top-level run if (i > runStart) { yield return(new Run() { Start = runStart, Length = i - runStart, Typeface = typeface, }); } // Count how many unmatched characters var unmatchedStart = i; var unmatchedEnd = i + 1; while (unmatchedEnd < codePoints.Length && glyphs[unmatchedEnd] == 0) { unmatchedEnd++; } var unmatchedLength = unmatchedEnd - unmatchedStart; // Match the missing characters while (unmatchedLength > 0) { // Find the font fallback using the first character subSpanTypeface = fontManager.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[unmatchedStart]); if (subSpanTypeface == null) { unmatchedEnd = unmatchedStart; break; } var subSpanFont = new SKFont(subSpanTypeface); // Get the glyphs over the current unmatched range subSpanFont.GetGlyphs(codePoints.SubSlice(unmatchedStart, unmatchedLength).AsSpan(), new Span <ushort>(glyphs, unmatchedStart, unmatchedLength)); // Count how many characters were matched var fallbackStart = unmatchedStart; var fallbackEnd = unmatchedStart + 1; while (fallbackEnd < unmatchedEnd && glyphs[fallbackEnd] != 0) { fallbackEnd++; } var fallbackLength = fallbackEnd - fallbackStart; // Yield this font fallback run yield return(new Run() { Start = fallbackStart, Length = fallbackLength, Typeface = subSpanTypeface, }); // Continue selecting font fallbacks until the entire unmatched ranges has been matched unmatchedStart += fallbackLength; unmatchedLength -= fallbackLength; } // Move onto the next top level span i = unmatchedEnd - 1; // account for i++ on for loop runStart = unmatchedEnd; } } // Flush find run if (codePoints.Length > runStart) { yield return(new Run() { Start = runStart, Length = codePoints.Length - runStart, Typeface = typeface, }); } }
string FromSlice(Slice <char> chars) { return(new string(chars.AsSpan())); }
/// <summary> /// Paint this font run /// </summary> /// <param name="ctx"></param> internal void Paint(PaintTextContext ctx) { // Paint selection? if (ctx.PaintSelectionBackground != null && RunKind != FontRunKind.Ellipsis) { float selStartXCoord; if (ctx.SelectionStart < Start) { selStartXCoord = Direction == TextDirection.LTR ? 0 : Width; } else if (ctx.SelectionStart >= End) { selStartXCoord = Direction == TextDirection.LTR ? Width : 0; } else { selStartXCoord = RelativeCodePointXCoords[ctx.SelectionStart - this.Start]; } float selEndXCoord; if (ctx.SelectionEnd < Start) { selEndXCoord = Direction == TextDirection.LTR ? 0 : Width; } else if (ctx.SelectionEnd >= End) { selEndXCoord = Direction == TextDirection.LTR ? Width : 0; } else { selEndXCoord = RelativeCodePointXCoords[ctx.SelectionEnd - this.Start]; } if (selStartXCoord != selEndXCoord) { var tl = new SKPoint(selStartXCoord + this.XCoord, Line.YCoord); var br = new SKPoint(selEndXCoord + this.XCoord, Line.YCoord + Line.Height); // Align coords to pixel boundaries // Not needed - disabled antialias on SKPaint instead /* * if (ctx.Canvas.TotalMatrix.TryInvert(out var inverse)) * { * tl = ctx.Canvas.TotalMatrix.MapPoint(tl); * br = ctx.Canvas.TotalMatrix.MapPoint(br); * tl = new SKPoint((float)Math.Round(tl.X), (float)Math.Round(tl.Y)); * br = new SKPoint((float)Math.Round(br.X), (float)Math.Round(br.Y)); * tl = inverse.MapPoint(tl); * br = inverse.MapPoint(br); * } */ var rect = new SKRect(tl.X, tl.Y, br.X, br.Y); ctx.Canvas.DrawRect(rect, ctx.PaintSelectionBackground); } } // Don't paint trailing whitespace runs if (RunKind == FontRunKind.TrailingWhitespace) { return; } // Text using (var paint = new SKPaint()) { // Work out font variant adjustments float glyphScale = 1; float glyphVOffset = 0; if (Style.FontVariant == FontVariant.SuperScript) { glyphScale = 0.65f; glyphVOffset = -Style.FontSize * 0.35f; } if (Style.FontVariant == FontVariant.SubScript) { glyphScale = 0.65f; glyphVOffset = Style.FontSize * 0.1f; } // Setup SKPaint paint.Color = Style.TextColor; paint.IsAntialias = ctx.Options.IsAntialias; paint.LcdRenderText = ctx.Options.LcdRenderText; unsafe { fixed(ushort *pGlyphs = Glyphs.Underlying) { // Get glyph positions var glyphPositions = GlyphPositions.ToArray(); // Create the font if (_font == null) { _font = new SKFont(this.Typeface, this.Style.FontSize * glyphScale); _font.Subpixel = true; } // Create the SKTextBlob (if necessary) if (_textBlob == null) { _textBlob = SKTextBlob.CreatePositioned( (IntPtr)(pGlyphs + Glyphs.Start), Glyphs.Length * sizeof(ushort), SKTextEncoding.GlyphId, _font, GlyphPositions.AsSpan()); } // Paint underline if (Style.Underline != UnderlineStyle.None && RunKind == FontRunKind.Normal) { // Work out underline metrics float underlineYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.UnderlinePosition ?? 0); paint.StrokeWidth = _font.Metrics.UnderlineThickness ?? 1; if (Style.Underline == UnderlineStyle.Gapped) { // Get intercept positions var interceptPositions = _textBlob.GetIntercepts(underlineYPos - paint.StrokeWidth / 2, underlineYPos + paint.StrokeWidth); // Paint gapped underlinline float x = XCoord; for (int i = 0; i < interceptPositions.Length; i += 2) { float b = interceptPositions[i] - paint.StrokeWidth; if (x < b) { ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(b, underlineYPos), paint); } x = interceptPositions[i + 1] + paint.StrokeWidth; } if (x < XCoord + Width) { ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(XCoord + Width, underlineYPos), paint); } } else { switch (Style.Underline) { case UnderlineStyle.ImeInput: paint.PathEffect = SKPathEffect.CreateDash(new float[] { paint.StrokeWidth, paint.StrokeWidth }, paint.StrokeWidth); break; case UnderlineStyle.ImeConverted: paint.PathEffect = SKPathEffect.CreateDash(new float[] { paint.StrokeWidth, paint.StrokeWidth }, paint.StrokeWidth); break; case UnderlineStyle.ImeTargetConverted: paint.StrokeWidth *= 2; break; case UnderlineStyle.ImeTargetNonConverted: break; } // Paint solid underline ctx.Canvas.DrawLine(new SKPoint(XCoord, underlineYPos), new SKPoint(XCoord + Width, underlineYPos), paint); paint.PathEffect = null; } } ctx.Canvas.DrawText(_textBlob, 0, 0, paint); } } // Paint strikethrough if (Style.StrikeThrough != StrikeThroughStyle.None && RunKind == FontRunKind.Normal) { paint.StrokeWidth = _font.Metrics.StrikeoutThickness ?? 0; float strikeYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.StrikeoutPosition ?? 0) + glyphVOffset; ctx.Canvas.DrawLine(new SKPoint(XCoord, strikeYPos), new SKPoint(XCoord + Width, strikeYPos), paint); } } }
/// <summary> /// Shape an array of utf-32 code points /// </summary> /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param> /// <param name="codePoints">The utf-32 code points to be shaped</param> /// <param name="style">The user style for the text</param> /// <param name="direction">LTR or RTL direction</param> /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param> /// <param name="asFallbackFor">The type face this font is a fallback for</param> /// <param name="textAlignment">The text alignment of the paragraph, used to control placement of glyphs within character cell when letter spacing used</param> /// <returns>A TextShaper.Result representing the shaped text</returns> public Result Shape(ResultBufferSet bufferSet, Slice <int> codePoints, IStyle style, TextDirection direction, int clusterAdjustment, SKTypeface asFallbackFor, TextAlignment textAlignment) { // Work out if we need to force this to a fixed pitch and if // so the unscale character width we need to use float forceFixedPitchWidth = 0; if (asFallbackFor != _typeface && asFallbackFor != null) { var originalTypefaceShaper = ForTypeface(asFallbackFor); if (originalTypefaceShaper._isFixedPitch) { forceFixedPitchWidth = originalTypefaceShaper._fixedCharacterWidth; } } // Work out how much to shift glyphs in the character cell when using letter spacing // The idea here is to align the glyphs within the character cell the same way as the // text block alignment so that left/right aligned text still aligns with the margin // and centered text is still centered (and not shifted slightly due to the extra // space that would be at the right with normal letter spacing). float glyphLetterSpacingAdjustment = 0; switch (textAlignment) { case TextAlignment.Right: glyphLetterSpacingAdjustment = style.LetterSpacing; break; case TextAlignment.Center: glyphLetterSpacingAdjustment = style.LetterSpacing / 2; break; } using (var buffer = new HarfBuzzSharp.Buffer()) { // Setup buffer buffer.AddUtf32(codePoints.AsSpan(), 0, -1); // Setup directionality (if supplied) switch (direction) { case TextDirection.LTR: buffer.Direction = Direction.LeftToRight; break; case TextDirection.RTL: buffer.Direction = Direction.RightToLeft; break; default: throw new ArgumentException(nameof(direction)); } // Guess other attributes buffer.GuessSegmentProperties(); // Shape it _font.Shape(buffer); // RTL? bool rtl = buffer.Direction == Direction.RightToLeft; // Work out glyph scaling and offsetting for super/subscript float glyphScale = style.FontSize / overScale; float glyphVOffset = 0; if (style.FontVariant == FontVariant.SuperScript) { glyphScale *= 0.65f; glyphVOffset -= style.FontSize * 0.35f; } if (style.FontVariant == FontVariant.SubScript) { glyphScale *= 0.65f; glyphVOffset += style.FontSize * 0.1f; } // Create results and get buffes var r = new Result(); r.GlyphIndicies = bufferSet.GlyphIndicies.Add((int)buffer.Length, false); r.GlyphPositions = bufferSet.GlyphPositions.Add((int)buffer.Length, false); r.Clusters = bufferSet.Clusters.Add((int)buffer.Length, false); r.CodePointXCoords = bufferSet.CodePointXCoords.Add(codePoints.Length, false); r.CodePointXCoords.Fill(0); // Convert points var gp = buffer.GlyphPositions; var gi = buffer.GlyphInfos; float cursorX = 0; float cursorY = 0; float cursorXCluster = 0; for (int i = 0; i < buffer.Length; i++) { r.GlyphIndicies[i] = (ushort)gi[i].Codepoint; r.Clusters[i] = (int)gi[i].Cluster + clusterAdjustment; // Update code point positions if (!rtl) { // First cluster, different cluster, or same cluster with lower x-coord if (i == 0 || (r.Clusters[i] != r.Clusters[i - 1]) || (cursorX < r.CodePointXCoords[r.Clusters[i] - clusterAdjustment])) { r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX; } } // Get the position var pos = gp[i]; // Update glyph position r.GlyphPositions[i] = new SKPoint( cursorX + pos.XOffset * glyphScale + glyphLetterSpacingAdjustment, cursorY - pos.YOffset * glyphScale + glyphVOffset ); // Update cursor position cursorX += pos.XAdvance * glyphScale; cursorY += pos.YAdvance * glyphScale; // Ensure paragraph separator character (0x2029) has some // width so it can be seen as part of the selection in the editor. if (pos.XAdvance == 0 && codePoints[(int)gi[i].Cluster] == 0x2029) { cursorX += style.FontSize * 2 / 3; } if (i + 1 == gi.Length || gi[i].Cluster != gi[i + 1].Cluster) { cursorX += style.LetterSpacing; } // Are we falling back for a fixed pitch font and is the next character a // new cluster? If so advance by the width of the original font, not this // fallback font if (forceFixedPitchWidth != 0) { // New cluster? if (i + 1 >= buffer.Length || gi[i].Cluster != gi[i + 1].Cluster) { // Work out fixed pitch position of next cluster cursorXCluster += forceFixedPitchWidth * glyphScale; if (cursorXCluster > cursorX) { // Nudge characters to center them in the fixed pitch width if (i == 0 || gi[i - 1].Cluster != gi[i].Cluster) { r.GlyphPositions[i].X += (cursorXCluster - cursorX) / 2; } // Use fixed width character position cursorX = cursorXCluster; } else { // Character is wider (probably an emoji) so we // allow it to exceed the fixed pitch character width cursorXCluster = cursorX; } } } // Store RTL cursor position if (rtl) { // First cluster, different cluster, or same cluster with lower x-coord if (i == 0 || (r.Clusters[i] != r.Clusters[i - 1]) || (cursorX > r.CodePointXCoords[r.Clusters[i] - clusterAdjustment])) { r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX; } } } // Finalize cursor positions by filling in any that weren't // referenced by a cluster if (rtl) { r.CodePointXCoords[0] = cursorX; for (int i = codePoints.Length - 2; i >= 0; i--) { if (r.CodePointXCoords[i] == 0) { r.CodePointXCoords[i] = r.CodePointXCoords[i + 1]; } } } else { for (int i = 1; i < codePoints.Length; i++) { if (r.CodePointXCoords[i] == 0) { r.CodePointXCoords[i] = r.CodePointXCoords[i - 1]; } } } // Also return the end cursor position r.EndXCoord = new SKPoint(cursorX, cursorY); // And some other useful metrics r.Ascent = _fontMetrics.Ascent * style.FontSize / overScale; r.Descent = _fontMetrics.Descent * style.FontSize / overScale; r.XMin = _fontMetrics.XMin * style.FontSize / overScale; // Done return(r); } }
/// <summary> /// Splits a sequence of code points into a series of runs with font fallback applied /// </summary> /// <param name="codePoints">The code points</param> /// <param name="typeface">The preferred typeface</param> /// <param name="replacementCharacter">The replacement character to be used for the run</param> /// <returns>A sequence of runs with unsupported code points replaced by a selected font fallback</returns> public static IEnumerable <Run> GetFontRuns(Slice <int> codePoints, SKTypeface typeface, char replacementCharacter = '\0') { var font = new SKFont(typeface); if (replacementCharacter != '\0') { var glyph = font.GetGlyph(replacementCharacter); if (glyph == 0) { var fallbackTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, replacementCharacter); if (fallbackTypeface != null) { typeface = fallbackTypeface; } } yield return(new Run() { Start = 0, Length = codePoints.Length, Typeface = typeface, }); yield break; } // Get glyphs using the top-level typeface var glyphs = new ushort[codePoints.Length]; font.GetGlyphs(codePoints.AsSpan(), glyphs); // Look for subspans that need font fallback (where glyphs are zero) int runStart = 0; for (int i = 0; i < codePoints.Length; i++) { // Do we need fallback for this character? if (glyphs[i] == 0) { // Check if there's a fallback available, if not, might as well continue with the current top-level typeface var subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[i]); if (subSpanTypeface == null) { continue; } // Don't fallback for whitespace characters if (UnicodeClasses.BoundaryGroup(codePoints[i]) == WordBoundaryClass.Space) { continue; } // Must be a cluster boundary if (!GraphemeClusterAlgorithm.IsBoundary(codePoints, i)) { continue; } // We can do font fallback... // Flush the current top-level run if (i > runStart) { yield return(new Run() { Start = runStart, Length = i - runStart, Typeface = typeface, }); } // Count how many unmatched characters var unmatchedStart = i; var unmatchedEnd = i + 1; while (unmatchedEnd < codePoints.Length && (glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary(codePoints, unmatchedEnd))) { unmatchedEnd++; } var unmatchedLength = unmatchedEnd - unmatchedStart; // Match the missing characters while (unmatchedLength > 0) { // Find the font fallback using the first character subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[unmatchedStart]); if (subSpanTypeface == null) { unmatchedEnd = unmatchedStart; break; } var subSpanFont = new SKFont(subSpanTypeface); // Get the glyphs over the current unmatched range subSpanFont.GetGlyphs(codePoints.SubSlice(unmatchedStart, unmatchedLength).AsSpan(), new Span <ushort>(glyphs, unmatchedStart, unmatchedLength)); // Count how many characters were matched var fallbackStart = unmatchedStart; var fallbackEnd = unmatchedStart + 1; while (fallbackEnd < unmatchedEnd && glyphs[fallbackEnd] != 0) { fallbackEnd++; } var fallbackLength = fallbackEnd - fallbackStart; // Yield this font fallback run yield return(new Run() { Start = fallbackStart, Length = fallbackLength, Typeface = subSpanTypeface, }); // Continue selecting font fallbacks until the entire unmatched ranges has been matched unmatchedStart += fallbackLength; unmatchedLength -= fallbackLength; } // Move onto the next top level span i = unmatchedEnd - 1; // account for i++ on for loop runStart = unmatchedEnd; } } // Flush find run if (codePoints.Length > runStart) { yield return(new Run() { Start = runStart, Length = codePoints.Length - runStart, Typeface = typeface, }); } }