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