72 changed files with 1927 additions and 361 deletions
@ -0,0 +1,19 @@ |
|||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Input |
||||
|
{ |
||||
|
public class PointerDeltaEventArgs : PointerEventArgs |
||||
|
{ |
||||
|
public Vector Delta { get; set; } |
||||
|
|
||||
|
public PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source, |
||||
|
IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, |
||||
|
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) |
||||
|
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition, |
||||
|
timestamp, properties, modifiers) |
||||
|
{ |
||||
|
Delta = delta; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
namespace Avalonia.Input.Raw |
||||
|
{ |
||||
|
public class RawPointerGestureEventArgs : RawPointerEventArgs |
||||
|
{ |
||||
|
public RawPointerGestureEventArgs( |
||||
|
IInputDevice device, |
||||
|
ulong timestamp, |
||||
|
IInputRoot root, |
||||
|
RawPointerEventType gestureType, |
||||
|
Point position, |
||||
|
Vector delta, RawInputModifiers inputModifiers) |
||||
|
: base(device, timestamp, root, gestureType, position, inputModifiers) |
||||
|
{ |
||||
|
Delta = delta; |
||||
|
} |
||||
|
|
||||
|
public Vector Delta { get; private set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace Avalonia.LinuxFramebuffer.Input; |
||||
|
|
||||
|
internal class RawEventGroupingThreadingHelper : IDisposable |
||||
|
{ |
||||
|
private readonly RawEventGrouper _grouper; |
||||
|
private readonly Queue<RawInputEventArgs> _rawQueue = new(); |
||||
|
private readonly Action _queueHandler; |
||||
|
|
||||
|
public RawEventGroupingThreadingHelper(Action<RawInputEventArgs> eventCallback) |
||||
|
{ |
||||
|
_grouper = new RawEventGrouper(eventCallback); |
||||
|
_queueHandler = QueueHandler; |
||||
|
} |
||||
|
|
||||
|
private void QueueHandler() |
||||
|
{ |
||||
|
lock (_rawQueue) |
||||
|
{ |
||||
|
while (_rawQueue.Count > 0) |
||||
|
_grouper.HandleEvent(_rawQueue.Dequeue()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void OnEvent(RawInputEventArgs args) |
||||
|
{ |
||||
|
lock (_rawQueue) |
||||
|
{ |
||||
|
_rawQueue.Enqueue(args); |
||||
|
if (_rawQueue.Count == 1) |
||||
|
{ |
||||
|
Dispatcher.UIThread.Post(_queueHandler, DispatcherPriority.Input); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() => |
||||
|
Dispatcher.UIThread.Post(() => _grouper.Dispose(), DispatcherPriority.Input + 1); |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
#nullable enable |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Collections.Pooled; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Threading; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Avalonia; |
||||
|
|
||||
|
/* |
||||
|
This helper maintains an input queue for backends that handle input asynchronously. |
||||
|
While doing that it groups Move and TouchUpdate events so we could provide GetIntermediatePoints API |
||||
|
*/ |
||||
|
|
||||
|
internal class RawEventGrouper : IDisposable |
||||
|
{ |
||||
|
private readonly Action<RawInputEventArgs> _eventCallback; |
||||
|
private readonly Queue<RawInputEventArgs> _inputQueue = new(); |
||||
|
private readonly Action _dispatchFromQueue; |
||||
|
readonly Dictionary<long, RawTouchEventArgs> _lastTouchPoints = new(); |
||||
|
RawInputEventArgs? _lastEvent; |
||||
|
|
||||
|
public RawEventGrouper(Action<RawInputEventArgs> eventCallback) |
||||
|
{ |
||||
|
_eventCallback = eventCallback; |
||||
|
_dispatchFromQueue = DispatchFromQueue; |
||||
|
} |
||||
|
|
||||
|
private void AddToQueue(RawInputEventArgs args) |
||||
|
{ |
||||
|
_lastEvent = args; |
||||
|
_inputQueue.Enqueue(args); |
||||
|
if (_inputQueue.Count == 1) |
||||
|
Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input); |
||||
|
} |
||||
|
|
||||
|
private void DispatchFromQueue() |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
if(_inputQueue.Count == 0) |
||||
|
return; |
||||
|
|
||||
|
var ev = _inputQueue.Dequeue(); |
||||
|
|
||||
|
if (_lastEvent == ev) |
||||
|
_lastEvent = null; |
||||
|
|
||||
|
if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate) |
||||
|
_lastTouchPoints.Remove(touchUpdate.TouchPointId); |
||||
|
|
||||
|
_eventCallback?.Invoke(ev); |
||||
|
|
||||
|
if (ev is RawPointerEventArgs { IntermediatePoints: PooledList<Point> list }) |
||||
|
list.Dispose(); |
||||
|
|
||||
|
if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1)) |
||||
|
{ |
||||
|
Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void HandleEvent(RawInputEventArgs args) |
||||
|
{ |
||||
|
/* |
||||
|
Try to update already enqueued events if |
||||
|
1) they are still not handled (_lastEvent and _lastTouchPoints shouldn't contain said event in that case) |
||||
|
2) previous event belongs to the same "event block", events in the same block: |
||||
|
- belong from the same device |
||||
|
- are pointer move events (Move/TouchUpdate) |
||||
|
- have the same type |
||||
|
- have same modifiers |
||||
|
|
||||
|
Even if nothing is updated and the event is actually enqueued, we need to update the relevant tracking info |
||||
|
*/ |
||||
|
if ( |
||||
|
args is RawPointerEventArgs pointerEvent |
||||
|
&& _lastEvent != null |
||||
|
&& _lastEvent.Device == args.Device |
||||
|
&& _lastEvent is RawPointerEventArgs lastPointerEvent |
||||
|
&& lastPointerEvent.InputModifiers == pointerEvent.InputModifiers |
||||
|
&& lastPointerEvent.Type == pointerEvent.Type |
||||
|
&& lastPointerEvent.Type is RawPointerEventType.Move or RawPointerEventType.TouchUpdate) |
||||
|
{ |
||||
|
if (args is RawTouchEventArgs touchEvent) |
||||
|
{ |
||||
|
if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent)) |
||||
|
MergeEvents(lastTouchEvent, touchEvent); |
||||
|
else |
||||
|
{ |
||||
|
_lastTouchPoints[touchEvent.TouchPointId] = touchEvent; |
||||
|
AddToQueue(touchEvent); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
MergeEvents(lastPointerEvent, pointerEvent); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_lastTouchPoints.Clear(); |
||||
|
if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent) |
||||
|
_lastTouchPoints[touchEvent.TouchPointId] = touchEvent; |
||||
|
} |
||||
|
AddToQueue(args); |
||||
|
} |
||||
|
|
||||
|
private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current) |
||||
|
{ |
||||
|
last.IntermediatePoints ??= new PooledList<Point>(); |
||||
|
((PooledList<Point>)last.IntermediatePoints).Add(last.Position); |
||||
|
last.Position = current.Position; |
||||
|
last.Timestamp = current.Timestamp; |
||||
|
last.InputModifiers = current.InputModifiers; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_inputQueue.Clear(); |
||||
|
_lastEvent = null; |
||||
|
_lastTouchPoints.Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,56 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Shapes; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Xunit; |
||||
|
|
||||
|
#if AVALONIA_SKIA
|
||||
|
namespace Avalonia.Skia.RenderTests |
||||
|
#else
|
||||
|
namespace Avalonia.Direct2D1.RenderTests.Media |
||||
|
#endif
|
||||
|
{ |
||||
|
public class StreamGeometryTests : TestBase |
||||
|
{ |
||||
|
public StreamGeometryTests() |
||||
|
: base(@"Media\StreamGeometry") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions() |
||||
|
{ |
||||
|
var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 }; |
||||
|
foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise }) |
||||
|
foreach (var isLargeArc in new[] { false, true }) |
||||
|
foreach (var isPrecise in new[] { false, true }) |
||||
|
{ |
||||
|
Point Pt(double x, double y) => new Point(x, y); |
||||
|
Size Sz(double w, double h) => new Size(w, h); |
||||
|
var streamGeometry = new StreamGeometry(); |
||||
|
using (var context = streamGeometry.Open()) |
||||
|
{ |
||||
|
context.BeginFigure(Pt(20, 20), true); |
||||
|
|
||||
|
if(isPrecise) |
||||
|
context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); |
||||
|
else |
||||
|
context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); |
||||
|
context.LineTo(Pt(40, 20)); |
||||
|
context.LineTo(Pt(20, 20)); |
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
var pathShape = new Avalonia.Controls.Shapes.Path(); |
||||
|
pathShape.Data = streamGeometry; |
||||
|
pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue); |
||||
|
pathShape.Fill = new SolidColorBrush(Colors.Gold); |
||||
|
pathShape.StrokeThickness = 2; |
||||
|
pathShape.Margin = new Thickness(20); |
||||
|
grid.Children.Add(pathShape); |
||||
|
} |
||||
|
await RenderToFile(grid); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Fonts; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.UnitTests |
||||
|
{ |
||||
|
public class HarfBuzzFontManagerImpl : IFontManagerImpl |
||||
|
{ |
||||
|
private readonly Typeface[] _customTypefaces; |
||||
|
private readonly string _defaultFamilyName; |
||||
|
|
||||
|
private static readonly Typeface _defaultTypeface = |
||||
|
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"); |
||||
|
private static readonly Typeface _italicTypeface = |
||||
|
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans"); |
||||
|
private static readonly Typeface _emojiTypeface = |
||||
|
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji"); |
||||
|
|
||||
|
public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono") |
||||
|
{ |
||||
|
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; |
||||
|
_defaultFamilyName = defaultFamilyName; |
||||
|
} |
||||
|
|
||||
|
public string GetDefaultFontFamilyName() |
||||
|
{ |
||||
|
return _defaultFamilyName; |
||||
|
} |
||||
|
|
||||
|
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) |
||||
|
{ |
||||
|
return _customTypefaces.Select(x => x.FontFamily!.Name); |
||||
|
} |
||||
|
|
||||
|
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, |
||||
|
CultureInfo culture, out Typeface fontKey) |
||||
|
{ |
||||
|
foreach (var customTypeface in _customTypefaces) |
||||
|
{ |
||||
|
var glyphTypeface = customTypeface.GlyphTypeface; |
||||
|
|
||||
|
if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
fontKey = customTypeface; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
fontKey = default; |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) |
||||
|
{ |
||||
|
var fontFamily = typeface.FontFamily; |
||||
|
|
||||
|
if (fontFamily == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (fontFamily.IsDefault) |
||||
|
{ |
||||
|
fontFamily = _defaultTypeface.FontFamily; |
||||
|
} |
||||
|
|
||||
|
if (fontFamily!.Key == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); |
||||
|
|
||||
|
var asset = fontAssets.First(); |
||||
|
|
||||
|
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>(); |
||||
|
|
||||
|
if (assetLoader == null) |
||||
|
{ |
||||
|
throw new NotSupportedException("IAssetLoader is not registered."); |
||||
|
} |
||||
|
|
||||
|
var stream = assetLoader.Open(asset); |
||||
|
|
||||
|
return new HarfBuzzGlyphTypefaceImpl(stream); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using Avalonia.Platform; |
||||
|
using HarfBuzzSharp; |
||||
|
|
||||
|
namespace Avalonia.UnitTests |
||||
|
{ |
||||
|
public class HarfBuzzGlyphTypefaceImpl : IGlyphTypefaceImpl |
||||
|
{ |
||||
|
private bool _isDisposed; |
||||
|
private Blob _blob; |
||||
|
|
||||
|
public HarfBuzzGlyphTypefaceImpl(Stream data, bool isFakeBold = false, bool isFakeItalic = false) |
||||
|
{ |
||||
|
_blob = Blob.FromStream(data); |
||||
|
|
||||
|
Face = new Face(_blob, 0); |
||||
|
|
||||
|
Font = new Font(Face); |
||||
|
|
||||
|
Font.SetFunctionsOpenType(); |
||||
|
|
||||
|
Font.GetScale(out var scale, out _); |
||||
|
|
||||
|
DesignEmHeight = (short)scale; |
||||
|
|
||||
|
var metrics = Font.OpenTypeMetrics; |
||||
|
|
||||
|
const double defaultFontRenderingEmSize = 12.0; |
||||
|
|
||||
|
Ascent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalAscender) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
Descent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalDescender) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
LineGap = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalLineGap) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
UnderlinePosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineOffset) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
UnderlineThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineSize) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
StrikethroughPosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutOffset) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
StrikethroughThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutSize) / defaultFontRenderingEmSize * DesignEmHeight); |
||||
|
|
||||
|
IsFixedPitch = GetGlyphAdvance(GetGlyph('a')) == GetGlyphAdvance(GetGlyph('b')); |
||||
|
|
||||
|
IsFakeBold = isFakeBold; |
||||
|
|
||||
|
IsFakeItalic = isFakeItalic; |
||||
|
} |
||||
|
|
||||
|
public Face Face { get; } |
||||
|
|
||||
|
public Font Font { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public short DesignEmHeight { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int Ascent { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int Descent { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int LineGap { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int UnderlinePosition { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int UnderlineThickness { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int StrikethroughPosition { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int StrikethroughThickness { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public bool IsFixedPitch { get; } |
||||
|
|
||||
|
public bool IsFakeBold { get; } |
||||
|
|
||||
|
public bool IsFakeItalic { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public ushort GetGlyph(uint codepoint) |
||||
|
{ |
||||
|
if (Font.TryGetGlyph(codepoint, out var glyph)) |
||||
|
{ |
||||
|
return (ushort)glyph; |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) |
||||
|
{ |
||||
|
var glyphs = new ushort[codepoints.Length]; |
||||
|
|
||||
|
for (var i = 0; i < codepoints.Length; i++) |
||||
|
{ |
||||
|
if (Font.TryGetGlyph(codepoints[i], out var glyph)) |
||||
|
{ |
||||
|
glyphs[i] = (ushort)glyph; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return glyphs; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int GetGlyphAdvance(ushort glyph) |
||||
|
{ |
||||
|
return Font.GetHorizontalGlyphAdvance(glyph); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
|
||||
|
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) |
||||
|
{ |
||||
|
var glyphIndices = new uint[glyphs.Length]; |
||||
|
|
||||
|
for (var i = 0; i < glyphs.Length; i++) |
||||
|
{ |
||||
|
glyphIndices[i] = glyphs[i]; |
||||
|
} |
||||
|
|
||||
|
return Font.GetHorizontalGlyphAdvances(glyphIndices); |
||||
|
} |
||||
|
|
||||
|
private void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (_isDisposed) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_isDisposed = true; |
||||
|
|
||||
|
if (!disposing) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Font?.Dispose(); |
||||
|
Face?.Dispose(); |
||||
|
_blob?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Dispose(true); |
||||
|
GC.SuppressFinalize(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utilities; |
||||
|
using HarfBuzzSharp; |
||||
|
using Buffer = HarfBuzzSharp.Buffer; |
||||
|
|
||||
|
namespace Avalonia.UnitTests |
||||
|
{ |
||||
|
public class HarfBuzzTextShaperImpl : ITextShaperImpl |
||||
|
{ |
||||
|
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, |
||||
|
CultureInfo culture) |
||||
|
{ |
||||
|
using (var buffer = new Buffer()) |
||||
|
{ |
||||
|
FillBuffer(buffer, text); |
||||
|
|
||||
|
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); |
||||
|
|
||||
|
buffer.GuessSegmentProperties(); |
||||
|
|
||||
|
var glyphTypeface = typeface.GlyphTypeface; |
||||
|
|
||||
|
var font = ((HarfBuzzGlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; |
||||
|
|
||||
|
font.Shape(buffer); |
||||
|
|
||||
|
font.GetScale(out var scaleX, out _); |
||||
|
|
||||
|
var textScale = fontRenderingEmSize / scaleX; |
||||
|
|
||||
|
var bufferLength = buffer.Length; |
||||
|
|
||||
|
var glyphInfos = buffer.GetGlyphInfoSpan(); |
||||
|
|
||||
|
var glyphPositions = buffer.GetGlyphPositionSpan(); |
||||
|
|
||||
|
var glyphIndices = new ushort[bufferLength]; |
||||
|
|
||||
|
var clusters = new ushort[bufferLength]; |
||||
|
|
||||
|
double[] glyphAdvances = null; |
||||
|
|
||||
|
Vector[] glyphOffsets = null; |
||||
|
|
||||
|
for (var i = 0; i < bufferLength; i++) |
||||
|
{ |
||||
|
glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; |
||||
|
|
||||
|
clusters[i] = (ushort)glyphInfos[i].Cluster; |
||||
|
|
||||
|
if (!glyphTypeface.IsFixedPitch) |
||||
|
{ |
||||
|
SetAdvance(glyphPositions, i, textScale, ref glyphAdvances); |
||||
|
} |
||||
|
|
||||
|
SetOffset(glyphPositions, i, textScale, ref glyphOffsets); |
||||
|
} |
||||
|
|
||||
|
return new GlyphRun(glyphTypeface, fontRenderingEmSize, |
||||
|
new ReadOnlySlice<ushort>(glyphIndices), |
||||
|
new ReadOnlySlice<double>(glyphAdvances), |
||||
|
new ReadOnlySlice<Vector>(glyphOffsets), |
||||
|
text, |
||||
|
new ReadOnlySlice<ushort>(clusters), |
||||
|
buffer.Direction == Direction.LeftToRight ? 0 : 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> text) |
||||
|
{ |
||||
|
buffer.ContentType = ContentType.Unicode; |
||||
|
|
||||
|
var i = 0; |
||||
|
|
||||
|
while (i < text.Length) |
||||
|
{ |
||||
|
var codepoint = Codepoint.ReadAt(text, i, out var count); |
||||
|
|
||||
|
var cluster = (uint)(text.Start + i); |
||||
|
|
||||
|
if (codepoint.IsBreakChar) |
||||
|
{ |
||||
|
if (i + 1 < text.Length) |
||||
|
{ |
||||
|
var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); |
||||
|
|
||||
|
if (nextCodepoint == '\n' && codepoint == '\r') |
||||
|
{ |
||||
|
count++; |
||||
|
|
||||
|
buffer.Add('\u200C', cluster); |
||||
|
|
||||
|
buffer.Add('\u200D', cluster); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
buffer.Add('\u200C', cluster); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
buffer.Add('\u200C', cluster); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
buffer.Add(codepoint, cluster); |
||||
|
} |
||||
|
|
||||
|
i += count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale, |
||||
|
ref Vector[] offsetBuffer) |
||||
|
{ |
||||
|
var position = glyphPositions[index]; |
||||
|
|
||||
|
if (position.XOffset == 0 && position.YOffset == 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
offsetBuffer ??= new Vector[glyphPositions.Length]; |
||||
|
|
||||
|
var offsetX = position.XOffset * textScale; |
||||
|
|
||||
|
var offsetY = position.YOffset * textScale; |
||||
|
|
||||
|
offsetBuffer[index] = new Vector(offsetX, offsetY); |
||||
|
} |
||||
|
|
||||
|
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale, |
||||
|
ref double[] advanceBuffer) |
||||
|
{ |
||||
|
advanceBuffer ??= new double[glyphPositions.Length]; |
||||
|
|
||||
|
// Depends on direction of layout
|
||||
|
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
|
||||
|
advanceBuffer[index] = glyphPositions[index].XAdvance * textScale; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
Loading…
Reference in new issue