internal LineRecord(int offset, TextBoxLine line) { _offset = offset; _length = line.Length; _contentLength = line.ContentLength; _width = line.Width; }
// Helper for IncrementalMeasureLinesAfterInsert, IncrementalMeasureLinesAfterDelete. // Formats the line preceding the first directly affected line after a TextContainer change. // In general this line might grow as content in the following line is absorbed. private void FormatFirstIncrementalLine(int lineIndex, double constraintWidth, LineProperties lineProperties, TextBoxLine line, out int lineOffset, out bool endOfParagraph) { int originalEndOffset = _lineMetrics[lineIndex].EndOffset; lineOffset = _lineMetrics[lineIndex].Offset; using (line) { line.Format(lineOffset, constraintWidth, constraintWidth, lineProperties, _cache.TextRunCache, _cache.TextFormatter); _lineMetrics[lineIndex] = new LineRecord(lineOffset, line); lineOffset += line.Length; endOfParagraph = line.EndOfParagraph; } // Don't clear the cached Visual unless something changed. if (originalEndOffset != _lineMetrics[lineIndex].EndOffset) { ClearLineVisual(lineIndex); } }
// Helper for IncrementalMeasureLinesAfterInsert, IncrementalMeasureLinesAfterDelete. // Formats line until we hit a synchronization point, a position where we know // following lines could not be affected by the change. private void SyncLineMetrics(DirtyTextRange range, double constraintWidth, LineProperties lineProperties, TextBoxLine line, bool endOfParagraph, int lineIndex, int lineOffset) { bool offsetSyncOk = (range.PositionsAdded == 0 || range.PositionsRemoved == 0); int lastCoveredCharOffset = range.StartIndex + Math.Max(range.PositionsAdded, range.PositionsRemoved); // Keep updating lines until we find a synchronized position. while (!endOfParagraph && (lineIndex == _lineMetrics.Count || !offsetSyncOk || lineOffset != _lineMetrics[lineIndex].Offset)) { if (lineIndex < _lineMetrics.Count && lineOffset >= _lineMetrics[lineIndex].EndOffset) { // If the current line offset starts past the current line metric offset, // remove the metric. This happens when the previous line // frees up enough space to completely consume the following line. // We can't simply replace the record without potentially missing our // [....] position. _lineMetrics.RemoveAt(lineIndex); // RemoveLineVisualRange(lineIndex, 1); } else { using (line) { line.Format(lineOffset, constraintWidth, constraintWidth, lineProperties, _cache.TextRunCache, _cache.TextFormatter); LineRecord record = new LineRecord(lineOffset, line); if (lineIndex == _lineMetrics.Count || lineOffset + line.Length <= _lineMetrics[lineIndex].Offset) { // The new line preceeds the old line, insert a new record. // _lineMetrics.Insert(lineIndex, record); AddLineVisualPlaceholder(lineIndex); } else { // We expect to be colliding with the old line directly. // If we extend past it, we're in danger of needlessly // re-formatting the entire doc (ie, we miss the real // [....] position and don't stop until EndOfParagraph). Invariant.Assert(lineOffset < _lineMetrics[lineIndex].EndOffset); _lineMetrics[lineIndex] = record; ClearLineVisual(lineIndex); // If this line ends past the invalidated region, and it // has a hard line break, it's safe to synchronize on the next // line metric with a matching start offset. offsetSyncOk |= lastCoveredCharOffset <= record.EndOffset && line.HasLineBreak; } lineIndex++; lineOffset += line.Length; endOfParagraph = line.EndOfParagraph; } } } // Remove any trailing lines that got absorbed into the new last line. if (endOfParagraph && lineIndex < _lineMetrics.Count) { int count = _lineMetrics.Count - lineIndex; _lineMetrics.RemoveRange(lineIndex, count); RemoveLineVisualRange(lineIndex, count); } }
// Measures content invalidated due to a TextContainer change. private void IncrementalMeasureLinesAfterDelete(double constraintWidth, LineProperties lineProperties, DirtyTextRange range, ref Size desiredSize) { int delta = range.PositionsAdded - range.PositionsRemoved; Invariant.Assert(delta < 0); int firstLineIndex = GetLineIndexFromOffset(range.StartIndex); // Clip the scope of the affected lines to the region of the document // we've already inspected. Clipping happens when background layout // has not yet completed but an incremental update happens. int endOffset = range.StartIndex + -delta - 1; if (endOffset > _lineMetrics[_lineMetrics.Count - 1].EndOffset) { Invariant.Assert(this.IsBackgroundLayoutPending); endOffset = _lineMetrics[_lineMetrics.Count - 1].EndOffset; if (range.StartIndex == endOffset) { // Nothing left to do until background layout runs. return; } } int lastLineIndex = GetLineIndexFromOffset(endOffset); // Increment the offsets of all following lines. // for (int i = lastLineIndex + 1; i < _lineMetrics.Count; i++) { _lineMetrics[i].Offset += delta; } TextBoxLine line = new TextBoxLine(this); int lineIndex = firstLineIndex; int lineOffset; bool endOfParagraph; // We need to re-format the previous line, because if someone inserted // a hard break, the first directly affected line might now be shorter // and mergeable with its predecessor. if (lineIndex > 0) // { FormatFirstIncrementalLine(lineIndex - 1, constraintWidth, lineProperties, line, out lineOffset, out endOfParagraph); } else { lineOffset = _lineMetrics[lineIndex].Offset; endOfParagraph = false; } // // Update the first affected line. If it's completely covered, remove it entirely below. if (!endOfParagraph && (range.StartIndex > lineOffset || range.StartIndex + -delta < _lineMetrics[lineIndex].EndOffset)) { // Only part of the line is covered, reformat it. using (line) { line.Format(lineOffset, constraintWidth, constraintWidth, lineProperties, _cache.TextRunCache, _cache.TextFormatter); _lineMetrics[lineIndex] = new LineRecord(lineOffset, line); lineOffset += line.Length; endOfParagraph = line.EndOfParagraph; } ClearLineVisual(lineIndex); lineIndex++; } // Remove all the following lines that are completely covered. // _lineMetrics.RemoveRange(lineIndex, lastLineIndex - lineIndex + 1); RemoveLineVisualRange(lineIndex, lastLineIndex - lineIndex + 1); // Recalc the following lines not directly affected as needed. SyncLineMetrics(range, constraintWidth, lineProperties, line, endOfParagraph, lineIndex, lineOffset); desiredSize = BruteForceCalculateDesiredSize(); }
// Measures content invalidated due to a TextContainer change. private void IncrementalMeasureLinesAfterInsert(double constraintWidth, LineProperties lineProperties, DirtyTextRange range, ref Size desiredSize) { int delta = range.PositionsAdded - range.PositionsRemoved; Invariant.Assert(delta >= 0); int lineIndex = GetLineIndexFromOffset(range.StartIndex, LogicalDirection.Forward); if (delta > 0) { // Increment of the offsets of all following lines. // for (int i = lineIndex + 1; i < _lineMetrics.Count; i++) { _lineMetrics[i].Offset += delta; } } TextBoxLine line = new TextBoxLine(this); int lineOffset; bool endOfParagraph = false; // We need to re-format the previous line, because if someone inserted // a hard break, the first directly affected line might now be shorter // and mergeable with its predecessor. if (lineIndex > 0) // { FormatFirstIncrementalLine(lineIndex - 1, constraintWidth, lineProperties, line, out lineOffset, out endOfParagraph); } else { lineOffset = _lineMetrics[lineIndex].Offset; } // Format the line directly affected by the change. // If endOfParagraph == true, then the line was absorbed into its // predessor (because its new content is thinner, or because the // TextWrapping property changed). if (!endOfParagraph) { using (line) { line.Format(lineOffset, constraintWidth, constraintWidth, lineProperties, _cache.TextRunCache, _cache.TextFormatter); _lineMetrics[lineIndex] = new LineRecord(lineOffset, line); lineOffset += line.Length; endOfParagraph = line.EndOfParagraph; } ClearLineVisual(lineIndex); lineIndex++; } // Recalc the following lines not directly affected as needed. SyncLineMetrics(range, constraintWidth, lineProperties, line, endOfParagraph, lineIndex, lineOffset); desiredSize = BruteForceCalculateDesiredSize(); }
// Performs one iteration of background measure. // Background measure always works at the end of the current // line metrics array -- invalidations to prevoiusly examined // content is handled by incremental layout, synchronously. // // Returns the full content size, omitting any unanalyzed content // at the document end. private Size FullMeasureTick(double constraintWidth, LineProperties lineProperties) { Size desiredSize; TextBoxLine line = new TextBoxLine(this); int lineOffset; bool endOfParagraph; // Find the next position for this iteration. if (_lineMetrics.Count == 0) { desiredSize = new Size(); lineOffset = 0; } else { desiredSize = _contentSize; lineOffset = _lineMetrics[_lineMetrics.Count - 1].EndOffset; } // Calculate a stop time. // We limit work to just a few milliseconds per iteration // to avoid blocking the thread. DateTime stopTime; if ((ScrollBarVisibility)((Control)_host).GetValue(ScrollViewer.VerticalScrollBarVisibilityProperty) == ScrollBarVisibility.Auto) { // Workaround for bug 1766924. // When VerticalScrollBarVisiblity == Auto, there's a problem with // our interaction with ScrollViewer. Disable background layout to // mitigate the problem until we can take a real fix in v.next. // stopTime = DateTime.MaxValue; } else { stopTime = DateTime.Now.AddMilliseconds(_maxMeasureTimeMs); } // Format lines until we hit the end of document or run out of time. do { using (line) { line.Format(lineOffset, constraintWidth, constraintWidth, lineProperties, _cache.TextRunCache, _cache.TextFormatter); // This is a loop invariant, but has negligable cost. // _lineHeight = lineProperties.CalcLineAdvance(line.Height); _lineMetrics.Add(new LineRecord(lineOffset, line)); // Desired width is always max of calculated line widths. // Desired height is sum of all line heights. desiredSize.Width = Math.Max(desiredSize.Width, line.Width); desiredSize.Height += _lineHeight; lineOffset += line.Length; endOfParagraph = line.EndOfParagraph; } } while (!endOfParagraph && DateTime.Now < stopTime); if (!endOfParagraph) { // Ran out of time. Defer to background layout. SetFlags(true, Flags.BackgroundLayoutPending); this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(OnBackgroundMeasure), null); } else { // Finished the entire document. Stop background layout. SetFlags(false, Flags.BackgroundLayoutPending); } return desiredSize; }
// Updates line visuals on an ArrangeOverride call. private void ArrangeVisuals(Size arrangeSize) { Invariant.Assert(_dirtyList == null); // We should never see pending incremental updates during arrange. // // Initialize state. // if (_visualChildren == null) { _visualChildren = new List<TextBoxLineDrawingVisual>(1) ; } EnsureCache(); LineProperties lineProperties = _cache.LineProperties; TextBoxLine line = new TextBoxLine(this); // // Calculate the current viewport extent, in lines. // We won't do any work for lines that aren't visible. // int firstLineIndex; int lastLineIndex; GetVisibleLines(out firstLineIndex, out lastLineIndex); SetViewportLines(firstLineIndex, lastLineIndex); double width = GetWrappingWidth(arrangeSize.Width); double horizontalOffset = GetTextAlignmentCorrection(lineProperties.TextAlignment, width); double verticalOffset = this.VerticalAlignmentOffset; if (_scrollData != null) { horizontalOffset -= _scrollData.HorizontalOffset; verticalOffset -= _scrollData.VerticalOffset; } // Remove invalidated lines from the visual tree. DetachDiscardedVisualChildren(); // // Iterate across the visible lines. // If we have a cached visual, simply update its current offset. // Otherwise, allocate and render a new visual. // double formatWidth = GetWrappingWidth(_previousConstraint.Width); for (int lineIndex = firstLineIndex; lineIndex <= lastLineIndex; lineIndex++) { TextBoxLineDrawingVisual lineVisual = GetLineVisual(lineIndex); if (lineVisual == null) { LineRecord metrics = _lineMetrics[lineIndex]; using (line) { line.Format(metrics.Offset, formatWidth, width, lineProperties, _cache.TextRunCache, _cache.TextFormatter); // We should be in [....] with current metrics, unless background layout is pending. if (!this.IsBackgroundLayoutPending) { Invariant.Assert(metrics.Length == line.Length, "Line is out of [....] with metrics!"); } lineVisual = line.CreateVisual(); } SetLineVisual(lineIndex, lineVisual); AttachVisualChild(lineVisual); } lineVisual.Offset = new Vector(horizontalOffset, verticalOffset + lineIndex * _lineHeight); } }
// Returns a formatted TextBoxLine at the specified index. // Caller must Dispose the TextBoxLine. // This method is expensive. private TextBoxLine GetFormattedLine(int lineIndex, out LineProperties lineProperties) { TextBoxLine line = new TextBoxLine(this); LineRecord metrics = _lineMetrics[lineIndex]; lineProperties = GetLineProperties(); Control hostControl = (Control)_host; TextFormattingMode textFormattingMode = TextOptions.GetTextFormattingMode(hostControl); TextFormatter formatter = TextFormatter.FromCurrentDispatcher(textFormattingMode); double width = GetWrappingWidth(this.RenderSize.Width); double formatWidth = GetWrappingWidth(_previousConstraint.Width); line.Format(metrics.Offset, formatWidth, width, lineProperties, new TextRunCache(), formatter); Invariant.Assert(metrics.Length == line.Length, "Line is out of [....] with metrics!"); return line; }