committed by
GitHub
58 changed files with 2637 additions and 619 deletions
@ -0,0 +1,16 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media.Fonts; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
internal class CompositeFontFamilyKey : FontFamilyKey |
|||
{ |
|||
public CompositeFontFamilyKey(Uri source, FontFamilyKey[] keys) : base(source, null) |
|||
{ |
|||
Keys = keys; |
|||
} |
|||
|
|||
public IReadOnlyList<FontFamilyKey> Keys { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
internal readonly record struct FontSourceIdentifier |
|||
{ |
|||
public FontSourceIdentifier(string name, Uri? source) |
|||
{ |
|||
Name = name; |
|||
Source = source; |
|||
} |
|||
|
|||
public string Name { get; init; } |
|||
|
|||
public Uri? Source { get; init; } |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
internal interface IGlyphTypeface2 : IGlyphTypeface |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream.</param>
|
|||
/// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
|
|||
bool TryGetStream([NotNullWhen(true)] out Stream? stream); |
|||
} |
|||
} |
|||
@ -0,0 +1,154 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
namespace Avalonia.Media.Imaging; |
|||
|
|||
internal static unsafe class PixelFormatTranscoder |
|||
{ |
|||
public static void Transcode( |
|||
IntPtr source, |
|||
PixelSize srcSize, |
|||
int sourceStride, |
|||
PixelFormat srcFormat, |
|||
AlphaFormat srcAlphaFormat, |
|||
IntPtr dest, |
|||
int destStride, |
|||
PixelFormat destFormat, |
|||
AlphaFormat destAlphaFormat) |
|||
{ |
|||
var reader = GetReader(srcFormat); |
|||
var writer = GetWriter(destFormat); |
|||
|
|||
var w = srcSize.Width; |
|||
var h = srcSize.Height; |
|||
|
|||
for (var y = 0; y < h; y++) |
|||
{ |
|||
reader.Reset(source + sourceStride * y); |
|||
|
|||
writer.Reset(dest + destStride * y); |
|||
|
|||
for (var x = 0; x < w; x++) |
|||
{ |
|||
writer.WriteNext(GetConvertedPixel(reader.ReadNext(), srcAlphaFormat, destAlphaFormat)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static Rgba8888Pixel GetConvertedPixel(Rgba8888Pixel pixel, AlphaFormat sourceAlpha, AlphaFormat destAlpha) |
|||
{ |
|||
if (sourceAlpha != destAlpha) |
|||
{ |
|||
if (sourceAlpha == AlphaFormat.Premul && destAlpha != AlphaFormat.Premul) |
|||
{ |
|||
return ConvertFromPremultiplied(pixel); |
|||
} |
|||
|
|||
if (sourceAlpha != AlphaFormat.Premul && destAlpha == AlphaFormat.Premul) |
|||
{ |
|||
return ConvertToPremultiplied(pixel); |
|||
} |
|||
} |
|||
|
|||
return pixel; |
|||
} |
|||
|
|||
private static Rgba8888Pixel ConvertToPremultiplied(Rgba8888Pixel pixel) |
|||
{ |
|||
var factor = pixel.A / 255F; |
|||
|
|||
return new Rgba8888Pixel |
|||
{ |
|||
R = (byte)(pixel.R * factor), |
|||
G = (byte)(pixel.G * factor), |
|||
B = (byte)(pixel.B * factor), |
|||
A = pixel.A |
|||
}; |
|||
} |
|||
|
|||
private static Rgba8888Pixel ConvertFromPremultiplied(Rgba8888Pixel pixel) |
|||
{ |
|||
var factor = 1F / (pixel.A / 255F); |
|||
|
|||
return new Rgba8888Pixel |
|||
{ |
|||
R = (byte)(pixel.R * factor), |
|||
G = (byte)(pixel.G * factor), |
|||
B = (byte)(pixel.B * factor), |
|||
A = pixel.A |
|||
}; |
|||
} |
|||
|
|||
private static IPixelFormatReader GetReader(PixelFormat format) |
|||
{ |
|||
switch (format.FormatEnum) |
|||
{ |
|||
case PixelFormatEnum.Rgb565: |
|||
return new PixelFormatReader.Bgr565PixelFormatReader(); |
|||
case PixelFormatEnum.Rgba8888: |
|||
return new PixelFormatReader.Rgba8888PixelFormatReader(); |
|||
case PixelFormatEnum.Bgra8888: |
|||
return new PixelFormatReader.Bgra8888PixelFormatReader(); |
|||
case PixelFormatEnum.BlackWhite: |
|||
return new PixelFormatReader.BlackWhitePixelFormatReader(); |
|||
case PixelFormatEnum.Gray2: |
|||
return new PixelFormatReader.Gray2PixelFormatReader(); |
|||
case PixelFormatEnum.Gray4: |
|||
return new PixelFormatReader.Gray4PixelFormatReader(); |
|||
case PixelFormatEnum.Gray8: |
|||
return new PixelFormatReader.Gray8PixelFormatReader(); |
|||
case PixelFormatEnum.Gray16: |
|||
return new PixelFormatReader.Gray16PixelFormatReader(); |
|||
case PixelFormatEnum.Gray32Float: |
|||
return new PixelFormatReader.Gray32FloatPixelFormatReader(); |
|||
case PixelFormatEnum.Rgba64: |
|||
return new PixelFormatReader.Rgba64PixelFormatReader(); |
|||
case PixelFormatEnum.Rgb24: |
|||
return new PixelFormatReader.Rgb24PixelFormatReader(); |
|||
case PixelFormatEnum.Bgr24: |
|||
return new PixelFormatReader.Bgr24PixelFormatReader(); |
|||
case PixelFormatEnum.Bgr555: |
|||
return new PixelFormatReader.Bgr555PixelFormatReader(); |
|||
case PixelFormatEnum.Bgr565: |
|||
return new PixelFormatReader.Bgr565PixelFormatReader(); |
|||
default: |
|||
throw new NotSupportedException($"Pixel format {format} is not supported"); |
|||
} |
|||
} |
|||
|
|||
private static IPixelFormatWriter GetWriter(PixelFormat format) |
|||
{ |
|||
switch (format.FormatEnum) |
|||
{ |
|||
case PixelFormatEnum.Rgb565: |
|||
return new PixelFormatWriter.Bgr565PixelFormatWriter(); |
|||
case PixelFormatEnum.Rgba8888: |
|||
return new PixelFormatWriter.Rgba8888PixelFormatWriter(); |
|||
case PixelFormatEnum.Bgra8888: |
|||
return new PixelFormatWriter.Bgra8888PixelFormatWriter(); |
|||
case PixelFormatEnum.BlackWhite: |
|||
return new PixelFormatWriter.BlackWhitePixelFormatWriter(); |
|||
case PixelFormatEnum.Gray2: |
|||
return new PixelFormatWriter.Gray2PixelFormatWriter(); |
|||
case PixelFormatEnum.Gray4: |
|||
return new PixelFormatWriter.Gray4PixelFormatWriter(); |
|||
case PixelFormatEnum.Gray8: |
|||
return new PixelFormatWriter.Gray8PixelFormatWriter(); |
|||
case PixelFormatEnum.Gray16: |
|||
return new PixelFormatWriter.Gray16PixelFormatWriter(); |
|||
case PixelFormatEnum.Gray32Float: |
|||
return new PixelFormatWriter.Gray32FloatPixelFormatWriter(); |
|||
case PixelFormatEnum.Rgba64: |
|||
return new PixelFormatWriter.Rgba64PixelFormatWriter(); |
|||
case PixelFormatEnum.Rgb24: |
|||
return new PixelFormatWriter.Rgb24PixelFormatWriter(); |
|||
case PixelFormatEnum.Bgr24: |
|||
return new PixelFormatWriter.Bgr24PixelFormatWriter(); |
|||
case PixelFormatEnum.Bgr555: |
|||
return new PixelFormatWriter.Bgr555PixelFormatWriter(); |
|||
case PixelFormatEnum.Bgr565: |
|||
return new PixelFormatWriter.Bgr565PixelFormatWriter(); |
|||
default: |
|||
throw new NotSupportedException($"Pixel format {format} is not supported"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,324 @@ |
|||
using System; |
|||
namespace Avalonia.Media.Imaging; |
|||
|
|||
internal interface IPixelFormatWriter |
|||
{ |
|||
void WriteNext(Rgba8888Pixel pixel); |
|||
void Reset(IntPtr address); |
|||
} |
|||
|
|||
internal static class PixelFormatWriter |
|||
{ |
|||
public unsafe struct Rgb24PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private byte* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
addr[0] = pixel.R; |
|||
addr[1] = pixel.G; |
|||
addr[2] = pixel.B; |
|||
|
|||
_address += 3; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Rgba64PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private Rgba64Pixel* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
*addr = new Rgba64Pixel((ushort)(pixel.R << 8), (ushort)(pixel.G << 8), (ushort)(pixel.B << 8), (ushort)(pixel.A << 8)); |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (Rgba64Pixel*)address; |
|||
} |
|||
|
|||
public unsafe struct Rgba8888PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private Rgba8888Pixel* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
*addr = pixel; |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (Rgba8888Pixel*)address; |
|||
} |
|||
|
|||
public unsafe struct Bgra8888PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private byte* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
addr[0] = pixel.B; |
|||
addr[1] = pixel.G; |
|||
addr[2] = pixel.R; |
|||
addr[3] = pixel.A; |
|||
|
|||
_address += 4; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Bgr24PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private byte* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
addr[2] = pixel.R; |
|||
addr[1] = pixel.G; |
|||
addr[0] = pixel.B; |
|||
|
|||
_address += 3; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Bgra32PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private byte* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
addr[3] = pixel.A; |
|||
addr[2] = pixel.R; |
|||
addr[1] = pixel.G; |
|||
addr[0] = pixel.B; |
|||
|
|||
_address += 4; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Bgr565PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private ushort* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
*addr = Pack(pixel); |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (ushort*)address; |
|||
|
|||
private static ushort Pack(Rgba8888Pixel pixel) |
|||
{ |
|||
return (ushort)((((int)Math.Round(pixel.R / 255F * 31F) & 0x1F) << 11) |
|||
| (((int)Math.Round(pixel.G / 255F * 63F) & 0x3F) << 5) |
|||
| ((int)Math.Round(pixel.B / 255F * 31F) & 0x1F)); |
|||
} |
|||
} |
|||
|
|||
public unsafe struct Bgr555PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private ushort* _address; |
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
*addr = Pack(pixel); |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (ushort*)address; |
|||
|
|||
private static ushort Pack(Rgba8888Pixel pixel) |
|||
{ |
|||
return (ushort)( |
|||
(((int)Math.Round(pixel.R / 255F * 31F) & 0x1F) << 10) |
|||
| (((int)Math.Round(pixel.G / 255F * 31F) & 0x1F) << 5) |
|||
| (((int)Math.Round(pixel.B / 255F * 31F) & 0x1F) << 0)); |
|||
} |
|||
} |
|||
|
|||
public unsafe struct Gray32FloatPixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private float* _address; |
|||
|
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
*addr = Pack(pixel); |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
private static float Pack(Rgba8888Pixel pixel) |
|||
{ |
|||
return (float)Math.Pow(pixel.R / 255F, 2.2); |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (float*)address; |
|||
} |
|||
|
|||
public unsafe struct BlackWhitePixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private int _bit; |
|||
private byte* _address; |
|||
|
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
var grayscale = Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); |
|||
|
|||
var value = grayscale > 0x7F ? 1 : 0; |
|||
|
|||
var shift = 7 - _bit; |
|||
var mask = 1 << shift; |
|||
|
|||
*addr = (byte)((*addr & ~mask) | value << shift); |
|||
|
|||
_bit++; |
|||
|
|||
if (_bit == 8) |
|||
{ |
|||
_address++; |
|||
|
|||
_bit = 0; |
|||
} |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Gray2PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private int _bit; |
|||
private byte* _address; |
|||
|
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
var value = 0; |
|||
|
|||
var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); |
|||
|
|||
if (grayscale > 0 && grayscale <= 0x55) |
|||
{ |
|||
//01
|
|||
value = 1; |
|||
} |
|||
|
|||
if (grayscale > 0x55 && grayscale <= 0xAA) |
|||
{ |
|||
//10
|
|||
|
|||
value = 2; |
|||
} |
|||
|
|||
if (grayscale > 0xAA) |
|||
{ |
|||
//11
|
|||
value = 3; |
|||
} |
|||
|
|||
var shift = 6 - _bit; |
|||
var mask = 3 << shift; |
|||
|
|||
*addr = (byte)((*addr & ~mask) | value << shift); |
|||
|
|||
_bit += 2; |
|||
|
|||
if (_bit == 8) |
|||
{ |
|||
_address++; |
|||
_bit = 0; |
|||
} |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Gray4PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private int _bit; |
|||
private byte* _address; |
|||
|
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); |
|||
|
|||
var value = (byte)(grayscale / 255F * 0xF); |
|||
|
|||
var shift = 4 - _bit; |
|||
var mask = 0xF << shift; |
|||
|
|||
*addr = (byte)((*addr & ~mask) | value << shift); |
|||
|
|||
_bit += 4; |
|||
|
|||
if (_bit == 8) |
|||
{ |
|||
_address++; |
|||
_bit = 0; |
|||
} |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Gray8PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private byte* _address; |
|||
|
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); |
|||
|
|||
*addr = grayscale; |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
public unsafe struct Gray16PixelFormatWriter : IPixelFormatWriter |
|||
{ |
|||
private ushort* _address; |
|||
|
|||
public void WriteNext(Rgba8888Pixel pixel) |
|||
{ |
|||
var addr = _address; |
|||
|
|||
var grayscale = (ushort)Math.Round((0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B) * 0x0101); |
|||
|
|||
*addr = grayscale; |
|||
|
|||
_address++; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (ushort*)address; |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -1,7 +1,16 @@ |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Platform; |
|||
|
|||
public interface IReadableBitmapImpl |
|||
{ |
|||
PixelFormat? Format { get; } |
|||
ILockedFramebuffer Lock(); |
|||
} |
|||
} |
|||
|
|||
//TODO12: Remove me once we can change IReadableBitmapImpl
|
|||
[Unstable] |
|||
public interface IReadableBitmapWithAlphaImpl : IReadableBitmapImpl |
|||
{ |
|||
AlphaFormat? AlphaFormat { get; } |
|||
} |
|||
|
|||
@ -0,0 +1,70 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Threading; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Rendering; |
|||
|
|||
[PrivateApi] |
|||
public sealed class ThreadProxyRenderTimer : IRenderTimer |
|||
{ |
|||
private readonly IRenderTimer _inner; |
|||
private readonly Stopwatch _stopwatch; |
|||
private readonly Thread _timerThread; |
|||
private readonly AutoResetEvent _autoResetEvent; |
|||
private Action<TimeSpan>? _tick; |
|||
private int _subscriberCount; |
|||
private bool _registered; |
|||
|
|||
public ThreadProxyRenderTimer(IRenderTimer inner, int maxStackSize = 1 * 1024 * 1024) |
|||
{ |
|||
_inner = inner; |
|||
_stopwatch = new Stopwatch(); |
|||
_autoResetEvent = new AutoResetEvent(false); |
|||
_timerThread = new Thread(RenderTimerThreadFunc, maxStackSize) { Name = "RenderTimerLoop", IsBackground = true }; |
|||
} |
|||
|
|||
public event Action<TimeSpan> Tick |
|||
{ |
|||
add |
|||
{ |
|||
_tick += value; |
|||
|
|||
if (!_registered) |
|||
{ |
|||
_registered = true; |
|||
_timerThread.Start(); |
|||
} |
|||
|
|||
if (_subscriberCount++ == 0) |
|||
{ |
|||
_inner.Tick += InnerTick; |
|||
} |
|||
} |
|||
|
|||
remove |
|||
{ |
|||
if (--_subscriberCount == 0) |
|||
{ |
|||
_inner.Tick -= InnerTick; |
|||
} |
|||
|
|||
_tick -= value; |
|||
} |
|||
} |
|||
|
|||
private void RenderTimerThreadFunc() |
|||
{ |
|||
while (_autoResetEvent.WaitOne()) |
|||
{ |
|||
_tick?.Invoke(_stopwatch.Elapsed); |
|||
} |
|||
} |
|||
|
|||
private void InnerTick(TimeSpan obj) |
|||
{ |
|||
_autoResetEvent.Set(); |
|||
} |
|||
|
|||
public bool RunsInBackground => true; |
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
|
|||
#nullable enable |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform; |
|||
using static Avalonia.X11.XLib; |
|||
namespace Avalonia.X11.Screens; |
|||
|
|||
internal partial class X11Screens |
|||
{ |
|||
internal class X11Screen |
|||
{ |
|||
public bool IsPrimary { get; } |
|||
public string Name { get; set; } |
|||
public PixelRect Bounds { get; set; } |
|||
public Size? PhysicalSize { get; set; } |
|||
public PixelRect WorkingArea { get; set; } |
|||
|
|||
public X11Screen( |
|||
PixelRect bounds, |
|||
bool isPrimary, |
|||
string name, |
|||
Size? physicalSize) |
|||
{ |
|||
IsPrimary = isPrimary; |
|||
Name = name; |
|||
Bounds = bounds; |
|||
PhysicalSize = physicalSize; |
|||
} |
|||
} |
|||
|
|||
internal interface IX11RawScreenInfoProvider |
|||
{ |
|||
X11Screen[] Screens { get; } |
|||
event Action Changed; |
|||
} |
|||
|
|||
|
|||
private class Randr15ScreensImpl : IX11RawScreenInfoProvider |
|||
{ |
|||
private X11Screen[] _cache; |
|||
private readonly X11Info _x11; |
|||
private readonly IntPtr _window; |
|||
|
|||
// Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4
|
|||
private const int EDIDStructureLength = 32; |
|||
|
|||
public event Action Changed; |
|||
|
|||
public Randr15ScreensImpl(AvaloniaX11Platform platform) |
|||
{ |
|||
_x11 = platform.Info; |
|||
_window = CreateEventWindow(platform, OnEvent); |
|||
XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); |
|||
} |
|||
|
|||
private void OnEvent(ref XEvent ev) |
|||
{ |
|||
if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) |
|||
{ |
|||
_cache = null; |
|||
Changed?.Invoke(); |
|||
} |
|||
} |
|||
|
|||
private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput) |
|||
{ |
|||
if (rrOutput == IntPtr.Zero) |
|||
return null; |
|||
var properties = XRRListOutputProperties(_x11.Display, rrOutput, out int propertyCount); |
|||
var hasEDID = false; |
|||
for (var pc = 0; pc < propertyCount; pc++) |
|||
{ |
|||
if (properties[pc] == _x11.Atoms.EDID) |
|||
hasEDID = true; |
|||
} |
|||
|
|||
if (!hasEDID) |
|||
return null; |
|||
XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false, |
|||
_x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, |
|||
out IntPtr prop); |
|||
if (actualType != _x11.Atoms.XA_INTEGER) |
|||
return null; |
|||
if (actualFormat != 8) // Expecting an byte array
|
|||
return null; |
|||
|
|||
var edid = new byte[bytesAfter]; |
|||
Marshal.Copy(prop, edid, 0, bytesAfter); |
|||
XFree(prop); |
|||
XFree(new IntPtr(properties)); |
|||
if (edid.Length < 22) |
|||
return null; |
|||
var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm.
|
|||
var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm.
|
|||
if (width == 0 && height == 0) |
|||
return null; |
|||
return new Size(width * 10, height * 10); |
|||
} |
|||
|
|||
public unsafe X11Screen[] Screens |
|||
{ |
|||
get |
|||
{ |
|||
if (_cache != null) |
|||
return _cache; |
|||
var monitors = XRRGetMonitors(_x11.Display, _window, true, out var count); |
|||
|
|||
var screens = new X11Screen[count]; |
|||
for (var c = 0; c < count; c++) |
|||
{ |
|||
var mon = monitors[c]; |
|||
var namePtr = XGetAtomName(_x11.Display, mon.Name); |
|||
var name = Marshal.PtrToStringAnsi(namePtr); |
|||
XFree(namePtr); |
|||
var bounds = new PixelRect(mon.X, mon.Y, mon.Width, mon.Height); |
|||
Size? pSize = null; |
|||
|
|||
for (int o = 0; o < mon.NOutput; o++) |
|||
{ |
|||
var outputSize = GetPhysicalMonitorSizeFromEDID(mon.Outputs[o]); |
|||
if (outputSize != null) |
|||
{ |
|||
pSize = outputSize; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
screens[c] = new X11Screen(bounds, mon.Primary != 0, name, pSize); |
|||
} |
|||
|
|||
XFree(new IntPtr(monitors)); |
|||
_cache = UpdateWorkArea(_x11, screens); |
|||
return screens; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class FallbackScreensImpl : IX11RawScreenInfoProvider |
|||
{ |
|||
private readonly X11Info _info; |
|||
public event Action? Changed; |
|||
|
|||
public FallbackScreensImpl(AvaloniaX11Platform platform) |
|||
{ |
|||
_info = platform.Info; |
|||
if (UpdateRootWindowGeometry()) |
|||
platform.Globals.RootGeometryChangedChanged += () => UpdateRootWindowGeometry(); |
|||
} |
|||
|
|||
bool UpdateRootWindowGeometry() |
|||
{ |
|||
var res = XGetGeometry(_info.Display, _info.RootWindow, out var geo); |
|||
if(res) |
|||
{ |
|||
Screens = UpdateWorkArea(_info, |
|||
new[] |
|||
{ |
|||
new X11Screen(new PixelRect(0, 0, geo.width, geo.height), true, "Default", null) |
|||
}); |
|||
} |
|||
|
|||
return res; |
|||
} |
|||
|
|||
public X11Screen[] Screens { get; private set; } = new[] |
|||
{ new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null) }; |
|||
} |
|||
} |
|||
@ -0,0 +1,249 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.X11.Screens; |
|||
|
|||
internal partial class X11Screens |
|||
{ |
|||
interface IScalingProvider |
|||
{ |
|||
double GetScaling(X11Screen screen, int index); |
|||
} |
|||
|
|||
interface IScalingProviderWithChanges : IScalingProvider |
|||
{ |
|||
event Action SettingsChanged; |
|||
} |
|||
|
|||
class PostMultiplyScalingProvider : IScalingProvider |
|||
{ |
|||
private readonly IScalingProvider _inner; |
|||
private readonly double _factor; |
|||
|
|||
public PostMultiplyScalingProvider(IScalingProvider inner, double factor) |
|||
{ |
|||
_inner = inner; |
|||
_factor = factor; |
|||
} |
|||
|
|||
public double GetScaling(X11Screen screen, int index) => _inner.GetScaling(screen, index) * _factor; |
|||
} |
|||
|
|||
class NullScalingProvider : IScalingProvider |
|||
{ |
|||
public double GetScaling(X11Screen screen, int index) => 1; |
|||
} |
|||
|
|||
|
|||
class XrdbScalingProvider : IScalingProviderWithChanges |
|||
{ |
|||
private readonly XResources _resources; |
|||
private double _factor = 1; |
|||
|
|||
public XrdbScalingProvider(AvaloniaX11Platform platform) |
|||
{ |
|||
_resources = platform.Resources; |
|||
_resources.ResourceChanged += name => |
|||
{ |
|||
if (name == "Xft.dpi") |
|||
Update(); |
|||
}; |
|||
Update(); |
|||
} |
|||
|
|||
void Update() |
|||
{ |
|||
var factor = 1d; |
|||
var stringValue = _resources.GetResource("Xft.dpi")?.Trim(); |
|||
if (!string.IsNullOrWhiteSpace(stringValue) && double.TryParse(stringValue, NumberStyles.Any, |
|||
CultureInfo.InvariantCulture, out var parsed)) |
|||
{ |
|||
factor = parsed / 96; |
|||
} |
|||
|
|||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
|||
if (_factor != factor) |
|||
{ |
|||
_factor = factor; |
|||
SettingsChanged?.Invoke(); |
|||
} |
|||
} |
|||
|
|||
public event Action? SettingsChanged; |
|||
|
|||
public double GetScaling(X11Screen screen, int index) => _factor; |
|||
} |
|||
|
|||
class PhysicalDpiScalingProvider : IScalingProvider |
|||
{ |
|||
private const int FullHDWidth = 1920; |
|||
private const int FullHDHeight = 1080; |
|||
|
|||
public double GetScaling(X11Screen screen, int index) |
|||
{ |
|||
if (screen.PhysicalSize == null) |
|||
return 1; |
|||
return GuessPixelDensity(screen.Bounds, screen.PhysicalSize.Value); |
|||
} |
|||
|
|||
double GuessPixelDensity(PixelRect pixel, Size physical) |
|||
{ |
|||
var calculatedDensity = 1d; |
|||
if (physical.Width > 0) |
|||
calculatedDensity = pixel.Width <= FullHDWidth |
|||
? 1 |
|||
: Math.Max(1, pixel.Width / physical.Width * 25.4 / 96); |
|||
else if (physical.Height > 0) |
|||
calculatedDensity = pixel.Height <= FullHDHeight |
|||
? 1 |
|||
: Math.Max(1, pixel.Height / physical.Height * 25.4 / 96); |
|||
|
|||
if (calculatedDensity > 3) |
|||
return 1; |
|||
else |
|||
{ |
|||
var sanePixelDensities = new double[] { 1, 1.25, 1.50, 1.75, 2 }; |
|||
foreach (var saneDensity in sanePixelDensities) |
|||
{ |
|||
if (calculatedDensity <= saneDensity + 0.20) |
|||
return saneDensity; |
|||
} |
|||
|
|||
return sanePixelDensities.Last(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
class UserConfiguredScalingProvider : IScalingProvider |
|||
{ |
|||
private readonly Dictionary<string, double>? _namedConfig; |
|||
private readonly List<double>? _indexedConfig; |
|||
|
|||
|
|||
public UserConfiguredScalingProvider(Dictionary<string, double>? namedConfig, List<double>? indexedConfig) |
|||
{ |
|||
_namedConfig = namedConfig; |
|||
_indexedConfig = indexedConfig; |
|||
} |
|||
|
|||
public double GetScaling(X11Screen screen, int index) |
|||
{ |
|||
if (_indexedConfig != null) |
|||
{ |
|||
if (index > 0 && index < _indexedConfig.Count) |
|||
return _indexedConfig[index]; |
|||
return 1; |
|||
} |
|||
if (_namedConfig?.TryGetValue(screen.Name, out var scaling) == true) |
|||
return scaling; |
|||
|
|||
return 1; |
|||
} |
|||
} |
|||
|
|||
class UserScalingConfiguration |
|||
{ |
|||
public Dictionary<string, double>? NamedConfig { get; set; } |
|||
public List<double>? IndexedConfig { get; set; } |
|||
} |
|||
|
|||
static (UserScalingConfiguration? config, double global, bool forceAuto)? TryGetEnvConfiguration( |
|||
string globalFactorName, string userConfigName, string[] autoNames) |
|||
{ |
|||
var globalFactorString = Environment.GetEnvironmentVariable(globalFactorName); |
|||
var screenFactorsString = Environment.GetEnvironmentVariable(userConfigName); |
|||
bool usePhysicalDpi = false; |
|||
foreach (var autoName in autoNames) |
|||
{ |
|||
var envValue = Environment.GetEnvironmentVariable(autoName); |
|||
if (envValue == "1") |
|||
usePhysicalDpi = true; |
|||
} |
|||
|
|||
double? globalFactor = null; |
|||
if (!string.IsNullOrWhiteSpace(globalFactorString) |
|||
&& double.TryParse(globalFactorString, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed)) |
|||
globalFactor = parsed; |
|||
|
|||
UserScalingConfiguration? userConfig = null; |
|||
if (!string.IsNullOrWhiteSpace(screenFactorsString)) |
|||
{ |
|||
try |
|||
{ |
|||
var split = screenFactorsString.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); |
|||
if (split[0].Contains("=")) |
|||
{ |
|||
userConfig = new UserScalingConfiguration |
|||
{ |
|||
NamedConfig = split.Select(x => x.Split(new[] { '=' }, 2)) |
|||
.ToDictionary(x => x[0], x => double.Parse(x[1], CultureInfo.InvariantCulture)) |
|||
}; |
|||
} |
|||
else |
|||
{ |
|||
userConfig = new UserScalingConfiguration |
|||
{ |
|||
IndexedConfig = split.Select(x => double.Parse(x, CultureInfo.InvariantCulture)).ToList() |
|||
}; |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
Console.Error.WriteLine($"Unable to parse {userConfigName}={screenFactorsString}"); |
|||
} |
|||
} |
|||
|
|||
|
|||
if (globalFactorString == null && screenFactorsString == null && usePhysicalDpi == null) |
|||
return null; |
|||
|
|||
return (userConfig, globalFactor ?? 1, usePhysicalDpi); |
|||
} |
|||
|
|||
|
|||
static IScalingProvider GetScalingProvider(AvaloniaX11Platform platform) |
|||
{ |
|||
var envSets = new[] |
|||
{ |
|||
("AVALONIA_GLOBAL_SCALE_FACTOR", "AVALONIA_SCREEN_SCALE_FACTORS", new[] { "AVALONIA_USE_PHYSICAL_DPI" }) |
|||
}.ToList(); |
|||
|
|||
if (Environment.GetEnvironmentVariable("AVALONIA_SCREEN_SCALE_IGNORE_QT") != "1") |
|||
{ |
|||
envSets.Add(("QT_SCALE_FACTOR", "QT_SCREEN_SCALE_FACTORS", |
|||
new[] { "QT_AUTO_SCREEN_SCALE_FACTOR", "QT_USE_PHYSICAL_DPI" })); |
|||
} |
|||
|
|||
UserScalingConfiguration? config = null; |
|||
double global = 1; |
|||
bool forceAuto = false; |
|||
|
|||
|
|||
foreach (var envSet in envSets) |
|||
{ |
|||
var envConfig = TryGetEnvConfiguration(envSet.Item1, envSet.Item2, envSet.Item3); |
|||
if (envConfig != null) |
|||
{ |
|||
(config, global, forceAuto) = envConfig.Value; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
IScalingProvider provider; |
|||
if (config != null) |
|||
provider = new UserConfiguredScalingProvider(config.NamedConfig, config.IndexedConfig); |
|||
else if (forceAuto) |
|||
provider = new PhysicalDpiScalingProvider(); |
|||
else |
|||
provider = new XrdbScalingProvider(platform); |
|||
|
|||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
|||
if (global != 1) |
|||
provider = new PostMultiplyScalingProvider(provider, global); |
|||
|
|||
return provider; |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform; |
|||
using static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11.Screens |
|||
{ |
|||
internal partial class X11Screens : IScreenImpl |
|||
{ |
|||
private IX11RawScreenInfoProvider _impl; |
|||
private IScalingProvider _scaling; |
|||
internal event Action Changed; |
|||
|
|||
public X11Screens(AvaloniaX11Platform platform) |
|||
{ |
|||
var info = platform.Info; |
|||
_impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5)) |
|||
? new Randr15ScreensImpl(platform) |
|||
: (IX11RawScreenInfoProvider)new FallbackScreensImpl(platform); |
|||
_impl.Changed += () => Changed?.Invoke(); |
|||
_scaling = GetScalingProvider(platform); |
|||
if (_scaling is IScalingProviderWithChanges scalingWithChanges) |
|||
scalingWithChanges.SettingsChanged += () => Changed?.Invoke(); |
|||
} |
|||
|
|||
private static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens) |
|||
{ |
|||
var rect = default(PixelRect); |
|||
foreach (var s in screens) |
|||
{ |
|||
rect = rect.Union(s.Bounds); |
|||
//Fallback value
|
|||
s.WorkingArea = s.Bounds; |
|||
} |
|||
|
|||
var res = XGetWindowProperty(info.Display, |
|||
info.RootWindow, |
|||
info.Atoms._NET_WORKAREA, |
|||
IntPtr.Zero, |
|||
new IntPtr(128), |
|||
false, |
|||
info.Atoms.AnyPropertyType, |
|||
out var type, |
|||
out var format, |
|||
out var count, |
|||
out var bytesAfter, |
|||
out var prop); |
|||
|
|||
if (res != (int)Status.Success || type == IntPtr.Zero || |
|||
format == 0 || bytesAfter.ToInt64() != 0 || count.ToInt64() % 4 != 0) |
|||
return screens; |
|||
|
|||
var pwa = (IntPtr*)prop; |
|||
var wa = new PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); |
|||
|
|||
|
|||
foreach (var s in screens) |
|||
{ |
|||
s.WorkingArea = s.Bounds.Intersect(wa); |
|||
if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0) |
|||
s.WorkingArea = s.Bounds; |
|||
} |
|||
|
|||
XFree(prop); |
|||
return screens; |
|||
} |
|||
|
|||
|
|||
public Screen ScreenFromPoint(PixelPoint point) |
|||
{ |
|||
return ScreenHelper.ScreenFromPoint(point, AllScreens); |
|||
} |
|||
|
|||
public Screen ScreenFromRect(PixelRect rect) |
|||
{ |
|||
return ScreenHelper.ScreenFromRect(rect, AllScreens); |
|||
} |
|||
|
|||
public Screen ScreenFromWindow(IWindowBaseImpl window) |
|||
{ |
|||
return ScreenHelper.ScreenFromWindow(window, AllScreens); |
|||
} |
|||
|
|||
public int ScreenCount => _impl.Screens.Length; |
|||
|
|||
public IReadOnlyList<Screen> AllScreens => |
|||
_impl.Screens.Select((s, i) => new Screen(_scaling.GetScaling(s, i), s.Bounds, s.WorkingArea, s.IsPrimary)) |
|||
.ToArray(); |
|||
} |
|||
} |
|||
@ -1,337 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform; |
|||
using static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11 |
|||
{ |
|||
internal class X11Screens : IScreenImpl |
|||
{ |
|||
private IX11Screens _impl; |
|||
|
|||
public X11Screens(IX11Screens impl) |
|||
{ |
|||
_impl = impl; |
|||
} |
|||
|
|||
private static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens) |
|||
{ |
|||
var rect = default(PixelRect); |
|||
foreach (var s in screens) |
|||
{ |
|||
rect = rect.Union(s.Bounds); |
|||
//Fallback value
|
|||
s.WorkingArea = s.Bounds; |
|||
} |
|||
|
|||
var res = XGetWindowProperty(info.Display, |
|||
info.RootWindow, |
|||
info.Atoms._NET_WORKAREA, |
|||
IntPtr.Zero, |
|||
new IntPtr(128), |
|||
false, |
|||
info.Atoms.AnyPropertyType, |
|||
out var type, |
|||
out var format, |
|||
out var count, |
|||
out var bytesAfter, |
|||
out var prop); |
|||
|
|||
if (res != (int)Status.Success || type == IntPtr.Zero || |
|||
format == 0 || bytesAfter.ToInt64() != 0 || count.ToInt64() % 4 != 0) |
|||
return screens; |
|||
|
|||
var pwa = (IntPtr*)prop; |
|||
var wa = new PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); |
|||
|
|||
|
|||
foreach (var s in screens) |
|||
{ |
|||
s.WorkingArea = s.Bounds.Intersect(wa); |
|||
if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0) |
|||
s.WorkingArea = s.Bounds; |
|||
} |
|||
|
|||
XFree(prop); |
|||
return screens; |
|||
} |
|||
|
|||
private class Randr15ScreensImpl : IX11Screens |
|||
{ |
|||
private readonly X11ScreensUserSettings _settings; |
|||
private X11Screen[] _cache; |
|||
private X11Info _x11; |
|||
private IntPtr _window; |
|||
private const int EDIDStructureLength = 32; // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4
|
|||
|
|||
public Randr15ScreensImpl(AvaloniaX11Platform platform, X11ScreensUserSettings settings) |
|||
{ |
|||
_settings = settings; |
|||
_x11 = platform.Info; |
|||
_window = CreateEventWindow(platform, OnEvent); |
|||
XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); |
|||
} |
|||
|
|||
private void OnEvent(ref XEvent ev) |
|||
{ |
|||
// Invalidate cache on RRScreenChangeNotify
|
|||
if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) |
|||
_cache = null; |
|||
} |
|||
|
|||
private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput) |
|||
{ |
|||
if(rrOutput == IntPtr.Zero) |
|||
return null; |
|||
var properties = XRRListOutputProperties(_x11.Display,rrOutput, out int propertyCount); |
|||
var hasEDID = false; |
|||
for(var pc = 0; pc < propertyCount; pc++) |
|||
{ |
|||
if(properties[pc] == _x11.Atoms.EDID) |
|||
hasEDID = true; |
|||
} |
|||
if(!hasEDID) |
|||
return null; |
|||
XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false, _x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, out IntPtr prop); |
|||
if(actualType != _x11.Atoms.XA_INTEGER) |
|||
return null; |
|||
if(actualFormat != 8) // Expecting an byte array
|
|||
return null; |
|||
|
|||
var edid = new byte[bytesAfter]; |
|||
Marshal.Copy(prop,edid,0,bytesAfter); |
|||
XFree(prop); |
|||
XFree(new IntPtr(properties)); |
|||
if(edid.Length < 22) |
|||
return null; |
|||
var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm.
|
|||
var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm.
|
|||
if(width == 0 && height == 0) |
|||
return null; |
|||
return new Size(width * 10, height * 10); |
|||
} |
|||
|
|||
public unsafe X11Screen[] Screens |
|||
{ |
|||
get |
|||
{ |
|||
if (_cache != null) |
|||
return _cache; |
|||
var monitors = XRRGetMonitors(_x11.Display, _window, true, out var count); |
|||
|
|||
var screens = new X11Screen[count]; |
|||
for (var c = 0; c < count; c++) |
|||
{ |
|||
var mon = monitors[c]; |
|||
var namePtr = XGetAtomName(_x11.Display, mon.Name); |
|||
var name = Marshal.PtrToStringAnsi(namePtr); |
|||
XFree(namePtr); |
|||
var bounds = new PixelRect(mon.X, mon.Y, mon.Width, mon.Height); |
|||
Size? pSize = null; |
|||
double density = 0; |
|||
if (_settings.NamedScaleFactors?.TryGetValue(name, out density) != true) |
|||
{ |
|||
for(int o = 0; o < mon.NOutput; o++) |
|||
{ |
|||
var outputSize = GetPhysicalMonitorSizeFromEDID(mon.Outputs[o]); |
|||
var outputDensity = 1d; |
|||
if(outputSize != null) |
|||
outputDensity = X11Screen.GuessPixelDensity(bounds, outputSize.Value); |
|||
if(density == 0 || density > outputDensity) |
|||
{ |
|||
density = outputDensity; |
|||
pSize = outputSize; |
|||
} |
|||
} |
|||
} |
|||
if(density == 0) |
|||
density = 1; |
|||
density *= _settings.GlobalScaleFactor; |
|||
screens[c] = new X11Screen(bounds, mon.Primary != 0, name, pSize, density); |
|||
} |
|||
|
|||
XFree(new IntPtr(monitors)); |
|||
_cache = UpdateWorkArea(_x11, screens); |
|||
return screens; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class FallbackScreensImpl : IX11Screens |
|||
{ |
|||
public FallbackScreensImpl(X11Info info, X11ScreensUserSettings settings) |
|||
{ |
|||
if (XGetGeometry(info.Display, info.RootWindow, out var geo)) |
|||
{ |
|||
|
|||
Screens = UpdateWorkArea(info, |
|||
new[] |
|||
{ |
|||
new X11Screen(new PixelRect(0, 0, geo.width, geo.height), true, "Default", null, |
|||
settings.GlobalScaleFactor) |
|||
}); |
|||
} |
|||
else |
|||
{ |
|||
Screens = new[] |
|||
{ |
|||
new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null, |
|||
settings.GlobalScaleFactor) |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public X11Screen[] Screens { get; } |
|||
} |
|||
|
|||
public static IX11Screens Init(AvaloniaX11Platform platform) |
|||
{ |
|||
var info = platform.Info; |
|||
var settings = X11ScreensUserSettings.Detect(); |
|||
var impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5)) |
|||
? new Randr15ScreensImpl(platform, settings) |
|||
: (IX11Screens)new FallbackScreensImpl(info, settings); |
|||
|
|||
return impl; |
|||
|
|||
} |
|||
|
|||
public Screen ScreenFromPoint(PixelPoint point) |
|||
{ |
|||
return ScreenHelper.ScreenFromPoint(point, AllScreens); |
|||
} |
|||
|
|||
public Screen ScreenFromRect(PixelRect rect) |
|||
{ |
|||
return ScreenHelper.ScreenFromRect(rect, AllScreens); |
|||
} |
|||
|
|||
public Screen ScreenFromWindow(IWindowBaseImpl window) |
|||
{ |
|||
return ScreenHelper.ScreenFromWindow(window, AllScreens); |
|||
} |
|||
|
|||
public int ScreenCount => _impl.Screens.Length; |
|||
|
|||
public IReadOnlyList<Screen> AllScreens => |
|||
_impl.Screens.Select(s => new Screen(s.Scaling, s.Bounds, s.WorkingArea, s.IsPrimary)).ToArray(); |
|||
} |
|||
|
|||
internal interface IX11Screens |
|||
{ |
|||
X11Screen[] Screens { get; } |
|||
} |
|||
|
|||
internal class X11ScreensUserSettings |
|||
{ |
|||
public double GlobalScaleFactor { get; set; } = 1; |
|||
public Dictionary<string, double> NamedScaleFactors { get; set; } |
|||
|
|||
private static double? TryParse(string s) |
|||
{ |
|||
if (s == null) |
|||
return null; |
|||
if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var rv)) |
|||
return rv; |
|||
return null; |
|||
} |
|||
|
|||
|
|||
public static X11ScreensUserSettings DetectEnvironment() |
|||
{ |
|||
var globalFactor = Environment.GetEnvironmentVariable("AVALONIA_GLOBAL_SCALE_FACTOR"); |
|||
var screenFactors = Environment.GetEnvironmentVariable("AVALONIA_SCREEN_SCALE_FACTORS"); |
|||
if (globalFactor == null && screenFactors == null) |
|||
return null; |
|||
|
|||
var rv = new X11ScreensUserSettings |
|||
{ |
|||
GlobalScaleFactor = TryParse(globalFactor) ?? 1 |
|||
}; |
|||
|
|||
try |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(screenFactors)) |
|||
{ |
|||
rv.NamedScaleFactors = screenFactors.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)) |
|||
.Select(x => x.Split('=')).ToDictionary(x => x[0], |
|||
x => double.Parse(x[1], CultureInfo.InvariantCulture)); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
//Ignore
|
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
|
|||
public static X11ScreensUserSettings Detect() |
|||
{ |
|||
return DetectEnvironment() ?? new X11ScreensUserSettings(); |
|||
} |
|||
} |
|||
|
|||
internal class X11Screen |
|||
{ |
|||
private const int FullHDWidth = 1920; |
|||
private const int FullHDHeight = 1080; |
|||
public bool IsPrimary { get; } |
|||
public string Name { get; set; } |
|||
public PixelRect Bounds { get; set; } |
|||
public Size? PhysicalSize { get; set; } |
|||
public double Scaling { get; set; } |
|||
public PixelRect WorkingArea { get; set; } |
|||
|
|||
public X11Screen( |
|||
PixelRect bounds, |
|||
bool isPrimary, |
|||
string name, |
|||
Size? physicalSize, |
|||
double? scaling) |
|||
{ |
|||
IsPrimary = isPrimary; |
|||
Name = name; |
|||
Bounds = bounds; |
|||
if (physicalSize == null && scaling == null) |
|||
{ |
|||
Scaling = 1; |
|||
} |
|||
else if (scaling == null) |
|||
{ |
|||
Scaling = GuessPixelDensity(bounds, physicalSize.Value); |
|||
} |
|||
else |
|||
{ |
|||
Scaling = scaling.Value; |
|||
PhysicalSize = physicalSize; |
|||
} |
|||
} |
|||
|
|||
public static double GuessPixelDensity(PixelRect pixel, Size physical) |
|||
{ |
|||
var calculatedDensity = 1d; |
|||
if(physical.Width > 0) |
|||
calculatedDensity = pixel.Width <= FullHDWidth ? 1 : Math.Max(1, pixel.Width / physical.Width * 25.4 / 96); |
|||
else if(physical.Height > 0) |
|||
calculatedDensity = pixel.Height <= FullHDHeight ? 1 : Math.Max(1, pixel.Height / physical.Height * 25.4 / 96); |
|||
|
|||
if(calculatedDensity > 3) |
|||
return 1; |
|||
else |
|||
{ |
|||
var sanePixelDensities = new double[] { 1, 1.25, 1.50, 1.75, 2 }; |
|||
foreach(var saneDensity in sanePixelDensities) |
|||
{ |
|||
if(calculatedDensity <= saneDensity + 0.20) |
|||
return saneDensity; |
|||
} |
|||
return sanePixelDensities.Last(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.InteropServices; |
|||
using static Avalonia.X11.XLib; |
|||
namespace Avalonia.X11; |
|||
|
|||
internal class XResources |
|||
{ |
|||
private Dictionary<string, string> _resources = new(); |
|||
private readonly X11Info _x11; |
|||
public event Action<string>? ResourceChanged; |
|||
|
|||
public XResources(AvaloniaX11Platform plat) |
|||
{ |
|||
_x11 = plat.Info; |
|||
plat.Globals.RootPropertyChanged += OnRootPropertyChanged; |
|||
UpdateResources(); |
|||
} |
|||
|
|||
void UpdateResources() |
|||
{ |
|||
var res = ReadResourcesString() ?? ""; |
|||
var items = res.Split('\n'); |
|||
var newResources = new Dictionary<string, string>(); |
|||
var missingResources = new HashSet<string>(_resources.Keys); |
|||
var changedResources = new HashSet<string>(); |
|||
foreach (var item in items) |
|||
{ |
|||
var sp = item.Split(new[] { ':' }, 2); |
|||
if (sp.Length < 2) |
|||
continue; |
|||
var key = sp[0]; |
|||
var value = sp[1].TrimStart(); |
|||
newResources[key] = value; |
|||
if (!missingResources.Remove(sp[0]) || _resources[key] != value) |
|||
changedResources.Add(key); |
|||
} |
|||
_resources = newResources; |
|||
foreach (var missing in missingResources) |
|||
ResourceChanged?.Invoke(missing); |
|||
foreach (var changed in changedResources) |
|||
ResourceChanged?.Invoke(changed); |
|||
} |
|||
|
|||
public string? GetResource(string key) |
|||
{ |
|||
_resources.TryGetValue(key, out var value); |
|||
return value; |
|||
} |
|||
|
|||
string ReadResourcesString() |
|||
{ |
|||
XGetWindowProperty(_x11.Display, _x11.RootWindow, _x11.Atoms.XA_RESOURCE_MANAGER, |
|||
IntPtr.Zero, new IntPtr(0x7fffffff), |
|||
false, _x11.Atoms.XA_STRING, out var actualType, out var actualFormat, |
|||
out var nitems, out _, out var prop); |
|||
try |
|||
{ |
|||
if (actualFormat != 8) |
|||
return null; |
|||
return Marshal.PtrToStringAnsi(prop, nitems.ToInt32()); |
|||
} |
|||
finally |
|||
{ |
|||
XFree(prop); |
|||
} |
|||
} |
|||
|
|||
private void OnRootPropertyChanged(IntPtr atom) |
|||
{ |
|||
if (atom == _x11.Atoms.XA_RESOURCE_MANAGER) |
|||
UpdateResources(); |
|||
} |
|||
} |
|||
@ -0,0 +1,355 @@ |
|||
using System; |
|||
using Avalonia.Media.Imaging; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Media.Imaging |
|||
{ |
|||
public class PixelFormatWriterTests |
|||
{ |
|||
private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = 255, |
|||
G = 255, |
|||
R = 255 |
|||
}; |
|||
|
|||
private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = 0, |
|||
G = 0, |
|||
R = 0 |
|||
}; |
|||
|
|||
[Fact] |
|||
public void Should_Write_Bgr555() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Bgr555), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Bgr555PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Bgr555PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { G = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { G = 255, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { B = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { B = 255, A = 255 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Bgra8888() |
|||
{ |
|||
var sourceMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Bgra8888), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(3, 1)); |
|||
|
|||
var sourceWriter = new PixelFormatWriter.Bgra8888PixelFormatWriter(); |
|||
var sourceReader = new PixelFormatReader.Bgra8888PixelFormatReader(); |
|||
|
|||
sourceWriter.Reset(sourceMemory.Address); |
|||
sourceReader.Reset(sourceMemory.Address); |
|||
|
|||
sourceWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255 }, sourceReader.ReadNext()); |
|||
|
|||
sourceWriter.WriteNext(new Rgba8888Pixel { G = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { G = 255 }, sourceReader.ReadNext()); |
|||
|
|||
sourceWriter.WriteNext(new Rgba8888Pixel { B = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { B = 255 }, sourceReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Rgba8888() |
|||
{ |
|||
var sourceMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Rgba8888), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Rgba8888PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Rgba8888PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(sourceMemory.Address); |
|||
pixelReader.Reset(sourceMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Rgb24() |
|||
{ |
|||
var sourceMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Rgb24), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Rgb24PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Rgb24PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(sourceMemory.Address); |
|||
pixelReader.Reset(sourceMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 255 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void Should_Write_Rgba64() |
|||
{ |
|||
var sourceMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Rgba64), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Rgba64PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Rgba64PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(sourceMemory.Address); |
|||
pixelReader.Reset(sourceMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Bgr565() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Bgr565), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Bgr565PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Bgr565PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { G = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { G = 255, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { B = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { B = 255, A = 255 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Gray32Float() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Gray32Float), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Gray32FloatPixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Gray32FloatPixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 255, G = 255, B = 255, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 125 }); |
|||
Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 125, A = 255 }, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel()); |
|||
Assert.Equal(new Rgba8888Pixel { A = 255 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_BlackWhite() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.BlackWhite), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.BlackWhitePixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.BlackWhitePixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(s_white); |
|||
Assert.Equal(s_white, pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(s_black); |
|||
Assert.Equal(s_black, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Gray2() |
|||
{ |
|||
var palette = new[] |
|||
{ |
|||
s_black, |
|||
new Rgba8888Pixel |
|||
{ |
|||
A = 255, B = 0x55, G = 0x55, R = 0x55 |
|||
}, |
|||
new Rgba8888Pixel |
|||
{ |
|||
A = 255, B = 0xAA, G = 0xAA, R = 0xAA |
|||
}, |
|||
s_white |
|||
}; |
|||
|
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Gray2), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Gray2PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Gray2PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(palette[0]); |
|||
Assert.Equal(palette[0], pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(palette[1]); |
|||
Assert.Equal(palette[1], pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(palette[2]); |
|||
Assert.Equal(palette[2], pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(palette[3]); |
|||
Assert.Equal(palette[3], pixelReader.ReadNext()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Gray4() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Gray4), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Gray4PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Gray4PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(GetGray4(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 17 }); |
|||
Assert.Equal(GetGray4(new Rgba8888Pixel { R = 17 }), pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel()); |
|||
Assert.Equal(new Rgba8888Pixel { A = 255 }, pixelReader.ReadNext()); |
|||
} |
|||
|
|||
private static Rgba8888Pixel GetGray4(Rgba8888Pixel pixel) |
|||
{ |
|||
var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); |
|||
|
|||
var value = (byte)(grayscale / 255F * 0xF); |
|||
|
|||
value = (byte)(value | (value << 4)); |
|||
|
|||
return new Rgba8888Pixel(value, value, value, 255); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Gray8() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Gray8), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Gray8PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Gray8PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(GetGray8(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 120 }); |
|||
Assert.Equal(GetGray8(new Rgba8888Pixel { R = 120 }), pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel()); |
|||
Assert.Equal(GetGray8(new Rgba8888Pixel { A = 255 }), pixelReader.ReadNext()); |
|||
} |
|||
|
|||
private static Rgba8888Pixel GetGray8(Rgba8888Pixel pixel) |
|||
{ |
|||
var value = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); |
|||
|
|||
return new Rgba8888Pixel(value, value, value, 255); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Write_Gray16() |
|||
{ |
|||
var bitmapMemory = new BitmapMemory( |
|||
new Platform.PixelFormat(Platform.PixelFormatEnum.Gray16), |
|||
Platform.AlphaFormat.Unpremul, |
|||
new PixelSize(10, 10)); |
|||
|
|||
var pixelWriter = new PixelFormatWriter.Gray16PixelFormatWriter(); |
|||
var pixelReader = new PixelFormatReader.Gray16PixelFormatReader(); |
|||
|
|||
pixelWriter.Reset(bitmapMemory.Address); |
|||
pixelReader.Reset(bitmapMemory.Address); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); |
|||
Assert.Equal(GetGray16(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel { R = 120 }); |
|||
Assert.Equal(GetGray16(new Rgba8888Pixel { R = 120 }), pixelReader.ReadNext()); |
|||
|
|||
pixelWriter.WriteNext(new Rgba8888Pixel()); |
|||
Assert.Equal(GetGray16(new Rgba8888Pixel { A = 255 }), pixelReader.ReadNext()); |
|||
} |
|||
|
|||
private static Rgba8888Pixel GetGray16(Rgba8888Pixel pixel) |
|||
{ |
|||
var grayscale = (ushort)Math.Round((0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B) * 0x0101); |
|||
|
|||
var value = (byte)(grayscale >> 8); |
|||
|
|||
return new Rgba8888Pixel(value, value, value, 255); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net472</TargetFramework> |
|||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> |
|||
<OutputType>Library</OutputType> |
|||
<IsPackable>false</IsPackable> |
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
|||
<Configuration Condition="'$(Configuration)'==''">Debug</Configuration> |
|||
</PropertyGroup> |
|||
|
|||
<Import Project="..\..\build\Moq.props" /> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Project="..\..\build\HarfBuzzSharp.props" /> |
|||
<Import Project="..\..\build\XUnit.props" /> |
|||
<Import Project="..\..\build\SharedVersion.props" /> |
|||
<ItemGroup> |
|||
<Content Include="..\TestFiles\BuildTasks\PInvoke\bin\$(Configuration)\netstandard2.0\PInvoke.dll" Link="Assets\PInvoke.dll"> |
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> |
|||
</Content> |
|||
<Content Include="..\TestFiles\BuildTasks\PInvoke\bin\$(Configuration)\netstandard2.0\PInvoke.dll.refs" Link="Assets\PInvoke.dll.refs"> |
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> |
|||
</Content> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" /> |
|||
<!-- Ensure PInvoke.csproj is build before Avalonia.Build.Tasks.UnitTest --> |
|||
<ProjectReference Include="..\TestFiles\BuildTasks\PInvoke\PInvoke.csproj" |
|||
SetConfiguration="Configuration=$(Configuration)" |
|||
SetTargetFramework="TargetFramework=netstandard2.0" |
|||
ReferenceOutputAssembly="false" |
|||
PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,38 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Reflection; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Build.Tasks.UnitTest; |
|||
|
|||
public class CompileAvaloniaXamlTaskTest |
|||
{ |
|||
|
|||
[Fact] |
|||
public void Does_Not_Fail_When_Codebehind_Contains_DllImport() |
|||
{ |
|||
using var engine = UnitTestBuildEngine.Start(); |
|||
var basePath = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath), "Assets"); |
|||
var originalAssemblyPath = Path.Combine(basePath, |
|||
"PInvoke.dll"); |
|||
var referencesPath = Path.Combine(basePath, |
|||
"PInvoke.dll.refs"); |
|||
var compiledAssemblyPath = "PInvoke.dll"; |
|||
|
|||
Assert.True(File.Exists(originalAssemblyPath), $"The original {originalAssemblyPath} don't exists."); |
|||
|
|||
new CompileAvaloniaXamlTask() |
|||
{ |
|||
AssemblyFile = originalAssemblyPath, |
|||
ReferencesFilePath = referencesPath, |
|||
OutputPath = compiledAssemblyPath, |
|||
RefAssemblyFile = null, |
|||
BuildEngine = engine, |
|||
ProjectDirectory = Directory.GetCurrentDirectory(), |
|||
VerifyIl = true |
|||
}.Execute(); |
|||
Assert.Equal(0, engine.Errors.Count); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Microsoft.Build.Framework; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Build.Tasks.UnitTest; |
|||
|
|||
/// <summary>
|
|||
/// This is fake BuildEngine using for testing build task
|
|||
/// at moment it manage only <see cref="BuildErrorEventArgs"/> and <see cref="BuildWarningEventArgs"/>
|
|||
/// other messages are ignored/>
|
|||
/// </summary>
|
|||
internal class UnitTestBuildEngine : IBuildEngine, IDisposable |
|||
{ |
|||
private readonly bool _treatWarningAsError; |
|||
private readonly bool _assertOnDispose; |
|||
private readonly List<UnitTestBuildEngineMessage> _errors = new(); |
|||
|
|||
/// <summary>
|
|||
/// Start new instance of <see cref="UnitTestBuildEngine"/>
|
|||
/// </summary>
|
|||
/// <param name="continueOnError">if it is <c>false</c> immediately assert error</param>
|
|||
/// <param name="treatWarningAsError">if it is <c>true</c> treat warning as error</param>
|
|||
/// <param name="assertOnDispose">if it is <c>true</c> assert on dispose if there are any errors.</param>
|
|||
/// <returns></returns>
|
|||
public static UnitTestBuildEngine Start(bool continueOnError = false, |
|||
bool treatWarningAsError = false, |
|||
bool assertOnDispose = false) => |
|||
new UnitTestBuildEngine(continueOnError, treatWarningAsError, assertOnDispose); |
|||
|
|||
private UnitTestBuildEngine(bool continueOnError, |
|||
bool treatWarningAsError, |
|||
bool assertOnDispose) |
|||
{ |
|||
ContinueOnError = continueOnError; |
|||
_treatWarningAsError = treatWarningAsError; |
|||
_assertOnDispose = assertOnDispose; |
|||
} |
|||
|
|||
public bool ContinueOnError { get; } |
|||
|
|||
public int LineNumberOfTaskNode { get; } |
|||
|
|||
public int ColumnNumberOfTaskNode { get; } |
|||
|
|||
public string ProjectFileOfTaskNode { get; } |
|||
|
|||
public IReadOnlyList<UnitTestBuildEngineMessage> Errors => _errors; |
|||
|
|||
public bool BuildProjectFile(string projectFileName, |
|||
string[] targetNames, |
|||
IDictionary globalProperties, |
|||
IDictionary targetOutputs) |
|||
=> throw new NotImplementedException(); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_assertOnDispose && _errors.Count > 0) |
|||
{ |
|||
Assert.Fail("There is one o more errors."); |
|||
} |
|||
} |
|||
|
|||
|
|||
public void LogCustomEvent(CustomBuildEventArgs e) |
|||
{ |
|||
} |
|||
|
|||
public void LogMessageEvent(BuildMessageEventArgs e) |
|||
{ |
|||
} |
|||
|
|||
public void LogErrorEvent(BuildErrorEventArgs e) |
|||
{ |
|||
var message = UnitTestBuildEngineMessage.From(e); |
|||
_errors.Add(message); |
|||
if (!ContinueOnError) |
|||
{ |
|||
Assert.Fail(message.Message); |
|||
} |
|||
} |
|||
|
|||
public void LogWarningEvent(BuildWarningEventArgs e) |
|||
{ |
|||
if (_treatWarningAsError) |
|||
{ |
|||
var message = UnitTestBuildEngineMessage.From(e); |
|||
_errors.Add(message); |
|||
if (!ContinueOnError) |
|||
{ |
|||
Assert.Fail(message.Message); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using Microsoft.Build.Framework; |
|||
|
|||
namespace Avalonia.Build.Tasks.UnitTest; |
|||
|
|||
enum MessageSource |
|||
{ |
|||
Unknown, |
|||
ErrorEvent, |
|||
MessageEvent, |
|||
CustomEvent, |
|||
WarningEvent |
|||
} |
|||
|
|||
record class UnitTestBuildEngineMessage |
|||
{ |
|||
private UnitTestBuildEngineMessage(MessageSource Type, LazyFormattedBuildEventArgs Source) |
|||
{ |
|||
this.Type = Type; |
|||
this.Source = Source; |
|||
Message = Source.Message; |
|||
} |
|||
|
|||
public MessageSource Type { get; } |
|||
public LazyFormattedBuildEventArgs Source { get; } |
|||
public string Message { get; } |
|||
|
|||
public static UnitTestBuildEngineMessage From(BuildWarningEventArgs buildWarning) => |
|||
new UnitTestBuildEngineMessage(MessageSource.WarningEvent, buildWarning); |
|||
|
|||
public static UnitTestBuildEngineMessage From(BuildMessageEventArgs buildMessage) => |
|||
new UnitTestBuildEngineMessage(MessageSource.MessageEvent, buildMessage); |
|||
|
|||
public static UnitTestBuildEngineMessage From(BuildErrorEventArgs buildError) => |
|||
new UnitTestBuildEngineMessage(MessageSource.ErrorEvent, buildError); |
|||
|
|||
public static UnitTestBuildEngineMessage From(CustomBuildEventArgs customBuild) => |
|||
new UnitTestBuildEngineMessage(MessageSource.CustomEvent, customBuild); |
|||
|
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
<Application |
|||
xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="PInvoke.App"> |
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,21 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace PInvoke; |
|||
|
|||
public class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) |
|||
{ |
|||
desktopLifetime.MainWindow = new MainWindow(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' |
|||
x:Class="PInvoke.MainWindow"> |
|||
</Window> |
|||
@ -0,0 +1,22 @@ |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace PInvoke; |
|||
|
|||
public partial class MainWindow : Window |
|||
{ |
|||
[DllImport(@"libhello")] |
|||
extern static int add(int a, int b); |
|||
|
|||
public MainWindow() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
protected override void OnLoaded(RoutedEventArgs e) |
|||
{ |
|||
base.OnLoaded(e); |
|||
var x = add(1, 2); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks> |
|||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
<!--<AvaloniaXamlIlDebuggerLaunch>true</AvaloniaXamlIlDebuggerLaunch>--> |
|||
<EnableAvaloniaXamlCompilation>false</EnableAvaloniaXamlCompilation> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
<IsPackable>false</IsPackable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\..\..\build\SourceGenerators.props" /> |
|||
</Project> |
|||
@ -0,0 +1,14 @@ |
|||
using Avalonia; |
|||
|
|||
namespace PInvoke; |
|||
|
|||
public class Program |
|||
{ |
|||
static void Main(string[] args) => BuildAvaloniaApp() |
|||
.StartWithClassicDesktopLifetime(args); |
|||
|
|||
public static AppBuilder BuildAvaloniaApp() => |
|||
AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
} |
|||
Loading…
Reference in new issue