// RichTextKit // Copyright © 2019-2020 Topten Software. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this product except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. using SkiaSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Topten.RichTextKit { /// /// Represents a string decorated with rich text information including /// various helper methods for constructing rich text strings with a /// fluent-like API. /// public class RichString { /// /// Constructs a new rich text string /// /// An initial piece of text to append to the string public RichString(string str = null) { _paragraphs.Add(new ParagraphInfo()); if (str != null) Add(str); } /// /// Append text to this RichString /// /// The text to append /// public RichString Add(string text) => Append(new TextItem(text)); /// /// Adds text with various style attributes changed /// /// The text to append /// The new font family /// The new font size /// The new font weight /// The new font width /// The new font italic /// The new underline style /// The new strike-through style /// The new line height /// The new text color /// The new background color /// The new halo color /// The new halo width /// The new halo blur width /// The new character spacing /// The new font variant /// The new text direction /// A reference to the same RichString instance public RichString Add(string text, string fontFamily = null, float? fontSize = null, int? fontWeight = null, SKFontStyleWidth? fontWidth = null, bool? fontItalic = null, UnderlineStyle? underline = null, StrikeThroughStyle? strikeThrough = null, float? lineHeight = null, SKColor? textColor = null, SKColor? backgroundColor = null, SKColor? haloColor = null, float? haloWidth = null, float? haloBlur = null, float? letterSpacing = null, FontVariant? fontVariant = null, TextDirection? textDirection = null ) { if (string.IsNullOrEmpty(text)) return this; Push(); if (fontFamily != null) FontFamily(fontFamily); if (fontSize.HasValue) FontSize(fontSize.Value); if (fontWeight.HasValue) FontWeight(fontWeight.Value); if (fontWidth.HasValue) FontWidth(fontWidth.Value); if (fontItalic.HasValue) FontItalic(fontItalic.Value); if (underline.HasValue) Underline(underline.Value); if (strikeThrough.HasValue) StrikeThrough(strikeThrough.Value); if (lineHeight.HasValue) LineHeight(lineHeight.Value); if (textColor.HasValue) TextColor(textColor.Value); if (backgroundColor.HasValue) BackgroundColor(backgroundColor.Value); if (haloColor.HasValue) HaloColor(haloColor.Value); if (haloWidth.HasValue) HaloWidth(haloWidth.Value); if (haloBlur.HasValue) HaloBlur(haloBlur.Value); if (fontVariant.HasValue) FontVariant(fontVariant.Value); if (letterSpacing.HasValue) LetterSpacing(letterSpacing.Value); if (textDirection.HasValue) TextDirection(textDirection.Value); Add(text); Pop(); return this; } /// /// Changes the font family /// /// The new font family /// A reference to the same RichString instance public RichString FontFamily(string value) => Append(new FontFamilyItem(value)); /// /// Changes the font size /// /// The new font size /// A reference to the same RichString instance public RichString FontSize(float value) => Append(new FontSizeItem(value)); /// /// Changes the font weight /// /// The new font weight /// A reference to the same RichString instance public RichString FontWeight(int value) => Append(new FontWeightItem(value)); /// /// Changes the font weight to bold or normal /// /// The new font bold setting /// A reference to the same RichString instance public RichString Bold(bool value = true) => Append(new FontWeightItem(value ? 700 : 400)); /// /// Changes the font width /// /// The new font width /// A reference to the same RichString instance public RichString FontWidth(SKFontStyleWidth value) => Append(new FontWidthItem(value)); /// /// Changes the font italic setting /// /// The new font italic setting /// A reference to the same RichString instance public RichString FontItalic(bool value = true) => Append(new FontItalicItem(value)); /// /// Changes the underline style /// /// The new underline style /// A reference to the same RichString instance public RichString Underline(UnderlineStyle value = UnderlineStyle.Gapped) => Append(new UnderlineItem(value)); /// /// Changes the strike-through style /// /// The new strike through style /// A reference to the same RichString instance public RichString StrikeThrough(StrikeThroughStyle value = StrikeThroughStyle.Solid) => Append(new StrikeThroughItem(value)); /// /// Changes the line height /// /// The new line height /// A reference to the same RichString instance public RichString LineHeight(float value) => Append(new LineHeightItem(value)); /// /// Changes the text color /// /// The new text color /// A reference to the same RichString instance public RichString TextColor(SKColor value) => Append(new TextColorItem(value)); /// /// Changes the background color /// /// The new background color /// A reference to the same RichString instance public RichString BackgroundColor(SKColor value) => Append(new BackgroundColorItem(value)); /// /// Changes the halo color /// /// The new halo color /// A reference to the same RichString instance public RichString HaloColor(SKColor value) => Append(new HaloColorItem(value)); /// /// Changes the halo width /// /// The new halo width /// A reference to the same RichString instance public RichString HaloWidth(float value) => Append(new HaloWidthItem(value)); /// /// Changes the halo blur width /// /// The new halo blur width /// A reference to the same RichString instance public RichString HaloBlur(float value) => Append(new HaloBlurItem(value)); /// /// Changes the character spacing /// /// The new character spacing /// A reference to the same RichString instance public RichString LetterSpacing(float value) => Append(new LetterSpacingItem(value)); /// /// Changes the font variant /// /// The new font variant /// A reference to the same RichString instance public RichString FontVariant(FontVariant value) => Append(new FontVariantItem(value)); /// /// Changes the text direction /// /// The new text direction /// A reference to the same RichString instance public RichString TextDirection(TextDirection value) => Append(new TextDirectionItem(value)); /// /// Saves the current style to an internal stack /// /// A reference to the same RichString instance public RichString Push() => Append(new PushItem()); /// /// Resets to normal font (normal weight, italic off, underline off, strike through off, font variant off /// /// A reference to the same RichString instance public RichString Normal() => Append(new NormalItem()); /// /// Restores a previous saved style from the internal stack /// /// A reference to the same RichString instance public RichString Pop() => Append(new PopItem()); /// /// Starts a new text paragraph /// /// A reference to the same RichString instance public RichString Paragraph() { // End the previous paragraph with a carriage return Add("\n"); // Start new paragraph _paragraphs.Add(new ParagraphInfo(_paragraphs[_paragraphs.Count - 1])); Invalidate(); return this; } /// /// Sets the left margin of the current paragraph /// /// The margin width /// A reference to the same RichString instance public RichString MarginLeft(float value) { _paragraphs[_paragraphs.Count - 1].MarginLeft = value; Invalidate(); return this; } /// /// Sets the right margin of the current paragraph /// /// The margin width /// A reference to the same RichString instance public RichString MarginRight(float value) { _paragraphs[_paragraphs.Count - 1].MarginRight = value; Invalidate(); return this; } /// /// Sets the top margin of the current paragraph /// /// The margin height /// A reference to the same RichString instance public RichString MarginTop(float value) { _paragraphs[_paragraphs.Count - 1].MarginTop = value; Invalidate(); return this; } /// /// Sets the bottom margin of the current paragraph /// /// The margin height /// A reference to the same RichString instance public RichString MarginBottom(float value) { _paragraphs[_paragraphs.Count - 1].MarginBottom = value; Invalidate(); return this; } /// /// Sets the text alignment of the current paragraph /// /// The text alignment /// A reference to the same RichString instance public RichString Alignment(TextAlignment value) { _paragraphs[_paragraphs.Count - 1].TextAlignment = value; Invalidate(); return this; } /// /// Sets the base text direction of the current paragraph /// /// The base text direction /// A reference to the same RichString instance public RichString BaseDirection(TextDirection value) { _paragraphs[_paragraphs.Count - 1].BaseDirection = value; Invalidate(); return this; } /// /// The max width property sets the maximum width of a line, after which /// the line will be wrapped onto the next line. /// /// /// This property can be set to null, in which case lines won't be wrapped. /// public float? MaxWidth { get => _maxWidth; set { if (value.HasValue && value.Value < 0) value = 0; if (_maxWidth != value) { _maxWidth = value; Invalidate(); } } } /// /// The maximum height of the TextBlock after which lines will be /// truncated and the final line will be appended with an /// ellipsis (`...`) character. /// /// /// This property can be set to null, in which case the vertical height of the text block /// won't be capped. /// public float? MaxHeight { get => _maxHeight; set { if (value.HasValue && value.Value < 0) value = 0; if (value != _maxHeight) { _maxHeight = value; Invalidate(); } } } /// /// The maximum number of lines after which lines will be /// truncated and the final line will be appended with an /// ellipsis (`...`) character. /// /// /// This property can be set to null, in which case the vertical height of /// the text block won't be capped. /// public int? MaxLines { get => _maxLines; set { if (value.HasValue && value.Value < 0) value = 0; if (value != _maxLines) { _maxLines = value; Invalidate(); } } } /// /// Sets the default text alignment for cases where /// the rich text doesn't specify an alignment /// public TextAlignment DefaultAlignment { get => _textAlignment; set { if (_textAlignment != value) { _textAlignment = value; Invalidate(); } } } /// /// The default base text direction for cases where the rich text /// doesn't explicitly specify a text direction /// public TextDirection DefaultDirection { get => _baseDirection; set { if (_baseDirection != value) { _baseDirection = value; Invalidate(); } } } /// /// The default text style to be used as the current style at the start of the rich string. /// Subsequent formatting operations will be applied over this base style. /// public IStyle DefaultStyle { get => _baseStyle; set { if (!_baseStyle.IsSame(value)) { _needsFullLayout = true; Invalidate(); } _baseStyle = value; } } /// /// Paint this text block /// /// The Skia canvas to paint to /// Options controlling the paint operation public void Paint( SKCanvas canvas, TextPaintOptions options = null) { Paint(canvas, SKPoint.Empty, options); } /// /// Paint this text block /// /// The Skia canvas to paint to /// The top left position within the canvas to draw at /// Options controlling the paint operation public void Paint( SKCanvas canvas, SKPoint position, TextPaintOptions options = null) { Layout(); var ctx = new PaintContext() { owner = this, canvas = canvas, paintPosition = position, renderWidth = _maxWidth ?? _measuredWidth, textPaintOptions = options, }; foreach (var p in _paragraphs) { p.Paint(ref ctx); } } /// /// Discards all internal layout structures /// public void DiscardLayout() { _needsLayout = true; foreach (var p in _paragraphs) { p.DiscardLayout(); } } /// /// The total height of all lines. /// public float MeasuredHeight { get { Layout(); return _measuredHeight; } } /// /// The width of the widest line of text. /// /// /// The returned width does not include any overhang. /// public float MeasuredWidth { get { Layout(); return _measuredWidth; } } /// /// The number of lines in the text /// public int LineCount { get { Layout(); return _measuredLines; } } /// /// Indicates if the text was truncated due to max height or max lines /// constraints /// public bool Truncated { get { Layout(); return _truncated; } } /// /// Gets the total length of the string in code points /// public int Length { get { Layout(); return _totalLength; } } /// /// Gets the measured length of the string up to the truncation point /// in code points /// public int MeasuredLength { get { Layout(); return _measuredLength; } } /// /// Returns the revision number of the content of this rich text string /// /// /// If the revision number of a text string has not changed then painting it /// again will result in the exact same representation as the previous time. /// public uint Revision { get { if (!_revisionValid) { _revision = (uint)System.Threading.Interlocked.Increment(ref _nextRevision); _revisionValid = true; } return _revision; } } /// /// Provides the plain-text equivalent of this RichString /// /// A plain-text string public override string ToString() { var sb = new StringBuilder(); foreach (var p in _paragraphs) { p.Build(sb); } return sb.ToString(); } /// /// Hit test this string /// /// The x-coordinate relative to top left of the string /// The x-coordinate relative to top left of the string /// A HitTestResult public HitTestResult HitTest(float x, float y) { // Make sure layout is up to date Layout(); // Find the closest paragraph var para = FindClosestParagraph(y); // Get it's paint positio var paintPos = para.TextBlockPaintPosition(this); // Hit test var htr = para.TextBlock.HitTest(x - paintPos.X, y - paintPos.Y); // Convert the hit test record from TextBlock relative indices // to rich string relative indicies htr.ClosestLine += para.LineOffset; htr.ClosestCodePointIndex += para.CodePointOffset; if (htr.OverLine >= 0) htr.OverLine += para.LineOffset; if (htr.OverCodePointIndex >= 0) htr.OverCodePointIndex += para.CodePointOffset; // Done return htr; } ParagraphInfo FindClosestParagraph(float y) { // Work out which text block is closest ParagraphInfo pPrev = null; foreach (var p in _paragraphs) { // Ignore truncated paragraphs if (p.Truncated) break; // Is it before this paragraph's text block? if (y < p.yPosition + p.MarginTop && pPrev != null) { // Is it closer to this paragraph or the previous // NB: We compare the text block coords, not the paragraphs // so that regardless of paragraph margins we always // hit test against the closer text block var distPrev = y - (pPrev.yPosition + pPrev.TextBlock.MeasuredHeight); var distThis = y - (p.yPosition + p.MarginTop); if (Math.Abs(distPrev) < Math.Abs(distThis)) return pPrev; else return p; } // Is it within this paragraph's textblock? if (y < p.yPosition + p.MarginTop + p.TextBlock.MeasuredHeight) { return p; } // Store the previous paragraph pPrev = p; } return pPrev; } /// public CaretInfo GetCaretInfo(CaretPosition position) { Layout(); // Is it outside the displayed range? if (position.CodePointIndex < 0 || position.CodePointIndex > MeasuredLength) return CaretInfo.None; // Find the paragraph containing that code point ParagraphInfo p; if (position.CodePointIndex == MeasuredLength) { // Special case for after the last displayed paragraph p = _paragraphs.LastOrDefault(x => !x.Truncated); } else { p = ParagraphForCodePointIndex(position.CodePointIndex); } // Get the caret info var ci = p.TextBlock.GetCaretInfo(new CaretPosition(position.CodePointIndex - p.CodePointOffset, position.AltPosition)); // Adjust it ci.CodePointIndex += p.CodePointOffset; // Get it's paint position var paintPos = p.TextBlockPaintPosition(this); ci.CaretXCoord += paintPos.X; ci.CaretRectangle.Offset(paintPos); return ci; } ParagraphInfo ParagraphForCodePointIndex(int index) { for (int i=1; i<_paragraphs.Count; i++) { if (index < _paragraphs[i].CodePointOffset) return _paragraphs[i - 1]; } return _paragraphs[_paragraphs.Count - 1]; } void Invalidate() { _needsLayout = true; _revisionValid = false; } void Layout() { // Full layout needed? if (_needsFullLayout) { _needsFullLayout = false; DiscardLayout(); } // Needed? if (!_needsLayout) return; _needsLayout = false; // Create a layout context var lctx = new LayoutContext() { yPosition = 0, maxWidth = _maxWidth, maxHeight = _maxHeight, maxLines = _maxLines, textAlignment = _textAlignment, baseDirection = _baseDirection, styleManager = StyleManager.Default.Value, previousParagraph = null, }; // Setup style manager lctx.styleManager.Reset(); if (_baseStyle != null) lctx.styleManager.CurrentStyle = _baseStyle; // Layout each paragraph _measuredWidth = 0; foreach (var p in _paragraphs) { // Layout the paragraph p.Layout(ref lctx); // If this paragraph wasn't completely truncated, then update the measured width if (!p.Truncated) { if (p.TextBlock.MeasuredWidth > _measuredWidth) _measuredWidth = p.TextBlock.MeasuredWidth; } // Store the this paragraph as the previous so a fully truncated subsequent // paragraph can add the ellipsis to this one lctx.previousParagraph = p; } _measuredHeight = lctx.yPosition; _measuredLines = lctx.lineCount; _truncated = lctx.Truncated; _measuredLength = lctx.MeasuredLength; _totalLength = lctx.TotalLength; } struct PaintContext { public RichString owner; public SKCanvas canvas; public SKPoint paintPosition; public float renderWidth; public TextPaintOptions textPaintOptions; } struct LayoutContext { public float yPosition; public int lineCount; public bool Truncated; public float? maxWidth; public float? maxHeight; public int? maxLines; public TextAlignment? textAlignment; public TextDirection? baseDirection; public StyleManager styleManager; public ParagraphInfo previousParagraph; public int MeasuredLength; public int TotalLength; } // Append an item to the current paragraph RichString Append(Item item) { _paragraphs[_paragraphs.Count - 1]._items.Add(item); _needsFullLayout = true; Invalidate(); return this; } static int _nextRevision = 0; bool _revisionValid = false; uint _revision = 0; bool _needsLayout = true; bool _needsFullLayout = true; float? _maxWidth; float? _maxHeight; int? _maxLines; TextAlignment _textAlignment; TextDirection _baseDirection; IStyle _baseStyle; float _measuredWidth; float _measuredHeight; int _measuredLines; bool _truncated; int _measuredLength; int _totalLength; List _paragraphs = new List(); class ParagraphInfo { public ParagraphInfo() { } public ParagraphInfo(ParagraphInfo other) { MarginLeft = other.MarginLeft; MarginRight = other.MarginRight; MarginTop = other.MarginTop; MarginBottom = other.MarginBottom; TextAlignment = other.TextAlignment; BaseDirection = other.BaseDirection; } public void DiscardLayout() { TextBlock = null; Truncated = false; } // The position at which to paint this text block // relative to the top left of the entire string public SKPoint TextBlockPaintPosition(RichString owner) { // Adjust x-position according to resolved text alignment to prevent // having to re-calculate the TextBlock's layout float yPos = this.yPosition + MarginTop; float xPos = MarginLeft; if (!owner.MaxWidth.HasValue) { switch (TextBlock.ResolveTextAlignment()) { case RichTextKit.TextAlignment.Center: xPos += (owner.MeasuredWidth - TextBlock.MeasuredWidth) / 2; break; case RichTextKit.TextAlignment.Right: xPos += owner.MeasuredWidth - TextBlock.MeasuredWidth; break; } } return new SKPoint(xPos, yPos); } public void Build(StringBuilder sb) { foreach (var i in _items) { i.Build(sb); } } public void Paint(ref PaintContext ctx) { if (Truncated) return; TextRange? oldSel = null; if (ctx.textPaintOptions != null) { // Save old selection ranges oldSel = ctx.textPaintOptions.Selection; if (ctx.textPaintOptions.Selection.HasValue) { ctx.textPaintOptions.Selection = ctx.textPaintOptions.Selection.Value.Offset(-this.CodePointOffset); } } // Paint it TextBlock.Paint(ctx.canvas, ctx.paintPosition + TextBlockPaintPosition(ctx.owner), ctx.textPaintOptions); // Restore selection indicies if (oldSel.HasValue) { ctx.textPaintOptions.Selection = oldSel; } } public int CodePointOffset; public int LineOffset; // Layout this paragraph public void Layout(ref LayoutContext ctx) { // Store y position of this block this.yPosition = ctx.yPosition; // Create the text block if (TextBlock == null) { TextBlock = new TextBlock(); var buildContext = new BuildContext() { StyleManager = ctx.styleManager, TextBlock = TextBlock, }; foreach (var i in _items) { i.Build(buildContext); } } // Store code point offset of this paragraph CodePointOffset = ctx.TotalLength; LineOffset = ctx.lineCount; ctx.TotalLength += TextBlock.Length; // Text already truncated? Truncated = ctx.Truncated; if (Truncated) return; // Set the TextBlock TextBlock.Alignment = TextAlignment ?? ctx.textAlignment ?? RichTextKit.TextAlignment.Auto; TextBlock.BaseDirection = BaseDirection ?? ctx.baseDirection ?? RichTextKit.TextDirection.Auto; // Setup max width if (ctx.maxWidth.HasValue) { TextBlock.MaxWidth = ctx.maxWidth.Value - (MarginLeft + MarginRight); } else { TextBlock.MaxWidth = null; } // Set max height if (ctx.maxHeight.HasValue) { TextBlock.MaxHeight = ctx.maxHeight.Value - (ctx.yPosition) - (MarginTop + MarginBottom); } else { TextBlock.MaxHeight = null; } // Set max lines if (ctx.maxLines.HasValue) { TextBlock.MaxLines = ctx.maxLines.Value - ctx.lineCount; } else { TextBlock.MaxLines = null; } // Truncated? if (TextBlock.MaxLines == 0 || TextBlock.MaxHeight == 0) { TextBlock = null; Truncated = true; ctx.Truncated = true; return; } // Update the yPosition and stop further processing if truncated ctx.yPosition += TextBlock.MeasuredHeight + MarginTop; ctx.lineCount += TextBlock.Lines.Count; ctx.MeasuredLength += TextBlock.MeasuredLength; if (!TextBlock.Truncated) { // Only add the bottom margin if it wasn't truncated ctx.yPosition += MarginBottom; } else { if (TextBlock.Lines.Count == 0 && ctx.previousParagraph != null) { ctx.previousParagraph.TextBlock.AddEllipsis(); } // All following blocks should be truncated ctx.Truncated = true; } } public TextBlock TextBlock; public float MarginLeft; public float MarginRight; public float MarginTop; public float MarginBottom; public TextAlignment? TextAlignment; public TextDirection? BaseDirection; public bool Truncated; public float yPosition; // Laid out y-position public List _items = new List(); } class BuildContext { public TextBlock TextBlock; public StyleManager StyleManager; } abstract class Item { public abstract void Build(BuildContext ctx); public virtual void Build(StringBuilder sb) { } } class TextItem : Item { public TextItem(string str) { _text = str; } string _text; public override void Build(BuildContext ctx) { ctx.TextBlock.AddText(_text, ctx.StyleManager.CurrentStyle); } public override void Build(StringBuilder sb) { sb.Append(_text); } } class FontFamilyItem : Item { public FontFamilyItem(string value) { _value = value; } string _value; public override void Build(BuildContext ctx) { ctx.StyleManager.FontFamily(_value); } } class FontSizeItem : Item { public FontSizeItem(float value) { _value = value; } float _value; public override void Build(BuildContext ctx) { ctx.StyleManager.FontSize(_value); } } class FontWeightItem : Item { public FontWeightItem(int value) { _value = value; } int _value; public override void Build(BuildContext ctx) { ctx.StyleManager.FontWeight(_value); } } class FontWidthItem : Item { public FontWidthItem(SKFontStyleWidth value) { _value = value; } SKFontStyleWidth _value; public override void Build(BuildContext ctx) { ctx.StyleManager.FontWidth(_value); } } class FontItalicItem : Item { public FontItalicItem(bool value) { _value = value; } bool _value; public override void Build(BuildContext ctx) { ctx.StyleManager.FontItalic(_value); } } class UnderlineItem : Item { public UnderlineItem(UnderlineStyle value) { _value = value; } UnderlineStyle _value; public override void Build(BuildContext ctx) { ctx.StyleManager.Underline(_value); } } class StrikeThroughItem : Item { public StrikeThroughItem(StrikeThroughStyle value) { _value = value; } StrikeThroughStyle _value; public override void Build(BuildContext ctx) { ctx.StyleManager.StrikeThrough(_value); } } class LineHeightItem : Item { public LineHeightItem(float value) { _value = value; } float _value; public override void Build(BuildContext ctx) { ctx.StyleManager.LineHeight(_value); } } class TextColorItem : Item { public TextColorItem(SKColor value) { _value = value; } SKColor _value; public override void Build(BuildContext ctx) { ctx.StyleManager.TextColor(_value); } } class BackgroundColorItem : Item { public BackgroundColorItem(SKColor value) { _value = value; } SKColor _value; public override void Build(BuildContext ctx) { ctx.StyleManager.BackgroundColor(_value); } } class HaloColorItem : Item { public HaloColorItem(SKColor value) { _value = value; } SKColor _value; public override void Build(BuildContext ctx) { ctx.StyleManager.HaloColor(_value); } } class HaloWidthItem : Item { public HaloWidthItem(float value) { _value = value; } float _value; public override void Build(BuildContext ctx) { ctx.StyleManager.HaloWidth(_value); } } class HaloBlurItem : Item { public HaloBlurItem(float value) { _value = value; } float _value; public override void Build(BuildContext ctx) { ctx.StyleManager.HaloBlur(_value); } } class LetterSpacingItem : Item { public LetterSpacingItem(float value) { _value = value; } float _value; public override void Build(BuildContext ctx) { ctx.StyleManager.LetterSpacing(_value); } } class FontVariantItem : Item { public FontVariantItem(FontVariant value) { _value = value; } FontVariant _value; public override void Build(BuildContext ctx) { ctx.StyleManager.FontVariant(_value); } } class TextDirectionItem : Item { public TextDirectionItem(TextDirection value) { _value = value; } TextDirection _value; public override void Build(BuildContext ctx) { ctx.StyleManager.TextDirection(_value); } } class PushItem : Item { public PushItem() { } public override void Build(BuildContext ctx) { ctx.StyleManager.Push(); } } class PopItem : Item { public PopItem() { } public override void Build(BuildContext ctx) { ctx.StyleManager.Pop(); } } class NormalItem : Item { public NormalItem() { } public override void Build(BuildContext ctx) { ctx.StyleManager.Bold(false); ctx.StyleManager.FontItalic(false); ctx.StyleManager.Underline(UnderlineStyle.None); ctx.StyleManager.StrikeThrough(StrikeThroughStyle.None); ctx.StyleManager.FontVariant(RichTextKit.FontVariant.Normal); } } } }