protected void AdvanceClusterPosition(ref ClusterPosition cluster) { // Looks forward in the cluster map until finding a new cluster, // or until we reach the end of a cluster run returned by shaping. // // Glyph shaping can produce a clustermap where a: // - A single codepoint maps to a single glyph (simple Latin and precomposed CJK) // - A single codepoint to several glyphs (diacritics decomposed into distinct glyphs) // - Multiple codepoints are coalesced into a single glyph. // int textPosition = cluster.textPosition; int clusterId = glyphClusters_[textPosition]; for (++textPosition; textPosition < cluster.runEndPosition; ++textPosition) { if (glyphClusters_[textPosition] != clusterId) { // Now pointing to the next cluster. cluster.textPosition = textPosition; return; } } if (textPosition == cluster.runEndPosition) { // We crossed a text analysis run. SetClusterPosition(ref cluster, textPosition); } }
protected int GetClusterGlyphStart(ref ClusterPosition cluster) { // Maps from text position to corresponding starting index in the glyph array. // This is needed because there isn't a 1:1 correspondence between text and // glyphs produced. int glyphStart = runs_[cluster.runIndex].glyphStart; return((cluster.textPosition < glyphClusters_.Length) ? glyphStart + glyphClusters_[cluster.textPosition] : glyphStart + runs_[cluster.runIndex].glyphCount); }
public void FlowText(FlowLayoutSource flowSource, FlowLayoutSink flowSink) { // Reflow all the text, from source to sink. if (!isTextAnalysisComplete_) { throw new Exception("FlowLayout: Text analysis in not complete"); } // Determine the font line height, needed by the flow source. FontMetrics fontMetrics = fontFace_.Metrics; float fontHeight = (fontMetrics.Ascent + fontMetrics.Descent + fontMetrics.LineGap) * fontEmSize_ / fontMetrics.DesignUnitsPerEm; // Set initial cluster position to beginning of text. ClusterPosition cluster = new ClusterPosition(); SetClusterPosition(ref cluster, 0); RectangleF rect; ClusterPosition nextCluster; // Iteratively pull rect's from the source, // and push as much text will fit to the sink. while (cluster.textPosition < text_.Length) { // Pull the next rect from the source. if (!flowSource.GetNextRect(fontHeight, out rect)) { break; } if (rect.Right - rect.Left <= 0) { break; // Stop upon reaching zero sized rects. } // Fit as many clusters between breakpoints that will go in. if (!FitText(ref cluster, text_.Length, rect.Right - rect.Left, out nextCluster)) { break; } // Push the glyph runs to the sink. if (!ProduceGlyphRuns(flowSink, ref rect, ref cluster, ref nextCluster)) { break; } cluster = nextCluster; } }
// Text/cluster navigation. protected void SetClusterPosition(ref ClusterPosition cluster, int textPosition) { // Since layout should never split text clusters, we want to move ahead whole // clusters at a time. // Updates the current position and seeks its matching text analysis run. cluster.textPosition = textPosition; // If the new text position is outside the previous analysis run, // find the right one. if (textPosition >= cluster.runEndPosition || !runs_[cluster.runIndex].ContainsTextPosition(textPosition)) { // If we can resume the search from the previous run index, // (meaning the new text position comes after the previously // seeked one), we can save some time. Otherwise restart from // the beginning. int newRunIndex = 0; if (textPosition >= runs_[cluster.runIndex].textStart) { newRunIndex = cluster.runIndex; } // Find new run that contains the text position. for (int i = 0; i < runs_.Length; i++) { if (runs_[i].ContainsTextPosition(textPosition)) { newRunIndex = i; break; } } // Keep run index within the list, rather than pointing off the end. if (newRunIndex >= runs_.Length) { newRunIndex = runs_.Length - 1; } // Cache the position of the next analysis run to efficiently // move forward in the clustermap. cluster.runIndex = newRunIndex; cluster.runEndPosition = runs_[newRunIndex].textStart + runs_[newRunIndex].textLength; } }
protected float GetClusterRangeWidth(ref ClusterPosition clusterStart, ref ClusterPosition clusterEnd) { // Sums the glyph advances between two cluster positions, // useful for determining how long a line or word is. return(GetClusterRangeWidth(GetClusterGlyphStart(ref clusterStart), GetClusterGlyphStart(ref clusterEnd), glyphAdvances_)); }
protected void ProduceJustifiedAdvances(ref RectangleF rect, ref ClusterPosition clusterStart, ref ClusterPosition clusterEnd, out List <float> justifiedAdvances) { // Performs simple inter-word justification // using the breakpoint analysis whitespace property. // Copy out default, unjustified advances. int glyphStart = GetClusterGlyphStart(ref clusterStart); int glyphEnd = GetClusterGlyphStart(ref clusterEnd); justifiedAdvances = new List <float>(glyphEnd - glyphStart + 1); for (int i = glyphStart; i < glyphEnd; i++) { justifiedAdvances.Add(glyphAdvances_[i]); } if (glyphEnd - glyphStart == 0) { return; // No glyphs to modify. } float maxWidth = rect.Right - rect.Left; if (maxWidth <= 0) { return; // Text can't fit anyway. } //////////////////////////////////////// // First, count how many spaces there are in the text range. ClusterPosition cluster = clusterStart; int whitespaceCount = 0; while (cluster.textPosition < clusterEnd.textPosition) { if (breakpoints_[cluster.textPosition].IsWhitespace) { ++whitespaceCount; } AdvanceClusterPosition(ref cluster); } if (whitespaceCount <= 0) { return; // Can't justify using spaces, since none exist. } //////////////////////////////////////// // Second, determine the needed contribution to each space. float lineWidth = GetClusterRangeWidth(glyphStart, glyphEnd, glyphAdvances_); float justificationPerSpace = (maxWidth - lineWidth) / whitespaceCount; if (justificationPerSpace <= 0) { return; // Either already justified or would be negative justification. } if (justificationPerSpace > maxSpaceWidth_) { return; // Avoid justification if it would space the line out awkwardly far. } //////////////////////////////////////// // Lastly, adjust the advance widths, adding the difference to each space character. cluster = clusterStart; while (cluster.textPosition < clusterEnd.textPosition) { if (breakpoints_[cluster.textPosition].IsWhitespace) { justifiedAdvances[GetClusterGlyphStart(ref cluster) - glyphStart] += justificationPerSpace; } AdvanceClusterPosition(ref cluster); } }
protected bool ProduceGlyphRuns(FlowLayoutSink flowSink, ref RectangleF rect, ref ClusterPosition clusterStart, ref ClusterPosition clusterEnd) { // Produce a series of glyph runs from the given range // and send them to the sink. If the entire text fit // into the rect, then we'll only pass on a single glyph // run. //////////////////////////////////////// // Figure out how many runs we cross, because this is the number // of distinct glyph runs we'll need to reorder visually. int runIndexEnd = clusterEnd.runIndex; if (clusterEnd.textPosition > runs_[runIndexEnd].textStart) { ++runIndexEnd; // Only partially cover the run, so round up. } int[] bidiOrdering = new int[100]; int totalRuns = Math.Min(bidiOrdering.Length, runIndexEnd - clusterStart.runIndex); ProduceBidiOrdering(clusterStart.runIndex, totalRuns, bidiOrdering); //////////////////////////////////////// // Ignore any trailing whitespace // Look backward from end until we find non-space. int trailingWsPosition; for (trailingWsPosition = clusterEnd.textPosition; trailingWsPosition > clusterStart.textPosition; --trailingWsPosition) { if (!breakpoints_[trailingWsPosition - 1].IsWhitespace) { break; // Encountered last significant character. } } // Set the glyph run's ending cluster to the last whitespace. ClusterPosition clusterWsEnd = clusterStart; SetClusterPosition(ref clusterWsEnd, trailingWsPosition); //////////////////////////////////////// // Produce justified advances to reduce the jagged edge. List <float> justifiedAdvances; ProduceJustifiedAdvances(ref rect, ref clusterStart, ref clusterWsEnd, out justifiedAdvances); int justificationGlyphStart = GetClusterGlyphStart(ref clusterStart); //////////////////////////////////////// // Determine starting point for alignment. float x = rect.Left; float y = rect.Bottom; FontMetrics fontMetrics = fontFace_.Metrics; float descent = (fontMetrics.Descent * fontEmSize_ / fontMetrics.DesignUnitsPerEm); y -= descent; if (readingDirection_ == ReadingDirection.RightToLeft) { // For RTL, we neeed the run width to adjust the origin // so it starts on the right side. int glyphStart = GetClusterGlyphStart(ref clusterStart); int glyphEnd = GetClusterGlyphStart(ref clusterWsEnd); if (glyphStart < glyphEnd) { float lineWidth = GetClusterRangeWidth( glyphStart - justificationGlyphStart, glyphEnd - justificationGlyphStart, justifiedAdvances.ToArray() ); x = rect.Right - lineWidth; } } //////////////////////////////////////// // Send each glyph run to the sink. for (int i = 0; i < totalRuns; ++i) { Run run = runs_[bidiOrdering[i]]; int glyphStart = run.glyphStart; int glyphEnd = glyphStart + run.glyphCount; // If the run is only partially covered, we'll need to find // the subsection of glyphs that were fit. if (clusterStart.textPosition > run.textStart) { glyphStart = GetClusterGlyphStart(ref clusterStart); } if (clusterWsEnd.textPosition < run.textStart + run.textLength) { glyphEnd = GetClusterGlyphStart(ref clusterWsEnd); } if ((glyphStart >= glyphEnd) || (run.script.Shapes == ScriptShapes.NoVisual)) { // The itemizer told us not to draw this character, // either because it was a formatting, control, or other hidden character. continue; } // The run width is needed to know how far to move forward, // and to flip the origin for right-to-left text. float runWidth = GetClusterRangeWidth( glyphStart - justificationGlyphStart, glyphEnd - justificationGlyphStart, justifiedAdvances.ToArray() ); // Flush this glyph run. //int glyphCount = glyphEnd - glyphStart; int glyphCount = justifiedAdvances.Count; short[] call_glyphIndices_ = new short[glyphCount]; Array.Copy(glyphIndices_, glyphStart, call_glyphIndices_, 0, call_glyphIndices_.Length); float[] call_justifiedAdvances = new float[justifiedAdvances.Count - (glyphStart - justificationGlyphStart)]; justifiedAdvances.CopyTo(glyphStart - justificationGlyphStart, call_justifiedAdvances, 0, call_justifiedAdvances.Length); GlyphOffset[] call_glyphOffsets_ = new GlyphOffset[glyphCount]; Array.Copy(glyphOffsets_, glyphStart, call_glyphOffsets_, 0, call_glyphOffsets_.Length); flowSink.SetGlyphRun( (run.bidiLevel % 2 != 0) ? (x + runWidth) : (x), // origin starts from right if RTL y, glyphCount, call_glyphIndices_, call_justifiedAdvances, call_glyphOffsets_, fontFace_, fontEmSize_, run.bidiLevel, run.isSideways ); x += runWidth; } return(true); }
protected bool FitText(ref ClusterPosition clusterStart, int textEnd, float maxWidth, out ClusterPosition clusterEnd) { // Fits as much text as possible into the given width, // using the clusters and advances returned by DWrite. //////////////////////////////////////// // Set the starting cluster to the starting text position, // and continue until we exceed the maximum width or hit // a hard break. ClusterPosition cluster = clusterStart; ClusterPosition nextCluster = clusterStart; int validBreakPosition = cluster.textPosition; int bestBreakPosition = cluster.textPosition; float textWidth = 0; while (cluster.textPosition < textEnd) { // Use breakpoint information to find where we can safely break words. AdvanceClusterPosition(ref nextCluster); LineBreakpoint breakpoint = breakpoints_[nextCluster.textPosition - 1]; // Check whether we exceeded the amount of text that can fit, // unless it's whitespace, which we allow to flow beyond the end. textWidth += GetClusterRangeWidth(ref cluster, ref nextCluster); if (textWidth > maxWidth && !breakpoint.IsWhitespace) { // Want a minimum of one cluster. if (validBreakPosition > clusterStart.textPosition) { break; } } validBreakPosition = nextCluster.textPosition; // See if we can break after this character cluster, and if so, // mark it as the new potential break point. if (breakpoint.BreakConditionAfter != BreakCondition.MayNotBreak) { bestBreakPosition = validBreakPosition; if (breakpoint.BreakConditionAfter == BreakCondition.MustBreak) { break; // we have a hard return, so we've fit all we can. } } cluster = nextCluster; } //////////////////////////////////////// // Want last best position that didn't break a word, but if that's not available, // fit at least one cluster (emergency line breaking). if (bestBreakPosition == clusterStart.textPosition) { bestBreakPosition = validBreakPosition; } SetClusterPosition(ref cluster, bestBreakPosition); clusterEnd = cluster; return(true); }
protected float GetClusterRangeWidth(ref ClusterPosition clusterStart, ref ClusterPosition clusterEnd) { // Sums the glyph advances between two cluster positions, // useful for determining how long a line or word is. return GetClusterRangeWidth(GetClusterGlyphStart(ref clusterStart), GetClusterGlyphStart(ref clusterEnd), glyphAdvances_); }
protected int GetClusterGlyphStart(ref ClusterPosition cluster) { // Maps from text position to corresponding starting index in the glyph array. // This is needed because there isn't a 1:1 correspondence between text and // glyphs produced. int glyphStart = runs_[cluster.runIndex].glyphStart; return (cluster.textPosition < glyphClusters_.Length) ? glyphStart + glyphClusters_[cluster.textPosition] : glyphStart + runs_[cluster.runIndex].glyphCount; }
// Text/cluster navigation. protected void SetClusterPosition(ref ClusterPosition cluster, int textPosition) { // Since layout should never split text clusters, we want to move ahead whole // clusters at a time. // Updates the current position and seeks its matching text analysis run. cluster.textPosition = textPosition; // If the new text position is outside the previous analysis run, // find the right one. if (textPosition >= cluster.runEndPosition || !runs_[cluster.runIndex].ContainsTextPosition(textPosition)) { // If we can resume the search from the previous run index, // (meaning the new text position comes after the previously // seeked one), we can save some time. Otherwise restart from // the beginning. int newRunIndex = 0; if (textPosition >= runs_[cluster.runIndex].textStart) { newRunIndex = cluster.runIndex; } // Find new run that contains the text position. for (int i = 0; i < runs_.Length; i++) if (runs_[i].ContainsTextPosition(textPosition)) { newRunIndex = i; break; } // Keep run index within the list, rather than pointing off the end. if (newRunIndex >= runs_.Length) { newRunIndex = runs_.Length - 1; } // Cache the position of the next analysis run to efficiently // move forward in the clustermap. cluster.runIndex = newRunIndex; cluster.runEndPosition = runs_[newRunIndex].textStart + runs_[newRunIndex].textLength; } }
protected void ProduceJustifiedAdvances(ref RectangleF rect, ref ClusterPosition clusterStart, ref ClusterPosition clusterEnd, out List<float> justifiedAdvances) { // Performs simple inter-word justification // using the breakpoint analysis whitespace property. // Copy out default, unjustified advances. int glyphStart = GetClusterGlyphStart(ref clusterStart); int glyphEnd = GetClusterGlyphStart(ref clusterEnd); justifiedAdvances = new List<float>(glyphEnd - glyphStart + 1); for (int i = glyphStart; i < glyphEnd; i++) justifiedAdvances.Add(glyphAdvances_[i]); if (glyphEnd - glyphStart == 0) return; // No glyphs to modify. float maxWidth = rect.Right - rect.Left; if (maxWidth <= 0) return; // Text can't fit anyway. //////////////////////////////////////// // First, count how many spaces there are in the text range. ClusterPosition cluster = clusterStart; int whitespaceCount = 0; while (cluster.textPosition < clusterEnd.textPosition) { if (breakpoints_[cluster.textPosition].IsWhitespace) ++whitespaceCount; AdvanceClusterPosition(ref cluster); } if (whitespaceCount <= 0) return; // Can't justify using spaces, since none exist. //////////////////////////////////////// // Second, determine the needed contribution to each space. float lineWidth = GetClusterRangeWidth(glyphStart, glyphEnd, glyphAdvances_); float justificationPerSpace = (maxWidth - lineWidth) / whitespaceCount; if (justificationPerSpace <= 0) return; // Either already justified or would be negative justification. if (justificationPerSpace > maxSpaceWidth_) return; // Avoid justification if it would space the line out awkwardly far. //////////////////////////////////////// // Lastly, adjust the advance widths, adding the difference to each space character. cluster = clusterStart; while (cluster.textPosition < clusterEnd.textPosition) { if (breakpoints_[cluster.textPosition].IsWhitespace) justifiedAdvances[GetClusterGlyphStart(ref cluster) - glyphStart] += justificationPerSpace; AdvanceClusterPosition(ref cluster); } }
protected bool ProduceGlyphRuns(FlowLayoutSink flowSink, ref RectangleF rect, ref ClusterPosition clusterStart, ref ClusterPosition clusterEnd) { // Produce a series of glyph runs from the given range // and send them to the sink. If the entire text fit // into the rect, then we'll only pass on a single glyph // run. //////////////////////////////////////// // Figure out how many runs we cross, because this is the number // of distinct glyph runs we'll need to reorder visually. int runIndexEnd = clusterEnd.runIndex; if (clusterEnd.textPosition > runs_[runIndexEnd].textStart) ++runIndexEnd; // Only partially cover the run, so round up. int[] bidiOrdering = new int[100]; int totalRuns = Math.Min(bidiOrdering.Length, runIndexEnd - clusterStart.runIndex); ProduceBidiOrdering(clusterStart.runIndex, totalRuns, bidiOrdering); //////////////////////////////////////// // Ignore any trailing whitespace // Look backward from end until we find non-space. int trailingWsPosition; for (trailingWsPosition = clusterEnd.textPosition; trailingWsPosition > clusterStart.textPosition; --trailingWsPosition) { if (!breakpoints_[trailingWsPosition - 1].IsWhitespace) break; // Encountered last significant character. } // Set the glyph run's ending cluster to the last whitespace. ClusterPosition clusterWsEnd = clusterStart; SetClusterPosition(ref clusterWsEnd, trailingWsPosition); //////////////////////////////////////// // Produce justified advances to reduce the jagged edge. List<float> justifiedAdvances; ProduceJustifiedAdvances(ref rect, ref clusterStart, ref clusterWsEnd, out justifiedAdvances); int justificationGlyphStart = GetClusterGlyphStart(ref clusterStart); //////////////////////////////////////// // Determine starting point for alignment. float x = rect.Left; float y = rect.Bottom; FontMetrics fontMetrics = fontFace_.Metrics; float descent = (fontMetrics.Descent * fontEmSize_ / fontMetrics.DesignUnitsPerEm); y -= descent; if (readingDirection_ == ReadingDirection.RightToLeft) { // For RTL, we neeed the run width to adjust the origin // so it starts on the right side. int glyphStart = GetClusterGlyphStart(ref clusterStart); int glyphEnd = GetClusterGlyphStart(ref clusterWsEnd); if (glyphStart < glyphEnd) { float lineWidth = GetClusterRangeWidth( glyphStart - justificationGlyphStart, glyphEnd - justificationGlyphStart, justifiedAdvances.ToArray() ); x = rect.Right - lineWidth; } } //////////////////////////////////////// // Send each glyph run to the sink. for (int i = 0; i < totalRuns; ++i) { Run run = runs_[bidiOrdering[i]]; int glyphStart = run.glyphStart; int glyphEnd = glyphStart + run.glyphCount; // If the run is only partially covered, we'll need to find // the subsection of glyphs that were fit. if (clusterStart.textPosition > run.textStart) { glyphStart = GetClusterGlyphStart(ref clusterStart); } if (clusterWsEnd.textPosition < run.textStart + run.textLength) { glyphEnd = GetClusterGlyphStart(ref clusterWsEnd); } if ((glyphStart >= glyphEnd) || (run.script.Shapes == ScriptShapes.NoVisual)) { // The itemizer told us not to draw this character, // either because it was a formatting, control, or other hidden character. continue; } // The run width is needed to know how far to move forward, // and to flip the origin for right-to-left text. float runWidth = GetClusterRangeWidth( glyphStart - justificationGlyphStart, glyphEnd - justificationGlyphStart, justifiedAdvances.ToArray() ); // Flush this glyph run. //int glyphCount = glyphEnd - glyphStart; int glyphCount = justifiedAdvances.Count; short[] call_glyphIndices_ = new short[glyphCount]; Array.Copy(glyphIndices_, glyphStart, call_glyphIndices_, 0, call_glyphIndices_.Length); float[] call_justifiedAdvances = new float[justifiedAdvances.Count - (glyphStart - justificationGlyphStart)]; justifiedAdvances.CopyTo(glyphStart - justificationGlyphStart, call_justifiedAdvances, 0, call_justifiedAdvances.Length); GlyphOffset[] call_glyphOffsets_ = new GlyphOffset[glyphCount]; Array.Copy(glyphOffsets_, glyphStart, call_glyphOffsets_, 0, call_glyphOffsets_.Length); flowSink.SetGlyphRun( (run.bidiLevel % 2 != 0) ? (x + runWidth) : (x), // origin starts from right if RTL y, glyphCount, call_glyphIndices_, call_justifiedAdvances, call_glyphOffsets_, fontFace_, fontEmSize_, run.bidiLevel, run.isSideways ); x += runWidth; } return true; }
protected bool FitText(ref ClusterPosition clusterStart, int textEnd, float maxWidth, out ClusterPosition clusterEnd) { // Fits as much text as possible into the given width, // using the clusters and advances returned by DWrite. //////////////////////////////////////// // Set the starting cluster to the starting text position, // and continue until we exceed the maximum width or hit // a hard break. ClusterPosition cluster = clusterStart; ClusterPosition nextCluster = clusterStart; int validBreakPosition = cluster.textPosition; int bestBreakPosition = cluster.textPosition; float textWidth = 0; while (cluster.textPosition < textEnd) { // Use breakpoint information to find where we can safely break words. AdvanceClusterPosition(ref nextCluster); LineBreakpoint breakpoint = breakpoints_[nextCluster.textPosition - 1]; // Check whether we exceeded the amount of text that can fit, // unless it's whitespace, which we allow to flow beyond the end. textWidth += GetClusterRangeWidth(ref cluster, ref nextCluster); if (textWidth > maxWidth && !breakpoint.IsWhitespace) { // Want a minimum of one cluster. if (validBreakPosition > clusterStart.textPosition) break; } validBreakPosition = nextCluster.textPosition; // See if we can break after this character cluster, and if so, // mark it as the new potential break point. if (breakpoint.BreakConditionAfter != BreakCondition.MayNotBreak) { bestBreakPosition = validBreakPosition; if (breakpoint.BreakConditionAfter == BreakCondition.MustBreak) break; // we have a hard return, so we've fit all we can. } cluster = nextCluster; } //////////////////////////////////////// // Want last best position that didn't break a word, but if that's not available, // fit at least one cluster (emergency line breaking). if (bestBreakPosition == clusterStart.textPosition) bestBreakPosition = validBreakPosition; SetClusterPosition(ref cluster, bestBreakPosition); clusterEnd = cluster; return true; }
public void FlowText(FlowLayoutSource flowSource, FlowLayoutSink flowSink) { // Reflow all the text, from source to sink. if (!isTextAnalysisComplete_) throw new Exception("FlowLayout: Text analysis in not complete"); // Determine the font line height, needed by the flow source. FontMetrics fontMetrics = fontFace_.Metrics; float fontHeight = (fontMetrics.Ascent + fontMetrics.Descent + fontMetrics.LineGap) * fontEmSize_ / fontMetrics.DesignUnitsPerEm; // Set initial cluster position to beginning of text. ClusterPosition cluster = new ClusterPosition(); SetClusterPosition(ref cluster, 0); RectangleF rect; ClusterPosition nextCluster; // Iteratively pull rect's from the source, // and push as much text will fit to the sink. while (cluster.textPosition < text_.Length) { // Pull the next rect from the source. if (!flowSource.GetNextRect(fontHeight, out rect)) break; if (rect.Right - rect.Left <= 0) break; // Stop upon reaching zero sized rects. // Fit as many clusters between breakpoints that will go in. if (!FitText(ref cluster, text_.Length, rect.Right - rect.Left, out nextCluster)) break; // Push the glyph runs to the sink. if (!ProduceGlyphRuns(flowSink, ref rect, ref cluster, ref nextCluster)) break; cluster = nextCluster; } }