From 70abfca7d059d0270adaec31dde2ae761e28b340 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2017 14:52:51 +0300 Subject: [PATCH 01/19] Implemented support for running on top of fbdev and libevdev2 --- Avalonia.sln | 47 +++- .../ControlCatalog.NetCore.csproj | 1 + samples/ControlCatalog.NetCore/Program.cs | 11 +- .../Avalonia.LinuxFramebuffer.csproj | 14 + .../Avalonia.LinuxFramebuffer/EvDevDevice.cs | 89 ++++++ .../FramebufferToplevelImpl.cs | 68 +++++ .../LinuxFramebuffer.cs | 138 ++++++++++ .../LinuxFramebufferPlatform.cs | 75 ++++++ .../LockedFramebuffer.cs | 47 ++++ src/Linux/Avalonia.LinuxFramebuffer/Mice.cs | 120 +++++++++ .../NativeUnsafeMethods.cs | 254 ++++++++++++++++++ .../PlatformThreadingInterface.cs | 112 ++++++++ src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 21 ++ 13 files changed, 993 insertions(+), 4 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Mice.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs diff --git a/Avalonia.sln b/Avalonia.sln index 3fb5ec2693..cc166bc495 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 +VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -185,6 +185,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6F build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 @@ -2503,6 +2507,46 @@ Global {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Release|Mono.Build.0 = Release|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Release|x86.ActiveCfg = Release|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Release|x86.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Mono.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Mono.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhone.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Mono.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Mono.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|x86.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|x86.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhone.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Mono.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Mono.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|x86.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhone.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhone.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Mono.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Mono.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|x86.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2561,5 +2605,6 @@ Global {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F3AC8BC1-27F5-4255-9AFC-04ABFD11683A} = {74487168-7D91-487E-BF93-055F2251461E} {4D6FAF79-58B4-482F-9122-0668C346364C} = {74487168-7D91-487E-BF93-055F2251461E} + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} EndGlobalSection EndGlobal diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index d43c3a060e..e0e848b91b 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 57a508f923..f1146de013 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Avalonia; namespace ControlCatalog.NetCore @@ -7,9 +8,13 @@ namespace ControlCatalog.NetCore { static void Main(string[] args) { - AppBuilder.Configure() - .UsePlatformDetect() - .Start(); + if (args.Contains("--fbdev")) + AppBuilder.Configure() + .InitializeWithLinuxFramebuffer(tl => tl.Content = new MainView()); + else + AppBuilder.Configure() + .UsePlatformDetect() + .Start(); } } } \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj new file mode 100644 index 0000000000..3a3e08135d --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -0,0 +1,14 @@ + + + netstandard1.3 + true + + + + + + + + + + \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs new file mode 100644 index 0000000000..7b399863f7 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.LinuxFramebuffer +{ + unsafe class EvDevDevice + { + private static readonly Lazy> AllMouseDevices = new Lazy>(() + => OpenMouseDevices()); + + private static List OpenMouseDevices() + { + var rv = new List(); + foreach (var dev in Directory.GetFiles("/dev/input", "event*").Select(Open)) + { + if (!dev.IsMouse) + NativeUnsafeMethods.close(dev.Fd); + else + rv.Add(dev); + } + return rv; + } + + public static IReadOnlyList MouseDevices => AllMouseDevices.Value; + + + public int Fd { get; } + private IntPtr _dev; + public string Name { get; } + public List EventTypes { get; private set; } = new List(); + public input_absinfo? AbsX { get; } + public input_absinfo? AbsY { get; } + + public EvDevDevice(int fd, IntPtr dev) + { + Fd = fd; + _dev = dev; + Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev)); + foreach (EvType type in Enum.GetValues(typeof(EvType))) + { + if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0) + EventTypes.Add(type); + } + var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X); + if (ptr != null) + AbsX = *ptr; + ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y); + if (ptr != null) + AbsY = *ptr; + } + + public input_event? NextEvent() + { + input_event ev; + if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0) + return ev; + return null; + } + + public bool IsMouse => EventTypes.Contains(EvType.EV_REL); + + public static EvDevDevice Open(string device) + { + var fd = NativeUnsafeMethods.open(device, 2048, 0); + if (fd <= 0) + throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}"); + IntPtr dev; + var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev); + if (rc < 0) + { + NativeUnsafeMethods.close(fd); + throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}"); + } + return new EvDevDevice(fd, dev); + } + + + } + + public class EvDevAxisInfo + { + public int Minimum { get; set; } + public int Maximum { get; set; } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs new file mode 100644 index 0000000000..193d2c1d05 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.LinuxFramebuffer +{ + class FramebufferToplevelImpl : IEmbeddableWindowImpl + { + private readonly LinuxFramebuffer _fb; + private bool _renderQueued; + public IInputRoot InputRoot { get; private set; } + + public FramebufferToplevelImpl(LinuxFramebuffer fb) + { + _fb = fb; + Invalidate(default(Rect)); + var mice = new Mice(ClientSize.Width, ClientSize.Height); + mice.Start(); + mice.Event += e => Input?.Invoke(e); + } + + public void Dispose() + { + throw new NotSupportedException(); + } + + + public void Invalidate(Rect rect) + { + if(_renderQueued) + return; + _renderQueued = true; + Dispatcher.UIThread.InvokeAsync(() => + { + Paint?.Invoke(new Rect(default(Point), ClientSize)); + _renderQueued = false; + }); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + InputRoot = inputRoot; + } + + public Point PointToClient(Point point) => point; + + public Point PointToScreen(Point point) => point; + + public void SetCursor(IPlatformHandle cursor) + { + } + + public Size ClientSize => _fb.PixelSize; + public double Scaling => 1; + public IEnumerable Surfaces => new object[] {_fb}; + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } + public Action Closed { get; set; } + public event Action LostFocus; + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs new file mode 100644 index 0000000000..5aec5408a4 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable + { + private readonly Size _dpi; + private int _fd; + private fb_fix_screeninfo _fixedInfo; + private fb_var_screeninfo _varInfo; + private IntPtr _mappedLength; + private IntPtr _mappedAddress; + + public LinuxFramebuffer(string fileName = null, Size? dpi = null) + { + _dpi = dpi ?? new Size(96, 96); + fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; + _fd = NativeUnsafeMethods.open(fileName, 2, 0); + if (_fd <= 0) + throw new Exception("Error: " + Marshal.GetLastWin32Error()); + + try + { + Init(); + } + catch + { + Dispose(); + throw; + } + } + + void Init() + { + fixed (void* pnfo = &_varInfo) + { + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + + SetBpp(); + + _varInfo.yoffset = 100; + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) + _varInfo.transp = new fb_bitfield(); + + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) + throw new Exception("FBIOPUT_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + + if (_varInfo.bits_per_pixel != 32) + throw new Exception("Unable to set 32-bit display mode"); + } + fixed(void*pnfo = &_fixedInfo) + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_FSCREENINFO, pnfo)) + throw new Exception("FBIOGET_FSCREENINFO error: " + Marshal.GetLastWin32Error()); + + _mappedLength = new IntPtr(_fixedInfo.line_length * _varInfo.yres); + _mappedAddress = NativeUnsafeMethods.mmap(IntPtr.Zero, _mappedLength, 3, 1, _fd, IntPtr.Zero); + if (_mappedAddress == new IntPtr(-1)) + throw new Exception($"Unable to mmap {_mappedLength} bytes, error {Marshal.GetLastWin32Error()}"); + fixed (fb_fix_screeninfo* pnfo = &_fixedInfo) + { + int idlen; + for (idlen = 0; idlen < 16 && pnfo->id[idlen] != 0; idlen++) ; + Id = Encoding.ASCII.GetString(pnfo->id, idlen); + } + } + + void SetBpp() + { + _varInfo.bits_per_pixel = 32; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + { + length = 8 + }; + _varInfo.green.offset = 8; + _varInfo.blue.offset = 16; + _varInfo.transp.offset = 24; + } + + public string Id { get; private set; } + + public Size PixelSize + { + get + { + fb_var_screeninfo nfo; + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, &nfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + return new Size(nfo.xres, nfo.yres); + } + } + + public ILockedFramebuffer Lock() + { + if (_fd <= 0) + throw new ObjectDisposedException("LinuxFramebuffer"); + return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _dpi); + } + + + private void ReleaseUnmanagedResources() + { + if (_mappedAddress != IntPtr.Zero) + { + NativeUnsafeMethods.munmap(_mappedAddress, _mappedLength); + _mappedAddress = IntPtr.Zero; + } + if(_fd == 0) + return; + NativeUnsafeMethods.close(_fd); + _fd = 0; + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~LinuxFramebuffer() + { + ReleaseUnmanagedResources(); + } + } +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs new file mode 100644 index 0000000000..aaf8db9151 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; +using Avalonia.Controls; +using Avalonia.Controls.Embedding; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.LinuxFramebuffer; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace Avalonia.LinuxFramebuffer +{ + class LinuxFramebufferPlatform + { + LinuxFramebuffer _fb; + public static KeyboardDevice KeyboardDevice = new KeyboardDevice(); + public static MouseDevice MouseDevice = new MouseDevice(); + private static readonly Stopwatch St = Stopwatch.StartNew(); + internal static uint Timestamp => (uint)St.ElapsedTicks; + public static FramebufferToplevelImpl TopLevel; + LinuxFramebufferPlatform(string fbdev = null) + { + _fb = new LinuxFramebuffer(fbdev); + } + + + void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToTransient() + .Bind().ToConstant(KeyboardDevice) + .Bind().ToConstant(MouseDevice) + .Bind().ToSingleton() + .Bind().ToConstant(PlatformThreadingInterface.Instance) + .Bind().ToConstant(PlatformThreadingInterface.Instance); + } + + internal static TopLevel Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() + { + var platform = new LinuxFramebufferPlatform(fbdev); + builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev") + .SetupWithoutStarting(); + var tl = new EmbeddableControlRoot(TopLevel = new FramebufferToplevelImpl(platform._fb)); + tl.Prepare(); + return tl; + } + } +} + +public static class LinuxFramebufferPlatformExtensions +{ + class TokenClosable : ICloseable + { + public event EventHandler Closed; + + public TokenClosable(CancellationToken token) + { + token.Register(() => Dispatcher.UIThread.InvokeAsync(() => Closed?.Invoke(this, new EventArgs()))); + } + } + + public static void InitializeWithLinuxFramebuffer(this T builder, Action setup, + CancellationToken stop = default(CancellationToken), string fbdev = null) + where T : AppBuilderBase, new() + { + setup(LinuxFramebufferPlatform.Initialize(builder, fbdev)); + builder.BeforeStartCallback(builder); + builder.Instance.Run(new TokenClosable(stop)); + } +} + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs new file mode 100644 index 0000000000..d8330fcb70 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + unsafe class LockedFramebuffer : ILockedFramebuffer + { + private readonly int _fb; + private readonly fb_fix_screeninfo _fixedInfo; + private fb_var_screeninfo _varInfo; + private readonly IntPtr _address; + + public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Size dpi) + { + _fb = fb; + _fixedInfo = fixedInfo; + _varInfo = varInfo; + _address = address; + Dpi = dpi; + //Use double buffering to avoid flicker + Address = Marshal.AllocHGlobal(RowBytes * Height); + } + + + void VSync() + { + NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); + } + + public void Dispose() + { + VSync(); + NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Height)); + + Marshal.FreeHGlobal(Address); + Address = IntPtr.Zero; + } + + public IntPtr Address { get; private set; } + public int Width => (int)_varInfo.xres; + public int Height => (int) _varInfo.yres; + public int RowBytes => (int) _fixedInfo.line_length; + public Size Dpi { get; } + public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; + } +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs new file mode 100644 index 0000000000..d1cf9eefb6 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + public unsafe class Mice + { + private readonly double _width; + private readonly double _height; + private double _x; + private double _y; + + public event Action Event; + + public Mice(double width, double height) + { + _width = width; + _height = height; + } + + public void Start() => AvaloniaLocator.Current.GetService().PostThreadPoolItem(Worker); + + private void Worker() + { + + var mouseDevices = EvDevDevice.MouseDevices.Where(d => d.IsMouse).ToList(); + if (mouseDevices.Count == 0) + return; + var are = new AutoResetEvent(false); + while (true) + { + try + { + var rfds = new fd_set {count = mouseDevices.Count}; + for (int c = 0; c < mouseDevices.Count; c++) + rfds.fds[c] = mouseDevices[c].Fd; + IntPtr* timeval = stackalloc IntPtr[2]; + timeval[0] = new IntPtr(0); + timeval[1] = new IntPtr(100); + are.WaitOne(30); + foreach (var dev in mouseDevices) + { + while(true) + { + var ev = dev.NextEvent(); + if (!ev.HasValue) + break; + + PlatformThreadingInterface.Instance.Send(() => ProcessEvent(dev, ev.Value)); + } + } + } + catch (Exception e) + { + Console.Error.WriteLine(e.ToString()); + } + } + } + + static double TranslateAxis(input_absinfo axis, int value, double max) + { + return (value - axis.minimum) / (double) (axis.maximum - axis.minimum) * max; + } + + private void ProcessEvent(EvDevDevice device, input_event ev) + { + if (ev.type == (short)EvType.EV_REL) + { + if (ev.code == (short) AxisEventCode.REL_X) + _x = Math.Min(_width, Math.Max(0, _x + ev.value)); + else if (ev.code == (short) AxisEventCode.REL_Y) + _y = Math.Min(_height, Math.Max(0, _y + ev.value)); + else + return; + Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + LinuxFramebufferPlatform.Timestamp, + LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y), + InputModifiers.None)); + } + if (ev.type ==(int) EvType.EV_ABS) + { + if (ev.code == (short) AbsAxis.ABS_X && device.AbsX.HasValue) + _x = TranslateAxis(device.AbsX.Value, ev.value, _width); + else if (ev.code == (short) AbsAxis.ABS_Y && device.AbsY.HasValue) + _y = TranslateAxis(device.AbsY.Value, ev.value, _height); + else + return; + Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + LinuxFramebufferPlatform.Timestamp, + LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y), + InputModifiers.None)); + } + if (ev.type == (short) EvType.EV_KEY) + { + RawMouseEventType? type = null; + if (ev.code == (ushort) EvKey.BTN_LEFT) + type = ev.value == 1 ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp; + if (ev.code == (ushort)EvKey.BTN_RIGHT) + type = ev.value == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp; + if (ev.code == (ushort) EvKey.BTN_MIDDLE) + type = ev.value == 1 ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp; + if (!type.HasValue) + return; + + Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + LinuxFramebufferPlatform.Timestamp, + LinuxFramebufferPlatform.TopLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers))); + } + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs new file mode 100644 index 0000000000..ad8def369d --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -0,0 +1,254 @@ +using __u32 = System.UInt32; +using __s32 = System.Int32; +using __u16 = System.UInt16; +using System; +using System.Runtime.InteropServices; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ArrangeTypeMemberModifiers +// ReSharper disable BuiltInTypeReferenceStyle +// ReSharper disable InconsistentNaming + +namespace Avalonia.LinuxFramebuffer +{ + unsafe class NativeUnsafeMethods + { + [DllImport("libc", EntryPoint = "open", SetLastError = true)] + public static extern int open(string pathname, int flags, int mode); + + [DllImport("libc", EntryPoint = "close", SetLastError = true)] + public static extern int close(int fd); + + [DllImport("libc", EntryPoint = "ioctl", SetLastError = true)] + public static extern int ioctl(int fd, FbIoCtl code, void* arg); + + [DllImport("libc", EntryPoint = "mmap", SetLastError = true)] + public static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, + int fd, IntPtr offset); + [DllImport("libc", EntryPoint = "munmap", SetLastError = true)] + public static extern int munmap(IntPtr addr, IntPtr length); + + [DllImport("libc", EntryPoint = "memcpy", SetLastError = true)] + public static extern int memcpy(IntPtr dest, IntPtr src, IntPtr length); + + [DllImport("libc", EntryPoint = "select", SetLastError = true)] + public static extern int select(int nfds, void* rfds, void* wfds, void* exfds, IntPtr* timevals); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_new_from_fd", SetLastError = true)] + public static extern int libevdev_new_from_fd(int fd, out IntPtr dev); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_has_event_type", SetLastError = true)] + public static extern int libevdev_has_event_type(IntPtr dev, EvType type); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_next_event", SetLastError = true)] + public static extern int libevdev_next_event(IntPtr dev, int flags, out input_event ev); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_get_name", SetLastError = true)] + public static extern IntPtr libevdev_get_name(IntPtr dev); + [DllImport("libevdev.so.2", EntryPoint = "libevdev_get_abs_info", SetLastError = true)] + public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code); + } + + enum FbIoCtl : uint + { + FBIOGET_VSCREENINFO = 0x4600, + FBIOPUT_VSCREENINFO = 0x4601, + FBIOGET_FSCREENINFO = 0x4602, + FBIOGET_VBLANK = 0x80204612u, + FBIO_WAITFORVSYNC = 0x40044620, + FBIOPAN_DISPLAY = 0x4606 + } + + [Flags] + enum VBlankFlags + { + FB_VBLANK_VBLANKING = 0x001 /* currently in a vertical blank */, + FB_VBLANK_HBLANKING = 0x002 /* currently in a horizontal blank */, + FB_VBLANK_HAVE_VBLANK = 0x004 /* vertical blanks can be detected */, + FB_VBLANK_HAVE_HBLANK = 0x008 /* horizontal blanks can be detected */, + FB_VBLANK_HAVE_COUNT = 0x010 /* global retrace counter is available */, + FB_VBLANK_HAVE_VCOUNT = 0x020 /* the vcount field is valid */, + FB_VBLANK_HAVE_HCOUNT = 0x040 /* the hcount field is valid */, + FB_VBLANK_VSYNCING = 0x080 /* currently in a vsync */, + FB_VBLANK_HAVE_VSYNC = 0x100 /* verical syncs can be detected */ + } + + unsafe struct fb_vblank { + public VBlankFlags flags; /* FB_VBLANK flags */ + __u32 count; /* counter of retraces since boot */ + __u32 vcount; /* current scanline position */ + __u32 hcount; /* current scandot position */ + fixed __u32 reserved[4]; /* reserved for future compatibility */ + }; + + [StructLayout(LayoutKind.Sequential)] + unsafe struct fb_fix_screeninfo + { + public fixed byte id[16]; /* identification string eg "TT Builtin" */ + + public IntPtr smem_start; /* Start of frame buffer mem */ + + /* (physical address) */ + public __u32 smem_len; /* Length of frame buffer mem */ + + public __u32 type; /* see FB_TYPE_* */ + public __u32 type_aux; /* Interleave for interleaved Planes */ + public __u32 visual; /* see FB_VISUAL_* */ + public __u16 xpanstep; /* zero if no hardware panning */ + public __u16 ypanstep; /* zero if no hardware panning */ + public __u16 ywrapstep; /* zero if no hardware ywrap */ + public __u32 line_length; /* length of a line in bytes */ + + public IntPtr mmio_start; /* Start of Memory Mapped I/O */ + + /* (physical address) */ + public __u32 mmio_len; /* Length of Memory Mapped I/O */ + + public __u32 accel; /* Type of acceleration available */ + public fixed __u16 reserved[3]; /* Reserved for future compatibility */ + }; + + [StructLayout(LayoutKind.Sequential)] + struct fb_bitfield + { + public __u32 offset; /* beginning of bitfield */ + public __u32 length; /* length of bitfield */ + + public __u32 msb_right; /* != 0 : Most significant bit is */ + /* right */ + }; + + [StructLayout(LayoutKind.Sequential)] + unsafe struct fb_var_screeninfo + { + public __u32 xres; /* visible resolution */ + public __u32 yres; + public __u32 xres_virtual; /* virtual resolution */ + public __u32 yres_virtual; + public __u32 xoffset; /* offset from virtual to visible */ + public __u32 yoffset; /* resolution */ + + public __u32 bits_per_pixel; /* guess what */ + public __u32 grayscale; /* != 0 Graylevels instead of colors */ + + public fb_bitfield red; /* bitfield in fb mem if true color, */ + public fb_bitfield green; /* else only length is significant */ + public fb_bitfield blue; + public fb_bitfield transp; /* transparency */ + + public __u32 nonstd; /* != 0 Non standard pixel format */ + + public __u32 activate; /* see FB_ACTIVATE_* */ + + public __u32 height; /* height of picture in mm */ + public __u32 width; /* width of picture in mm */ + + public __u32 accel_flags; /* acceleration flags (hints) */ + + /* Timing: All values in pixclocks, except pixclock (of course) */ + public __u32 pixclock; /* pixel clock in ps (pico seconds) */ + + public __u32 left_margin; /* time from sync to picture */ + public __u32 right_margin; /* time from picture to sync */ + public __u32 upper_margin; /* time from sync to picture */ + public __u32 lower_margin; + public __u32 hsync_len; /* length of horizontal sync */ + public __u32 vsync_len; /* length of vertical sync */ + public __u32 sync; /* see FB_SYNC_* */ + public __u32 vmode; /* see FB_VMODE_* */ + public fixed __u32 reserved[6]; /* Reserved for future compatibility */ + }; + + + enum EvType + { + EV_SYN = 0x00, + EV_KEY = 0x01, + EV_REL = 0x02, + EV_ABS = 0x03, + EV_MSC = 0x04, + EV_SW = 0x05, + EV_LED = 0x11, + EV_SND = 0x12, + EV_REP = 0x14, + EV_FF = 0x15, + EV_PWR = 0x16, + EV_FF_STATUS = 0x17, + } + + [StructLayout(LayoutKind.Sequential)] + struct input_event + { + private IntPtr crap1, crap2; + public ushort type, code; + public int value; + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct fd_set + { + public int count; + public fixed int fds [256]; + } + + enum AxisEventCode + { + REL_X = 0x00, + REL_Y = 0x01, + REL_Z = 0x02, + REL_RX = 0x03, + REL_RY = 0x04, + REL_RZ = 0x05, + REL_HWHEEL = 0x06, + REL_DIAL = 0x07, + REL_WHEEL = 0x08, + REL_MISC = 0x09, + REL_MAX = 0x0f + } + + enum AbsAxis + { + ABS_X = 0x00, + ABS_Y = 0x01, + ABS_Z = 0x02, + ABS_RX = 0x03, + ABS_RY = 0x04, + ABS_RZ = 0x05, + ABS_THROTTLE = 0x06, + ABS_RUDDER = 0x07, + ABS_WHEEL = 0x08, + ABS_GAS = 0x09, + ABS_BRAKE = 0x0a, + ABS_HAT0X = 0x10, + ABS_HAT0Y = 0x11, + ABS_HAT1X = 0x12, + ABS_HAT1Y = 0x13, + ABS_HAT2X = 0x14, + ABS_HAT2Y = 0x15, + ABS_HAT3X = 0x16, + ABS_HAT3Y = 0x17, + ABS_PRESSURE = 0x18, + ABS_DISTANCE = 0x19, + ABS_TILT_X = 0x1a, + ABS_TILT_Y = 0x1b, + ABS_TOOL_WIDTH = 0x1c + } + + enum EvKey + { + BTN_LEFT = 0x110, + BTN_RIGHT = 0x111, + BTN_MIDDLE = 0x112 + } + + [StructLayout(LayoutKind.Sequential)] + struct input_absinfo + { + public __s32 value; + public __s32 minimum; + public __s32 maximum; + public __s32 fuzz; + public __s32 flat; + public __s32 resolution; + + } +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs b/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs new file mode 100644 index 0000000000..9231649754 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering; + +namespace Avalonia.LinuxFramebuffer +{ + class PlatformThreadingInterface : IPlatformThreadingInterface, IRenderLoop + { + public static PlatformThreadingInterface Instance { get; } = new PlatformThreadingInterface(); + + public PlatformThreadingInterface() + { + TlsCurrentThreadIsLoopThread = true; + StartTimer(new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs())); + } + + private readonly AutoResetEvent _signaled = new AutoResetEvent(false); + private readonly AutoResetEvent _queued = new AutoResetEvent(false); + + private readonly Queue _actions = new Queue(); + + public void RunLoop(CancellationToken cancellationToken) + { + var handles = new[] {_signaled, _queued}; + while (true) + { + if (0 == WaitHandle.WaitAny(handles)) + Signaled?.Invoke(); + else + { + while (true) + { + Action item; + lock(_actions) + if (_actions.Count == 0) + break; + else + item = _actions.Dequeue(); + item(); + } + } + } + } + + public void Send(Action cb) + { + lock (_actions) + { + _actions.Enqueue(cb); + _queued.Set(); + } + } + + class WatTimer : IDisposable + { + private readonly IDisposable _timer; + private GCHandle _handle; + + public WatTimer(IDisposable timer) + { + _timer = timer; + _handle = GCHandle.Alloc(_timer); + } + public void Dispose() + { + _handle.Free(); + _timer.Dispose(); + } + } + + public IDisposable StartTimer(TimeSpan interval, Action tick) + { + return new WatTimer(new System.Threading.Timer(delegate + { + var tcs = new TaskCompletionSource(); + Send(() => + { + try + { + tick(); + } + finally + { + tcs.SetResult(0); + } + }); + + + tcs.Task.Wait(); + }, null, TimeSpan.Zero, interval)); + + + } + + public void Signal() + { + _signaled.Set(); + } + + [ThreadStatic] private static bool TlsCurrentThreadIsLoopThread; + + public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; + public event Action Signaled; + public event EventHandler Tick; + + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs new file mode 100644 index 0000000000..67d62e3410 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Input; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + internal class CursorFactoryStub : IStandardCursorFactory + { + public IPlatformHandle GetCursor(StandardCursorType cursorType) + { + return new PlatformHandle(IntPtr.Zero, null); + } + } + internal class PlatformSettings : IPlatformSettings + { + public Size DoubleClickSize { get; } = new Size(4, 4); + public TimeSpan DoubleClickTime { get; } = new TimeSpan(0, 0, 0, 0, 500); + } +} From fdcc4be654d6e9b24bb13efb885a29c1492ebd60 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2017 15:08:14 +0300 Subject: [PATCH 02/19] Handle console input in fbdev mode --- samples/ControlCatalog.NetCore/Program.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index f1146de013..ac7429cfd4 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -8,13 +8,22 @@ namespace ControlCatalog.NetCore { static void Main(string[] args) { - if (args.Contains("--fbdev")) - AppBuilder.Configure() - .InitializeWithLinuxFramebuffer(tl => tl.Content = new MainView()); + if (args.Contains("--fbdev")) AppBuilder.Configure().InitializeWithLinuxFramebuffer(tl => + { + tl.Content = new MainView(); + System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); + }); else AppBuilder.Configure() .UsePlatformDetect() .Start(); } + + static void ConsoleSilencer() + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(); + } } } \ No newline at end of file From 93b96038c3a455e42e7c635055f813feb5374f8e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2017 18:01:43 +0300 Subject: [PATCH 03/19] intercept --- samples/ControlCatalog.NetCore/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index ac7429cfd4..7c67a239e6 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -23,7 +23,7 @@ namespace ControlCatalog.NetCore { Console.CursorVisible = false; while (true) - Console.ReadKey(); + Console.ReadKey(true); } } } \ No newline at end of file From 5be8f17eb1e62b6845d992813054b60fab5db458 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 1 Apr 2017 19:06:01 +0200 Subject: [PATCH 04/19] Make all mutable brushes implement IMutableBrush. --- src/Avalonia.Visuals/Media/ImageBrush.cs | 12 +++++++++--- src/Avalonia.Visuals/Media/LinearGradientBrush.cs | 8 +++++++- src/Avalonia.Visuals/Media/RadialGradientBrush.cs | 8 +++++++- src/Avalonia.Visuals/Media/VisualBrush.cs | 8 +++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ImageBrush.cs b/src/Avalonia.Visuals/Media/ImageBrush.cs index 85c1f6c71a..69b98fd35c 100644 --- a/src/Avalonia.Visuals/Media/ImageBrush.cs +++ b/src/Avalonia.Visuals/Media/ImageBrush.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// /// Paints an area with an . /// - public class ImageBrush : TileBrush, IImageBrush + public class ImageBrush : TileBrush, IImageBrush, IMutableBrush { /// /// Defines the property. @@ -17,14 +17,14 @@ namespace Avalonia.Media AvaloniaProperty.Register("Source"); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public ImageBrush() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The image to draw. public ImageBrush(IBitmap source) @@ -40,5 +40,11 @@ namespace Avalonia.Media get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } + + /// + IBrush IMutableBrush.ToImmutable() + { + return new Immutable.ImmutableImageBrush(this); + } } } diff --git a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs b/src/Avalonia.Visuals/Media/LinearGradientBrush.cs index 33eea4fbad..6c0d500343 100644 --- a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/LinearGradientBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// A brush that draws with a linear gradient. /// - public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush + public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush, IMutableBrush { /// /// Defines the property. @@ -41,5 +41,11 @@ namespace Avalonia.Media get { return GetValue(EndPointProperty); } set { SetValue(EndPointProperty, value); } } + + /// + IBrush IMutableBrush.ToImmutable() + { + return new Immutable.ImmutableLinearGradientBrush(this); + } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs index b721e19138..9f9c2733ec 100644 --- a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// Paints an area with a radial gradient. A focal point defines the beginning of the gradient, /// and a circle defines the end point of the gradient. /// - public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush + public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush, IMutableBrush { /// /// Defines the property. @@ -61,5 +61,11 @@ namespace Avalonia.Media get { return GetValue(RadiusProperty); } set { SetValue(RadiusProperty, value); } } + + /// + IBrush IMutableBrush.ToImmutable() + { + return new Immutable.ImmutableRadialGradientBrush(this); + } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/VisualBrush.cs b/src/Avalonia.Visuals/Media/VisualBrush.cs index 85ccf7afc9..a6d2b8ae8f 100644 --- a/src/Avalonia.Visuals/Media/VisualBrush.cs +++ b/src/Avalonia.Visuals/Media/VisualBrush.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush + public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush { /// /// Defines the property. @@ -40,5 +40,11 @@ namespace Avalonia.Media get { return GetValue(VisualProperty); } set { SetValue(VisualProperty, value); } } + + /// + IBrush IMutableBrush.ToImmutable() + { + return new Immutable.ImmutableVisualBrush(this); + } } } From f73a5605598fdc247f396ae78d75086854a46f14 Mon Sep 17 00:00:00 2001 From: Darnell Williams Date: Sat, 1 Apr 2017 19:35:38 -0400 Subject: [PATCH 05/19] Corrects gitter.im badges Url was missing it's encoding. --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0cc2b05e9d..0e54aae0bc 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ | Gitter Chat | Windows Build Status | Linux/Mac Build Status | Code Coverage | |---|---|---|---| -| [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | [![codecov](https://codecov.io/gh/AvaloniaUI/Avalonia/branch/master/graph/badge.svg)](https://codecov.io/gh/AvaloniaUI/Avalonia) | +| [![Gitter](https://badges.gitter.im/Join%20Chat.svg))](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | [![codecov](https://codecov.io/gh/AvaloniaUI/Avalonia/branch/master/graph/badge.svg)](https://codecov.io/gh/AvaloniaUI/Avalonia) | A multi-platform .NET UI framework. It can run on Windows, Linux, Mac OS X, iOS and Android. From 7966e8c0c738c077730a585ff652eb4b12edd0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 2 Apr 2017 13:06:50 +0200 Subject: [PATCH 06/19] Added Avalonia.LinuxFramebuffer NuGet package --- packages.cake | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages.cake b/packages.cake index c3d0eb0bab..b4b08f886f 100644 --- a/packages.cake +++ b/packages.cake @@ -460,6 +460,29 @@ public class Packages }, BasePath = context.Directory("./"), OutputDirectory = parameters.NugetRoot + }, + /////////////////////////////////////////////////////////////////////////////// + // Avalonia.LinuxFramebuffer + /////////////////////////////////////////////////////////////////////////////// + new NuGetPackSettings() + { + Id = "Avalonia.LinuxFramebuffer", + Dependencies = new [] + { + new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version }, + new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", Version = parameters.Version }, + //netstandard1.3 + new NuSpecDependency() { Id = "Avalonia", TargetFramework = "netstandard1.3", Version = parameters.Version }, + new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", TargetFramework = "netstandard1.3", Version = parameters.Version }, + new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netstandard1.3", Version = "1.6.0" }, + new NuSpecDependency() { Id = "Microsoft.NETCore.Portable.Compatibility", TargetFramework = "netstandard1.3", Version = "1.0.1" } + }, + Files = new [] + { + new NuSpecContent { Source = "Avalonia.LinuxFramebuffer/bin/" + parameters.DirSuffix + "/netstandard1.3/Avalonia.LinuxFramebuffer.dll", Target = "lib/netstandard1.3" } + }, + BasePath = context.Directory("./src/Linux/"), + OutputDirectory = parameters.NugetRoot } }; From bd2b35cc42f9eb7db30d2ecc7a9d9c0a40aee70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 2 Apr 2017 20:28:37 +0200 Subject: [PATCH 07/19] Remove unnecessary package dependencies --- packages.cake | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages.cake b/packages.cake index b4b08f886f..eb7defce7f 100644 --- a/packages.cake +++ b/packages.cake @@ -198,7 +198,6 @@ public class Packages new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp1.0", Version = "4.3.0" }, new NuSpecDependency() { Id = "Microsoft.Extensions.DependencyModel", TargetFramework = "netcoreapp1.0", Version = "1.1.0" }, new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netcoreapp1.0", Version = "1.6.0" }, - new NuSpecDependency() { Id = "Microsoft.NETCore.Portable.Compatibility", TargetFramework = "netcoreapp1.0", Version = "1.0.1" }, new NuSpecDependency() { Id = "Splat", TargetFramework = "netcoreapp1.0", Version = SplatVersion }, new NuSpecDependency() { Id = "Serilog", TargetFramework = "netcoreapp1.0", Version = SerilogVersion }, new NuSpecDependency() { Id = "Sprache", TargetFramework = "netcoreapp1.0", Version = SpracheVersion }, @@ -429,8 +428,7 @@ public class Packages //netstandard1.3 new NuSpecDependency() { Id = "Avalonia", TargetFramework = "netstandard1.3", Version = parameters.Version }, new NuSpecDependency() { Id = "SkiaSharp", TargetFramework = "netstandard1.3", Version = SkiaSharpVersion }, - new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netstandard1.3", Version = "1.6.0" }, - new NuSpecDependency() { Id = "Microsoft.NETCore.Portable.Compatibility", TargetFramework = "netstandard1.3", Version = "1.0.1" } + new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netstandard1.3", Version = "1.6.0" } }, Files = new [] { @@ -473,9 +471,7 @@ public class Packages new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", Version = parameters.Version }, //netstandard1.3 new NuSpecDependency() { Id = "Avalonia", TargetFramework = "netstandard1.3", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", TargetFramework = "netstandard1.3", Version = parameters.Version }, - new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netstandard1.3", Version = "1.6.0" }, - new NuSpecDependency() { Id = "Microsoft.NETCore.Portable.Compatibility", TargetFramework = "netstandard1.3", Version = "1.0.1" } + new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", TargetFramework = "netstandard1.3", Version = parameters.Version } }, Files = new [] { From cc4150db28943e505d3e8125d73cfad21e3fcc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 3 Apr 2017 00:21:50 +0200 Subject: [PATCH 08/19] Removed duplicate dependencies --- packages.cake | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages.cake b/packages.cake index eb7defce7f..f030c73a06 100644 --- a/packages.cake +++ b/packages.cake @@ -468,10 +468,7 @@ public class Packages Dependencies = new [] { new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", Version = parameters.Version }, - //netstandard1.3 - new NuSpecDependency() { Id = "Avalonia", TargetFramework = "netstandard1.3", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", TargetFramework = "netstandard1.3", Version = parameters.Version } + new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", Version = parameters.Version } }, Files = new [] { From a26c261aadbb8d13cb55c90a8adfc1ef033b38c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 4 Apr 2017 20:04:27 +0200 Subject: [PATCH 09/19] Fixed OnPointerReleased parameter type --- src/Avalonia.Controls/Button.cs | 2 +- src/Avalonia.Controls/Primitives/Thumb.cs | 2 +- src/Avalonia.Controls/TextBox.cs | 2 +- src/Avalonia.HtmlRenderer/HtmlControl.cs | 2 +- src/Avalonia.Input/InputElement.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8f617466ed..24daa545ba 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -226,7 +226,7 @@ namespace Avalonia.Controls } /// - protected override void OnPointerReleased(PointerEventArgs e) + protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 1fef48f55b..065b1aedbe 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(ev); } - protected override void OnPointerReleased(PointerEventArgs e) + protected override void OnPointerReleased(PointerReleasedEventArgs e) { if (_lastPoint.HasValue) { diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 719e888dd4..9748e5e772 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -522,7 +522,7 @@ namespace Avalonia.Controls } } - protected override void OnPointerReleased(PointerEventArgs e) + protected override void OnPointerReleased(PointerReleasedEventArgs e) { if (_presenter != null && e.Device.Captured == _presenter) { diff --git a/src/Avalonia.HtmlRenderer/HtmlControl.cs b/src/Avalonia.HtmlRenderer/HtmlControl.cs index ca8dcec2e1..88a6e5fda4 100644 --- a/src/Avalonia.HtmlRenderer/HtmlControl.cs +++ b/src/Avalonia.HtmlRenderer/HtmlControl.cs @@ -413,7 +413,7 @@ namespace Avalonia.Controls.Html /// /// Handle mouse up to handle selection and link click. /// - protected override void OnPointerReleased(PointerEventArgs e) + protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); LeftMouseButton = false; diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 652b58f0b5..6385f7197b 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -473,7 +473,7 @@ namespace Avalonia.Input /// Called before the event occurs. /// /// The event args. - protected virtual void OnPointerReleased(PointerEventArgs e) + protected virtual void OnPointerReleased(PointerReleasedEventArgs e) { } From 0e6f48e8a0ee258b7fc1fd8c753f4888283b811a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 4 Apr 2017 22:35:15 +0200 Subject: [PATCH 10/19] Use different FromEventPattern method Fixes #946 --- src/Avalonia.Controls/Window.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d7a7a9f0eb..75587dcaec 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -281,7 +281,9 @@ namespace Avalonia.Controls var modal = PlatformImpl.ShowDialog(); var result = new TaskCompletionSource(); - Observable.FromEventPattern(this, nameof(Closed)) + Observable.FromEventPattern( + x => this.Closed += x, + x => this.Closed -= x) .Take(1) .Subscribe(_ => { From e6cb529878d3646a80ae19bc81bf7ea34b16c585 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 4 Apr 2017 23:36:11 +0200 Subject: [PATCH 11/19] Ported immutable media impls from scenegraph. The deferred renderer requires immutable objects for drawing as it renders on a separate thread. --- .../Presenters/TextPresenter.cs | 27 ++- .../Primitives/AccessText.cs | 17 +- src/Avalonia.Controls/TextBlock.cs | 21 +-- .../Adapters/GraphicsAdapter.cs | 6 +- src/Avalonia.Visuals/Media/FormattedText.cs | 176 ++++++++---------- .../Media/FormattedTextStyleSpan.cs | 21 +++ src/Avalonia.Visuals/Media/Geometry.cs | 24 ++- src/Avalonia.Visuals/Media/Typeface.cs | 35 ++++ .../Platform/IFormattedTextImpl.cs | 25 +-- .../Platform/IGeometryImpl.cs | 30 ++- .../Platform/IPlatformRenderInterface.cs | 16 +- .../Rendering/RendererMixin.cs | 15 +- src/Gtk/Avalonia.Cairo/CairoPlatform.cs | 18 +- .../Avalonia.Cairo/Media/FormattedTextImpl.cs | 85 ++++----- .../Media/StreamGeometryContextImpl.cs | 9 + .../Media/StreamGeometryImpl.cs | 32 ++-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 87 ++++----- .../Avalonia.Skia/PlatformRenderInterface.cs | 11 +- src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 63 ++++--- .../Avalonia.Direct2D1.csproj | 1 + .../Avalonia.Direct2D1/Direct2D1Platform.cs | 17 +- .../Media/DrawingContextImpl.cs | 2 +- .../Media/FormattedTextImpl.cs | 73 ++++---- .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 99 ++++------ .../Media/StreamGeometryImpl.cs | 36 ++-- .../Media/TransformedGeometryImpl.cs | 24 +++ .../InputElement_HitTesting.cs | 17 +- .../FullLayoutTests.cs | 18 +- .../Media/FormattedTextImplTests.cs | 136 ++++++-------- tests/Avalonia.UnitTests/TestServices.cs | 10 +- .../Media/FormattedTextTests.cs | 28 --- .../Media/TypefaceTests.cs | 21 +++ .../VisualTree/MockRenderInterface.cs | 24 ++- 34 files changed, 656 insertions(+), 570 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs create mode 100644 src/Avalonia.Visuals/Media/Typeface.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs delete mode 100644 tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 39759f78f1..a2b5f3f8b4 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -127,11 +127,11 @@ namespace Avalonia.Controls.Presenters base.Render(context); if (selectionStart == selectionEnd) - { + { var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; var caretBrush = Brushes.Black; - if(backgroundColor.HasValue) + if (backgroundColor.HasValue) { byte red = (byte)~(backgroundColor.Value.R); byte green = (byte)~(backgroundColor.Value.G); @@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); } - + if (_caretBlink) { var charPos = FormattedText.HitTestTextPosition(CaretIndex); @@ -212,7 +212,10 @@ namespace Avalonia.Controls.Presenters if (length > 0) { - result.SetForegroundBrush(Brushes.White, start, length); + result.Spans = new[] + { + new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White), + }; } return result; @@ -228,17 +231,13 @@ namespace Avalonia.Controls.Presenters } else { - // TODO: Pretty sure that measuring "X" isn't the right way to do this... - using (var formattedText = new FormattedText( - "X", - FontFamily, - FontSize, - FontStyle, - TextAlignment, - FontWeight)) + return new FormattedText { - return formattedText.Measure(); - } + Text = "X", + Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + TextAlignment = TextAlignment, + Constraint = availableSize, + }.Measure(); } } diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 42a6f9123b..4bb80e6d3f 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -85,15 +85,14 @@ namespace Avalonia.Controls.Primitives /// A object. protected override FormattedText CreateFormattedText(Size constraint) { - var result = new FormattedText( - StripAccessKey(Text), - FontFamily, - FontSize, - FontStyle, - TextAlignment, - FontWeight); - result.Constraint = constraint; - return result; + return new FormattedText + { + Constraint = constraint, + Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Text = StripAccessKey(Text), + TextAlignment = TextAlignment, + Wrapping = TextWrapping, + }; } /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 028df729a5..c7a77bdf0e 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -116,7 +116,7 @@ namespace Avalonia.Controls this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), - this.GetObservable(FontWeightProperty).Select(_=>Unit.Default)) + this.GetObservable(FontWeightProperty).Select(_ => Unit.Default)) .Subscribe(_ => { InvalidateFormattedText(); @@ -350,16 +350,14 @@ namespace Avalonia.Controls /// A object. protected virtual FormattedText CreateFormattedText(Size constraint) { - var result = new FormattedText( - Text ?? string.Empty, - FontFamily, - FontSize, - FontStyle, - TextAlignment, - FontWeight, - TextWrapping); - result.Constraint = constraint; - return result; + return new FormattedText + { + Constraint = constraint, + Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Text = Text ?? string.Empty, + TextAlignment = TextAlignment, + Wrapping = TextWrapping, + }; } /// @@ -370,7 +368,6 @@ namespace Avalonia.Controls if (_formattedText != null) { _constraint = _formattedText.Constraint; - _formattedText.Dispose(); _formattedText = null; } diff --git a/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs b/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs index c9dd2a786c..93f8790741 100644 --- a/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs +++ b/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs @@ -117,7 +117,11 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters FormattedText GetText(string str, RFont font) { var f = ((FontAdapter)font); - return new FormattedText(str, f.Name, font.Size, f.FontStyle, TextAlignment.Left, f.Weight); + return new FormattedText + { + Text = str, + Typeface = new Typeface(f.Name, font.Size, f.FontStyle, f.Weight), + }; } public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index f6aeaf5f5f..9a052f4eb9 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -10,63 +10,32 @@ namespace Avalonia.Media /// /// Represents a piece of text with formatting. /// - public class FormattedText : AvaloniaDisposable + public class FormattedText { + private readonly IPlatformRenderInterface _platform; + private Size _constraint = Size.Infinity; + private IFormattedTextImpl _platformImpl; + private IReadOnlyList _spans; + private Typeface _typeface; + private string _text; + private TextAlignment _textAlignment; + private TextWrapping _wrapping; + /// /// Initializes a new instance of the class. /// - /// The text. - /// The font family. - /// The font size. - /// The font style. - /// The text alignment. - /// The font weight. - /// The text wrapping mode. - public FormattedText( - string text, - string fontFamilyName, - double fontSize, - FontStyle fontStyle = FontStyle.Normal, - TextAlignment textAlignment = TextAlignment.Left, - FontWeight fontWeight = FontWeight.Normal, - TextWrapping wrapping = TextWrapping.Wrap) + public FormattedText() { - Contract.Requires(text != null); - Contract.Requires(fontFamilyName != null); - - if (fontSize <= 0) - { - throw new ArgumentException("FontSize must be greater than 0"); - } - - if (fontWeight <= 0) - { - throw new ArgumentException("FontWeight must be greater than 0"); - } - - Text = text; - FontFamilyName = fontFamilyName; - FontSize = fontSize; - FontStyle = fontStyle; - FontWeight = fontWeight; - TextAlignment = textAlignment; - Wrapping = wrapping; - - var platform = AvaloniaLocator.Current.GetService(); - - if (platform == null) - { - throw new Exception("Could not create FormattedText: IPlatformRenderInterface not registered."); - } + _platform = AvaloniaLocator.Current.GetService(); + } - PlatformImpl = platform.CreateFormattedText( - text, - fontFamilyName, - fontSize, - fontStyle, - textAlignment, - fontWeight, - wrapping); + /// + /// Initializes a new instance of the class. + /// + /// The platform render interface. + public FormattedText(IPlatformRenderInterface platform) + { + _platform = platform; } /// @@ -74,64 +43,76 @@ namespace Avalonia.Media /// public Size Constraint { - get - { - CheckDisposed(); - return PlatformImpl.Constraint; - } - set - { - CheckDisposed(); - PlatformImpl.Constraint = value; - } + get => _constraint; + set => Set(ref _constraint, value); } /// - /// Gets the font family. + /// Gets or sets the base typeface. /// - public string FontFamilyName { get; } + public Typeface Typeface + { + get => _typeface; + set => Set(ref _typeface, value); + } /// - /// Gets the font size. + /// Gets or sets a collection of spans that describe the formatting of subsections of the + /// text. /// - public double FontSize { get; } + public IReadOnlyList Spans + { + get => _spans; + set => Set(ref _spans, value); + } /// - /// Gets the font style. + /// Gets or sets the text. /// - public FontStyle FontStyle { get; } + public string Text + { + get => _text; + set => Set(ref _text, value); + } /// - /// Gets the font weight. + /// Gets or sets the aligment of the text. /// - public FontWeight FontWeight { get; } + public TextAlignment TextAlignment + { + get => _textAlignment; + set => Set(ref _textAlignment, value); + } /// - /// Gets the text. + /// Gets or sets the text wrapping. /// - public string Text { get; } + public TextWrapping Wrapping + { + get => _wrapping; + set => Set(ref _wrapping, value); + } /// /// Gets platform-specific platform implementation. /// - public IFormattedTextImpl PlatformImpl { get; } - - /// - /// Gets the text alignment. - /// - public TextAlignment TextAlignment { get; } - - /// - /// Gets the text wrapping. - /// - public TextWrapping Wrapping { get; } - - /// - /// Disposes of unmanaged resources associated with the formatted text. - /// - protected override void DoDispose() + public IFormattedTextImpl PlatformImpl { - PlatformImpl.Dispose(); + get + { + if (_platformImpl == null) + { + _platformImpl = _platform.CreateFormattedText( + _text, + _typeface, + _textAlignment, + _wrapping, + _constraint, + _spans); + } + + return _platformImpl; + } } /// @@ -142,7 +123,6 @@ namespace Avalonia.Media /// public IEnumerable GetLines() { - CheckDisposed(); return PlatformImpl.GetLines(); } @@ -155,7 +135,6 @@ namespace Avalonia.Media /// public TextHitTestResult HitTestPoint(Point point) { - CheckDisposed(); return PlatformImpl.HitTestPoint(point); } @@ -166,7 +145,6 @@ namespace Avalonia.Media /// The character bounds. public Rect HitTestTextPosition(int index) { - CheckDisposed(); return PlatformImpl.HitTestTextPosition(index); } @@ -178,7 +156,6 @@ namespace Avalonia.Media /// The character bounds. public IEnumerable HitTestTextRange(int index, int length) { - CheckDisposed(); return PlatformImpl.HitTestTextRange(index, length); } @@ -188,20 +165,13 @@ namespace Avalonia.Media /// The bounds box of the text. public Size Measure() { - CheckDisposed(); - return PlatformImpl.Measure(); + return PlatformImpl.Size; } - /// - /// Sets the foreground brush for the specified text range. - /// - /// The brush. - /// The start of the text range. - /// The length of the text range. - public void SetForegroundBrush(IBrush brush, int startIndex, int length) + private void Set(ref T field, T value) { - CheckDisposed(); - PlatformImpl.SetForegroundBrush(brush, startIndex, length); + field = value; + _platformImpl = null; } } } diff --git a/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs new file mode 100644 index 0000000000..75bf76c996 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs @@ -0,0 +1,21 @@ +using System; + +namespace Avalonia.Media +{ + public class FormattedTextStyleSpan + { + public FormattedTextStyleSpan( + int startIndex, + int length, + IBrush foregroundBrush = null) + { + StartIndex = startIndex; + Length = length; + ForegroundBrush = foregroundBrush; + } + + public int StartIndex { get; } + public int Length { get; } + public IBrush ForegroundBrush { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index 092aea0c61..591c7d0468 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -22,10 +22,7 @@ namespace Avalonia.Media /// static Geometry() { - TransformProperty.Changed.Subscribe(x => - { - ((Geometry)x.Sender).PlatformImpl.Transform = ((Transform)x.NewValue).Value; - }); + TransformProperty.Changed.AddClassHandler(x => x.TransformChanged); } /// @@ -68,7 +65,7 @@ namespace Avalonia.Media } /// - /// Indicates whether the geometry contains the specified point. + /// Indicates whether the geometry's fill contains the specified point. /// /// The point. /// true if the geometry contains the point; otherwise, false. @@ -76,5 +73,22 @@ namespace Avalonia.Media { return PlatformImpl.FillContains(point); } + + /// + /// Indicates whether the geometry's stroke contains the specified point. + /// + /// The pen to use. + /// The point. + /// true if the geometry contains the point; otherwise, false. + public bool StrokeContains(Pen pen, Point point) + { + return PlatformImpl.StrokeContains(pen, point); + } + + private void TransformChanged(AvaloniaPropertyChangedEventArgs e) + { + var transform = (Transform)e.NewValue; + PlatformImpl = PlatformImpl.WithTransform(transform.Value); + } } } diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs new file mode 100644 index 0000000000..b05d5d02e5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -0,0 +1,35 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Media +{ + public class Typeface + { + public Typeface( + string fontFamilyName, + double fontSize, + FontStyle style = FontStyle.Normal, + FontWeight weight = FontWeight.Normal) + { + if (fontSize <= 0) + { + throw new ArgumentException("Font size must be > 0."); + } + + if (weight <= 0) + { + throw new ArgumentException("Font weight must be > 0."); + } + + FontFamilyName = fontFamilyName; + FontSize = fontSize; + Style = style; + Weight = weight; + } + + public string FontFamilyName { get; } + public double FontSize { get; } + public FontStyle Style { get; } + public FontWeight Weight { get; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs index e3a44c437e..606da02c49 100644 --- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs @@ -10,12 +10,17 @@ namespace Avalonia.Platform /// /// Defines the platform-specific interface for . /// - public interface IFormattedTextImpl : IDisposable + public interface IFormattedTextImpl { /// - /// Gets or sets the constraint of the text. + /// Gets the constraint of the text. /// - Size Constraint { get; set; } + Size Constraint { get; } + + /// + /// The measured size of the text. + /// + Size Size { get; } /// /// Gets the text. @@ -53,19 +58,5 @@ namespace Avalonia.Platform /// The number of characters in the text range. /// The character bounds. IEnumerable HitTestTextRange(int index, int length); - - /// - /// Gets the size of the text, taking into account. - /// - /// The bounds box of the text. - Size Measure(); - - /// - /// Sets the foreground brush for the specified text range. - /// - /// The brush. - /// The start of the text range. - /// The length of the text range. - void SetForegroundBrush(IBrush brush, int startIndex, int length); } } diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index e18a76d739..132e00e56b 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Media; + namespace Avalonia.Platform { /// @@ -14,9 +16,9 @@ namespace Avalonia.Platform Rect Bounds { get; } /// - /// Gets or sets a transform to apply to the geometry. + /// Gets the transform to applied to the geometry. /// - Matrix Transform { get; set; } + Matrix Transform { get; } /// /// Gets the geometry's bounding rectangle with the specified stroke thickness. @@ -26,10 +28,32 @@ namespace Avalonia.Platform Rect GetRenderBounds(double strokeThickness); /// - /// Indicates whether the geometry contains the specified point. + /// Indicates whether the geometry's fill contains the specified point. /// /// The point. /// true if the geometry contains the point; otherwise, false. bool FillContains(Point point); + + /// + /// Intersects the geometry with another geometry. + /// + /// The other geometry. + /// A new representing the intersection. + IGeometryImpl Intersect(IGeometryImpl geometry); + + /// + /// Indicates whether the geometry's stroke contains the specified point. + /// + /// The stroke to use. + /// The point. + /// true if the geometry contains the point; otherwise, false. + bool StrokeContains(Pen pen, Point point); + + /// + /// Makes a clone of the geometry with the specified transform. + /// + /// The transform. + /// The cloned geometry. + IGeometryImpl WithTransform(Matrix transform); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index ef58d52b4f..ba7b758484 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -17,21 +17,19 @@ namespace Avalonia.Platform /// Creates a formatted text implementation. /// /// The text. - /// The font family. - /// The font size. - /// The font style. + /// The base typeface. /// The text alignment. - /// The font weight. /// The text wrapping mode. + /// The text layout constraints. + /// The style spans. /// An . IFormattedTextImpl CreateFormattedText( string text, - string fontFamilyName, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, - TextWrapping wrapping); + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans); /// /// Creates a stream geometry implementation. diff --git a/src/Avalonia.Visuals/Rendering/RendererMixin.cs b/src/Avalonia.Visuals/Rendering/RendererMixin.cs index 3d13501edd..a80f89930b 100644 --- a/src/Avalonia.Visuals/Rendering/RendererMixin.cs +++ b/src/Avalonia.Visuals/Rendering/RendererMixin.cs @@ -58,16 +58,13 @@ namespace Avalonia.Rendering s_lastMeasure = now; } var pt = new Point(40, 40); - using ( - var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18, - FontStyle.Normal, - TextAlignment.Left, - FontWeight.Normal, - TextWrapping.NoWrap)) + var txt = new FormattedText { - ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); - ctx.DrawText(Brushes.Black, pt, txt); - } + Text = "Frame #" + s_frameNum + " FPS: " + s_fps, + Typeface = new Typeface("Arial", 18) + }; + ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); + ctx.DrawText(Brushes.Black, pt, txt); } } } diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index 9cf16312e9..dde92d5870 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -42,14 +42,20 @@ namespace Avalonia.Cairo public IFormattedTextImpl CreateFormattedText( string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - Avalonia.Media.FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { - return new FormattedTextImpl(s_pangoContext, text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight); + return new FormattedTextImpl( + s_pangoContext, + text, + typeface, + textAlignment, + wrapping, + constraint, + spans); } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) diff --git a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs index 6c023c212e..b8e4d1efec 100644 --- a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Cairo.Media { public class FormattedTextImpl : IFormattedTextImpl { - private Size _size; + private Size _constraint; static double CorrectScale(double input) { @@ -22,54 +22,64 @@ namespace Avalonia.Cairo.Media public FormattedTextImpl( Pango.Context context, string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { Contract.Requires(context != null); Contract.Requires(text != null); + Layout = new Pango.Layout(context); Layout.SetText(text); + Layout.FontDescription = new Pango.FontDescription { - Family = fontFamily, - Size = Pango.Units.FromDouble(CorrectScale(fontSize)), - Style = (Pango.Style)fontStyle, - Weight = fontWeight.ToCairo() + Family = typeface?.FontFamilyName ?? "monospace", + Size = Pango.Units.FromDouble(CorrectScale(typeface?.FontSize ?? 12)), + Style = (Pango.Style)(typeface?.Style ?? FontStyle.Normal), + Weight = (typeface?.Weight ?? FontWeight.Normal).ToCairo(), }; Layout.Alignment = textAlignment.ToCairo(); Layout.Attributes = new Pango.AttrList(); - } + Layout.Width = double.IsPositiveInfinity(constraint.Width) ? -1 : Pango.Units.FromDouble(constraint.Width); - public string Text => Layout.Text; - - public Size Constraint - { - get + if (spans != null) { - return _size; + foreach (var span in spans) + { + if (span.ForegroundBrush is SolidColorBrush scb) + { + var color = new Pango.Color(); + color.Parse(string.Format("#{0}", scb.Color.ToString().Substring(3))); + + var brushAttr = new Pango.AttrForeground(color); + brushAttr.StartIndex = (uint)TextIndexToPangoIndex(span.StartIndex); + brushAttr.EndIndex = (uint)TextIndexToPangoIndex(span.StartIndex + span.Length); + + this.Layout.Attributes.Insert(brushAttr); + } + } } - set - { - _size = value; - Layout.Width = double.IsPositiveInfinity(value.Width) ? - -1 : Pango.Units.FromDouble(value.Width); - } + Size = Measure(); } - public Pango.Layout Layout + public FormattedTextImpl(Pango.Layout layout) { - get; + Layout = layout; + Size = Measure(); } - public void Dispose() - { - Layout.Dispose(); - } + public string Text => Layout.Text; + + public Size Constraint => _constraint; + + public Size Size { get; } + + public Pango.Layout Layout { get; } public IEnumerable GetLines() { @@ -124,7 +134,7 @@ namespace Avalonia.Cairo.Media return ranges; } - public Size Measure() + private Size Measure() { int width; int height; @@ -132,22 +142,5 @@ namespace Avalonia.Cairo.Media return new Size(width, height); } - - public void SetForegroundBrush(IBrush brush, int startIndex, int count) - { - var scb = brush as SolidColorBrush; - if (scb != null) - { - - var color = new Pango.Color(); - color.Parse(string.Format("#{0}", scb.Color.ToString().Substring(3))); - - var brushAttr = new Pango.AttrForeground(color); - brushAttr.StartIndex = (uint)TextIndexToPangoIndex(startIndex); - brushAttr.EndIndex = (uint)TextIndexToPangoIndex(startIndex + count); - - Layout.Attributes.Insert(brushAttr); - } - } } } diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs index 0aca4b0b33..ca16ab5630 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs @@ -71,6 +71,15 @@ namespace Avalonia.Cairo.Media } } + internal bool StrokeContains(Pen pen, Point point) + { + using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0))) + { + context.AppendPath(Path); + return context.InStroke(point.X, point.Y); + } + } + public void LineTo(Point point) { if (this.Path == null) diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index c325ebab14..7d59988918 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -35,19 +35,11 @@ namespace Avalonia.Cairo.Media private readonly StreamGeometryContextImpl _impl; private Matrix _transform = Matrix.Identity; + public Matrix Transform { get { return _transform; } - set - { - if (value != Transform) - { - if (!value.IsIdentity) - { - _transform = value; - } - } - } + private set { _transform = value; } } public FillRule FillRule { get; set; } @@ -60,7 +52,7 @@ namespace Avalonia.Cairo.Media public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public IStreamGeometryContextImpl Open() @@ -72,5 +64,23 @@ namespace Avalonia.Cairo.Media { return _impl.FillContains(point); } + + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + + public bool StrokeContains(Pen pen, Point point) + { + return _impl.StrokeContains(pen, point); + } + + /// + public IGeometryImpl WithTransform(Matrix transform) + { + var result = (StreamGeometryImpl)Clone(); + result.Transform = transform; + return result; + } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index eade31523d..6d29e9f70c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -292,7 +292,7 @@ namespace Avalonia.Skia public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(foreground, text.Measure())) + using (var paint = CreatePaint(foreground, text.Size)) { var textImpl = (FormattedTextImpl)text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint); diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index e427dc0f58..133d9cd789 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -12,15 +12,23 @@ namespace Avalonia.Skia { public class FormattedTextImpl : IFormattedTextImpl { - public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, - TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) + public FormattedTextImpl( + string text, + Typeface typeface, + TextAlignment textAlignment, + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { Text = text ?? string.Empty; // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight); + var skiaTypeface = TypefaceCache.GetTypeface( + typeface?.FontFamilyName ?? "monospace", + typeface?.Style ?? FontStyle.Normal, + typeface?.Weight ?? FontWeight.Normal); _paint = new SKPaint(); @@ -31,34 +39,31 @@ namespace Avalonia.Skia _paint.IsAntialias = true; _paint.LcdRenderText = true; _paint.SubpixelText = true; - _paint.Typeface = typeface; - _paint.TextSize = (float)fontSize; + _paint.Typeface = skiaTypeface; + _paint.TextSize = (float)(typeface?.FontSize ?? 12); _paint.TextAlign = textAlignment.ToSKTextAlign(); + _paint.XferMode = SKXferMode.Src; _wrapping = wrapping; + _constraint = constraint; - Rebuild(); - } - - public Size Constraint - { - get { return _constraint; } - set + if (spans != null) { - if (_constraint == value) - return; - - _constraint = value; - - Rebuild(); + foreach (var span in spans) + { + if (span.ForegroundBrush != null) + { + SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length); + } + } } + + Rebuild(); } - public string Text { get; } + public Size Constraint => _constraint; - public void Dispose() - { - } + public Size Size => _size; public IEnumerable GetLines() { @@ -161,27 +166,6 @@ namespace Avalonia.Skia return result; } - public Size Measure() - { - return _size; - } - - public void SetForegroundBrush(IBrush brush, int startIndex, int length) - { - var key = new FBrushRange(startIndex, length); - int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key)); - - if (index > -1) - { - _foregroundBrushes.RemoveAt(index); - } - - if (brush != null) - { - _foregroundBrushes.Insert(0, new KeyValuePair(key, brush)); - } - } - public override string ToString() { return Text; @@ -286,6 +270,7 @@ namespace Avalonia.Skia private readonly List _lines = new List(); private readonly SKPaint _paint; private readonly List _rects = new List(); + public string Text { get; } private readonly TextWrapping _wrapping; private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); private float _lineHeight = 0; @@ -619,6 +604,22 @@ namespace Avalonia.Skia return x; } + private void SetForegroundBrush(IBrush brush, int startIndex, int length) + { + var key = new FBrushRange(startIndex, length); + int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key)); + + if (index > -1) + { + _foregroundBrushes.RemoveAt(index); + } + + if (brush != null) + { + _foregroundBrushes.Insert(0, new KeyValuePair(key, brush)); + } + } + private struct AvaloniaFormattedTextLine { public float Height; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 72f8d08d44..6912c52515 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -17,10 +17,15 @@ namespace Avalonia.Skia return CreateRenderTargetBitmap(width, height); } - public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, - TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) + public IFormattedTextImpl CreateFormattedText( + string text, + Typeface typeface, + TextAlignment textAlignment, + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { - return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping); + return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } public IStreamGeometryImpl CreateStreamGeometry() diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 33d4ce10c7..9a0a1dc434 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -13,16 +13,15 @@ namespace Avalonia.Skia class StreamGeometryImpl : IStreamGeometryImpl { SKPath _path; - SKPath _transformedPath; private Matrix _transform = Matrix.Identity; - public SKPath EffectivePath => (_transformedPath ?? _path); + public SKPath EffectivePath => _path; public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public Rect Bounds { get; private set; } @@ -30,32 +29,6 @@ namespace Avalonia.Skia public Matrix Transform { get { return _transform; } - set - { - if (_transform == value) - return; - - _transform = value; - ApplyTransform(); - } - } - - void ApplyTransform() - { - if (_path == null) - return; - - if (_transformedPath != null) - { - _transformedPath.Dispose(); - _transformedPath = null; - } - - if (!Transform.IsIdentity) - { - _transformedPath = new SKPath(_path); - _transformedPath.Transform(Transform.ToSKMatrix()); - } } public IStreamGeometryImpl Clone() @@ -63,7 +36,6 @@ namespace Avalonia.Skia return new StreamGeometryImpl { _path = _path?.Clone(), - _transformedPath = _transformedPath?.Clone(), _transform = Transform, Bounds = Bounds }; @@ -84,6 +56,36 @@ namespace Avalonia.Skia return GetRenderBounds(0).Contains(point); } + public bool StrokeContains(Pen pen, Point point) + { + // TODO: Not supported by SkiaSharp yet, so use expanded Rect + // return EffectivePath.Contains(point.X, point.Y); + return GetRenderBounds(0).Contains(point); + } + + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + + public IGeometryImpl WithTransform(Matrix transform) + { + var result = (StreamGeometryImpl)Clone(); + + if (result.Transform != Matrix.Identity) + { + result._path.Transform(result.Transform.Invert().ToSKMatrix()); + } + + if (transform != Matrix.Identity) + { + result._path.Transform(transform.ToSKMatrix()); + } + + result._transform = transform; + return result; + } + class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; @@ -100,7 +102,6 @@ namespace Avalonia.Skia { SKRect rc; _path.GetBounds(out rc); - _geometryImpl.ApplyTransform(); _geometryImpl.Bounds = rc.ToAvaloniaRect(); } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index b2f65ef45f..ffba7a11ff 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -68,6 +68,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d5b0f22090..2b6a4e9844 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -88,14 +88,19 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { - return new FormattedTextImpl(text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight, wrapping); + return new FormattedTextImpl( + text, + typeface, + textAlignment, + wrapping, + constraint, + spans); } public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 2c5ad778c2..332670681a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -207,7 +207,7 @@ namespace Avalonia.Direct2D1.Media { var impl = (FormattedTextImpl)text; - using (var brush = CreateBrush(foreground, impl.Measure())) + using (var brush = CreateBrush(foreground, impl.Size)) using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush)) { if (brush.PlatformBrush != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index ae226a5087..5578abc32c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -14,51 +14,52 @@ namespace Avalonia.Direct2D1.Media { public FormattedTextImpl( string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { + Text = text; var factory = AvaloniaLocator.Current.GetService(); using (var format = new DWrite.TextFormat( factory, - fontFamily, - (DWrite.FontWeight)fontWeight, - (DWrite.FontStyle)fontStyle, - (float)fontSize)) + typeface?.FontFamilyName ?? "Courier New", + (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal), + (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal), + (float)(typeface?.FontSize ?? 12))) { - Text = text; format.WordWrapping = wrapping == TextWrapping.Wrap ? - DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; + DWrite.WordWrapping.Wrap : + DWrite.WordWrapping.NoWrap; TextLayout = new DWrite.TextLayout( factory, text ?? string.Empty, format, - float.MaxValue, - float.MaxValue); + (float)constraint.Width, + (float)constraint.Height) + { + TextAlignment = textAlignment.ToDirect2D() + }; } - TextLayout.TextAlignment = textAlignment.ToDirect2D(); - } - - public Size Constraint - { - get + if (spans != null) { - return new Size(TextLayout.MaxWidth, TextLayout.MaxHeight); + foreach (var span in spans) + { + ApplySpan(span); + } } - set - { - TextLayout.MaxWidth = (float)value.Width; - TextLayout.MaxHeight = (float)value.Height; - } + Size = Measure(); } + public Size Constraint => new Size(TextLayout.MaxWidth, TextLayout.MaxHeight); + + public Size Size { get; } + public string Text { get; } public DWrite.TextLayout TextLayout { get; } @@ -113,7 +114,20 @@ namespace Avalonia.Direct2D1.Media return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height)); } - public Size Measure() + private void ApplySpan(FormattedTextStyleSpan span) + { + if (span.Length > 0) + { + if (span.ForegroundBrush != null) + { + TextLayout.SetDrawingEffect( + new BrushWrapper(span.ForegroundBrush), + new DWrite.TextRange(span.StartIndex, span.Length)); + } + } + } + + private Size Measure() { var metrics = TextLayout.Metrics; var width = metrics.WidthIncludingTrailingWhitespace; @@ -125,12 +139,5 @@ namespace Avalonia.Direct2D1.Media return new Size(width, TextLayout.Metrics.Height); } - - public void SetForegroundBrush(IBrush brush, int startIndex, int count) - { - TextLayout.SetDrawingEffect( - new BrushWrapper(brush), - new DWrite.TextRange(startIndex, count)); - } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index b98c944c7d..8bb901a1e4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Platform; using SharpDX.Direct2D1; @@ -11,85 +12,61 @@ namespace Avalonia.Direct2D1.Media /// public abstract class GeometryImpl : IGeometryImpl { - private TransformedGeometry _transformed; - - /// - /// Gets the geometry's bounding rectangle. - /// - public abstract Rect Bounds + public GeometryImpl(Geometry geometry) { - get; + Geometry = geometry; } - /// - /// Gets the geomentry without any transforms applied. - /// - public abstract Geometry DefiningGeometry + /// + public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia(); + + /// + public Geometry Geometry { get; } + + /// + public virtual Matrix Transform => Matrix.Identity; + + /// + public Rect GetRenderBounds(double strokeThickness) { - get; + return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); } - /// - /// Gets the Direct2D . - /// - public Geometry Geometry => _transformed ?? DefiningGeometry; + /// + public bool FillContains(Point point) + { + return Geometry.FillContainsPoint(point.ToSharpDX()); + } - /// - /// Gets or sets the transform for the geometry. - /// - public Matrix Transform + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) { - get - { - return _transformed != null ? - _transformed.Transform.ToAvalonia() : - Matrix.Identity; - } + var result = new PathGeometry(Geometry.Factory); - set + using (var sink = result.Open()) { - if (value != Transform) - { - if (_transformed != null) - { - _transformed.Dispose(); - _transformed = null; - } - - if (!value.IsIdentity) - { - Factory factory = AvaloniaLocator.Current.GetService(); - _transformed = new TransformedGeometry( - factory, - DefiningGeometry, - value.ToDirect2D()); - } - } + Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink); + return new StreamGeometryImpl(result); } } - /// - /// Gets the geometry's bounding rectangle with the specified stroke thickness. - /// - /// The stroke thickness. - /// The bounding rectangle. - public Rect GetRenderBounds(double strokeThickness) + /// + public bool StrokeContains(Avalonia.Media.Pen pen, Point point) { - if (_transformed != null) - { - return _transformed.GetWidenedBounds((float)strokeThickness).ToAvalonia(); - } - else - { - return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); - } + return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); } - - public bool FillContains(Point point) + /// + public IGeometryImpl WithTransform(Matrix transform) { - return Geometry.FillContainsPoint(point.ToSharpDX()); + var factory = AvaloniaLocator.Current.GetService(); + return new TransformedGeometryImpl( + new TransformedGeometry( + factory, + GetSourceGeometry(), + transform.ToDirect2D())); } + protected virtual Geometry GetSourceGeometry() => Geometry; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index 213c4b5c03..4c1bc3d6f7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -3,7 +3,6 @@ using Avalonia.Platform; using SharpDX.Direct2D1; -using D2DGeometry = SharpDX.Direct2D1.Geometry; namespace Avalonia.Direct2D1.Media { @@ -12,55 +11,44 @@ namespace Avalonia.Direct2D1.Media /// public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { - private readonly PathGeometry _path; - /// /// Initializes a new instance of the class. /// public StreamGeometryImpl() + : base(CreateGeometry()) { - Factory factory = AvaloniaLocator.Current.GetService(); - _path = new PathGeometry(factory); } /// /// Initializes a new instance of the class. /// /// An existing Direct2D . - protected StreamGeometryImpl(PathGeometry geometry) + public StreamGeometryImpl(PathGeometry geometry) + : base(geometry) { - _path = geometry; } /// - public override Rect Bounds => _path.GetWidenedBounds(0).ToAvalonia(); - - /// - public override D2DGeometry DefiningGeometry => _path; - - /// - /// Clones the geometry. - /// - /// A cloned geometry. public IStreamGeometryImpl Clone() { Factory factory = AvaloniaLocator.Current.GetService(); var result = new PathGeometry(factory); var sink = result.Open(); - _path.Stream(sink); + ((PathGeometry)Geometry).Stream(sink); sink.Close(); return new StreamGeometryImpl(result); } - /// - /// Opens the geometry to start defining it. - /// - /// - /// An which can be used to define the geometry. - /// + /// public IStreamGeometryContextImpl Open() { - return new StreamGeometryContextImpl(_path.Open()); + return new StreamGeometryContextImpl(((PathGeometry)Geometry).Open()); + } + + private static Geometry CreateGeometry() + { + Factory factory = AvaloniaLocator.Current.GetService(); + return new PathGeometry(factory); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs new file mode 100644 index 0000000000..4043e180dc --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs @@ -0,0 +1,24 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using SharpDX.Direct2D1; + +namespace Avalonia.Direct2D1.Media +{ + public class TransformedGeometryImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + /// An existing Direct2D . + public TransformedGeometryImpl(TransformedGeometry geometry) + : base(geometry) + { + } + + /// + public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia(); + + protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry; + } +} diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs index a70130990a..6d1f321b5f 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs @@ -335,7 +335,7 @@ namespace Avalonia.Input.UnitTests class MockRenderInterface : IPlatformRenderInterface { - public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) + public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) { throw new NotImplementedException(); } @@ -414,11 +414,26 @@ namespace Avalonia.Input.UnitTests throw new NotImplementedException(); } + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + public IStreamGeometryContextImpl Open() { return _impl; } + public bool StrokeContains(Pen pen, Point point) + { + throw new NotImplementedException(); + } + + public IGeometryImpl WithTransform(Matrix transform) + { + throw new NotImplementedException(); + } + class MockStreamGeometryContext : IStreamGeometryContextImpl { private List points = new List(); diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index d996f881d2..6b7c73da2a 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -142,6 +142,8 @@ namespace Avalonia.Layout.UnitTests public string Text { get; } + public Size Size => new Size(); + public void Dispose() { } @@ -155,20 +157,22 @@ namespace Avalonia.Layout.UnitTests public IEnumerable HitTestTextRange(int index, int length) => new Rect[0]; public Size Measure() => Constraint; - - public void SetForegroundBrush(IBrush brush, int startIndex, int length) - { - } } private void RegisterServices() { var globalStyles = new Mock(); var renderInterface = new Mock(); - renderInterface.Setup(x => x.CreateFormattedText(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + renderInterface.Setup(x => + x.CreateFormattedText( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) .Returns(new FormattedTextMock("TEST")); - + var windowImpl = new Mock(); Size clientSize = default(Size); diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 813db56fed..4f8aeb93ac 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -50,37 +50,40 @@ namespace Avalonia.Direct2D1.RenderTests.Media FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + double widthConstraint) { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - fontFamily, - fontSize, - fontStyle, + new Typeface(fontFamily, fontSize, fontStyle, fontWeight), textAlignment, - fontWeight, - wrapping); + wrapping, + widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), + null); } private IFormattedTextImpl Create(string text, double fontSize) { return Create(text, FontName, fontSize, FontStyle.Normal, TextAlignment.Left, - FontWeight.Normal, TextWrapping.NoWrap); + FontWeight.Normal, TextWrapping.NoWrap, + -1); } - private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment) + private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment, double widthConstraint) { return Create(text, FontName, fontSize, FontStyle.Normal, alignment, - FontWeight.Normal, TextWrapping.NoWrap); + FontWeight.Normal, TextWrapping.NoWrap, + widthConstraint); } - private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap) + private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap, double widthConstraint) { return Create(text, FontName, fontSize, FontStyle.Normal, TextAlignment.Left, - FontWeight.Normal, wrap); + FontWeight.Normal, wrap, + widthConstraint); } #if AVALONIA_CAIRO @@ -101,17 +104,15 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)] public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight) { - using (var fmt = Create(input, fontSize)) - { - var size = fmt.Measure(); + var fmt = Create(input, fontSize); + var size = fmt.Size; - Assert.Equal(expWidth, size.Width, 2); - Assert.Equal(expHeight, size.Height, 2); + Assert.Equal(expWidth, size.Width, 2); + Assert.Equal(expHeight, size.Height, 2); - var linesHeight = fmt.GetLines().Sum(l => l.Height); + var linesHeight = fmt.GetLines().Sum(l => l.Height); - Assert.Equal(expHeight, linesHeight, 2); - } + Assert.Equal(expHeight, linesHeight, 2); } #if AVALONIA_CAIRO @@ -135,16 +136,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media double widthConstraint, TextWrapping wrap) { - using (var fmt = Create(input, FontSize, wrap)) - { - if (widthConstraint != -1) - { - fmt.Constraint = new Size(widthConstraint, 10000); - } + var fmt = Create(input, FontSize, wrap, widthConstraint); + var constrained = fmt; - var lines = fmt.GetLines().ToArray(); - Assert.Equal(linesCount, lines.Count()); - } + var lines = constrained.GetLines().ToArray(); + Assert.Equal(linesCount, lines.Count()); } #if AVALONIA_CAIRO @@ -178,14 +174,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media double x, double y, bool isInside, bool isTrailing, int pos) { - using (var fmt = Create(input, FontSize)) - { - var htRes = fmt.HitTestPoint(new Point(x, y)); + var fmt = Create(input, FontSize); + var htRes = fmt.HitTestPoint(new Point(x, y)); - Assert.Equal(pos, htRes.TextPosition); - Assert.Equal(isInside, htRes.IsInside); - Assert.Equal(isTrailing, htRes.IsTrailing); - } + Assert.Equal(pos, htRes.TextPosition); + Assert.Equal(isInside, htRes.IsInside); + Assert.Equal(isTrailing, htRes.IsTrailing); } #if AVALONIA_CAIRO @@ -205,15 +199,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media public void Should_HitTestPosition_Correctly(string input, int index, double x, double y, double width, double height) { - using (var fmt = Create(input, FontSize)) - { - var r = fmt.HitTestTextPosition(index); + var fmt = Create(input, FontSize); + var r = fmt.HitTestTextPosition(index); - Assert.Equal(x, r.X, 2); - Assert.Equal(y, r.Y, 2); - Assert.Equal(width, r.Width, 2); - Assert.Equal(height, r.Height, 2); - } + Assert.Equal(x, r.X, 2); + Assert.Equal(y, r.Y, 2); + Assert.Equal(width, r.Width, 2); + Assert.Equal(height, r.Height, 2); } #if AVALONIA_CAIRO @@ -229,17 +221,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media double x, double y, double width, double height) { //parse expected - using (var fmt = Create(input, FontSize, TextAlignment.Right)) - { - fmt.Constraint = new Size(widthConstraint, 100); - - var r = fmt.HitTestTextPosition(index); + var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint); + var constrained = fmt; + var r = constrained.HitTestTextPosition(index); - Assert.Equal(x, r.X, 2); - Assert.Equal(y, r.Y, 2); - Assert.Equal(width, r.Width, 2); - Assert.Equal(height, r.Height, 2); - } + Assert.Equal(x, r.X, 2); + Assert.Equal(y, r.Y, 2); + Assert.Equal(width, r.Width, 2); + Assert.Equal(height, r.Height, 2); } #if AVALONIA_CAIRO @@ -255,17 +244,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media double x, double y, double width, double height) { //parse expected - using (var fmt = Create(input, FontSize, TextAlignment.Center)) - { - fmt.Constraint = new Size(widthConstraint, 100); + var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint); + var constrained = fmt; + var r = constrained.HitTestTextPosition(index); - var r = fmt.HitTestTextPosition(index); - - Assert.Equal(x, r.X, 2); - Assert.Equal(y, r.Y, 2); - Assert.Equal(width, r.Width, 2); - Assert.Equal(height, r.Height, 2); - } + Assert.Equal(x, r.X, 2); + Assert.Equal(y, r.Y, 2); + Assert.Equal(width, r.Width, 2); + Assert.Equal(height, r.Height, 2); } #if AVALONIA_CAIRO @@ -291,22 +277,20 @@ namespace Avalonia.Direct2D1.RenderTests.Media return new Rect(v[0], v[1], v[2], v[3]); }).ToArray(); - using (var fmt = Create(input, FontSize)) - { - var htRes = fmt.HitTestTextRange(index, length).ToArray(); + var fmt = Create(input, FontSize); + var htRes = fmt.HitTestTextRange(index, length).ToArray(); - Assert.Equal(rects.Length, htRes.Length); + Assert.Equal(rects.Length, htRes.Length); - for (int i = 0; i < rects.Length; i++) - { - var exr = rects[i]; - var r = htRes[i]; + for (int i = 0; i < rects.Length; i++) + { + var exr = rects[i]; + var r = htRes[i]; - Assert.Equal(exr.X, r.X, 2); - Assert.Equal(exr.Y, r.Y, 2); - Assert.Equal(exr.Width, r.Width, 2); - Assert.Equal(exr.Height, r.Height, 2); - } + Assert.Equal(exr.X, r.X, 2); + Assert.Equal(exr.Y, r.Y, 2); + Assert.Equal(exr.Width, r.Width, 2); + Assert.Equal(exr.Height, r.Height, 2); } } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 3efbd6c9be..8c887343ee 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -13,6 +13,7 @@ using Avalonia.Styling; using Avalonia.Themes.Default; using Avalonia.Rendering; using System.Reactive.Concurrency; +using System.Collections.Generic; namespace Avalonia.UnitTests { @@ -163,12 +164,11 @@ namespace Avalonia.UnitTests return Mock.Of(x => x.CreateFormattedText( It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), + It.IsAny(), It.IsAny(), - It.IsAny(), - It.IsAny()) == Mock.Of() && + It.IsAny(), + It.IsAny(), + It.IsAny>()) == Mock.Of() && x.CreateStreamGeometry() == Mock.Of( y => y.Open() == Mock.Of())); } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs deleted file mode 100644 index 648595fa2b..0000000000 --- a/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Avalonia.Media; -using Xunit; - -namespace Avalonia.Visuals.UnitTests.Media -{ - public class FormattedTextTests - { - [Fact] - public void Exception_Should_Be_Thrown_If_FontSize_0() - { - Assert.Throws(() => new FormattedText( - "foo", - "Ariel", - 0)); - } - - [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_0() - { - Assert.Throws(() => new FormattedText( - "foo", - "Ariel", - 12, - fontWeight: 0)); - } - } -} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs new file mode 100644 index 0000000000..6b8bf16fd9 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class TypefaceTests + { + [Fact] + public void Exception_Should_Be_Thrown_If_FontSize_0() + { + Assert.Throws(() => new Typeface("foo", 0)); + } + + [Fact] + public void Exception_Should_Be_Thrown_If_FontWeight_0() + { + Assert.Throws(() => new Typeface("foo", 12, weight: 0)); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 303736be9c..57399f3df0 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -10,12 +10,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree { public IFormattedTextImpl CreateFormattedText( string text, - string fontFamilyName, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { throw new NotImplementedException(); } @@ -94,11 +93,26 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + public IStreamGeometryContextImpl Open() { return _impl; } + public bool StrokeContains(Pen pen, Point point) + { + throw new NotImplementedException(); + } + + public IGeometryImpl WithTransform(Matrix transform) + { + throw new NotImplementedException(); + } + class MockStreamGeometryContext : IStreamGeometryContextImpl { private List points = new List(); From 457d7bccaadfd50a0fde8898550235bfcc909c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 5 Apr 2017 08:44:12 +0200 Subject: [PATCH 12/19] Updated NuGet.CommandLine package version --- build.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cake b/build.cake index a36a3bb536..1e29b33fc1 100644 --- a/build.cake +++ b/build.cake @@ -4,7 +4,7 @@ #addin "nuget:?package=Polly&version=4.2.0" #addin "nuget:?package=NuGet.Core&version=2.12.0" -#tool "nuget:https://dotnet.myget.org/F/nuget-build/?package=NuGet.CommandLine&version=4.3.0-beta1-2361&prerelease" +#tool "nuget:https://dotnet.myget.org/F/nuget-build/?package=NuGet.CommandLine&version=4.3.0-preview1-3980&prerelease" #tool "nuget:?package=JetBrains.dotMemoryUnit&version=2.1.20150828.125449" /////////////////////////////////////////////////////////////////////////////// // TOOLS From 92331915b4a89e5a91fafa08323abfc5443683d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Apr 2017 23:00:11 +0200 Subject: [PATCH 13/19] Enable SharpDX EnableReleaseOnFinalizer. We need to enable to avoid memory leaks this as we're not doing cleanup of SharpDX objects ourselves. --- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d5b0f22090..5362f417a3 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -72,14 +72,18 @@ namespace Avalonia.Direct2D1 } } - public static void Initialize() => AvaloniaLocator.CurrentMutable - .Bind().ToConstant(s_instance) - .Bind().ToConstant(s_instance) - .BindToSelf(s_d2D1Factory) - .BindToSelf(s_dwfactory) - .BindToSelf(s_imagingFactory) - .BindToSelf(s_dxgiDevice) - .BindToSelf(s_d2D1Device); + public static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(s_instance) + .Bind().ToConstant(s_instance) + .BindToSelf(s_d2D1Factory) + .BindToSelf(s_dwfactory) + .BindToSelf(s_imagingFactory) + .BindToSelf(s_dxgiDevice) + .BindToSelf(s_d2D1Device); + SharpDX.Configuration.EnableReleaseOnFinalizer = true; + } public IBitmapImpl CreateBitmap(int width, int height) { From 4a6deb7d8b2e2b5e7d733cb6ec7e3d980aadd262 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Apr 2017 23:52:00 +0200 Subject: [PATCH 14/19] Added some doc comments. And removed some tabs. --- .../Media/FormattedTextStyleSpan.cs | 20 ++++++++++++++ src/Avalonia.Visuals/Media/Typeface.cs | 26 ++++++++++++++++++- .../Media/StreamGeometryImpl.cs | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs index 75bf76c996..96a5c05c74 100644 --- a/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs +++ b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs @@ -2,8 +2,17 @@ namespace Avalonia.Media { + /// + /// Describes the formatting for a span of text in a object. + /// public class FormattedTextStyleSpan { + /// + /// Initializes a new instance of the class. + /// + /// The index of the first character in the span. + /// The length of the span. + /// The span's foreground brush. public FormattedTextStyleSpan( int startIndex, int length, @@ -14,8 +23,19 @@ namespace Avalonia.Media ForegroundBrush = foregroundBrush; } + /// + /// Gets the index of the first character in the span. + /// public int StartIndex { get; } + + /// + /// Gets the length of the span. + /// public int Length { get; } + + /// + /// Gets the span's foreground brush. + /// public IBrush ForegroundBrush { get; } } } diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index b05d5d02e5..12540b67e7 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -1,10 +1,19 @@ using System; -using Avalonia.Media; namespace Avalonia.Media { + /// + /// Represents a typeface. + /// public class Typeface { + /// + /// Initializes a new instance of the class. + /// + /// The name of the font family. + /// The font size, in DIPs. + /// The font style. + /// The font weight. public Typeface( string fontFamilyName, double fontSize, @@ -27,9 +36,24 @@ namespace Avalonia.Media Weight = weight; } + /// + /// Gets the name of the font family. + /// public string FontFamilyName { get; } + + /// + /// Gets the size of the font in DIPs. + /// public double FontSize { get; } + + /// + /// Gets the font style. + /// public FontStyle Style { get; } + + /// + /// Gets the font weight. + /// public FontWeight Weight { get; } } } diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index 7d59988918..b0a8cd4fb0 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Cairo.Media public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public IStreamGeometryContextImpl Open() From 0145b06eeb5831749f9a6f29f738e1ca79d70b8e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 6 Apr 2017 12:23:54 +0300 Subject: [PATCH 15/19] Use image from appveyor settings --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3c36c2fcd3..a218f99791 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,3 @@ -os: Visual Studio 2017 platform: - Any CPU configuration: From a870111fd4bd6bf8d58d76cb897002407c0b2d14 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 6 Apr 2017 12:48:53 +0300 Subject: [PATCH 16/19] Update appveyor.yml --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index a218f99791..c21ced4e4a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,4 @@ +os: Previous Visual Studio 2017 platform: - Any CPU configuration: From 59f76152c47453c41c8942004e12bbcb0dc52e75 Mon Sep 17 00:00:00 2001 From: Stanislav Prokop Date: Sat, 8 Apr 2017 22:35:03 +0200 Subject: [PATCH 17/19] Removed incorrect bracket from the readme file. --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0e54aae0bc..2149aff386 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ | Gitter Chat | Windows Build Status | Linux/Mac Build Status | Code Coverage | |---|---|---|---| -| [![Gitter](https://badges.gitter.im/Join%20Chat.svg))](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | [![codecov](https://codecov.io/gh/AvaloniaUI/Avalonia/branch/master/graph/badge.svg)](https://codecov.io/gh/AvaloniaUI/Avalonia) | +| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | [![codecov](https://codecov.io/gh/AvaloniaUI/Avalonia/branch/master/graph/badge.svg)](https://codecov.io/gh/AvaloniaUI/Avalonia) | A multi-platform .NET UI framework. It can run on Windows, Linux, Mac OS X, iOS and Android. From 1af559c52ed11e15c6b712e06ca0070946572939 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 15 Apr 2017 14:35:35 +0200 Subject: [PATCH 18/19] Fixed name of tests. --- tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs index 6b8bf16fd9..2c8f8eb9b2 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -7,13 +7,13 @@ namespace Avalonia.Visuals.UnitTests.Media public class TypefaceTests { [Fact] - public void Exception_Should_Be_Thrown_If_FontSize_0() + public void Exception_Should_Be_Thrown_If_FontSize_LessThanEqualTo_0() { Assert.Throws(() => new Typeface("foo", 0)); } [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_0() + public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0() { Assert.Throws(() => new Typeface("foo", 12, weight: 0)); } From a0fa312abf29e42713587abb8f13210af6109a27 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 15 Apr 2017 15:50:10 +0300 Subject: [PATCH 19/19] Skip excess AppVeyor CI run if Pull Request opened from non default (master) branch. --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 3c36c2fcd3..84bdb2355d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,7 @@ os: Visual Studio 2017 platform: - Any CPU +skip_branch_with_pr: true configuration: - Release environment: