Extracted from GuiKit to standalone project
This commit is contained in:
Коммит
9cc489fac2
|
@ -0,0 +1,21 @@
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.tt text eol=crlf
|
||||||
|
*.sln text eol=crlf
|
||||||
|
*.csproj text eol=crlf
|
||||||
|
*.vbproj text eol=crlf
|
||||||
|
*.vcxproj text eol=crlf
|
||||||
|
*.vcproj text eol=crlf
|
||||||
|
*.dbproj text eol=crlf
|
||||||
|
*.fsproj text eol=crlf
|
||||||
|
*.lsproj text eol=crlf
|
||||||
|
*.wixproj text eol=crlf
|
||||||
|
*.modelproj text eol=crlf
|
||||||
|
*.sqlproj text eol=crlf
|
||||||
|
*.wmaproj text eol=crlf
|
||||||
|
|
||||||
|
*.xproj text eol=crlf
|
||||||
|
*.props text eol=crlf
|
||||||
|
*.filters text eol=crlf
|
||||||
|
*.vcxitems text eol=crlf
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# git-ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
.vs/
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
build
|
||||||
|
*.suo
|
||||||
|
*.vsp
|
||||||
|
*.psess
|
||||||
|
.DS_Store
|
||||||
|
*.userprefs
|
||||||
|
*.bin
|
||||||
|
.idea
|
||||||
|
packages
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
|
||||||
|
</startup>
|
||||||
|
<System.Windows.Forms.ApplicationConfigurationSection>
|
||||||
|
<add key="DpiAwareness" value="PerMonitorV2" />
|
||||||
|
</System.Windows.Forms.ApplicationConfigurationSection>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,50 @@
|
||||||
|
namespace Sandbox
|
||||||
|
{
|
||||||
|
partial class Form1
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Required designer variable.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up any resources being used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required method for Designer support - do not modify
|
||||||
|
/// the contents of this method with the code editor.
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
this.SuspendLayout();
|
||||||
|
//
|
||||||
|
// Form1
|
||||||
|
//
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.BackColor = System.Drawing.Color.White;
|
||||||
|
this.ClientSize = new System.Drawing.Size(1402, 724);
|
||||||
|
this.Name = "Form1";
|
||||||
|
this.Text = "Form1";
|
||||||
|
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
|
||||||
|
this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.Form1_KeyDown);
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Topten.RichText;
|
||||||
|
|
||||||
|
namespace Sandbox
|
||||||
|
{
|
||||||
|
public partial class Form1 : Form
|
||||||
|
{
|
||||||
|
public Form1()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ResizeRedraw = true;
|
||||||
|
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap _bitmap;
|
||||||
|
|
||||||
|
private void Form1_Paint(object sender, PaintEventArgs e)
|
||||||
|
{
|
||||||
|
// Create bitmap
|
||||||
|
var info = new SKImageInfo(Width, Height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
|
||||||
|
if (_bitmap == null || _bitmap.Width != info.Width || _bitmap.Height != info.Height)
|
||||||
|
{
|
||||||
|
_bitmap?.Dispose();
|
||||||
|
|
||||||
|
if (info.Width != 0 && info.Height != 0)
|
||||||
|
_bitmap = new Bitmap(info.Width, info.Height, PixelFormat.Format32bppPArgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock bits
|
||||||
|
var data = _bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, _bitmap.PixelFormat);
|
||||||
|
|
||||||
|
// create the surface
|
||||||
|
using (var surface = SKSurface.Create(info, data.Scan0, data.Stride))
|
||||||
|
{
|
||||||
|
surface.Canvas.Scale(DeviceDpi/96.0f);
|
||||||
|
OnRender(surface.Canvas);
|
||||||
|
surface.Canvas.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the bitmap to the graphics
|
||||||
|
_bitmap.UnlockBits(data);
|
||||||
|
e.Graphics.DrawImage(_bitmap, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SandboxDriver.SandboxDriver _driver = new SandboxDriver.SandboxDriver();
|
||||||
|
|
||||||
|
void OnRender(SKCanvas canvas)
|
||||||
|
{
|
||||||
|
_driver.Render(canvas, canvas.LocalClipBounds.Width, canvas.LocalClipBounds.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Form1_KeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.KeyCode)
|
||||||
|
{
|
||||||
|
case Keys.Space:
|
||||||
|
_driver.ContentMode = (_driver.ContentMode + 1) % _driver.ContentModeCount;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.Left:
|
||||||
|
if (e.Modifiers.HasFlag(Keys.Control))
|
||||||
|
{
|
||||||
|
_driver.BaseDirection = TextDirection.LTR;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_driver.TextAlignment > TextAlignment.Auto)
|
||||||
|
_driver.TextAlignment--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.Right:
|
||||||
|
if (e.Modifiers.HasFlag(Keys.Control))
|
||||||
|
{
|
||||||
|
_driver.BaseDirection = TextDirection.RTL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_driver.TextAlignment < TextAlignment.Right)
|
||||||
|
_driver.TextAlignment++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.Up:
|
||||||
|
_driver.Scale += 0.5f;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.Down:
|
||||||
|
_driver.Scale -= 0.5f;
|
||||||
|
if (_driver.Scale < 0.5f)
|
||||||
|
_driver.Scale = 0.5f;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.Z:
|
||||||
|
_driver.UseMSWordStyleRTLLayout = !_driver.UseMSWordStyleRTLLayout;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.W:
|
||||||
|
_driver.UseMaxWidth = !_driver.UseMaxWidth;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.H:
|
||||||
|
_driver.UseMaxHeight = !_driver.UseMaxHeight;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keys.M:
|
||||||
|
_driver.ShowMeasuredSize = !_driver.ShowMeasuredSize;
|
||||||
|
Invalidate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace Sandbox
|
||||||
|
{
|
||||||
|
static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The main entry point for the application.
|
||||||
|
/// </summary>
|
||||||
|
[STAThread]
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
Application.EnableVisualStyles();
|
||||||
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
|
Application.Run(new Form1());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("Sandbox")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Sandbox")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2019")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("a8440e53-0d1e-477b-81b3-7916ecaf2ef2")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@ -0,0 +1,71 @@
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Sandbox.Properties
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
internal class Resources
|
||||||
|
{
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Resources.ResourceManager ResourceManager
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if ((resourceMan == null))
|
||||||
|
{
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sandbox.Properties.Resources", typeof(Resources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Globalization.CultureInfo Culture
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
|
@ -0,0 +1,30 @@
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Sandbox.Properties
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||||
|
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
|
||||||
|
{
|
||||||
|
|
||||||
|
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||||
|
|
||||||
|
public static Settings Default
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return defaultInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||||
|
<Profiles>
|
||||||
|
<Profile Name="(Default)" />
|
||||||
|
</Profiles>
|
||||||
|
<Settings />
|
||||||
|
</SettingsFile>
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{A8440E53-0D1E-477B-81B3-7916ECAF2EF2}</ProjectGuid>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<RootNamespace>Sandbox</RootNamespace>
|
||||||
|
<AssemblyName>Sandbox</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<NuGetPackageImportStamp>
|
||||||
|
</NuGetPackageImportStamp>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="SkiaSharp, Version=1.68.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\SkiaSharp.1.68.0\lib\net45\SkiaSharp.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Deployment" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Windows.Forms" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Form1.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Form1.Designer.cs">
|
||||||
|
<DependentUpon>Form1.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<EmbeddedResource Include="Form1.resx">
|
||||||
|
<DependentUpon>Form1.cs</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Properties\Resources.resx">
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<Compile Include="Properties\Resources.Designer.cs">
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<None Include="app.manifest" />
|
||||||
|
<None Include="packages.config" />
|
||||||
|
<None Include="Properties\Settings.settings">
|
||||||
|
<Generator>SettingsSingleFileGenerator</Generator>
|
||||||
|
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||||
|
</None>
|
||||||
|
<Compile Include="Properties\Settings.Designer.cs">
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Settings.settings</DependentUpon>
|
||||||
|
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SandboxDriver\SandboxDriver.csproj">
|
||||||
|
<Project>{76ae34f1-d8b7-4593-a9fa-a82cdb438a5c}</Project>
|
||||||
|
<Name>SandboxDriver</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\Topten.RichText\Topten.RichText.csproj">
|
||||||
|
<Project>{6472d685-fd75-4475-8de2-53917b0ba189}</Project>
|
||||||
|
<Name>Topten.RichText</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Import Project="..\packages\SkiaSharp.1.68.0\build\net45\SkiaSharp.targets" Condition="Exists('..\packages\SkiaSharp.1.68.0\build\net45\SkiaSharp.targets')" />
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('..\packages\SkiaSharp.1.68.0\build\net45\SkiaSharp.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\SkiaSharp.1.68.0\build\net45\SkiaSharp.targets'))" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<!-- UAC Manifest Options
|
||||||
|
If you want to change the Windows User Account Control level replace the
|
||||||
|
requestedExecutionLevel node with one of the following.
|
||||||
|
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||||
|
|
||||||
|
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||||
|
Remove this element if your application requires this virtualization for backwards
|
||||||
|
compatibility.
|
||||||
|
-->
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows Vista -->
|
||||||
|
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||||
|
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||||
|
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||||
|
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
|
||||||
|
<!--
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||||
|
<!--
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"
|
||||||
|
/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</assembly>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="SkiaSharp" version="1.68.0" targetFramework="net471" />
|
||||||
|
</packages>
|
|
@ -0,0 +1,192 @@
|
||||||
|
using System;
|
||||||
|
using Topten.RichText;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SandboxDriver
|
||||||
|
{
|
||||||
|
public class SandboxDriver
|
||||||
|
{
|
||||||
|
public int ContentModeCount = 9;
|
||||||
|
public int ContentMode = 0;
|
||||||
|
public TextDirection BaseDirection = TextDirection.LTR;
|
||||||
|
public TextAlignment TextAlignment = TextAlignment.Auto;
|
||||||
|
public bool UseMSWordStyleRTLLayout = false;
|
||||||
|
public float Scale = 1.0f;
|
||||||
|
public bool UseMaxWidth = true;
|
||||||
|
public bool UseMaxHeight = false;
|
||||||
|
public bool ShowMeasuredSize = false;
|
||||||
|
|
||||||
|
public void Render(SKCanvas canvas, float canvasWidth, float canvasHeight)
|
||||||
|
{
|
||||||
|
canvas.Clear(new SKColor(0xFFFFFFFF));
|
||||||
|
|
||||||
|
const float margin = 80;
|
||||||
|
|
||||||
|
float? height = (float)(canvasHeight - margin * 2);
|
||||||
|
float? width = (float)(canvasWidth - margin * 2);
|
||||||
|
|
||||||
|
if (!UseMaxHeight)
|
||||||
|
height = null;
|
||||||
|
if (!UseMaxWidth)
|
||||||
|
width = null;
|
||||||
|
|
||||||
|
using (var gridlinePaint = new SKPaint() { Color = new SKColor(0xFFFF0000), StrokeWidth = 1 })
|
||||||
|
{
|
||||||
|
canvas.DrawLine(new SKPoint(margin, 0), new SKPoint(margin, (float)canvasHeight), gridlinePaint);
|
||||||
|
if (width.HasValue)
|
||||||
|
canvas.DrawLine(new SKPoint(margin + width.Value, 0), new SKPoint(margin + width.Value, (float)canvasHeight), gridlinePaint);
|
||||||
|
canvas.DrawLine(new SKPoint(0, margin), new SKPoint((float)canvasWidth, margin), gridlinePaint);
|
||||||
|
if (height.HasValue)
|
||||||
|
canvas.DrawLine(new SKPoint(0, margin + height.Value), new SKPoint((float)canvasWidth, margin + height.Value), gridlinePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
//string typefaceName = "Times New Roman";
|
||||||
|
string typefaceName = "Segoe UI";
|
||||||
|
|
||||||
|
var styleNormal = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, LineHeight = 1.0f };
|
||||||
|
var styleUnderline = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, Underline = UnderlineStyle.Gapped, TextColor = new SKColor(0xFF0000FF) };
|
||||||
|
var styleStrike = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, StrikeThrough = StrikeThroughStyle.Solid, TextColor = new SKColor(0xFFFF0000) };
|
||||||
|
var styleSubScript = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, FontVariant = FontVariant.SubScript };
|
||||||
|
var styleSuperScript = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, FontVariant = FontVariant.SuperScript };
|
||||||
|
var styleItalic = new Style() { FontFamily = typefaceName, FontItalic = true, FontSize = 18 * Scale };
|
||||||
|
var styleBold = new Style() { FontFamily = typefaceName, FontSize = 28 * Scale, FontWeight = 700 };
|
||||||
|
var styleRed = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, TextColor = new SKColor(0xFFFF0000) };
|
||||||
|
|
||||||
|
|
||||||
|
var tle = new TextBlock();
|
||||||
|
tle.MaxWidth = width;
|
||||||
|
tle.MaxHeight = height;
|
||||||
|
tle.Clear();
|
||||||
|
|
||||||
|
tle.BaseDirection = BaseDirection;
|
||||||
|
tle.Alignment = TextAlignment;
|
||||||
|
tle.UseMSWordStyleRTLLayout = UseMSWordStyleRTLLayout;
|
||||||
|
|
||||||
|
switch (ContentMode)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
tle.AddText("Hello Wor", styleNormal);
|
||||||
|
tle.AddText("ld", styleRed);
|
||||||
|
tle.AddText(". This is normal 18px. These are emojis: 🌐 🍪 🍕 🚀 ", styleNormal);
|
||||||
|
tle.AddText("This is ", styleNormal);
|
||||||
|
tle.AddText("bold 28px", styleBold);
|
||||||
|
tle.AddText(". ", styleNormal);
|
||||||
|
tle.AddText("This is italic", styleItalic);
|
||||||
|
tle.AddText(". This is ", styleNormal);
|
||||||
|
tle.AddText("red", styleRed);
|
||||||
|
tle.AddText(". This is Arabic: (", styleNormal);
|
||||||
|
tle.AddText("تسجّل ", styleNormal);
|
||||||
|
tle.AddText("يتكلّم", styleNormal);
|
||||||
|
tle.AddText("), Hindi: ", styleNormal);
|
||||||
|
tle.AddText("हालाँकि प्रचलित रूप पूज", styleNormal);
|
||||||
|
tle.AddText(", Han: ", styleNormal);
|
||||||
|
tle.AddText("緳 踥踕", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
tle.AddText("Hello Wor", styleNormal);
|
||||||
|
tle.AddText("ld", styleRed);
|
||||||
|
tle.AddText(".\nThis is normal 18px.\nThese are emojis: 🌐 🍪 🍕 🚀\n", styleNormal);
|
||||||
|
tle.AddText("This is ", styleNormal);
|
||||||
|
tle.AddText("bold 28px", styleBold);
|
||||||
|
tle.AddText(".\n", styleNormal);
|
||||||
|
tle.AddText("This is italic", styleItalic);
|
||||||
|
tle.AddText(".\nThis is ", styleNormal);
|
||||||
|
tle.AddText("red", styleRed);
|
||||||
|
tle.AddText(".\nThis is Arabic: (", styleNormal);
|
||||||
|
tle.AddText("تسجّل ", styleNormal);
|
||||||
|
tle.AddText("يتكلّم", styleNormal);
|
||||||
|
tle.AddText("), Hindi: ", styleNormal);
|
||||||
|
tle.AddText("हालाँकि प्रचलित रूप पूज", styleNormal);
|
||||||
|
tle.AddText(", Han: ", styleNormal);
|
||||||
|
tle.AddText("緳 踥踕", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
tle.AddText("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus semper, sapien vitae placerat sollicitudin, lorem diam aliquet quam, id finibus nisi quam eget lorem.\nDonec facilisis sem nec rhoncus elementum. Cras laoreet porttitor malesuada.\n\nVestibulum sed lacinia diam. Mauris a mollis enim. Cras in rhoncus mauris, at vulputate nisl. Sed nec lobortis dolor, hendrerit posuere quam. Vivamus malesuada sit amet nunc ac cursus. Praesent volutpat euismod efficitur. Nam eu ante.", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
tle.AddText("مرحبا بالعالم. هذا هو اختبار التفاف الخط للتأكد من أنه يعمل للغات من اليمين إلى اليسار.", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
tle.AddText("مرحبا بالعالم. هذا هو اختبار التفاف الخط للتأكد من Hello World أنه يعمل للغات من اليمين إلى اليسار.", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
tle.AddText("Subscript: H", styleNormal);
|
||||||
|
tle.AddText("2", styleSubScript);
|
||||||
|
tle.AddText("O Superscript: E=mc", styleNormal);
|
||||||
|
tle.AddText("2", styleSuperScript);
|
||||||
|
tle.AddText(" Key: C", styleNormal);
|
||||||
|
tle.AddText("♯", styleSuperScript);
|
||||||
|
tle.AddText(" B", styleNormal);
|
||||||
|
tle.AddText("♭", styleSubScript);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
tle.AddText("The quick brown fox jumps over the lazy dog.", styleUnderline);
|
||||||
|
tle.AddText(" ", styleNormal);
|
||||||
|
tle.AddText("Strike Through", styleStrike);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
tle.AddText("Apples and Bananas\r\n", styleNormal);
|
||||||
|
tle.AddText("Pears\r\n", styleNormal);
|
||||||
|
tle.AddText("Bananas\r\n", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
tle.AddText("Hello World", styleNormal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var sw = new Stopwatch();
|
||||||
|
sw.Start();
|
||||||
|
tle.Layout();
|
||||||
|
var elapsed = sw.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
if (ShowMeasuredSize)
|
||||||
|
{
|
||||||
|
using (var paint = new SKPaint()
|
||||||
|
{
|
||||||
|
Color = new SKColor(0x1000FF00),
|
||||||
|
IsStroke = false,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var rect = new SKRect(margin + tle.MeasuredInset, margin, margin + tle.MeasureWidth + tle.MeasuredInset, margin + tle.MeasureHeight);
|
||||||
|
canvas.DrawRect(rect, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new TextPaintOptions()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
SelectionStart = 10,
|
||||||
|
SelectionEnd = 50,
|
||||||
|
SelectionColor = Color.Orange.WithAlpha(0.2f).ToSK(),
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tle.RequiredLeftMargin > 0)
|
||||||
|
{
|
||||||
|
using (var paint = new SKPaint() { Color = new SKColor(0xFFf0f0f0), StrokeWidth = 1 })
|
||||||
|
{
|
||||||
|
canvas.DrawLine(new SKPoint(margin - tle.RequiredLeftMargin, 0), new SKPoint(margin - tle.RequiredLeftMargin, (float)canvasHeight), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tle.Paint(canvas, new SKPoint(margin, margin), options);
|
||||||
|
|
||||||
|
var state = $"Size: {width} x {height} Base Direction: {BaseDirection} Alignment: {TextAlignment} Content: {ContentMode} scale: {Scale} time: {elapsed} msword: {UseMSWordStyleRTLLayout}";
|
||||||
|
canvas.DrawText(state, margin, 20, new SKPaint()
|
||||||
|
{
|
||||||
|
Typeface = SKTypeface.FromFamilyName("Arial"),
|
||||||
|
TextSize = 12,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netcoreapp2.1;net45</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SkiaSharp" Version="1.68.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Topten.RichText\Topten.RichText.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.28307.645
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Topten.RichText", "Topten.RichText\Topten.RichText.csproj", "{6472D685-FD75-4475-8DE2-53917B0BA189}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "Sandbox\Sandbox.csproj", "{A8440E53-0D1E-477B-81B3-7916ECAF2EF2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SandboxDriver", "SandboxDriver\SandboxDriver.csproj", "{76AE34F1-D8B7-4593-A9FA-A82CDB438A5C}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{6472D685-FD75-4475-8DE2-53917B0BA189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6472D685-FD75-4475-8DE2-53917B0BA189}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6472D685-FD75-4475-8DE2-53917B0BA189}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6472D685-FD75-4475-8DE2-53917B0BA189}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A8440E53-0D1E-477B-81B3-7916ECAF2EF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A8440E53-0D1E-477B-81B3-7916ECAF2EF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A8440E53-0D1E-477B-81B3-7916ECAF2EF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A8440E53-0D1E-477B-81B3-7916ECAF2EF2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{76AE34F1-D8B7-4593-A9FA-A82CDB438A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{76AE34F1-D8B7-4593-A9FA-A82CDB438A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{76AE34F1-D8B7-4593-A9FA-A82CDB438A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{76AE34F1-D8B7-4593-A9FA-A82CDB438A5C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {45C5DB84-4136-4F76-ADA4-9BC87F3C6A95}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,120 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a unicode string and all associated attributes
|
||||||
|
/// for each character required for the Bidi algorithm
|
||||||
|
/// </summary>
|
||||||
|
class BidiData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Construct a new empty BidiData
|
||||||
|
/// </summary>
|
||||||
|
public BidiData()
|
||||||
|
{
|
||||||
|
_directionality = new Buffer<Directionality>();
|
||||||
|
_pairedBracketTypes= new Buffer<PairedBracketType>();
|
||||||
|
_pairedBracketValues = new Buffer<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _paragraphPositions = new List<int>();
|
||||||
|
|
||||||
|
byte _paragraphEmbeddingLevel;
|
||||||
|
|
||||||
|
public byte ParagraphEmbeddingLevel => _paragraphEmbeddingLevel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize with an array of Unicode code points
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoints"></param>
|
||||||
|
public void Init(Slice<int> codePoints, byte paragraphEmbeddingLevel)
|
||||||
|
{
|
||||||
|
// Set working buffer sizes
|
||||||
|
_directionality.Length = codePoints.Length;
|
||||||
|
_pairedBracketTypes.Length = codePoints.Length;
|
||||||
|
_pairedBracketValues.Length = codePoints.Length;
|
||||||
|
|
||||||
|
_paragraphPositions.Clear();
|
||||||
|
_paragraphEmbeddingLevel = paragraphEmbeddingLevel;
|
||||||
|
|
||||||
|
// Resolve the directionality, paired bracket type and paired bracket values for
|
||||||
|
// all code points
|
||||||
|
for (int i = 0; i < codePoints.Length; i++)
|
||||||
|
{
|
||||||
|
var bidiData = UnicodeClasses.BidiData(codePoints[i]);
|
||||||
|
_directionality[i] = (Directionality)(bidiData >> 24);
|
||||||
|
_pairedBracketTypes[i] = (PairedBracketType)((bidiData >> 16) & 0xFF);
|
||||||
|
if (_pairedBracketTypes[i] == PairedBracketType.o)
|
||||||
|
{
|
||||||
|
_pairedBracketValues[i] = MapCanon((int)(bidiData & 0xFFFF));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pairedBracketValues[i] = MapCanon(codePoints[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_directionality[i] == RichText.Directionality.B)
|
||||||
|
{
|
||||||
|
_directionality[i] = (Directionality)_paragraphEmbeddingLevel;
|
||||||
|
_paragraphPositions.Add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create slices on work buffers
|
||||||
|
Directionality = _directionality.AsSlice();
|
||||||
|
PairedBracketTypes = _pairedBracketTypes.AsSlice();
|
||||||
|
PairedBracketValues = _pairedBracketValues.AsSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map bracket types 0x3008 and 0x3009 to their canonical equivalents
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point to be mapped</param>
|
||||||
|
/// <returns>The mapped canonical code point, or the passed code point</returns>
|
||||||
|
static int MapCanon(int codePoint)
|
||||||
|
{
|
||||||
|
if (codePoint == 0x3008)
|
||||||
|
return 0x2379;
|
||||||
|
if (codePoint == 0x3009)
|
||||||
|
return 0x237A;
|
||||||
|
else
|
||||||
|
return codePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the length of the data held by the BidiData
|
||||||
|
/// </summary>
|
||||||
|
public int Length => _directionality.Length;
|
||||||
|
|
||||||
|
Buffer<Directionality> _directionality;
|
||||||
|
Buffer<PairedBracketType> _pairedBracketTypes;
|
||||||
|
Buffer<int> _pairedBracketValues;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The directionality of each code point
|
||||||
|
/// </summary>
|
||||||
|
public Slice<Directionality> Directionality;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The paired bracket type for each code point
|
||||||
|
/// </summary>
|
||||||
|
public Slice<PairedBracketType> PairedBracketTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The paired bracket value for code code point
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The paired bracket values are the code points
|
||||||
|
/// of each character where the opening code point
|
||||||
|
/// is replaced with the closing code point for easier
|
||||||
|
/// matching. Also, bracket code points are mapped
|
||||||
|
/// to their canonical equivalents
|
||||||
|
/// </remarks>
|
||||||
|
public Slice<int> PairedBracketValues;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,555 @@
|
||||||
|
/*
|
||||||
|
* Ported from https://www.unicode.org/Public/PROGRAMS/BidiReferenceJava/BidiPBAReference.java
|
||||||
|
* /
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Last Revised: 2016-09-21
|
||||||
|
*
|
||||||
|
* Credits:
|
||||||
|
* Originally written by Asmus Freytag
|
||||||
|
*
|
||||||
|
* Updated for Unicode 8.0 by Deepak Jois, with feedback from Ken Whistler
|
||||||
|
*
|
||||||
|
* (C) Copyright ASMUS, Inc. 2013, All Rights Reserved
|
||||||
|
* (C) Copyright Deepak Jois 2016, All Rights Reserved
|
||||||
|
*
|
||||||
|
* Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Revision info (2016-09-21):
|
||||||
|
* - Added MAX_PAIRING_DEPTH to support max depth for nested brackets in Unicode 8.0
|
||||||
|
* - Changes to support updated definitions BD14 and BD15 in Unicode 8.0
|
||||||
|
* - Changes to support clarifications to rule N0 in Unicode 8.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference implementation of the BPA algorithm of the Unicode 6.3 Bidi algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation is not optimized for performance. It is intended
|
||||||
|
* as a reference implementation that closely follows the specification
|
||||||
|
* of the paired bracket part of the Bidirectional Algorithm in
|
||||||
|
* The Unicode Standard version 6.3 (and revised in Unicode Standard version 8.0)
|
||||||
|
* <p>
|
||||||
|
* The implementation covers definitions BD14-BD16 and rule N0.
|
||||||
|
* <p>
|
||||||
|
* Like the BidiReference class which uses the BidiBPAReference class, the implementation is
|
||||||
|
* designed to decouple the mapping of Unicode properties to characters from the handling
|
||||||
|
* of the Bidi Paired-bracket Algorithm. Such mappings are to be performed by the caller.
|
||||||
|
* <p>
|
||||||
|
* One of the properties, the Bidi_Paired_Bracket requires some pre-processing to translate
|
||||||
|
* it into the format used here. Instead of being a code-point mapping from a bracket character
|
||||||
|
* to the other partner of the bracket pair, this implementation accepts any unique identifier
|
||||||
|
* common to BOTH parts of the pair, and 0 or some unique value for all non-bracket characters.
|
||||||
|
* The actual values of these unique identifiers are not defined.
|
||||||
|
* <p>
|
||||||
|
* The BPA algorithm requires that bracket characters that are canonical equivalents of each
|
||||||
|
* other must be able to be substituted for each other. Callers can accomplish this by re-using
|
||||||
|
* the same unique identifier for such equivalent characters.
|
||||||
|
* <p>
|
||||||
|
* The resultant values become the pairValues array used in calling the resolvePairedBrackets member.
|
||||||
|
* <p>
|
||||||
|
* In implementing BD16, this implementation departs slightly from the "logical" algorithm defined
|
||||||
|
* in UAX#9. In particular, the stack referenced there supports operations that go beyond a "basic"
|
||||||
|
* stack. An equivalent implementation based on a linked list is used here.
|
||||||
|
*
|
||||||
|
* @author Asmus Freytag
|
||||||
|
* @author Deepak Jois
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
class BidiPBA
|
||||||
|
{
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BD14. An opening paired bracket is a character whose
|
||||||
|
* Bidi_Paired_Bracket_Type property value is Open.
|
||||||
|
*
|
||||||
|
* BD15. A closing paired bracket is a character whose
|
||||||
|
* Bidi_Paired_Bracket_Type property value is Close.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public BidiPBA()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holds a pair of index values for opening and closing bracket location of
|
||||||
|
// a bracket pair
|
||||||
|
// Contains additional methods to allow pairs to be sorted by the location
|
||||||
|
// of the opening bracket
|
||||||
|
public struct BracketPair : IEquatable<BracketPair>, IComparable<BracketPair>
|
||||||
|
{
|
||||||
|
private int ichOpener;
|
||||||
|
private int ichCloser;
|
||||||
|
|
||||||
|
public BracketPair(int ichOpener, int ichCloser)
|
||||||
|
{
|
||||||
|
this.ichOpener = ichOpener;
|
||||||
|
this.ichCloser = ichCloser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "(" + ichOpener + ", " + ichCloser + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return ichOpener.GetHashCode() ^ ichCloser.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is BracketPair && Equals((BracketPair)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(BracketPair p)
|
||||||
|
{
|
||||||
|
return ichOpener == p.ichOpener && ichCloser == p.ichCloser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(BracketPair other)
|
||||||
|
{
|
||||||
|
if (this.ichOpener == other.ichOpener)
|
||||||
|
return 0;
|
||||||
|
if (this.ichOpener < other.ichOpener)
|
||||||
|
return -1;
|
||||||
|
else
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Opener => ichOpener;
|
||||||
|
public int Closer => ichCloser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following is a restatement of BD 16 using non-algorithmic language.
|
||||||
|
//
|
||||||
|
// A bracket pair is a pair of characters consisting of an opening
|
||||||
|
// paired bracket and a closing paired bracket such that the
|
||||||
|
// Bidi_Paired_Bracket property value of the former equals the latter,
|
||||||
|
// subject to the following constraints.
|
||||||
|
// - both characters of a pair occur in the same isolating run sequence
|
||||||
|
// - the closing character of a pair follows the opening character
|
||||||
|
// - any bracket character can belong at most to one pair, the earliest possible one
|
||||||
|
// - any bracket character not part of a pair is treated like an ordinary character
|
||||||
|
// - pairs may nest properly, but their spans may not overlap otherwise
|
||||||
|
|
||||||
|
// Bracket characters with canonical decompositions are supposed to be treated
|
||||||
|
// as if they had been normalized, to allow normalized and non-normalized text
|
||||||
|
// to give the same result. In this implementation that step is pushed out to
|
||||||
|
// the caller - see definition of the pairValues array.
|
||||||
|
|
||||||
|
List<int> _openers; // list of positions for opening brackets
|
||||||
|
|
||||||
|
// bracket pair positions sorted by location of opening bracket
|
||||||
|
private SortedSet<BracketPair> _pairPositions;
|
||||||
|
|
||||||
|
/*
|
||||||
|
public String getPairPositionsString()
|
||||||
|
{
|
||||||
|
SortedSet<BidiPBAReference.BracketPair> tempPositions = new TreeSet<BidiPBAReference.BracketPair>();
|
||||||
|
for (BidiPBAReference.BracketPair pair : pairPositions) {
|
||||||
|
tempPositions.add(new BidiPBAReference.BracketPair(indexes[pair.getOpener()], indexes[pair.getCloser()]));
|
||||||
|
}
|
||||||
|
return tempPositions.toString();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Directionality _sos; // direction corresponding to start of sequence
|
||||||
|
private Slice<Directionality> _initialCodes; // direction bidi codes initially assigned to the original string
|
||||||
|
public Directionality[] _codesIsolatedRun; // directional bidi codes for an isolated run
|
||||||
|
private Slice<int> _indexes; // array of index values into the original string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check whether characters at putative positions could form a bracket pair
|
||||||
|
* based on the paired bracket character properties
|
||||||
|
*
|
||||||
|
* @param pairValues
|
||||||
|
* - unique ID for the pair (or set) of canonically matched
|
||||||
|
* brackets
|
||||||
|
* @param ichOpener
|
||||||
|
* - position of the opening bracket
|
||||||
|
* @param ichCloser
|
||||||
|
* - position of the closing bracket
|
||||||
|
* @return true if match
|
||||||
|
*/
|
||||||
|
private bool matchOpener(Slice<int> pairValues, int ichOpener, int ichCloser)
|
||||||
|
{
|
||||||
|
return pairValues[_indexes[ichOpener]] == pairValues[_indexes[ichCloser]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public const int MAX_PAIRING_DEPTH = 63;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* locate all Paired Bracket characters and determine whether they form
|
||||||
|
* pairs according to BD16. This implementation uses a linked list instead
|
||||||
|
* of a stack, because, while elements are added at the front (like a push)
|
||||||
|
* there are not generally removed in atomic 'pop' operations, reducing the
|
||||||
|
* benefit of the stack archetype.
|
||||||
|
*
|
||||||
|
* @param pairTypes
|
||||||
|
* - array of paired Bracket types
|
||||||
|
* @param pairValues
|
||||||
|
* - array of characters codes such that for all bracket
|
||||||
|
* characters it contains the same unique value if their
|
||||||
|
* Bidi_Paired_Bracket properties map between them. For
|
||||||
|
* brackets hat have canonical decompositions (singleton
|
||||||
|
* mappings) it contains the same value as for the canonically
|
||||||
|
* decomposed character. For characters that have paired
|
||||||
|
* bracket type of "n" the value is ignored.
|
||||||
|
*/
|
||||||
|
private void locateBrackets(Slice<PairedBracketType> pairTypes, Slice<int> pairValues)
|
||||||
|
{
|
||||||
|
_openers = new List<int>();
|
||||||
|
_pairPositions = new SortedSet<BracketPair>();
|
||||||
|
|
||||||
|
// traverse the run
|
||||||
|
// do that explicitly (not in a for-each) so we can record position
|
||||||
|
for (int ich = 0; ich < _indexes.Length; ich++)
|
||||||
|
{
|
||||||
|
// look at the bracket type for each character
|
||||||
|
if ((pairTypes[_indexes[ich]] == PairedBracketType.n) || (_codesIsolatedRun[ich] != Directionality.ON))
|
||||||
|
{
|
||||||
|
continue; // continue scanning
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (pairTypes[_indexes[ich]])
|
||||||
|
{
|
||||||
|
// opening bracket found, note location
|
||||||
|
case PairedBracketType.o:
|
||||||
|
// check if maximum pairing depth reached
|
||||||
|
if (_openers.Count() == MAX_PAIRING_DEPTH)
|
||||||
|
{
|
||||||
|
_openers.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remember opener location, most recent first
|
||||||
|
_openers.Insert(0, ich);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// closing bracket found
|
||||||
|
case PairedBracketType.c:
|
||||||
|
// see if there is a match
|
||||||
|
if (_openers.Count == 0)
|
||||||
|
continue; // no opening bracket defined
|
||||||
|
|
||||||
|
for (int i = 0; i < _openers.Count - 1; i++)
|
||||||
|
{
|
||||||
|
if (matchOpener(pairValues, _openers[i], ich))
|
||||||
|
{
|
||||||
|
// if the opener matches, add nested pair to the ordered list
|
||||||
|
_pairPositions.Add(new BracketPair(_openers[i], ich));
|
||||||
|
// remove up to and including matched opener
|
||||||
|
_openers.RemoveRange(0, i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we get here, the closing bracket matched no openers
|
||||||
|
// and gets ignored
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bracket pairs within an isolating run sequence are processed as units so
|
||||||
|
* that both the opening and the closing paired bracket in a pair resolve to
|
||||||
|
* the same direction.
|
||||||
|
*
|
||||||
|
* N0. Process bracket pairs in an isolating run sequence sequentially in
|
||||||
|
* the logical order of the text positions of the opening paired brackets
|
||||||
|
* using the logic given below. Within this scope, bidirectional types EN
|
||||||
|
* and AN are treated as R.
|
||||||
|
*
|
||||||
|
* Identify the bracket pairs in the current isolating run sequence
|
||||||
|
* according to BD16. For each bracket-pair element in the list of pairs of
|
||||||
|
* text positions:
|
||||||
|
*
|
||||||
|
* a Inspect the bidirectional types of the characters enclosed within the
|
||||||
|
* bracket pair.
|
||||||
|
*
|
||||||
|
* b If any strong type (either L or R) matching the embedding direction is
|
||||||
|
* found, set the type for both brackets in the pair to match the embedding
|
||||||
|
* direction.
|
||||||
|
*
|
||||||
|
* o [ e ] o -> o e e e o
|
||||||
|
*
|
||||||
|
* o [ o e ] -> o e o e e
|
||||||
|
*
|
||||||
|
* o [ NI e ] -> o e NI e e
|
||||||
|
*
|
||||||
|
* c Otherwise, if a strong type (opposite the embedding direction) is
|
||||||
|
* found, test for adjacent strong types as follows: 1 First, check
|
||||||
|
* backwards before the opening paired bracket until the first strong type
|
||||||
|
* (L, R, or sos) is found. If that first preceding strong type is opposite
|
||||||
|
* the embedding direction, then set the type for both brackets in the pair
|
||||||
|
* to that type. 2 Otherwise, set the type for both brackets in the pair to
|
||||||
|
* the embedding direction.
|
||||||
|
*
|
||||||
|
* o [ o ] e -> o o o o e
|
||||||
|
*
|
||||||
|
* o [ o NI ] o -> o o o NI o o
|
||||||
|
*
|
||||||
|
* e [ o ] o -> e e o e o
|
||||||
|
*
|
||||||
|
* e [ o ] e -> e e o e e
|
||||||
|
*
|
||||||
|
* e ( o [ o ] NI ) e -> e e o o o o NI e e
|
||||||
|
*
|
||||||
|
* d Otherwise, do not set the type for the current bracket pair. Note that
|
||||||
|
* if the enclosed text contains no strong types the paired brackets will
|
||||||
|
* both resolve to the same level when resolved individually using rules N1
|
||||||
|
* and N2.
|
||||||
|
*
|
||||||
|
* e ( NI ) o -> e ( NI ) o
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* map character's directional code to strong type as required by rule N0
|
||||||
|
*
|
||||||
|
* @param ich
|
||||||
|
* - index into array of directional codes
|
||||||
|
* @return R or L for strong directional codes, ON for anything else
|
||||||
|
*/
|
||||||
|
private Directionality getStrongTypeN0(int ich)
|
||||||
|
{
|
||||||
|
|
||||||
|
switch (_codesIsolatedRun[ich])
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
return Directionality.ON;
|
||||||
|
// in the scope of N0, number types are treated as R
|
||||||
|
case Directionality.EN:
|
||||||
|
case Directionality.AN:
|
||||||
|
case Directionality.AL:
|
||||||
|
case Directionality.R:
|
||||||
|
return Directionality.R;
|
||||||
|
case Directionality.L:
|
||||||
|
return Directionality.L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determine which strong types are contained inside a Bracket Pair
|
||||||
|
*
|
||||||
|
* @param pairedLocation
|
||||||
|
* - a bracket pair
|
||||||
|
* @param dirEmbed
|
||||||
|
* - the embedding direction
|
||||||
|
* @return ON if no strong type found, otherwise return the embedding
|
||||||
|
* direction, unless the only strong type found is opposite the
|
||||||
|
* embedding direction, in which case that is returned
|
||||||
|
*/
|
||||||
|
Directionality classifyPairContent(BracketPair pairedLocation, Directionality dirEmbed)
|
||||||
|
{
|
||||||
|
var dirOpposite = Directionality.ON;
|
||||||
|
for (int ich = pairedLocation.Opener + 1; ich < pairedLocation.Closer; ich++)
|
||||||
|
{
|
||||||
|
var dir = getStrongTypeN0(ich);
|
||||||
|
if (dir == Directionality.ON)
|
||||||
|
continue;
|
||||||
|
if (dir == dirEmbed)
|
||||||
|
return dir; // type matching embedding direction found
|
||||||
|
dirOpposite = dir;
|
||||||
|
}
|
||||||
|
// return ON if no strong type found, or class opposite to dirEmbed
|
||||||
|
return dirOpposite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determine which strong types are present before a Bracket Pair
|
||||||
|
*
|
||||||
|
* @param pairedLocation
|
||||||
|
* - a bracket pair
|
||||||
|
* @return R or L if strong type found, otherwise ON
|
||||||
|
*/
|
||||||
|
Directionality classBeforePair(BracketPair pairedLocation)
|
||||||
|
{
|
||||||
|
for (int ich = pairedLocation.Opener - 1; ich >= 0; --ich)
|
||||||
|
{
|
||||||
|
var dir = getStrongTypeN0(ich);
|
||||||
|
if (dir != Directionality.ON)
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
// no strong types found, return sos
|
||||||
|
return _sos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement rule N0 for a single bracket pair
|
||||||
|
*
|
||||||
|
* @param pairedLocation
|
||||||
|
* - a bracket pair
|
||||||
|
* @param dirEmbed
|
||||||
|
* - the embedding direction
|
||||||
|
*/
|
||||||
|
void assignBracketType(BracketPair pairedLocation, Directionality dirEmbed)
|
||||||
|
{
|
||||||
|
// rule "N0, a", inspect contents of pair
|
||||||
|
var dirPair = classifyPairContent(pairedLocation, dirEmbed);
|
||||||
|
|
||||||
|
// dirPair is now L, R, or N (no strong type found)
|
||||||
|
|
||||||
|
// the following logical tests are performed out of order compared to
|
||||||
|
// the statement of the rules but yield the same results
|
||||||
|
if (dirPair == Directionality.ON)
|
||||||
|
return; // case "d" - nothing to do
|
||||||
|
|
||||||
|
if (dirPair != dirEmbed)
|
||||||
|
{
|
||||||
|
// case "c": strong type found, opposite - check before (c.1)
|
||||||
|
dirPair = classBeforePair(pairedLocation);
|
||||||
|
if (dirPair == dirEmbed || dirPair == Directionality.ON)
|
||||||
|
{
|
||||||
|
// no strong opposite type found before - use embedding (c.2)
|
||||||
|
dirPair = dirEmbed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else: case "b", strong type found matching embedding,
|
||||||
|
// no explicit action needed, as dirPair is already set to embedding
|
||||||
|
// direction
|
||||||
|
|
||||||
|
// set the bracket types to the type found
|
||||||
|
setBracketsToType(pairedLocation, dirPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBracketsToType(BracketPair pairedLocation, Directionality dirPair)
|
||||||
|
{
|
||||||
|
_codesIsolatedRun[pairedLocation.Opener] = dirPair;
|
||||||
|
_codesIsolatedRun[pairedLocation.Closer] = dirPair;
|
||||||
|
|
||||||
|
for (int i = pairedLocation.Opener + 1; i < pairedLocation.Closer; i++)
|
||||||
|
{
|
||||||
|
int index = _indexes[i];
|
||||||
|
if (_initialCodes[index] == Directionality.NSM)
|
||||||
|
{
|
||||||
|
_codesIsolatedRun[i] = dirPair;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = pairedLocation.Closer + 1; i < _indexes.Length; i++)
|
||||||
|
{
|
||||||
|
int index = _indexes[i];
|
||||||
|
if (_initialCodes[index] == Directionality.NSM)
|
||||||
|
{
|
||||||
|
_codesIsolatedRun[i] = dirPair;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this implements rule N0 for a list of pairs
|
||||||
|
public void resolveBrackets(Directionality dirEmbed)
|
||||||
|
{
|
||||||
|
foreach (var pair in _pairPositions)
|
||||||
|
{
|
||||||
|
assignBracketType(pair, dirEmbed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* runAlgorithm - runs the paired bracket part of the UBA algorithm
|
||||||
|
*
|
||||||
|
* @param indexes
|
||||||
|
* - indexes into the original string
|
||||||
|
* @param initialCodes
|
||||||
|
* - bidi classes (directional codes) initially assigned to each
|
||||||
|
* character in the original string (prior to any modifications by
|
||||||
|
* subsequent steps.
|
||||||
|
* @param codes
|
||||||
|
* - bidi classes (directional codes) for each character in the
|
||||||
|
* original string
|
||||||
|
* @param pairTypes
|
||||||
|
* - array of paired bracket types for each character in the
|
||||||
|
* original string
|
||||||
|
* @param pairValues
|
||||||
|
* - array of unique integers identifying which pair of brackets
|
||||||
|
* (or canonically equivalent set) a bracket character
|
||||||
|
* belongs to. For example in the string "[Test(s)>" the
|
||||||
|
* characters "(" and ")" would share one value and "[" and ">"
|
||||||
|
* share another (assuming that "]" and ">" are canonically equivalent).
|
||||||
|
* Characters that have pairType = n might always get pairValue = 0.
|
||||||
|
*
|
||||||
|
* The actual values are no important as long as they are unique,
|
||||||
|
* so one way to assign them is to use the code position value for
|
||||||
|
* the closing element of a paired set for both opening and closing
|
||||||
|
* character - paying attention to first applying canonical decomposition.
|
||||||
|
* @param sos
|
||||||
|
* - direction for sos
|
||||||
|
* @param level
|
||||||
|
* - the embedding level
|
||||||
|
*/
|
||||||
|
public void resolvePairedBrackets(
|
||||||
|
Slice<int> indexes,
|
||||||
|
Slice<Directionality> initialCodes,
|
||||||
|
Slice<Directionality> codes,
|
||||||
|
Slice<PairedBracketType> pairTypes,
|
||||||
|
Slice<int> pairValues,
|
||||||
|
Directionality sos,
|
||||||
|
byte level)
|
||||||
|
{
|
||||||
|
var dirEmbed = 1 == (level & 1) ? Directionality.R : Directionality.L;
|
||||||
|
|
||||||
|
_sos = sos;
|
||||||
|
_indexes = indexes;
|
||||||
|
_codesIsolatedRun = codes.ToArray();
|
||||||
|
_initialCodes = initialCodes;
|
||||||
|
|
||||||
|
locateBrackets(pairTypes, pairValues);
|
||||||
|
resolveBrackets(dirEmbed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for testing the BPA algorithm in isolation. Does not use an indexes
|
||||||
|
* array for indirection. Actual work is carried out by resolvePairedBrackets.
|
||||||
|
*
|
||||||
|
* @param codes
|
||||||
|
* - bidi classes (directional codes) for each character
|
||||||
|
* @param pairTypes
|
||||||
|
* - array of paired bracket type values for each character
|
||||||
|
* @param pairValues
|
||||||
|
* - array of unique integers identifying which bracket pair
|
||||||
|
* see resolvePairedBrackets for details.
|
||||||
|
* @param sos
|
||||||
|
* - direction for sos
|
||||||
|
* @param level
|
||||||
|
* - the embedding level
|
||||||
|
*/
|
||||||
|
public void runAlgorithm(
|
||||||
|
Slice<Directionality> codes,
|
||||||
|
Slice<PairedBracketType> pairTypes,
|
||||||
|
Slice<int> pairValues,
|
||||||
|
Directionality sos,
|
||||||
|
byte level
|
||||||
|
)
|
||||||
|
{
|
||||||
|
|
||||||
|
// dummy up an indexes array that represents an identity mapping
|
||||||
|
var indexes = new int[codes.Length];
|
||||||
|
for (int ich = 0; ich < _indexes.Length; ich++)
|
||||||
|
indexes[ich] = ich;
|
||||||
|
resolvePairedBrackets(new Slice<int>(indexes), codes, codes, pairTypes, pairValues, sos, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
class FontFallback
|
||||||
|
{
|
||||||
|
public struct Run
|
||||||
|
{
|
||||||
|
public int Start;
|
||||||
|
public int Length;
|
||||||
|
public SKTypeface Typeface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<Run> GetFontRuns(Slice<int> codePoints, SKTypeface typeface)
|
||||||
|
{
|
||||||
|
var fontManager = SKFontManager.Default;
|
||||||
|
|
||||||
|
int currentRunPos = 0;
|
||||||
|
SKTypeface currentRunTypeface = null;
|
||||||
|
int pos = 0;
|
||||||
|
List<Run> runs = new List<Run>();
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (int* pCodePoints = codePoints.Underlying)
|
||||||
|
{
|
||||||
|
int* pch = pCodePoints + codePoints.Start;
|
||||||
|
int length = codePoints.Length;
|
||||||
|
while (pos < length)
|
||||||
|
{
|
||||||
|
var RunFace = typeface;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
if (pch[pos] <= 32)
|
||||||
|
{
|
||||||
|
// Control characters and space always map to current typeface
|
||||||
|
count = 1;
|
||||||
|
RunFace = currentRunTypeface ?? typeface;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Consume as many characters as possible using the requested type face
|
||||||
|
count = typeface.GetGlyphs((IntPtr)(pch + pos), length - pos, SKEncoding.Utf32, out var glyphs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't be mapped to current font, try to find a replacement
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
// Find fallback font
|
||||||
|
RunFace = fontManager.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, pch[pos]);
|
||||||
|
|
||||||
|
// If couldn't use the specified font
|
||||||
|
if (RunFace == null)
|
||||||
|
RunFace = typeface;
|
||||||
|
|
||||||
|
// Consume as many characters as possible using the requested type face
|
||||||
|
count = 1;// RunFace.GetGlyphs((IntPtr)(pch + pos), length - pos, SKEncoding.Utf32, out var glyphs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we need to start a new Run?
|
||||||
|
if (currentRunTypeface != RunFace)
|
||||||
|
{
|
||||||
|
flushCurrentRun();
|
||||||
|
currentRunTypeface = RunFace;
|
||||||
|
currentRunPos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on
|
||||||
|
pos += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the final Run
|
||||||
|
flushCurrentRun();
|
||||||
|
|
||||||
|
// Done
|
||||||
|
return runs;
|
||||||
|
|
||||||
|
void flushCurrentRun()
|
||||||
|
{
|
||||||
|
if (currentRunTypeface != null)
|
||||||
|
{
|
||||||
|
runs.Add(new Run()
|
||||||
|
{
|
||||||
|
Start = currentRunPos,
|
||||||
|
Length = pos - currentRunPos,
|
||||||
|
Typeface = currentRunTypeface,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,535 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single font run
|
||||||
|
/// </summary>
|
||||||
|
public class FontRun
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The kind of run
|
||||||
|
/// </summary>
|
||||||
|
public FontRunKind RunKind = FontRunKind.Normal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user run this typeface run was derived from
|
||||||
|
/// </summary>
|
||||||
|
public StyledRun StyledRun;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the code points of this run
|
||||||
|
/// </summary>
|
||||||
|
public Slice<int> CodePoints => CodePointBuffer.SubSlice(Start, Length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Index into _codePoints buffer of the start of this run
|
||||||
|
/// </summary>
|
||||||
|
public int Start;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of this run (in codepoints)
|
||||||
|
/// </summary>
|
||||||
|
public int Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the first character after this run
|
||||||
|
/// </summary>
|
||||||
|
public int End => Start + Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user supplied style for this run
|
||||||
|
/// </summary>
|
||||||
|
public IStyle Style;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The direction of this run
|
||||||
|
/// </summary>
|
||||||
|
public TextDirection Direction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The typeface of this run (use this over Style.Fontface)
|
||||||
|
/// </summary>
|
||||||
|
public SKTypeface Typeface;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The glyph indicies
|
||||||
|
/// </summary>
|
||||||
|
public Slice<ushort> Glyphs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The glyph positions
|
||||||
|
/// </summary>
|
||||||
|
public Slice<SKPoint> GlyphPositions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cluster numbers for each glyph
|
||||||
|
/// </summary>
|
||||||
|
public Slice<int> Clusters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of each code point, relative to this text run
|
||||||
|
/// </summary>
|
||||||
|
public Slice<float> CodePointPositions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ascent of the font used in this run
|
||||||
|
/// </summary>
|
||||||
|
public float Ascent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The descent of the font used in this run
|
||||||
|
/// </summary>
|
||||||
|
public float Descent;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The height of text in this run (ascent + descent)
|
||||||
|
/// </summary>
|
||||||
|
public float TextHeight => -Ascent + Descent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculate the half leading height for text in this run
|
||||||
|
/// </summary>
|
||||||
|
public float HalfLeading => (TextHeight * Style.LineHeight - TextHeight) / 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Width of this typeface run
|
||||||
|
/// </summary>
|
||||||
|
public float Width;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Horizontal position of this run, relative to the left margin
|
||||||
|
/// </summary>
|
||||||
|
public float XPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The line that owns this font run
|
||||||
|
/// </summary>
|
||||||
|
public TextLine Line { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For debugging
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Debug string</returns>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Start} - {End} @ {XPosition} - {XPosition + Width} = '{Utf32Utils.FromUtf32(CodePoints)}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves all glyphs by the specified offset amount
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dx">The x-delta to move glyphs by</param>
|
||||||
|
/// <param name="dy">The y-delta to move glyphs by</param>
|
||||||
|
public void MoveGlyphs(float dx, float dy)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < GlyphPositions.Length; i++)
|
||||||
|
{
|
||||||
|
GlyphPositions[i].X += dx;
|
||||||
|
GlyphPositions[i].Y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the leading width of all character from the start of the run (either
|
||||||
|
/// the left or right depending on run direction) to the specified code point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point index to measure to</param>
|
||||||
|
/// <returns>The distance from the start to the specified code point</returns>
|
||||||
|
public float LeadingWidth(int codePoint)
|
||||||
|
{
|
||||||
|
// At either end?
|
||||||
|
if (codePoint == this.End)
|
||||||
|
return this.Width;
|
||||||
|
if (codePoint == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Internal, calculate the leading width (ie from code point 0 to code point N)
|
||||||
|
int codePointIndex = codePoint - this.Start;
|
||||||
|
if (this.Direction == TextDirection.LTR)
|
||||||
|
{
|
||||||
|
return this.CodePointPositions[codePointIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return this.Width - this.CodePointPositions[codePointIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculate the position at which to break a text run
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxWidth">The max width available</param>
|
||||||
|
/// <param name="force">Whether to force the use of at least one glyph</param>
|
||||||
|
/// <returns>The code point position to break at</returns>
|
||||||
|
internal int FindBreakPosition(float maxWidth, bool force)
|
||||||
|
{
|
||||||
|
int lastFittingCodePoint = this.Start;
|
||||||
|
int firstNonZeroWidthCodePoint = -1;
|
||||||
|
var prevWidth = 0f;
|
||||||
|
for (int i = this.Start; i < this.End; i++)
|
||||||
|
{
|
||||||
|
var width = this.LeadingWidth(i);
|
||||||
|
if (prevWidth != width)
|
||||||
|
{
|
||||||
|
if (firstNonZeroWidthCodePoint < 0)
|
||||||
|
firstNonZeroWidthCodePoint = i;
|
||||||
|
|
||||||
|
if (width < maxWidth)
|
||||||
|
{
|
||||||
|
lastFittingCodePoint = i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevWidth = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFittingCodePoint > this.Start || !force)
|
||||||
|
return lastFittingCodePoint;
|
||||||
|
|
||||||
|
if (firstNonZeroWidthCodePoint > this.Start)
|
||||||
|
return firstNonZeroWidthCodePoint;
|
||||||
|
|
||||||
|
// Split at the end
|
||||||
|
return this.End;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split a typeface run into two separate runs, truncating this run at
|
||||||
|
/// the specified code point index and returning a new run containing the
|
||||||
|
/// split off part.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="splitAtCodePoint">The code point index to split at</param>
|
||||||
|
/// <returns>A new typeface run for the split off part</returns>
|
||||||
|
internal FontRun Split(int splitAtCodePoint)
|
||||||
|
{
|
||||||
|
if (this.Direction == TextDirection.LTR)
|
||||||
|
{
|
||||||
|
return SplitLTR(splitAtCodePoint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return SplitRTL(splitAtCodePoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split a LTR typeface run into two separate runs, truncating the passed
|
||||||
|
/// run (LHS) and returning a new run containing the split off part (RHS)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="run">The run to split</param>
|
||||||
|
/// <param name="splitAtCodePoint">To code point position to split at</param>
|
||||||
|
/// <returns>The RHS run after splitting</returns>
|
||||||
|
private FontRun SplitLTR(int splitAtCodePoint)
|
||||||
|
{
|
||||||
|
// Check split point is internal to the run
|
||||||
|
System.Diagnostics.Debug.Assert(this.Direction == TextDirection.LTR);
|
||||||
|
System.Diagnostics.Debug.Assert(splitAtCodePoint > this.Start);
|
||||||
|
System.Diagnostics.Debug.Assert(splitAtCodePoint < this.End);
|
||||||
|
|
||||||
|
// Work out the split position
|
||||||
|
int codePointSplitPos = splitAtCodePoint - this.Start;
|
||||||
|
|
||||||
|
// Work out the width that we're slicing off
|
||||||
|
float sliceLeftWidth = this.CodePointPositions[codePointSplitPos];
|
||||||
|
float sliceRightWidth = this.Width - sliceLeftWidth;
|
||||||
|
|
||||||
|
// Work out the glyph split position
|
||||||
|
// TODO: Optimize this
|
||||||
|
int glyphSplitPos = 0;
|
||||||
|
for (glyphSplitPos = 0; glyphSplitPos < this.Clusters.Length; glyphSplitPos++)
|
||||||
|
{
|
||||||
|
if (this.Clusters[glyphSplitPos] >= splitAtCodePoint)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the other run
|
||||||
|
var newRun = new FontRun()
|
||||||
|
{
|
||||||
|
StyledRun = this.StyledRun,
|
||||||
|
CodePointBuffer = this.CodePointBuffer,
|
||||||
|
Direction = this.Direction,
|
||||||
|
Ascent = this.Ascent,
|
||||||
|
Descent = this.Descent,
|
||||||
|
Style = this.Style,
|
||||||
|
Typeface = this.Typeface,
|
||||||
|
Start = splitAtCodePoint,
|
||||||
|
Length = this.End - splitAtCodePoint,
|
||||||
|
Width = sliceRightWidth,
|
||||||
|
CodePointPositions = this.CodePointPositions.SubSlice(codePointSplitPos),
|
||||||
|
GlyphPositions = this.GlyphPositions.SubSlice(glyphSplitPos),
|
||||||
|
Glyphs = this.Glyphs.SubSlice(glyphSplitPos),
|
||||||
|
Clusters = this.Clusters.SubSlice(glyphSplitPos),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adjust code point positions
|
||||||
|
for (int i = 0; i < newRun.CodePointPositions.Length; i++)
|
||||||
|
{
|
||||||
|
newRun.CodePointPositions[i] -= sliceLeftWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust glyph positions
|
||||||
|
for (int i = 0; i < newRun.GlyphPositions.Length; i++)
|
||||||
|
{
|
||||||
|
newRun.GlyphPositions[i].X -= sliceLeftWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update this run
|
||||||
|
this.CodePointPositions = this.CodePointPositions.SubSlice(0, codePointSplitPos);
|
||||||
|
this.Glyphs = this.Glyphs.SubSlice(0, glyphSplitPos);
|
||||||
|
this.GlyphPositions = this.GlyphPositions.SubSlice(0, glyphSplitPos);
|
||||||
|
this.Clusters = this.Clusters.SubSlice(0, glyphSplitPos);
|
||||||
|
this.Width = sliceLeftWidth;
|
||||||
|
this.Length = codePointSplitPos;
|
||||||
|
|
||||||
|
// Return the new run
|
||||||
|
return newRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split a RTL typeface run into two separate runs, truncating the passed
|
||||||
|
/// run (RHS) and returning a new run containing the split off part (LHS)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="run">The run to split</param>
|
||||||
|
/// <param name="splitAtCodePoint">To code point position to split at</param>
|
||||||
|
/// <returns>The LHS run after splitting</returns>
|
||||||
|
private FontRun SplitRTL(int splitAtCodePoint)
|
||||||
|
{
|
||||||
|
// Check split point is internal to the run
|
||||||
|
System.Diagnostics.Debug.Assert(this.Direction == TextDirection.RTL);
|
||||||
|
System.Diagnostics.Debug.Assert(splitAtCodePoint > this.Start);
|
||||||
|
System.Diagnostics.Debug.Assert(splitAtCodePoint < this.End);
|
||||||
|
|
||||||
|
// Work out the split position
|
||||||
|
int codePointSplitPos = splitAtCodePoint - this.Start;
|
||||||
|
|
||||||
|
// Work out the width that we're slicing off
|
||||||
|
float sliceLeftWidth = this.CodePointPositions[codePointSplitPos];
|
||||||
|
float sliceRightWidth = this.Width - sliceLeftWidth;
|
||||||
|
|
||||||
|
// Work out the glyph split position
|
||||||
|
// TODO: Optimize this
|
||||||
|
int glyphSplitPos = 0;
|
||||||
|
for (glyphSplitPos = this.Clusters.Length; glyphSplitPos > 0; glyphSplitPos--)
|
||||||
|
{
|
||||||
|
if (this.Clusters[glyphSplitPos - 1] >= splitAtCodePoint)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the other run
|
||||||
|
var newRun = new FontRun()
|
||||||
|
{
|
||||||
|
StyledRun = this.StyledRun,
|
||||||
|
CodePointBuffer = this.CodePointBuffer,
|
||||||
|
Direction = this.Direction,
|
||||||
|
Ascent = this.Ascent,
|
||||||
|
Descent = this.Descent,
|
||||||
|
Style = this.Style,
|
||||||
|
Typeface = this.Typeface,
|
||||||
|
Start = splitAtCodePoint,
|
||||||
|
Length = this.End - splitAtCodePoint,
|
||||||
|
Width = sliceLeftWidth,
|
||||||
|
CodePointPositions = this.CodePointPositions.SubSlice(codePointSplitPos),
|
||||||
|
GlyphPositions = this.GlyphPositions.SubSlice(0, glyphSplitPos),
|
||||||
|
Glyphs = this.Glyphs.SubSlice(0, glyphSplitPos),
|
||||||
|
Clusters = this.Clusters.SubSlice(0, glyphSplitPos),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update this run
|
||||||
|
this.CodePointPositions = this.CodePointPositions.SubSlice(0, codePointSplitPos);
|
||||||
|
this.Glyphs = this.Glyphs.SubSlice(glyphSplitPos);
|
||||||
|
this.GlyphPositions = this.GlyphPositions.SubSlice(glyphSplitPos);
|
||||||
|
this.Clusters = this.Clusters.SubSlice(glyphSplitPos);
|
||||||
|
this.Width = sliceRightWidth;
|
||||||
|
this.Length = codePointSplitPos;
|
||||||
|
|
||||||
|
// Adjust code point positions
|
||||||
|
for (int i = 0; i < this.CodePointPositions.Length; i++)
|
||||||
|
{
|
||||||
|
this.CodePointPositions[i] -= sliceLeftWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust glyph positions
|
||||||
|
for (int i = 0; i < this.GlyphPositions.Length; i++)
|
||||||
|
{
|
||||||
|
this.GlyphPositions[i].X -= sliceLeftWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the new run
|
||||||
|
return newRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The global list of code points
|
||||||
|
/// </summary>
|
||||||
|
internal Buffer<int> CodePointBuffer;
|
||||||
|
|
||||||
|
internal float CalculateRequiredLeftMargin()
|
||||||
|
{
|
||||||
|
if (Glyphs.Length == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
using (var paint = new SKPaint())
|
||||||
|
{
|
||||||
|
float glyphScale = 1;
|
||||||
|
if (Style.FontVariant == FontVariant.SuperScript)
|
||||||
|
{
|
||||||
|
glyphScale = 0.65f;
|
||||||
|
}
|
||||||
|
if (Style.FontVariant == FontVariant.SubScript)
|
||||||
|
{
|
||||||
|
glyphScale = 0.65f;
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.TextEncoding = SKTextEncoding.GlyphId;
|
||||||
|
paint.Typeface = Typeface;
|
||||||
|
paint.TextSize = Style.FontSize * glyphScale;
|
||||||
|
paint.SubpixelText = true;
|
||||||
|
paint.IsAntialias = true;
|
||||||
|
paint.LcdRenderText = false;
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (ushort* pGlyphs = Glyphs.Underlying)
|
||||||
|
{
|
||||||
|
paint.GetGlyphWidths((IntPtr)(pGlyphs + Start), sizeof(ushort), out var bounds);
|
||||||
|
if (bounds != null && bounds.Length >= 1)
|
||||||
|
{
|
||||||
|
var lhs = XPosition + bounds[0].Left;
|
||||||
|
if (lhs < 0)
|
||||||
|
return -lhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paint this font run
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx"></param>
|
||||||
|
internal void Paint(PaintTextContext ctx)
|
||||||
|
{
|
||||||
|
// Paint selection?
|
||||||
|
if (ctx.PaintSelectionBackground != null)
|
||||||
|
{
|
||||||
|
float startSelPos;
|
||||||
|
if (ctx.SelectionStart < Start)
|
||||||
|
startSelPos = 0;
|
||||||
|
else if (ctx.SelectionStart >= End)
|
||||||
|
startSelPos = Width;
|
||||||
|
else
|
||||||
|
startSelPos = CodePointPositions[ctx.SelectionStart - this.Start];
|
||||||
|
|
||||||
|
float selEndPos;
|
||||||
|
if (ctx.SelectionEnd < Start)
|
||||||
|
selEndPos = 0;
|
||||||
|
else if (ctx.SelectionEnd >= End)
|
||||||
|
selEndPos = Width;
|
||||||
|
else
|
||||||
|
selEndPos = CodePointPositions[ctx.SelectionEnd - this.Start];
|
||||||
|
|
||||||
|
if (startSelPos != selEndPos)
|
||||||
|
{
|
||||||
|
var rect = new SKRect(this.XPosition + startSelPos, Line.YPosition,
|
||||||
|
this.XPosition + selEndPos, Line.YPosition + Line.Height);
|
||||||
|
ctx.Canvas.DrawRect(rect, ctx.PaintSelectionBackground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RunKind == FontRunKind.TrailingWhitespace)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
using (var paint = new SKPaint())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.Color = Style.TextColor;
|
||||||
|
paint.TextEncoding = SKTextEncoding.GlyphId;
|
||||||
|
paint.Typeface = Typeface;
|
||||||
|
paint.TextSize = Style.FontSize * glyphScale;
|
||||||
|
paint.SubpixelText = true;
|
||||||
|
paint.IsAntialias = ctx.Options.IsAntialias;
|
||||||
|
paint.LcdRenderText = ctx.Options.LcdRenderText;
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (ushort* pGlyphs = Glyphs.Underlying)
|
||||||
|
{
|
||||||
|
// Get glyph positions
|
||||||
|
var glyphPositions = GlyphPositions.ToArray();
|
||||||
|
|
||||||
|
// Paint underline
|
||||||
|
if (Style.Underline != UnderlineStyle.None && RunKind == FontRunKind.Normal)
|
||||||
|
{
|
||||||
|
// Work out underline metrics
|
||||||
|
paint.TextSize = Style.FontSize;
|
||||||
|
float underlineYPos = Line.YPosition + Line.BaseLine + (paint.FontMetrics.UnderlinePosition ?? 0);
|
||||||
|
paint.StrokeWidth = paint.FontMetrics.UnderlineThickness ?? 0;
|
||||||
|
paint.TextSize = Style.FontSize * glyphScale;
|
||||||
|
|
||||||
|
if (Style.Underline == UnderlineStyle.Gapped)
|
||||||
|
{
|
||||||
|
// Get intercept positions
|
||||||
|
var interceptPositions = paint.GetPositionedTextIntercepts(
|
||||||
|
(IntPtr)(pGlyphs + Glyphs.Start),
|
||||||
|
Glyphs.Length * sizeof(ushort),
|
||||||
|
glyphPositions, underlineYPos - paint.StrokeWidth / 2, underlineYPos + paint.StrokeWidth);
|
||||||
|
|
||||||
|
float x = XPosition;
|
||||||
|
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 < XPosition + Width)
|
||||||
|
{
|
||||||
|
ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(XPosition + Width, underlineYPos), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.Canvas.DrawLine(new SKPoint(XPosition, underlineYPos), new SKPoint(XPosition + Width, underlineYPos), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Canvas.DrawPositionedText((IntPtr)(pGlyphs + Glyphs.Start), Glyphs.Length * sizeof(ushort), glyphPositions, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint strikethrough
|
||||||
|
if (Style.StrikeThrough != StrikeThroughStyle.None && RunKind == FontRunKind.Normal)
|
||||||
|
{
|
||||||
|
paint.StrokeWidth = paint.FontMetrics.StrikeoutThickness ?? 0;
|
||||||
|
float strikeYPos = Line.YPosition + Line.BaseLine + (paint.FontMetrics.StrikeoutPosition ?? 0) + glyphVOffset;
|
||||||
|
ctx.Canvas.DrawLine(new SKPoint(XPosition, strikeYPos), new SKPoint(XPosition + Width, strikeYPos), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates the kind of font run
|
||||||
|
/// </summary>
|
||||||
|
public enum FontRunKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is a normal text font run
|
||||||
|
/// </summary>
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This font run covers the trailing white space on a line
|
||||||
|
/// </summary>
|
||||||
|
TrailingWhitespace,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a special font run created for the truncation ellipsis
|
||||||
|
/// </summary>
|
||||||
|
Ellipsis,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeration for font variants, super-script and sub-script
|
||||||
|
/// </summary>
|
||||||
|
public enum FontVariant
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normal text
|
||||||
|
/// </summary>
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Super-script Text
|
||||||
|
/// </summary>
|
||||||
|
SuperScript,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sub-script Text
|
||||||
|
/// </summary>
|
||||||
|
SubScript,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the style of a piece of text
|
||||||
|
/// </summary>
|
||||||
|
public interface IStyle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The font family
|
||||||
|
/// </summary>
|
||||||
|
string FontFamily { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The font size
|
||||||
|
/// </summary>
|
||||||
|
float FontSize { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The font weight
|
||||||
|
/// </summary>
|
||||||
|
int FontWeight { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True for italic font; otherwise false
|
||||||
|
/// </summary>
|
||||||
|
bool FontItalic { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The underline style of the text
|
||||||
|
/// </summary>
|
||||||
|
UnderlineStyle Underline { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The strike through style of the text
|
||||||
|
/// </summary>
|
||||||
|
StrikeThroughStyle StrikeThrough { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The line height as a multiplier
|
||||||
|
/// </summary>
|
||||||
|
float LineHeight { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The text color
|
||||||
|
/// </summary>
|
||||||
|
SKColor TextColor { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The font variant (ie: super/sub-script)
|
||||||
|
/// </summary>
|
||||||
|
FontVariant FontVariant { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
class PaintTextContext
|
||||||
|
{
|
||||||
|
public SKCanvas Canvas;
|
||||||
|
public int SelectionStart;
|
||||||
|
public int SelectionEnd;
|
||||||
|
public SKPaint PaintSelectionBackground;
|
||||||
|
public TextPaintOptions Options;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeration of different strike-through styles
|
||||||
|
/// </summary>
|
||||||
|
public enum StrikeThroughStyle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No strike through
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard strike through
|
||||||
|
/// </summary>
|
||||||
|
Solid,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public class Style : IStyle
|
||||||
|
{
|
||||||
|
public string FontFamily
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = "Arial";
|
||||||
|
|
||||||
|
public float FontSize
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = 12;
|
||||||
|
|
||||||
|
public int FontWeight
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = 400;
|
||||||
|
|
||||||
|
public bool FontItalic
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnderlineStyle Underline
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StrikeThroughStyle StrikeThrough
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float LineHeight
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = 1.0f;
|
||||||
|
|
||||||
|
public SKColor TextColor
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = new SKColor(0xFF000000);
|
||||||
|
|
||||||
|
public FontVariant FontVariant
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represets a styled run of text as provided by the client
|
||||||
|
/// </summary>
|
||||||
|
public class StyledRun
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the code points of this run
|
||||||
|
/// </summary>
|
||||||
|
public Slice<int> CodePoints => CodePointBuffer.SubSlice(Start, Length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Index into _codePoints buffer of the start of this run
|
||||||
|
/// </summary>
|
||||||
|
public int Start;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of this run (in codepoints)
|
||||||
|
/// </summary>
|
||||||
|
public int Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the first code point after this run
|
||||||
|
/// </summary>
|
||||||
|
public int End => Start + Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The style of this run
|
||||||
|
/// </summary>
|
||||||
|
public IStyle Style;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The global list of code points
|
||||||
|
/// </summary>
|
||||||
|
internal Buffer<int> CodePointBuffer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Alignment of text within a text block
|
||||||
|
/// </summary>
|
||||||
|
public enum TextAlignment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Use base direction of the text block
|
||||||
|
/// </summary>
|
||||||
|
Auto,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Left-aligned
|
||||||
|
/// </summary>
|
||||||
|
Left,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Centered
|
||||||
|
/// </summary>
|
||||||
|
Center,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Right aligneed
|
||||||
|
/// </summary>
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,24 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Text writing direction
|
||||||
|
/// </summary>
|
||||||
|
public enum TextDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Left to right
|
||||||
|
/// </summary>
|
||||||
|
LTR,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Right to left
|
||||||
|
/// </summary>
|
||||||
|
RTL,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a laid out line of text
|
||||||
|
/// </summary>
|
||||||
|
public class TextLine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
public TextLine()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of text runs in this line
|
||||||
|
/// </summary>
|
||||||
|
public List<FontRun> Runs = new List<FontRun>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position of this line relative to the paragraph
|
||||||
|
/// </summary>
|
||||||
|
public float YPosition
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base line for text in this line (relative to YPosition)
|
||||||
|
/// </summary>
|
||||||
|
public float BaseLine
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum ascent of all font runs in this line
|
||||||
|
/// </summary>
|
||||||
|
public float MaxAscent
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum desscent of all font runs in this line
|
||||||
|
/// </summary>
|
||||||
|
public float MaxDescent
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The height of all text elements in this line
|
||||||
|
/// </summary>
|
||||||
|
public float TextHeight => -MaxAscent + MaxDescent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total height of this line
|
||||||
|
/// </summary>
|
||||||
|
public float Height
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The width of the content on this line (excluding trailing whitespace)
|
||||||
|
/// </summary>
|
||||||
|
public float Width
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paint this line
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="canvas"></param>
|
||||||
|
internal void Paint(PaintTextContext ctx)
|
||||||
|
{
|
||||||
|
foreach (var r in Runs)
|
||||||
|
{
|
||||||
|
r.Paint(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options controlling how TextBlock is rendered
|
||||||
|
/// </summary>
|
||||||
|
public class TextPaintOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optional start selection position
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Both start and end selection need to be set for selection
|
||||||
|
/// painting to occur. Coordinates are in utf-16 characters
|
||||||
|
/// </remarks>
|
||||||
|
public int? SelectionStart
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option end selection position
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Both start and end selection need to be set for selection
|
||||||
|
/// painting to occur. Coordinates are in utf-16 characters
|
||||||
|
/// </remarks>
|
||||||
|
public int? SelectionEnd
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Color to draw selection background with
|
||||||
|
/// </summary>
|
||||||
|
public SKColor SelectionColor
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAntialias
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = true;
|
||||||
|
|
||||||
|
public bool LcdRenderText
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default paint options (no selection)
|
||||||
|
/// </summary>
|
||||||
|
public static TextPaintOptions Default = new TextPaintOptions();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeration of different underline styles
|
||||||
|
/// </summary>
|
||||||
|
public enum UnderlineStyle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No underline
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Underline with gaps over descenders
|
||||||
|
/// </summary>
|
||||||
|
Gapped,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Underline with no gaps over descenders
|
||||||
|
/// </summary>
|
||||||
|
Solid,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
// Ported from: https://github.com/foliojs/linebreak
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public class LineBreak
|
||||||
|
{
|
||||||
|
public LineBreak()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public LineBreak(int positionA, int positionB, bool required = false)
|
||||||
|
{
|
||||||
|
this.PositionMeasure = positionA;
|
||||||
|
this.PositionWrap = positionB;
|
||||||
|
this.Required = required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{PositionMeasure}/{PositionWrap} @ {Required}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The break position, before any trailing whitespace
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This doesn't include trailing whitespace
|
||||||
|
/// </remarks>
|
||||||
|
public int PositionMeasure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The break position, after any trailing whitespace
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This include trailing whitespace
|
||||||
|
/// </remarks>
|
||||||
|
public int PositionWrap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if there should be a forced line break here
|
||||||
|
/// </summary>
|
||||||
|
public bool Required;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
// Ported from: https://github.com/foliojs/linebreak
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public static class LineBreakPairTable
|
||||||
|
{
|
||||||
|
public const byte DI_BRK = 0; // Direct break opportunity
|
||||||
|
public const byte IN_BRK = 1; // Indirect break opportunity
|
||||||
|
public const byte CI_BRK = 2; // Indirect break opportunity for combining marks
|
||||||
|
public const byte CP_BRK = 3; // Prohibited break for combining marks
|
||||||
|
public const byte PR_BRK = 4; // Prohibited break
|
||||||
|
|
||||||
|
public static byte[][] table= new byte[][]
|
||||||
|
{
|
||||||
|
new byte[] {PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, CP_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, CI_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, CI_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, DI_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, DI_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, CI_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, PR_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {IN_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, CI_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, IN_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, DI_BRK},
|
||||||
|
new byte[] {DI_BRK, PR_BRK, PR_BRK, IN_BRK, IN_BRK, IN_BRK, PR_BRK, PR_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK, IN_BRK, DI_BRK, DI_BRK, PR_BRK, CI_BRK, PR_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, DI_BRK, IN_BRK }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
|
||||||
|
// Ported from: https://github.com/foliojs/linebreak
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of the Unicode Line Break Algorithm
|
||||||
|
/// </summary>
|
||||||
|
public class LineBreaker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
static LineBreaker()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset this line breaker
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to be broken</param>
|
||||||
|
public void Reset(string str)
|
||||||
|
{
|
||||||
|
Reset(new Slice<int>(Utf32Utils.ToUtf32(str)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset this line breaker
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoints">The code points of the string to be broken</param>
|
||||||
|
public void Reset(Slice<int> codePoints)
|
||||||
|
{
|
||||||
|
_codePoints = codePoints;
|
||||||
|
_pos = 0;
|
||||||
|
_lastPos = 0;
|
||||||
|
_curClass = null;
|
||||||
|
_nextClass = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerate all line breaks (optionally in reverse order)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A collection of line break positions</returns>
|
||||||
|
public List<LineBreak> GetBreaks()
|
||||||
|
{
|
||||||
|
var list = new List<LineBreak>();
|
||||||
|
while (NextBreak(out var lb))
|
||||||
|
list.Add(lb);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the next line break
|
||||||
|
public bool NextBreak(out LineBreak lineBreak)
|
||||||
|
{
|
||||||
|
// get the first char if we're at the beginning of the string
|
||||||
|
if (!_curClass.HasValue)
|
||||||
|
{
|
||||||
|
if (this.peekCharClass() == LineBreakClass.SP)
|
||||||
|
this._curClass = LineBreakClass.WJ;
|
||||||
|
else
|
||||||
|
this._curClass = mapFirst(this.readCharClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_pos < _codePoints.Length)
|
||||||
|
{
|
||||||
|
_lastPos = _pos;
|
||||||
|
var lastClass = _nextClass;
|
||||||
|
_nextClass = this.readCharClass();
|
||||||
|
|
||||||
|
// explicit newline
|
||||||
|
if (_curClass.HasValue && ((_curClass == LineBreakClass.BK) || ((_curClass == LineBreakClass.CR) && (this._nextClass != LineBreakClass.LF))))
|
||||||
|
{
|
||||||
|
_curClass = mapFirst(mapClass(_nextClass.Value));
|
||||||
|
lineBreak = new LineBreak(findPriorNonWhitespace(_lastPos), _lastPos, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle classes not handled by the pair table
|
||||||
|
LineBreakClass? cur = null;
|
||||||
|
switch (_nextClass.Value)
|
||||||
|
{
|
||||||
|
case LineBreakClass.SP:
|
||||||
|
cur = _curClass;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LineBreakClass.BK:
|
||||||
|
case LineBreakClass.LF:
|
||||||
|
case LineBreakClass.NL:
|
||||||
|
cur = LineBreakClass.BK;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LineBreakClass.CR:
|
||||||
|
cur = LineBreakClass.CR;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LineBreakClass.CB:
|
||||||
|
cur = LineBreakClass.BA;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cur != null)
|
||||||
|
{
|
||||||
|
_curClass = cur;
|
||||||
|
if (_nextClass.HasValue && _nextClass.Value == LineBreakClass.CB)
|
||||||
|
{
|
||||||
|
lineBreak = new LineBreak(findPriorNonWhitespace(_lastPos), _lastPos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not handled already, use the pair table
|
||||||
|
var shouldBreak = false;
|
||||||
|
switch (LineBreakPairTable.table[(int)this._curClass.Value][(int)this._nextClass.Value])
|
||||||
|
{
|
||||||
|
case LineBreakPairTable.DI_BRK: // Direct break
|
||||||
|
shouldBreak = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LineBreakPairTable.IN_BRK: // possible indirect break
|
||||||
|
shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.SP;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LineBreakPairTable.CI_BRK:
|
||||||
|
shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.SP;
|
||||||
|
if (!shouldBreak)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LineBreakPairTable.CP_BRK: // prohibited for combining marks
|
||||||
|
if (!lastClass.HasValue || lastClass.Value != LineBreakClass.SP)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_curClass = _nextClass;
|
||||||
|
if (shouldBreak)
|
||||||
|
{
|
||||||
|
lineBreak = new LineBreak(findPriorNonWhitespace(_lastPos), _lastPos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pos >= _codePoints.Length)
|
||||||
|
{
|
||||||
|
if (_lastPos < _codePoints.Length)
|
||||||
|
{
|
||||||
|
_lastPos = _codePoints.Length;
|
||||||
|
var cls = UnicodeClasses.LineBreakClass(_codePoints[_codePoints.Length - 1]);
|
||||||
|
bool required = cls == LineBreakClass.BK || cls == LineBreakClass.LF || cls == LineBreakClass.CR;
|
||||||
|
lineBreak = new LineBreak(findPriorNonWhitespace(_codePoints.Length), _codePoints.Length, required);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineBreak = new LineBreak(0, 0, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int findPriorNonWhitespace(int from)
|
||||||
|
{
|
||||||
|
if (from > 0)
|
||||||
|
{
|
||||||
|
var cls = UnicodeClasses.LineBreakClass(_codePoints[from - 1]);
|
||||||
|
if (cls == LineBreakClass.BK || cls == LineBreakClass.LF || cls == LineBreakClass.CR)
|
||||||
|
from--;
|
||||||
|
}
|
||||||
|
while (from > 0)
|
||||||
|
{
|
||||||
|
var cls = UnicodeClasses.LineBreakClass(_codePoints[from - 1]);
|
||||||
|
if (cls == LineBreakClass.SP)
|
||||||
|
from--;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
|
int findNextNonWhitespace(int from)
|
||||||
|
{
|
||||||
|
while (from < _codePoints.Length && UnicodeClasses.LineBreakClass(_codePoints[from]) == LineBreakClass.SP)
|
||||||
|
from++;
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State
|
||||||
|
Slice<int> _codePoints;
|
||||||
|
int _pos;
|
||||||
|
int _lastPos;
|
||||||
|
LineBreakClass? _curClass;
|
||||||
|
LineBreakClass? _nextClass;
|
||||||
|
|
||||||
|
// Get the next character class
|
||||||
|
LineBreakClass readCharClass()
|
||||||
|
{
|
||||||
|
return mapClass(UnicodeClasses.LineBreakClass(_codePoints[_pos++]));
|
||||||
|
}
|
||||||
|
|
||||||
|
LineBreakClass peekCharClass()
|
||||||
|
{
|
||||||
|
return mapClass(UnicodeClasses.LineBreakClass(_codePoints[_pos]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static LineBreakClass mapClass(LineBreakClass c)
|
||||||
|
{
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case LineBreakClass.AI:
|
||||||
|
return LineBreakClass.AL;
|
||||||
|
|
||||||
|
case LineBreakClass.SA:
|
||||||
|
case LineBreakClass.SG:
|
||||||
|
case LineBreakClass.XX:
|
||||||
|
return LineBreakClass.AL;
|
||||||
|
|
||||||
|
case LineBreakClass.CJ:
|
||||||
|
return LineBreakClass.NS;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static LineBreakClass mapFirst(LineBreakClass c)
|
||||||
|
{
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case LineBreakClass.LF:
|
||||||
|
case LineBreakClass.NL:
|
||||||
|
return LineBreakClass.BK;
|
||||||
|
|
||||||
|
case LineBreakClass.CB:
|
||||||
|
return LineBreakClass.BA;
|
||||||
|
|
||||||
|
case LineBreakClass.SP:
|
||||||
|
return LineBreakClass.WJ;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Двоичный файл не отображается.
Двоичный файл не отображается.
|
@ -0,0 +1,262 @@
|
||||||
|
using HarfBuzzSharp;
|
||||||
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.HarfBuzz;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public class TextShaper : IDisposable
|
||||||
|
{
|
||||||
|
static Dictionary<SKTypeface, TextShaper> _shapers = new Dictionary<SKTypeface, TextShaper>();
|
||||||
|
|
||||||
|
public static TextShaper ForTypeface(SKTypeface typeface)
|
||||||
|
{
|
||||||
|
lock (_shapers)
|
||||||
|
{
|
||||||
|
if (!_shapers.TryGetValue(typeface, out var shaper))
|
||||||
|
{
|
||||||
|
shaper = new TextShaper(typeface);
|
||||||
|
_shapers.Add(typeface, shaper);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shaper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextShaper(SKTypeface typeface)
|
||||||
|
{
|
||||||
|
int index;
|
||||||
|
using (var blob = typeface.OpenStream(out index).ToHarfBuzzBlob())
|
||||||
|
using (var face = new Face(blob, (uint)index))
|
||||||
|
{
|
||||||
|
face.UnitsPerEm = (uint)typeface.UnitsPerEm;
|
||||||
|
|
||||||
|
_font = new HarfBuzzSharp.Font(face);
|
||||||
|
_font.SetScale(overScale, overScale);
|
||||||
|
_font.SetFunctionsOpenType();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var paint = new SKPaint())
|
||||||
|
{
|
||||||
|
paint.Typeface = typeface;
|
||||||
|
paint.TextSize = overScale;
|
||||||
|
_fontMetrics = paint.FontMetrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_font != null)
|
||||||
|
{
|
||||||
|
_font.Dispose();
|
||||||
|
_font = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HarfBuzzSharp.Font _font;
|
||||||
|
SKFontMetrics _fontMetrics;
|
||||||
|
|
||||||
|
public struct Result
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The glyph indicies of all glyphs required to render the shaped text
|
||||||
|
/// </summary>
|
||||||
|
public ushort[] GlyphIndicies;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the index of the code point represents by the glyph at a specified index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="glyphIndex">The glyph index</param>
|
||||||
|
/// <returns>The index into the original code point array</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public int CodePointIndexFromGlyphIndex(int glyphIndex)
|
||||||
|
{
|
||||||
|
if (glyphIndex < Clusters.Length)
|
||||||
|
return Clusters[glyphIndex];
|
||||||
|
else
|
||||||
|
return CodePointPositions.Length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of each glyph
|
||||||
|
/// </summary>
|
||||||
|
public SKPoint[] Points;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One entry for each glyph, showing the code point index
|
||||||
|
/// of the characters it was derived from
|
||||||
|
/// </summary>
|
||||||
|
public int[] Clusters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The end position of the rendered text
|
||||||
|
/// </summary>
|
||||||
|
public SKPoint EndPoint;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The X-Position of each passed code point
|
||||||
|
/// </summary>
|
||||||
|
public float[] CodePointPositions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The indicies into the original code points array
|
||||||
|
/// of all valid cursor stops
|
||||||
|
/// </summary>
|
||||||
|
public int[] CursorIndicies;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ascent of the font
|
||||||
|
/// </summary>
|
||||||
|
public float Ascent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The descent of the font
|
||||||
|
/// </summary>
|
||||||
|
public float Descent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The XMin for the font
|
||||||
|
/// </summary>
|
||||||
|
public float XMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int overScale = 512;
|
||||||
|
|
||||||
|
// Temporary hack until newer HarfBuzzSharp is released with support for AddUtf32
|
||||||
|
[DllImport("libHarfBuzzSharp", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public extern static void hb_buffer_add_utf32 (IntPtr buffer, IntPtr text, int text_length, int item_offset, int item_length);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shape an array of utf32- code points
|
||||||
|
/// </summary>
|
||||||
|
/// <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>
|
||||||
|
/// <returns>A TextShaper.Result representing the shaped text</returns>
|
||||||
|
public Result Shape(Slice<int> codePoints, IStyle style, TextDirection direction, int clusterAdjustment = 0)
|
||||||
|
{
|
||||||
|
using (var buffer = new HarfBuzzSharp.Buffer())
|
||||||
|
{
|
||||||
|
// Setup buffer
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (int* pCodePoints = codePoints.Underlying)
|
||||||
|
{
|
||||||
|
hb_buffer_add_utf32(buffer.Handle, (IntPtr)(pCodePoints + codePoints.Start), codePoints.Length, 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);
|
||||||
|
|
||||||
|
// Create results
|
||||||
|
var r = new Result();
|
||||||
|
r.GlyphIndicies = buffer.GlyphInfos.Select(x => (ushort)x.Codepoint).ToArray();
|
||||||
|
r.Clusters = buffer.GlyphInfos.Select(x => (int)x.Cluster + clusterAdjustment).ToArray();
|
||||||
|
r.CursorIndicies = r.Clusters.Distinct().OrderBy(x => x).Select(x => (int)x).ToArray();
|
||||||
|
r.CodePointPositions = new float[codePoints.Length];
|
||||||
|
r.Points = new SKPoint[buffer.Length];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert points
|
||||||
|
var gp = buffer.GlyphPositions;
|
||||||
|
float cursorX = 0;
|
||||||
|
float cursorY = 0;
|
||||||
|
for (int i = 0; i < buffer.Length; i++)
|
||||||
|
{
|
||||||
|
// Update code point positions
|
||||||
|
if (!rtl && r.CodePointPositions[r.Clusters[i] - clusterAdjustment] == 0)
|
||||||
|
r.CodePointPositions[r.Clusters[i] - clusterAdjustment] = cursorX;
|
||||||
|
|
||||||
|
// Get the position
|
||||||
|
var pos = gp[i];
|
||||||
|
|
||||||
|
// Update glyph position
|
||||||
|
r.Points[i] = new SKPoint(
|
||||||
|
cursorX + pos.XOffset * glyphScale,
|
||||||
|
cursorY - pos.YOffset * glyphScale + glyphVOffset
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update cursor position
|
||||||
|
cursorX += pos.XAdvance * glyphScale;
|
||||||
|
cursorY += pos.YAdvance * glyphScale;
|
||||||
|
|
||||||
|
// Store RTL cursor position
|
||||||
|
if (rtl)
|
||||||
|
r.CodePointPositions[r.Clusters[i] - clusterAdjustment] = cursorX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize cursor positions by filling in any that weren't
|
||||||
|
// referenced by a cluster
|
||||||
|
if (rtl)
|
||||||
|
{
|
||||||
|
r.CodePointPositions[0] = cursorX;
|
||||||
|
for (int i = codePoints.Length - 2; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (r.CodePointPositions[i] == 0)
|
||||||
|
r.CodePointPositions[i] = r.CodePointPositions[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 1; i < codePoints.Length; i++)
|
||||||
|
{
|
||||||
|
if (r.CodePointPositions[i] == 0)
|
||||||
|
r.CodePointPositions[i] = r.CodePointPositions[i - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also return the end cursor position
|
||||||
|
r.EndPoint = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netcoreapp2.1;net45</TargetFrameworks>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Resources\BidiData.trie" />
|
||||||
|
<None Remove="Resources\LineBreakClasses.trie" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\BidiData.trie" />
|
||||||
|
<EmbeddedResource Include="Resources\LineBreakClasses.trie" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SkiaSharp" Version="1.68.0" />
|
||||||
|
<PackageReference Include="SkiaSharp.HarfBuzz" Version="1.68.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,52 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
enum Directionality : byte
|
||||||
|
{
|
||||||
|
// Strong types
|
||||||
|
L = 0,
|
||||||
|
R = 1,
|
||||||
|
AL = 2,
|
||||||
|
|
||||||
|
// Weak Types
|
||||||
|
EN = 3,
|
||||||
|
ES = 4,
|
||||||
|
ET = 5,
|
||||||
|
AN = 6,
|
||||||
|
CS = 7,
|
||||||
|
NSM = 8,
|
||||||
|
BN = 9,
|
||||||
|
|
||||||
|
// Neutral Types
|
||||||
|
B = 10,
|
||||||
|
S = 11,
|
||||||
|
WS = 12,
|
||||||
|
ON = 13,
|
||||||
|
|
||||||
|
// Explicit Formatting Types
|
||||||
|
LRE = 14,
|
||||||
|
LRO = 15,
|
||||||
|
RLE = 16,
|
||||||
|
RLO = 17,
|
||||||
|
PDF = 18,
|
||||||
|
LRI = 19,
|
||||||
|
RLI = 20,
|
||||||
|
FSI = 21,
|
||||||
|
PDI = 22,
|
||||||
|
|
||||||
|
/** Minimum bidi type value. */
|
||||||
|
TYPE_MIN = 0,
|
||||||
|
|
||||||
|
/** Maximum bidi type value. */
|
||||||
|
TYPE_MAX = 22,
|
||||||
|
|
||||||
|
/* Unknown */
|
||||||
|
Unknown = 0xFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Ported from: https://github.com/foliojs/linebreak
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
enum LineBreakClass
|
||||||
|
{
|
||||||
|
// The following break classes are handled by the pair table
|
||||||
|
OP = 0, // Opening punctuation
|
||||||
|
CL = 1, // Closing punctuation
|
||||||
|
CP = 2, // Closing parenthesis
|
||||||
|
QU = 3, // Ambiguous quotation
|
||||||
|
GL = 4, // Glue
|
||||||
|
NS = 5, // Non-starters
|
||||||
|
EX = 6, // Exclamation/Interrogation
|
||||||
|
SY = 7, // Symbols allowing break after
|
||||||
|
IS = 8, // Infix separator
|
||||||
|
PR = 9, // Prefix
|
||||||
|
PO = 10, // Postfix
|
||||||
|
NU = 11, // Numeric
|
||||||
|
AL = 12, // Alphabetic
|
||||||
|
HL = 13, // Hebrew Letter
|
||||||
|
ID = 14, // Ideographic
|
||||||
|
IN = 15, // Inseparable characters
|
||||||
|
HY = 16, // Hyphen
|
||||||
|
BA = 17, // Break after
|
||||||
|
BB = 18, // Break before
|
||||||
|
B2 = 19, // Break on either side (but not pair)
|
||||||
|
ZW = 20, // Zero-width space
|
||||||
|
CM = 21, // Combining marks
|
||||||
|
WJ = 22, // Word joiner
|
||||||
|
H2 = 23, // Hangul LV
|
||||||
|
H3 = 24, // Hangul LVT
|
||||||
|
JL = 25, // Hangul L Jamo
|
||||||
|
JV = 26, // Hangul V Jamo
|
||||||
|
JT = 27, // Hangul T Jamo
|
||||||
|
RI = 28, // Regional Indicator
|
||||||
|
|
||||||
|
// The following break classes are not handled by the pair table
|
||||||
|
AI = 29, // Ambiguous (Alphabetic or Ideograph)
|
||||||
|
BK = 30, // Break (mandatory)
|
||||||
|
CB = 31, // Contingent break
|
||||||
|
CJ = 32, // Conditional Japanese Starter
|
||||||
|
CR = 33, // Carriage return
|
||||||
|
LF = 34, // Line feed
|
||||||
|
NL = 35, // Next line
|
||||||
|
SA = 36, // South-East Asian
|
||||||
|
SG = 37, // Surrogates
|
||||||
|
SP = 38, // Space
|
||||||
|
XX = 39, // Unknown
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
enum PairedBracketType : byte
|
||||||
|
{
|
||||||
|
n,
|
||||||
|
o,
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
static class UnicodeClasses
|
||||||
|
{
|
||||||
|
static UnicodeClasses()
|
||||||
|
{
|
||||||
|
_bidiTrie = new UnicodeTrie(typeof(LineBreaker).Assembly.GetManifestResourceStream("Topten.RichText.Resources.BidiData.trie"));
|
||||||
|
_classesTrie = new UnicodeTrie(typeof(LineBreaker).Assembly.GetManifestResourceStream("Topten.RichText.Resources.LineBreakClasses.trie"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static UnicodeTrie _bidiTrie;
|
||||||
|
static UnicodeTrie _classesTrie;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the directionality of a Unicode Code Point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point in question</param>
|
||||||
|
/// <returns>The code point's directionality</returns>
|
||||||
|
public static Directionality Directionality(int codePoint)
|
||||||
|
{
|
||||||
|
return (Directionality)(_bidiTrie.Get(codePoint) >> 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the directionality of a Unicode Code Point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point in question</param>
|
||||||
|
/// <returns>The code point's directionality</returns>
|
||||||
|
public static uint BidiData(int codePoint)
|
||||||
|
{
|
||||||
|
return _bidiTrie.Get(codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the bracket type for a Unicode Code Point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point in question</param>
|
||||||
|
/// <returns>The code point's paired bracked type</returns>
|
||||||
|
public static PairedBracketType PairedBracketType(int codePoint)
|
||||||
|
{
|
||||||
|
return (PairedBracketType)((_bidiTrie.Get(codePoint) >> 16) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the associated bracket type for a Unicode Code Point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point in question</param>
|
||||||
|
/// <returns>The code point's opposite bracket, or 0 if not a bracket</returns>
|
||||||
|
public static int AssociatedBracket(int codePoint)
|
||||||
|
{
|
||||||
|
return (int)(_bidiTrie.Get(codePoint) & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the line break class for a Unicode Code Point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The code point in question</param>
|
||||||
|
/// <returns>The code point's line break class</returns>
|
||||||
|
public static LineBreakClass LineBreakClass(int codePoint)
|
||||||
|
{
|
||||||
|
return (LineBreakClass)_classesTrie.Get(codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
// Ported from: https://github.com/foliojs/unicode-trie
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public class UnicodeTrie
|
||||||
|
{
|
||||||
|
public UnicodeTrie(Stream stream)
|
||||||
|
{
|
||||||
|
int dataLength;
|
||||||
|
using (var bw = new BinaryReader(stream, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
_highStart = bw.ReadInt32BE();
|
||||||
|
_errorValue = bw.ReadUInt32BE();
|
||||||
|
dataLength = bw.ReadInt32BE() / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var infl1 = new DeflateStream(stream, CompressionMode.Decompress, true))
|
||||||
|
using (var infl2 = new DeflateStream(infl1, CompressionMode.Decompress, true))
|
||||||
|
using (var bw = new BinaryReader(infl2, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
_data = new int[dataLength];
|
||||||
|
for (int i = 0; i < _data.Length; i++)
|
||||||
|
{
|
||||||
|
_data[i] = bw.ReadInt32();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnicodeTrie(byte[] buf) : this(new MemoryStream(buf))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal UnicodeTrie(int[] data, int highStart, uint errorValue)
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
_highStart = highStart;
|
||||||
|
_errorValue = errorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Save(Stream stream)
|
||||||
|
{
|
||||||
|
// Write the header info
|
||||||
|
using (var bw = new BinaryWriter(stream, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
bw.WriteBE(_highStart);
|
||||||
|
bw.WriteBE(_errorValue);
|
||||||
|
bw.WriteBE(_data.Length * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double compress the data
|
||||||
|
using (var def1 = new DeflateStream(stream, CompressionLevel.Optimal, true))
|
||||||
|
using (var def2 = new DeflateStream(def1, CompressionLevel.Optimal, true))
|
||||||
|
using (var bw = new BinaryWriter(def2, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
foreach (var v in _data)
|
||||||
|
{
|
||||||
|
bw.Write(v);
|
||||||
|
}
|
||||||
|
bw.Flush();
|
||||||
|
def2.Flush();
|
||||||
|
def1.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int[] _data;
|
||||||
|
int _highStart;
|
||||||
|
uint _errorValue;
|
||||||
|
|
||||||
|
public uint Get(int codePoint)
|
||||||
|
{
|
||||||
|
int index;
|
||||||
|
if ((codePoint < 0) || (codePoint > 0x10ffff))
|
||||||
|
{
|
||||||
|
return _errorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((codePoint < 0xd800) || ((codePoint > 0xdbff) && (codePoint <= 0xffff)))
|
||||||
|
{
|
||||||
|
// Ordinary BMP code point, excluding leading surrogates.
|
||||||
|
// BMP uses a single level lookup. BMP index starts at offset 0 in the index.
|
||||||
|
// data is stored in the index array itself.
|
||||||
|
index = (_data[codePoint >> UnicodeTrieBuilder.SHIFT_2] << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK);
|
||||||
|
return (uint)_data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codePoint <= 0xffff)
|
||||||
|
{
|
||||||
|
// Lead Surrogate Code Point. A Separate index section is stored for
|
||||||
|
// lead surrogate code units and code points.
|
||||||
|
// The main index has the code unit data.
|
||||||
|
// For this function, we need the code point data.
|
||||||
|
index = (_data[UnicodeTrieBuilder.LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UnicodeTrieBuilder.SHIFT_2)] << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK);
|
||||||
|
return (uint)_data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codePoint < _highStart)
|
||||||
|
{
|
||||||
|
// Supplemental code point, use two-level lookup.
|
||||||
|
index = _data[(UnicodeTrieBuilder.INDEX_1_OFFSET - UnicodeTrieBuilder.OMITTED_BMP_INDEX_1_LENGTH) + (codePoint >> UnicodeTrieBuilder.SHIFT_1)];
|
||||||
|
index = _data[index + ((codePoint >> UnicodeTrieBuilder.SHIFT_2) & UnicodeTrieBuilder.INDEX_2_MASK)];
|
||||||
|
index = (index << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK);
|
||||||
|
return (uint)_data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (uint)_data[_data.Length - UnicodeTrieBuilder.DATA_GRANULARITY];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,126 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a slice of an array
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The array type</typeparam>
|
||||||
|
public struct Slice<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new array slice covering the entire array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array"></param>
|
||||||
|
public Slice(T[] array)
|
||||||
|
: this(array, 0, array.Length)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new array slice for part of an array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array"></param>
|
||||||
|
/// <param name="start"></param>
|
||||||
|
/// <param name="length"></param>
|
||||||
|
public Slice(T[] array, int start, int length)
|
||||||
|
{
|
||||||
|
if (start < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(start));
|
||||||
|
if (start + length > array.Length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length));
|
||||||
|
|
||||||
|
_array = array;
|
||||||
|
_start = start;
|
||||||
|
_length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the length of the array slice
|
||||||
|
/// </summary>
|
||||||
|
public int Length
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _length;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear the entire slice content
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
System.Array.Clear(_array, _start, _length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get/set an element in the slice
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The element index</param>
|
||||||
|
/// <returns>The element value</returns>
|
||||||
|
public ref T this[int index]
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
return ref _array[_start + index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T[] _array;
|
||||||
|
int _start;
|
||||||
|
int _length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a subslice of an array slice
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">The slice start index</param>
|
||||||
|
/// <param name="length">The slice length</param>
|
||||||
|
/// <returns>A new array slice</returns>
|
||||||
|
public Slice<T> SubSlice(int start, int length)
|
||||||
|
{
|
||||||
|
return new Slice<T>(_array, _start + start, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a subslice of an array slice, from a specified position to the end
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">The slice start index</param>
|
||||||
|
/// <returns>A new array slice</returns>
|
||||||
|
public Slice<T> SubSlice(int start)
|
||||||
|
{
|
||||||
|
return SubSlice(start, Length - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the slice contents as a new array
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public T[] ToArray()
|
||||||
|
{
|
||||||
|
var array = new T[_length];
|
||||||
|
System.Array.Copy(_array, _start, array, 0, _length);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the underlying array
|
||||||
|
/// </summary>
|
||||||
|
public T[] Underlying => _array;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the offset of this slice within the underlying array
|
||||||
|
/// </summary>
|
||||||
|
public int Start => _start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public static class BinaryUtils
|
||||||
|
{
|
||||||
|
public static int ReadInt32BE(this BinaryReader reader)
|
||||||
|
{
|
||||||
|
var bytes = reader.ReadBytes(4);
|
||||||
|
Array.Reverse(bytes);
|
||||||
|
return BitConverter.ToInt32(bytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint ReadUInt32BE(this BinaryReader reader)
|
||||||
|
{
|
||||||
|
var bytes = reader.ReadBytes(4);
|
||||||
|
Array.Reverse(bytes);
|
||||||
|
return BitConverter.ToUInt32(bytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteBE(this BinaryWriter writer, int value)
|
||||||
|
{
|
||||||
|
var bytes = BitConverter.GetBytes(value);
|
||||||
|
Array.Reverse(bytes);
|
||||||
|
writer.Write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteBE(this BinaryWriter writer, uint value)
|
||||||
|
{
|
||||||
|
var bytes = BitConverter.GetBytes(value);
|
||||||
|
Array.Reverse(bytes);
|
||||||
|
writer.Write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A buffer of T
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The buffer element type</typeparam>
|
||||||
|
public class Buffer<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new buffer
|
||||||
|
/// </summary>
|
||||||
|
public Buffer()
|
||||||
|
{
|
||||||
|
// Create initial array
|
||||||
|
_data = new T[1024];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data held by this buffer
|
||||||
|
/// </summary>
|
||||||
|
T[] _data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The used length of the buffer
|
||||||
|
/// </summary>
|
||||||
|
int _length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get or set the length of the buffer
|
||||||
|
/// </summary>
|
||||||
|
public int Length
|
||||||
|
{
|
||||||
|
get => _length;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value > _data.Length)
|
||||||
|
{
|
||||||
|
var newData = new T[value];
|
||||||
|
Array.Copy(_data, 0, newData, 0, _data.Length);
|
||||||
|
_data = newData;
|
||||||
|
}
|
||||||
|
_length = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear the buffer (keep internal allocated memory)
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add to the buffer, returning a slice of requested size
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">Number of items to add</param>
|
||||||
|
/// <param name="clear">True to clear the content</param>
|
||||||
|
/// <returns>A slice representing the allocated area</returns>
|
||||||
|
public Slice<T> Add(int length, bool clear = true)
|
||||||
|
{
|
||||||
|
// Grow internal buffer?
|
||||||
|
if (_length + length > _data.Length)
|
||||||
|
{
|
||||||
|
var newData = new T[_length + length + 1024];
|
||||||
|
Array.Copy(_data, 0, newData, 0, _length);
|
||||||
|
_data = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear it?
|
||||||
|
if (clear)
|
||||||
|
Array.Clear(_data, _length, length);
|
||||||
|
|
||||||
|
// Capture where it was placed
|
||||||
|
var pos = _length;
|
||||||
|
|
||||||
|
// Update length
|
||||||
|
_length += length;
|
||||||
|
|
||||||
|
// Return position
|
||||||
|
return SubSlice(pos, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a slice of data to this buffer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slice">The slice to be added</param>
|
||||||
|
public Slice<T> Add(Slice<T> slice)
|
||||||
|
{
|
||||||
|
// Grow internal buffer?
|
||||||
|
if (_length + slice.Length > _data.Length)
|
||||||
|
{
|
||||||
|
var newData = new T[_length + slice.Length + 1024];
|
||||||
|
Array.Copy(_data, 0, newData, 0, _length);
|
||||||
|
_data = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy in the slice
|
||||||
|
Array.Copy(slice.Underlying, slice.Start, _data, _length, slice.Length);
|
||||||
|
|
||||||
|
// Capture where it was placed
|
||||||
|
var pos = _length;
|
||||||
|
|
||||||
|
// Update length
|
||||||
|
_length += slice.Length;
|
||||||
|
|
||||||
|
// Return position
|
||||||
|
return SubSlice(pos, slice.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get/set an element in the slice
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The element index</param>
|
||||||
|
/// <returns>The element value</returns>
|
||||||
|
public ref T this[int index]
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
return ref _data[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a subslice of data from this array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Start offset of the slice</param>
|
||||||
|
/// <param name="length">Length of the slice</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Slice<T> SubSlice(int start, int length)
|
||||||
|
{
|
||||||
|
if (start < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(start));
|
||||||
|
if (start + length > _length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length));
|
||||||
|
|
||||||
|
return new Slice<T>(_data, start, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the entire buffer contents as a slice
|
||||||
|
public Slice<T> AsSlice()
|
||||||
|
{
|
||||||
|
return SubSlice(0, _length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A buffer of utf-32 character data
|
||||||
|
/// </summary>
|
||||||
|
public class Utf32Buffer : Buffer<int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
public Utf32Buffer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a UTF32 buffer with an initial string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to initialize with</param>
|
||||||
|
public Utf32Buffer(string str)
|
||||||
|
{
|
||||||
|
Add(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear this text buffer
|
||||||
|
/// </summary>
|
||||||
|
public new void Clear()
|
||||||
|
{
|
||||||
|
_surrogatePositions.Clear();
|
||||||
|
base.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add text to this UTF-32 buffer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The text to add</param>
|
||||||
|
/// <returns>A slice representing the added UTF-32 data</returns>
|
||||||
|
public Slice<int> Add(string str)
|
||||||
|
{
|
||||||
|
// Remember old length
|
||||||
|
int oldLength = Length;
|
||||||
|
|
||||||
|
// For performance reasons and to save copying to intermediate arrays if we use
|
||||||
|
// (Encoding.UTF32), we do our own utf16 to utf32 decoding directly to our
|
||||||
|
// internal code point buffer. Also stores the indicies of any surrogate pairs
|
||||||
|
// for later back conversion.
|
||||||
|
// Also use pointers for performance reasons too (maybe)
|
||||||
|
Slice<int> codePointBuffer = this.Add(str.Length);
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (int* pDestBuf = codePointBuffer.Underlying)
|
||||||
|
fixed (char* pSrcBuf = str)
|
||||||
|
{
|
||||||
|
int* pDestStart = pDestBuf + codePointBuffer.Start;
|
||||||
|
int* pDest = pDestStart;
|
||||||
|
char* pSrc = pSrcBuf;
|
||||||
|
char* pSrcEnd = pSrcBuf + str.Length;
|
||||||
|
while (pSrc < pSrcEnd)
|
||||||
|
{
|
||||||
|
char ch = *pSrc++;
|
||||||
|
|
||||||
|
// Normalize line endings to '\n'
|
||||||
|
if (ch == '\r' && pSrc < pSrcEnd && *pSrc == '\n')
|
||||||
|
{
|
||||||
|
*pDest++ = '\n';
|
||||||
|
pSrc++;
|
||||||
|
_surrogatePositions.Add((int)(pDest - pDestBuf - 1));
|
||||||
|
}
|
||||||
|
else if (ch >= 0xD800 && ch <= 0xDFFF)
|
||||||
|
{
|
||||||
|
if (ch <= 0xDBFF)
|
||||||
|
{
|
||||||
|
// High surrogate
|
||||||
|
var chL = pSrc < pSrcEnd ? (*pSrc++) : 0;
|
||||||
|
*pDest++ = 0x10000 | ((ch - 0xD800) << 10) | (chL - 0xDC00);
|
||||||
|
_surrogatePositions.Add((int)(pDest - pDestBuf - 1));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Single low surrogte?
|
||||||
|
*pDest++ = 0x10000 + ch - 0xDC00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
*pDest++ = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate length of buffer (may be shorter due to surrogates)
|
||||||
|
this.Length = (int)(pDest - pDestBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the encapsulating slice
|
||||||
|
return SubSlice(oldLength, Length - oldLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convers an offset into this utf32 buffer to a utf16 offset
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This function assumes the was text added to the buffer as utf16
|
||||||
|
/// and hasn't been modified in any way since.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="utf32Offset">The utf32 offset to convert</param>
|
||||||
|
/// <returns>The utf16 character offset</returns>
|
||||||
|
public int Utf32OffsetToUtf16Offset(int utf32Offset)
|
||||||
|
{
|
||||||
|
// How many surrogate pairs were there before this utf32 offset?
|
||||||
|
int pos = _surrogatePositions.BinarySearch(utf32Offset);
|
||||||
|
if (pos < 0)
|
||||||
|
{
|
||||||
|
pos = ~pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return utf32Offset + pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a utf-16 index, convert it to a utf-32 index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="utf16Offset">The utf-16 character index</param>
|
||||||
|
/// <returns>The utf-32 code point index</returns>
|
||||||
|
public int Utf16OffsetToUtf32Offset(int utf16Offset)
|
||||||
|
{
|
||||||
|
var pos = utf16Offset;
|
||||||
|
for (int i = 0; i < _surrogatePositions.Count; i++)
|
||||||
|
{
|
||||||
|
var sp = _surrogatePositions[i];
|
||||||
|
if (sp < pos)
|
||||||
|
pos--;
|
||||||
|
if (sp > pos)
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enture buffer's content as a string
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetString()
|
||||||
|
{
|
||||||
|
return Utf32Utils.FromUtf32(AsSlice());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a slide of the buffer as a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Start offset (in utf-32 coords)</param>
|
||||||
|
/// <param name="length">Length of the slice (in utf-32 coords)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetString(int start, int length)
|
||||||
|
{
|
||||||
|
return Utf32Utils.FromUtf32(SubSlice(start, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicies of all code points in the in the buffer
|
||||||
|
/// that were decoded from a surrogate pair
|
||||||
|
/// </summary>
|
||||||
|
List<int> _surrogatePositions = new List<int>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Topten.RichText
|
||||||
|
{
|
||||||
|
public static class Utf32Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a slice of utf32 integer code points back to a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The code points to convert</param>
|
||||||
|
/// <returns>A string</returns>
|
||||||
|
public static string FromUtf32(Slice<int> buffer)
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (int* p = buffer.Underlying)
|
||||||
|
{
|
||||||
|
var pBuf = p + buffer.Start;
|
||||||
|
return new string((sbyte*)pBuf, 0, buffer.Length * sizeof(int), Encoding.UTF32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a string to an integer array of utf-32 code points
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to convert</param>
|
||||||
|
/// <returns>The converted code points</returns>
|
||||||
|
public static int[] ToUtf32(string str)
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (char* pstr = str)
|
||||||
|
{
|
||||||
|
// Get required byte count
|
||||||
|
int byteCount = Encoding.UTF32.GetByteCount(pstr, str.Length);
|
||||||
|
System.Diagnostics.Debug.Assert((byteCount % 4) == 0);
|
||||||
|
|
||||||
|
// Allocate buffer
|
||||||
|
int[] utf32 = new int[byteCount / sizeof(int)];
|
||||||
|
fixed (int* putf32 = utf32)
|
||||||
|
{
|
||||||
|
// Convert
|
||||||
|
Encoding.UTF32.GetBytes(pstr, str.Length, (byte*)putf32, byteCount);
|
||||||
|
|
||||||
|
// Done
|
||||||
|
return utf32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче